Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/web/src/app/api/edit/completions/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/app/api/edit/completions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/app/api/fim/completions/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 7 additions & 15 deletions apps/web/src/app/api/fim/completions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -333,6 +334,8 @@ async function syncProviders(
}
}

injectSupportedFimModels(providerModelData);

applyFreeEndpointDataPolicy({
providerModelData,
openRouterFreeEndpoints,
Expand Down
150 changes: 150 additions & 0 deletions apps/web/src/lib/ai-gateway/supported-fim-models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it } from '@jest/globals';
import type {
NormalizedOpenRouterResponse,
OpenRouterModel,
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: OpenRouterProvider; models: OpenRouterModel[] } => ({
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('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());

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<string>();
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();
});
});
109 changes: 109 additions & 0 deletions apps/web/src/lib/ai-gateway/supported-fim-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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<OpenRouterProvider, 'slug'>;
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<SupportedFimModel>;

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) {
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;

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,
});
}
}
Loading