From 2e40076768f226a0183d38433ccb9cb165ab0f8a Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 7 Aug 2026 12:03:40 +0200 Subject: [PATCH 1/2] fix(organizations): expose autocomplete models --- .../app/api/edit/completions/route.test.ts | 16 +++ .../web/src/app/api/edit/completions/route.ts | 14 +-- .../src/app/api/fim/completions/route.test.ts | 18 +++ apps/web/src/app/api/fim/completions/route.ts | 22 ++-- .../providers/openrouter/sync-providers.ts | 3 + .../ai-gateway/supported-fim-models.test.ts | 104 ++++++++++++++++++ .../lib/ai-gateway/supported-fim-models.ts | 90 +++++++++++++++ 7 files changed, 245 insertions(+), 22 deletions(-) create mode 100644 apps/web/src/lib/ai-gateway/supported-fim-models.test.ts create mode 100644 apps/web/src/lib/ai-gateway/supported-fim-models.ts diff --git a/apps/web/src/app/api/edit/completions/route.test.ts b/apps/web/src/app/api/edit/completions/route.test.ts index 0e74f0cb61..fdebfb8873 100644 --- a/apps/web/src/app/api/edit/completions/route.test.ts +++ b/apps/web/src/app/api/edit/completions/route.test.ts @@ -192,6 +192,22 @@ describe('POST /api/edit/completions', () => { expect(mockedFetch).not.toHaveBeenCalled(); }); + it.each(['mercury-edit-2', 'inception/mercury-edit-2:free', 'inception/mercury-edit-latest'])( + 'rejects unknown edit model or alias %s', + async model => { + setOrganizationAuth(); + + const { POST } = await import('./route'); + const response = await POST(makeRequest({ ...makeValidRequestBody(), model }) as never); + + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ + error_type: ProxyErrorType.unsupported_edit_model, + }); + expect(mockedFetch).not.toHaveBeenCalled(); + } + ); + it('rejects requests with non-positive max_tokens', async () => { setOrganizationAuth(); diff --git a/apps/web/src/app/api/edit/completions/route.ts b/apps/web/src/app/api/edit/completions/route.ts index ec2d99a8d4..dbad05c866 100644 --- a/apps/web/src/app/api/edit/completions/route.ts +++ b/apps/web/src/app/api/edit/completions/route.ts @@ -30,6 +30,7 @@ import { sentryLogger } from '@/lib/utils.server'; import { getBYOKforOrganization, getBYOKforUser } from '@/lib/ai-gateway/byok'; import type { UserByokProviderId } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; import { resolveOrganizationMemberModelDecision } from '@/lib/organizations/effective-model-access.server'; +import { findSupportedFimModel } from '@/lib/ai-gateway/supported-fim-models'; // Inception's edit endpoint mirrors a chat completion shape but is hosted at // a separate path. It accepts a single `role: "user"` message; the system prompt @@ -44,13 +45,12 @@ function resolveEditProvider(model: string): { provider: EditProvider; upstreamModel: string; } | null { - if (model.startsWith('inception/')) { - return { - provider: 'inception', - upstreamModel: model.slice('inception/'.length), - }; - } - return null; + const supportedModel = findSupportedFimModel(model); + if (supportedModel?.provider !== 'inception') return null; + return { + provider: supportedModel.provider, + upstreamModel: supportedModel.upstreamModel, + }; } function getSystemApiKey(provider: EditProvider): string | null { diff --git a/apps/web/src/app/api/fim/completions/route.test.ts b/apps/web/src/app/api/fim/completions/route.test.ts index 37d62bb301..cfa90d1f5e 100644 --- a/apps/web/src/app/api/fim/completions/route.test.ts +++ b/apps/web/src/app/api/fim/completions/route.test.ts @@ -187,6 +187,24 @@ describe('POST /api/fim/completions', () => { expect(mockedFetch).not.toHaveBeenCalled(); }); + it.each([ + 'codestral-2508', + 'mistralai/codestral-2508:free', + 'mistralai/codestral-latest', + 'inception/mercury-edit-latest', + ])('rejects unknown FIM model or alias %s', async model => { + setOrganizationAuth(1000); + + const { POST } = await import('./route'); + const response = await POST(makeRequest(model) as never); + + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ + error_type: ProxyErrorType.unsupported_fim_model, + }); + expect(mockedFetch).not.toHaveBeenCalled(); + }); + it('continues to allow Inception BYOK when the promotion is disabled', async () => { mockInceptionPromoRunning = false; setOrganizationAuth(0); diff --git a/apps/web/src/app/api/fim/completions/route.ts b/apps/web/src/app/api/fim/completions/route.ts index 3475343998..e8383ae9c2 100644 --- a/apps/web/src/app/api/fim/completions/route.ts +++ b/apps/web/src/app/api/fim/completions/route.ts @@ -29,6 +29,7 @@ import { sentryLogger } from '@/lib/utils.server'; import { getBYOKforOrganization, getBYOKforUser } from '@/lib/ai-gateway/byok'; import type { UserByokProviderId } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; import { resolveOrganizationMemberModelDecision } from '@/lib/organizations/effective-model-access.server'; +import { findSupportedFimModel, type FimProvider } from '@/lib/ai-gateway/supported-fim-models'; // Mistral exposes FIM on two separate, key-incompatible endpoints: // - https://api.mistral.ai (La Plateforme, paid tier keys) @@ -41,25 +42,16 @@ const MISTRAL_CODESTRAL_FIM_URL = 'https://codestral.mistral.ai/v1/fim/completio const INCEPTION_FIM_URL = 'https://api.inceptionlabs.ai/v1/fim/completions'; const FIM_MAX_TOKENS_LIMIT = 1000; -type FimProvider = 'mistral' | 'inception'; - function resolveFimProvider(model: string): { provider: FimProvider; upstreamModel: string; } | null { - if (model.startsWith('mistralai/')) { - return { - provider: 'mistral', - upstreamModel: model.slice('mistralai/'.length), - }; - } - if (model.startsWith('inception/')) { - return { - provider: 'inception', - upstreamModel: model.slice('inception/'.length), - }; - } - return null; + const supportedModel = findSupportedFimModel(model); + if (!supportedModel) return null; + return { + provider: supportedModel.provider, + upstreamModel: supportedModel.upstreamModel, + }; } function resolveFimUpstreamUrl(provider: FimProvider, usingCodestralByok: boolean): string { 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 b4db833f6e..f7fa2d70a3 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 @@ -47,6 +47,7 @@ import { } 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'; +import { injectSupportedFimModels } from '@/lib/ai-gateway/supported-fim-models'; /** * Advisory lock key hashed from a stable identifier. Serializes concurrent @@ -333,6 +334,8 @@ async function syncProviders( } } + injectSupportedFimModels(providerModelData); + applyFreeEndpointDataPolicy({ providerModelData, openRouterFreeEndpoints, diff --git a/apps/web/src/lib/ai-gateway/supported-fim-models.test.ts b/apps/web/src/lib/ai-gateway/supported-fim-models.test.ts new file mode 100644 index 0000000000..897073dab9 --- /dev/null +++ b/apps/web/src/lib/ai-gateway/supported-fim-models.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from '@jest/globals'; +import type { + NormalizedOpenRouterResponse, + OpenRouterProvider, +} from '@/lib/ai-gateway/providers/openrouter/openrouter-types'; +import { + CODESTRAL_FIM_MODEL_ID, + findSupportedFimModel, + injectSupportedFimModels, + MERCURY_EDIT_FIM_MODEL_ID, +} from '@/lib/ai-gateway/supported-fim-models'; +import { buildModelIdToProviderSlugsIndex } from '@/lib/ai-gateway/providers/openrouter/models-by-provider-index.server'; +import { createAllowPredicateFromProviderAllowList } from '@/lib/model-allow.server'; + +function provider(slug: string): OpenRouterProvider { + return { + name: slug, + displayName: slug, + slug, + dataPolicy: { + training: false, + retainsPrompts: false, + canPublish: false, + }, + }; +} + +function makeProviderModelData() { + return ['mistral', 'inception', 'other'].map(slug => ({ + provider: provider(slug), + models: [], + })); +} + +function makeSnapshot(): NormalizedOpenRouterResponse { + const providerModelData = makeProviderModelData(); + injectSupportedFimModels(providerModelData); + return { + providers: providerModelData.map(({ provider: item, models }) => ({ + ...item, + models, + })), + total_providers: providerModelData.length, + total_models: providerModelData.reduce((total, item) => total + item.models.length, 0), + generated_at: '2026-08-07T00:00:00.000Z', + }; +} + +describe('supported FIM models', () => { + it('injects known models into their direct providers for selector visibility', () => { + const snapshot = makeSnapshot(); + + expect(snapshot.providers.find(item => item.slug === 'mistral')?.models).toEqual([ + expect.objectContaining({ slug: CODESTRAL_FIM_MODEL_ID }), + ]); + expect(snapshot.providers.find(item => item.slug === 'inception')?.models).toEqual([ + expect.objectContaining({ slug: MERCURY_EDIT_FIM_MODEL_ID }), + ]); + expect(snapshot.providers.find(item => item.slug === 'other')?.models).toEqual([]); + }); + + it('does not duplicate a model already present under its direct provider', () => { + const providerModelData = makeProviderModelData(); + + injectSupportedFimModels(providerModelData); + injectSupportedFimModels(providerModelData); + + expect(providerModelData.flatMap(item => item.models)).toHaveLength(2); + }); + + it('indexes the exact provider associations used by Enterprise restrictions', () => { + const index = buildModelIdToProviderSlugsIndex(makeSnapshot()); + + expect(index.get(CODESTRAL_FIM_MODEL_ID)).toEqual(new Set(['mistral'])); + expect(index.get(MERCURY_EDIT_FIM_MODEL_ID)).toEqual(new Set(['inception'])); + expect(index.has('codestral-2508')).toBe(false); + expect(index.has('mistralai/codestral-2508:free')).toBe(false); + expect(index.has('inception/mercury-edit-latest')).toBe(false); + }); + + it('applies provider allow lists to each FIM model using its direct provider', async () => { + const index = buildModelIdToProviderSlugsIndex(makeSnapshot()); + const providerLookup = async (modelId: string) => index.get(modelId) ?? new Set(); + const mistralOnly = createAllowPredicateFromProviderAllowList([], ['mistral'], providerLookup); + const inceptionOnly = createAllowPredicateFromProviderAllowList( + [], + ['inception'], + providerLookup + ); + + await expect(mistralOnly(CODESTRAL_FIM_MODEL_ID)).resolves.toBe(true); + await expect(mistralOnly(MERCURY_EDIT_FIM_MODEL_ID)).resolves.toBe(false); + await expect(inceptionOnly(CODESTRAL_FIM_MODEL_ID)).resolves.toBe(false); + await expect(inceptionOnly(MERCURY_EDIT_FIM_MODEL_ID)).resolves.toBe(true); + }); + + it('only resolves exact supported ids and never invents aliases', () => { + expect(findSupportedFimModel(CODESTRAL_FIM_MODEL_ID)?.provider).toBe('mistral'); + expect(findSupportedFimModel(MERCURY_EDIT_FIM_MODEL_ID)?.provider).toBe('inception'); + expect(findSupportedFimModel('codestral-2508')).toBeUndefined(); + expect(findSupportedFimModel('mistralai/codestral-2508:free')).toBeUndefined(); + expect(findSupportedFimModel('inception/mercury-edit-latest')).toBeUndefined(); + }); +}); diff --git a/apps/web/src/lib/ai-gateway/supported-fim-models.ts b/apps/web/src/lib/ai-gateway/supported-fim-models.ts new file mode 100644 index 0000000000..761c5393b7 --- /dev/null +++ b/apps/web/src/lib/ai-gateway/supported-fim-models.ts @@ -0,0 +1,90 @@ +import type { + OpenRouterModel, + OpenRouterProvider, +} from '@/lib/ai-gateway/providers/openrouter/openrouter-types'; +import { INCEPTION_PROMO_MODEL } from '@/lib/constants'; + +export type FimProvider = 'mistral' | 'inception'; + +type SupportedFimModel = { + id: string; + upstreamModel: string; + provider: FimProvider; + snapshotModel: OpenRouterModel; +}; + +type ProviderModels = Array<{ + provider: Pick; + models: OpenRouterModel[]; +}>; + +export const CODESTRAL_FIM_MODEL_ID = 'mistralai/codestral-2508'; +export const MERCURY_EDIT_FIM_MODEL_ID = INCEPTION_PROMO_MODEL; + +/** Exact public IDs accepted by Kilo's FIM and edit routes. Aliases are intentionally omitted. */ +export const SUPPORTED_FIM_MODELS = [ + { + id: CODESTRAL_FIM_MODEL_ID, + upstreamModel: 'codestral-2508', + provider: 'mistral', + snapshotModel: { + slug: CODESTRAL_FIM_MODEL_ID, + name: 'Mistral: Codestral 2508', + author: 'Mistral', + description: 'Mistral code model for fill-in-the-middle completions.', + context_length: 256_000, + input_modalities: ['text'], + output_modalities: ['text'], + group: 'Mistral', + updated_at: '2025-08-01T00:00:00.000Z', + endpoint: { + provider_display_name: 'Mistral', + is_free: false, + pricing: { + prompt: '0.000000300000', + completion: '0.000000900000', + }, + }, + }, + }, + { + id: MERCURY_EDIT_FIM_MODEL_ID, + upstreamModel: 'mercury-edit-2', + provider: 'inception', + snapshotModel: { + slug: MERCURY_EDIT_FIM_MODEL_ID, + name: 'Inception: Mercury Edit 2', + author: 'Inception', + description: 'Inception diffusion model for autocomplete and next-edit prediction.', + context_length: 128_000, + input_modalities: ['text'], + output_modalities: ['text'], + group: 'Inception', + updated_at: '2026-07-29T00:00:00.000Z', + endpoint: { + provider_display_name: 'Inception', + is_free: false, + pricing: { + prompt: '0.000000250000', + completion: '0.000000750000', + }, + }, + }, + }, +] as const satisfies ReadonlyArray; + +export function findSupportedFimModel(modelId: string): SupportedFimModel | undefined { + return SUPPORTED_FIM_MODELS.find(model => model.id === modelId); +} + +export function injectSupportedFimModels(providerModelData: ProviderModels): void { + for (const supportedModel of SUPPORTED_FIM_MODELS) { + const providerData = providerModelData.find( + data => data.provider.slug === supportedModel.provider + ); + if (!providerData) continue; + if (providerData.models.some(model => model.slug === supportedModel.id)) continue; + + providerData.models.unshift({ ...supportedModel.snapshotModel }); + } +} From f15cc69f183482a7ca3210e8b445ddf8f5c182e9 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 7 Aug 2026 12:19:30 +0200 Subject: [PATCH 2/2] fix(ai-gateway): harden FIM snapshot injection --- .../ai-gateway/supported-fim-models.test.ts | 54 +++++++++++++++++-- .../lib/ai-gateway/supported-fim-models.ts | 23 +++++++- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/supported-fim-models.test.ts b/apps/web/src/lib/ai-gateway/supported-fim-models.test.ts index 897073dab9..89893bc8e8 100644 --- a/apps/web/src/lib/ai-gateway/supported-fim-models.test.ts +++ b/apps/web/src/lib/ai-gateway/supported-fim-models.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import type { NormalizedOpenRouterResponse, + OpenRouterModel, OpenRouterProvider, } from '@/lib/ai-gateway/providers/openrouter/openrouter-types'; import { @@ -26,10 +27,12 @@ function provider(slug: string): OpenRouterProvider { } function makeProviderModelData() { - return ['mistral', 'inception', 'other'].map(slug => ({ - provider: provider(slug), - models: [], - })); + return ['mistral', 'inception', 'other'].map( + (slug): { provider: OpenRouterProvider; models: OpenRouterModel[] } => ({ + provider: provider(slug), + models: [], + }) + ); } function makeSnapshot(): NormalizedOpenRouterResponse { @@ -68,6 +71,49 @@ describe('supported FIM models', () => { expect(providerModelData.flatMap(item => item.models)).toHaveLength(2); }); + it('warns when a direct provider is missing from the upstream snapshot', () => { + const providerModelData = makeProviderModelData().filter( + item => item.provider.slug !== 'inception' + ); + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + try { + injectSupportedFimModels(providerModelData); + + expect(warn).toHaveBeenCalledWith( + '[injectSupportedFimModels] Missing provider %s for supported FIM model %s', + 'inception', + MERCURY_EDIT_FIM_MODEL_ID + ); + } finally { + warn.mockRestore(); + } + }); + + it('does not share mutable snapshot metadata with the supported-model catalog', () => { + const providerModelData = makeProviderModelData(); + injectSupportedFimModels(providerModelData); + + const injectedModel = providerModelData + .find(item => item.provider.slug === 'mistral') + ?.models.find(model => model.slug === CODESTRAL_FIM_MODEL_ID); + const catalogModel = findSupportedFimModel(CODESTRAL_FIM_MODEL_ID)?.snapshotModel; + if (!injectedModel?.endpoint || !catalogModel?.endpoint) { + throw new Error('Expected Codestral snapshot metadata'); + } + + expect(injectedModel.endpoint).not.toBe(catalogModel.endpoint); + expect(injectedModel.endpoint.pricing).not.toBe(catalogModel.endpoint.pricing); + expect(injectedModel.input_modalities).not.toBe(catalogModel.input_modalities); + expect(injectedModel.output_modalities).not.toBe(catalogModel.output_modalities); + + injectedModel.endpoint.data_policy = { training: true, retainsPrompts: true }; + injectedModel.endpoint.pricing.prompt = 'mutated'; + + expect(catalogModel.endpoint.data_policy).toBeUndefined(); + expect(catalogModel.endpoint.pricing.prompt).toBe('0.000000300000'); + }); + it('indexes the exact provider associations used by Enterprise restrictions', () => { const index = buildModelIdToProviderSlugsIndex(makeSnapshot()); diff --git a/apps/web/src/lib/ai-gateway/supported-fim-models.ts b/apps/web/src/lib/ai-gateway/supported-fim-models.ts index 761c5393b7..ce6723bc6d 100644 --- a/apps/web/src/lib/ai-gateway/supported-fim-models.ts +++ b/apps/web/src/lib/ai-gateway/supported-fim-models.ts @@ -82,9 +82,28 @@ export function injectSupportedFimModels(providerModelData: ProviderModels): voi const providerData = providerModelData.find( data => data.provider.slug === supportedModel.provider ); - if (!providerData) continue; + if (!providerData) { + console.warn( + '[injectSupportedFimModels] Missing provider %s for supported FIM model %s', + supportedModel.provider, + supportedModel.id + ); + continue; + } if (providerData.models.some(model => model.slug === supportedModel.id)) continue; - providerData.models.unshift({ ...supportedModel.snapshotModel }); + const endpoint: OpenRouterModel['endpoint'] = supportedModel.snapshotModel.endpoint; + providerData.models.unshift({ + ...supportedModel.snapshotModel, + input_modalities: [...supportedModel.snapshotModel.input_modalities], + output_modalities: [...supportedModel.snapshotModel.output_modalities], + endpoint: endpoint + ? { + ...endpoint, + pricing: { ...endpoint.pricing }, + data_policy: endpoint.data_policy ? { ...endpoint.data_policy } : endpoint.data_policy, + } + : null, + }); } }