diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.test.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.test.ts index a2b954d990..0846259f5c 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from '@jest/globals'; import { modelRetainsPrompts, modelTrains, + withWorstProviderDataPolicy, } from '@/lib/ai-gateway/providers/openrouter/model-data-policy'; import { OpenRouterSearchResponse } from '@/lib/ai-gateway/providers/openrouter/openrouter-types'; @@ -18,6 +19,76 @@ const baseModel = { }; describe('model data policy', () => { + test('reports prompt retention when a provider offers both standard and ZDR routes', () => { + const response = OpenRouterSearchResponse.parse({ + data: { + models: [ + { + ...baseModel, + endpoint: { + provider_display_name: 'SpaceXAI (ZDR)', + is_free: false, + pricing: { prompt: '0.000002', completion: '0.000006' }, + data_policy: { + training: false, + retainsPrompts: false, + }, + }, + }, + ], + }, + }); + + const model = response.data.models[0]; + if (!model) throw new Error('expected model'); + const normalizedModel = withWorstProviderDataPolicy(model, { + training: false, + retainsPrompts: true, + }); + + expect(normalizedModel.endpoint?.data_policy).toEqual({ + training: false, + retainsPrompts: true, + }); + expect(model.endpoint?.data_policy).toEqual({ + training: false, + retainsPrompts: false, + }); + }); + + test('preserves data collection reported by a model route', () => { + const response = OpenRouterSearchResponse.parse({ + data: { + models: [ + { + ...baseModel, + endpoint: { + provider_display_name: 'Test Provider', + is_free: false, + pricing: { prompt: '0.000002', completion: '0.000006' }, + data_policy: { + training: true, + retainsPrompts: true, + }, + }, + }, + ], + }, + }); + + const model = response.data.models[0]; + if (!model) throw new Error('expected model'); + const normalizedModel = withWorstProviderDataPolicy(model, { + training: false, + retainsPrompts: false, + }); + + expect(normalizedModel.endpoint?.data_policy).toEqual({ + training: true, + retainsPrompts: true, + }); + }); + test('preserves and uses model endpoint policy overrides', () => { const response = OpenRouterSearchResponse.parse({ data: { diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.ts index 9e90afe5dd..6ac115eee2 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/model-data-policy.ts @@ -1,5 +1,28 @@ import type { OpenRouterModel } from '@/lib/ai-gateway/providers/openrouter/openrouter-types'; +/** + * OpenRouter returns one route per model even when a provider offers routes with different data + * policies. Report data collection if either that route or the provider-wide policy allows it. + */ +export function withWorstProviderDataPolicy( + model: OpenRouterModel, + providerPolicy: { training: boolean; retainsPrompts: boolean } +): OpenRouterModel { + if (!model.endpoint) return model; + + return { + ...model, + endpoint: { + ...model.endpoint, + data_policy: { + training: providerPolicy.training || model.endpoint.data_policy?.training === true, + retainsPrompts: + providerPolicy.retainsPrompts || model.endpoint.data_policy?.retainsPrompts === true, + }, + }, + }; +} + export function modelTrains(model: OpenRouterModel, providerTrains: boolean): boolean { return model.endpoint?.data_policy?.training ?? providerTrains; } diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts index d2386d4001..b4db833f6e 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/sync-providers.ts @@ -45,6 +45,7 @@ import { applyFreeEndpointDataPolicy, getOpenRouterFreeEndpoints, } from '@/lib/ai-gateway/providers/openrouter/free-endpoint-data-policy'; +import { withWorstProviderDataPolicy } from '@/lib/ai-gateway/providers/openrouter/model-data-policy'; import { isUnavailableModel } from '@/lib/ai-gateway/unavailable-models'; /** @@ -346,7 +347,9 @@ async function syncProviders( // Deduplicate models within each provider by slug const uniqueModelsMap = new Map(); data.models.forEach(model => { - uniqueModelsMap.set(normalizeModelId(model.slug), model); + // A model may show a ZDR route even though the same provider also offers data-retaining routes. + const normalizedModel = withWorstProviderDataPolicy(model, data.provider.dataPolicy); + uniqueModelsMap.set(normalizeModelId(model.slug), normalizedModel); }); const uniqueModels = Array.from(uniqueModelsMap.values());