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
27 changes: 14 additions & 13 deletions apps/web/src/app/api/openrouter/[...path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,21 +784,22 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () =>
expect(mockedUpstreamRequest).not.toHaveBeenCalled();
});

it.each(['google/gemma-4-26b-a4b-it:free', 'google/gemma-4-31b-it:free'])(
'rejects the unavailable free model %s before upstream',
async modelId => {
mockedCheckFreeModelRateLimit.mockResolvedValue({ allowed: true, requestCount: 0 });
it.each([
'google/gemma-4-26b-a4b-it:free',
'google/gemma-4-31b-it:free',
'thinkingmachines/inkling:free',
])('rejects the unavailable free model %s before upstream', async modelId => {
mockedCheckFreeModelRateLimit.mockResolvedValue({ allowed: true, requestCount: 0 });

const { POST } = await import('./route');
const response = await POST(makeRequest(makeBody(modelId)) as never);
const { POST } = await import('./route');
const response = await POST(makeRequest(makeBody(modelId)) as never);

expect(response.status).toBe(404);
expect(await response.json()).toMatchObject({
error_type: 'unavailable_model',
});
expect(mockedUpstreamRequest).not.toHaveBeenCalled();
}
);
expect(response.status).toBe(404);
expect(await response.json()).toMatchObject({
error_type: 'unavailable_model',
});
expect(mockedUpstreamRequest).not.toHaveBeenCalled();
});

it('rate limits rules-engine rate-limit actions before upstream', async () => {
mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('rate-limit'));
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/ai-gateway/unavailable-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { normalizeModelId } from '@/lib/ai-gateway/model-utils';
const unavailableModelIds: ReadonlySet<string> = new Set([
'google/gemma-4-26b-a4b-it:free', // usable through kilo-auto
'google/gemma-4-31b-it:free',
'thinkingmachines/inkling:free',
]);

export function isUnavailableModel(modelId: string): boolean {
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/tests/openrouter-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,18 @@ describe('GET /api/openrouter/models', () => {
expect(Array.isArray(responseData.data)).toBe(true);
});

test('excludes unavailable free Gemma models advertised upstream while retaining paid Gemma', async () => {
test('excludes unavailable free models advertised upstream while retaining paid Gemma', async () => {
const original = mockOpenRouterModels.data.find(model => model.id === 'some-other-model');
if (!original) throw new Error('Expected catalog fixture');
const upstream = {
data: [
...mockOpenRouterModels.data,
{ ...original, id: 'google/gemma-4-31b-it', name: 'Google: Gemma 4 31B IT' },
...['google/gemma-4-26b-a4b-it:free', 'google/gemma-4-31b-it:free'].map(id => ({
...[
'google/gemma-4-26b-a4b-it:free',
'google/gemma-4-31b-it:free',
'thinkingmachines/inkling:free',
].map(id => ({
...original,
id,
name: id,
Expand All @@ -310,6 +314,7 @@ describe('GET /api/openrouter/models', () => {
expect(response.status).toBe(200);
expect(modelIds).not.toContain('google/gemma-4-26b-a4b-it:free');
expect(modelIds).not.toContain('google/gemma-4-31b-it:free');
expect(modelIds).not.toContain('thinkingmachines/inkling:free');
expect(modelIds).toContain('google/gemma-4-31b-it');
});

Expand Down