Skip to content
Merged
26 changes: 26 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/direct-byok/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jest.mock('./direct-byok-definitions', () => ({
flags: ['reasoning'],
context_length: 4096,
max_completion_tokens: 1024,
variants: {
high: { reasoning: { enabled: true, effort: 'high' } },
},
},
{
id: 'non-reasoning-model',
Expand Down Expand Up @@ -113,5 +116,28 @@ describe('getDirectByokModel', () => {
]);
expect(models[1].supported_parameters).toEqual(['max_tokens', 'temperature', 'tools']);
expect(models.flatMap(model => model.supported_parameters)).not.toContain('include_reasoning');
expect(models[0].opencode.variants).toEqual({
high: { reasoning: { enabled: true, effort: 'high' } },
});
});

test('falls back to model-name variants when synced variants are unavailable', async () => {
const { getDirectByokModelsForUser } = await loadDirectByokModule();
const { getBYOKforUser } = await import('@/lib/ai-gateway/byok');
const { getModelVariants } = await import('@/lib/ai-gateway/providers/model-settings');
const fallback = { thinking: { reasoning: { enabled: true, effort: 'high' as const } } };
jest
.mocked(getBYOKforUser)
.mockResolvedValueOnce([{ providerId: 'chutes-byok', decryptedAPIKey: 'test-key' }]);
jest.mocked(getModelVariants).mockReturnValue(fallback);

const models = await getDirectByokModelsForUser('user-id');

expect(models[0].opencode.variants).toEqual({
high: { reasoning: { enabled: true, effort: 'high' } },
});
expect(models[1].opencode.variants).toBe(fallback);
expect(getModelVariants).toHaveBeenCalledTimes(1);
expect(getModelVariants).toHaveBeenCalledWith('chutes-byok/non-reasoning-model');
});
});
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/providers/direct-byok/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function convertModel(
hasUserByokAvailable: true,
opencode: {
ai_sdk_provider: getAiSdkProvider(id, provider.id) ?? provider.default_ai_sdk_provider,
variants: getModelVariants(id),
variants: model.variants ?? getModelVariants(id),
} satisfies OpenCodeSettings,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type { DirectUserByokInferenceProviderId } from '@/lib/ai-gateway/provide
import { createCachedFetch } from '@/lib/cached-fetch';
import { redisClient } from '@/lib/redis';
import { directByokModelsRedisKey } from '@/lib/redis-keys';
import type { OpenCodeVariant } from '@kilocode/db/schema-types';

type CachedEnhancedModelListOptions = {
providerId: DirectUserByokInferenceProviderId;
Expand Down Expand Up @@ -36,9 +35,9 @@ function enhanceDirectByokModelList({
}: {
recommendedModels: ReadonlyArray<DirectByokModel>;
remainingModels: ReadonlyArray<DirectByokModel>;
variants?: Record<string, OpenCodeVariant>;
}): ReadonlyArray<DirectByokModel> {
const seenIds = new Set<string>();
const syncedModels = new Map(remainingModels.map(model => [model.id, model]));
return [...recommendedModels, ...remainingModels]
.filter(model => (seenIds.has(model.id) ? false : (seenIds.add(model.id), true)))
.map(model => {
Expand All @@ -47,6 +46,7 @@ function enhanceDirectByokModelList({
return {
...model,
flags: flags.size > 0 ? [...flags] : undefined,
variants: model.variants ?? syncedModels.get(model.id)?.variants,
};
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,40 +59,49 @@ describe('parseOpenAICompatibleProviderModels', () => {

describe('parseModelsDevProviderModels', () => {
test('excludes deprecated and non-text-output models while retaining other statuses', () => {
const models = parseModelsDevProviderModels({
models: {
stable: {
id: 'stable',
name: 'provider/stable',
reasoning: true,
limit: { context: 128_000, output: 32_000 },
modalities: { input: ['text', 'image'], output: ['text'] },
},
alpha: {
id: 'alpha',
status: 'alpha',
},
beta: {
id: 'beta',
status: 'beta',
reasoning: false,
},
unknownStatus: {
id: 'unknown-status',
status: 'active',
},
deprecated: {
id: 'mimo-v2-omni',
name: 'MiMo V2 Omni',
status: 'deprecated',
},
imageOnly: {
id: 'wan2.7-image',
name: 'Wan2.7 Image',
modalities: { input: ['text'], output: ['image'] },
const models = parseModelsDevProviderModels(
{
models: {
stable: {
id: 'stable',
name: 'provider/stable',
reasoning: true,
reasoning_options: [
{ type: 'toggle' },
{ type: 'effort', values: ['high', 'max', 'default', null] },
],
limit: { context: 128_000, output: 32_000 },
modalities: { input: ['text', 'image'], output: ['text'] },
},
alpha: {
id: 'alpha',
status: 'alpha',
reasoning: true,
reasoning_options: [{ type: 'toggle' }],
},
beta: {
id: 'beta',
status: 'beta',
reasoning: false,
},
unknownStatus: {
id: 'unknown-status',
status: 'active',
},
deprecated: {
id: 'mimo-v2-omni',
name: 'MiMo V2 Omni',
status: 'deprecated',
},
imageOnly: {
id: 'wan2.7-image',
name: 'Wan2.7 Image',
modalities: { input: ['text'], output: ['image'] },
},
},
},
});
'alibaba-token-plan'
);

expect(models).toEqual([
{
Expand All @@ -102,14 +111,23 @@ describe('parseModelsDevProviderModels', () => {
max_completion_tokens: 32_000,
input_modalities: ['text', 'image'],
flags: ['reasoning'],
variants: {
none: { reasoning: { enabled: false, effort: 'none' } },
high: { reasoning: { enabled: true, effort: 'high' } },
max: { reasoning: { enabled: true, effort: 'max' } },
},
},
{
id: 'alpha',
name: undefined,
context_length: undefined,
max_completion_tokens: undefined,
input_modalities: undefined,
flags: undefined,
flags: ['reasoning'],
variants: {
instant: { reasoning: { enabled: false, effort: 'none' } },
thinking: { reasoning: { enabled: true, effort: 'high' } },
},
},
{
id: 'beta',
Expand All @@ -118,6 +136,7 @@ describe('parseModelsDevProviderModels', () => {
max_completion_tokens: undefined,
input_modalities: undefined,
flags: undefined,
variants: undefined,
},
{
id: 'unknown-status',
Expand All @@ -126,10 +145,73 @@ describe('parseModelsDevProviderModels', () => {
max_completion_tokens: undefined,
input_modalities: undefined,
flags: undefined,
variants: undefined,
},
]);
});

test('ignores reasoning option types that are not supported locally', () => {
const models = parseModelsDevProviderModels(
{
models: {
futureControl: {
id: 'future-control',
reasoning: true,
reasoning_options: [{ type: 'budget_tokens', min: 0, max: 32_000 }],
},
},
},
'alibaba-token-plan'
);

expect(models[0]).toMatchObject({
id: 'future-control',
flags: ['reasoning'],
});
expect(models[0].variants).toBeUndefined();
});

test('sets verbosity from effort for Anthropic-backed models', () => {
const models = parseModelsDevProviderModels(
{
models: {
qwen: {
id: 'qwen-test',
reasoning: true,
reasoning_options: [{ type: 'toggle' }, { type: 'effort', values: ['low', 'high'] }],
},
},
},
'opencode-go'
);

expect(models[0].variants).toEqual({
none: { reasoning: { enabled: false, effort: 'none' } },
low: { reasoning: { enabled: true, effort: 'low' }, verbosity: 'low' },
high: { reasoning: { enabled: true, effort: 'high' }, verbosity: 'high' },
});
});

test('sets verbosity on fallback variants for Anthropic-backed models', () => {
const models = parseModelsDevProviderModels(
{
models: {
qwen: {
id: 'qwen-test',
reasoning: true,
reasoning_options: [{ type: 'toggle' }, { type: 'budget_tokens', max: 32_000 }],
},
},
},
'opencode-go'
);

expect(models[0].variants).toEqual({
instant: { reasoning: { enabled: false, effort: 'none' } },
thinking: { reasoning: { enabled: true, effort: 'high' }, verbosity: 'high' },
});
});

test('excludes models missing from the provider model list', () => {
const models = parseModelsDevProviderModels(
{
Expand All @@ -138,6 +220,7 @@ describe('parseModelsDevProviderModels', () => {
removed: { id: 'removed', limit: { context: 64_000 } },
},
},
'alibaba-token-plan',
new Set(['available', 'provider-only'])
);

Expand Down
Loading