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
5 changes: 5 additions & 0 deletions .changeset/shaggy-pandas-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Recognize the support_efforts and default_effort fields when importing a custom registry, so thinking effort levels are available for those models.
22 changes: 20 additions & 2 deletions packages/oauth/src/custom-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface CustomRegistryModelEntry {
input?: readonly string[];
output?: readonly string[];
};
readonly support_efforts?: readonly string[];
readonly default_effort?: string;
}

export interface CustomRegistryProviderEntry {
Expand Down Expand Up @@ -102,6 +104,8 @@ function toModelEntry(value: unknown): CustomRegistryModelEntry | undefined {
tool_call?: boolean;
reasoning?: boolean;
modalities?: { input?: readonly string[]; output?: readonly string[] };
support_efforts?: readonly string[];
default_effort?: string;
} = { id };

const name = value['name'];
Expand All @@ -126,6 +130,13 @@ function toModelEntry(value: unknown): CustomRegistryModelEntry | undefined {
if (typeof value['tool_call'] === 'boolean') entry.tool_call = value['tool_call'];
if (typeof value['reasoning'] === 'boolean') entry.reasoning = value['reasoning'];

const supportEfforts = toStringArrayOrUndefined(value['support_efforts']);
if (supportEfforts !== undefined) entry.support_efforts = supportEfforts;
const defaultEffort = value['default_effort'];
if (typeof defaultEffort === 'string' && defaultEffort.length > 0) {
entry.default_effort = defaultEffort;
}

const modalities = value['modalities'];
if (isRecord(modalities)) {
const input = toStringArrayOrUndefined(modalities['input']);
Expand Down Expand Up @@ -238,7 +249,11 @@ export async function fetchCustomRegistry(
export function capabilitiesFromCustomEntry(model: CustomRegistryModelEntry): string[] {
const caps = new Set<string>();
if (model.tool_call === true) caps.add('tool_use');
if (model.reasoning === true) caps.add('thinking');
// Declaring concrete effort levels implies thinking support even when the
// legacy `reasoning` boolean is absent.
if (model.reasoning === true || (model.support_efforts?.length ?? 0) > 0) {
caps.add('thinking');
}
if (model.modalities?.input?.includes('image') === true) caps.add('image_in');
if (model.modalities?.input?.includes('video') === true) caps.add('video_in');
if (model.modalities?.output?.includes('image') === true) caps.add('image_out');
Expand All @@ -250,7 +265,8 @@ function hasRichCapabilityHints(model: CustomRegistryModelEntry): boolean {
return (
typeof model.tool_call === 'boolean' ||
typeof model.reasoning === 'boolean' ||
model.modalities !== undefined
model.modalities !== undefined ||
model.support_efforts !== undefined
);
}

Expand Down Expand Up @@ -323,6 +339,8 @@ export function applyCustomRegistryProvider(
maxContextSize,
capabilities,
displayName,
...(model.support_efforts !== undefined ? { supportEfforts: model.support_efforts } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark effort-only registry models as thinking-capable

When a registry model supplies support_efforts/default_effort but does not also set reasoning: true, this stores the effort levels while capabilities remains resolved without thinking because resolveCapabilities only considers the reasoning flag. Downstream thinking defaults check capabilities first, so these imported models are treated as unsupported/off despite advertising concrete efforts. Add the thinking capability when accepted support_efforts is non-empty, or ignore the effort fields unless reasoning is true.

Useful? React with 👍 / 👎.

...(model.default_effort !== undefined ? { defaultEffort: model.default_effort } : {}),
};
existingModels[aliasKey] = mergeRefreshedModelAlias(
existing,
Expand Down
2 changes: 2 additions & 0 deletions packages/oauth/src/model-alias-merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const CUSTOM_REGISTRY_MODEL_FIELDS: ReadonlySet<string> = new Set([
'maxContextSize',
'capabilities',
'displayName',
'supportEfforts',
'defaultEffort',
]);

function cloneOverrides(
Expand Down
119 changes: 115 additions & 4 deletions packages/oauth/test/custom-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ describe('fetchCustomRegistry', () => {
);
});

it('parses support_efforts and default_effort from model entries', async () => {
const body = makeKokubResponseBody();
body['registry_chat-completions']!.models['gpt-5.5'] = {
id: 'gpt-5.5',
name: 'GPT 5.5',
support_efforts: ['low', 'high', 'max'],
default_effort: 'high',
};
const fetchMock = vi.fn(async () => makeJsonResponse(body));

const result = await fetchCustomRegistry(
KOKUB_SOURCE,
fetchMock as unknown as typeof fetch,
);

expect(result['registry_chat-completions']?.models['gpt-5.5']).toEqual({
id: 'gpt-5.5',
name: 'GPT 5.5',
support_efforts: ['low', 'high', 'max'],
default_effort: 'high',
});
});

it('omits the Authorization header when the apiKey is empty', async () => {
const fetchMock = vi.fn(async () => makeJsonResponse(makeKokubResponseBody()));

Expand Down Expand Up @@ -328,8 +351,7 @@ describe('applyCustomRegistryProvider', () => {
provider: 'registry_chat-completions',
model: 'gpt-5.5',
maxContextSize: 131072,
supportEfforts: ['low', 'high', 'max'],
defaultEffort: 'high',
betaApi: true,
} as Record<string, unknown>,
},
};
Expand All @@ -349,11 +371,100 @@ describe('applyCustomRegistryProvider', () => {
);

const alias = config.models?.['registry_chat-completions/gpt-5.5'];
expect(alias?.['supportEfforts']).toEqual(['low', 'high', 'max']);
expect(alias?.['defaultEffort']).toBe('high');
expect(alias?.['betaApi']).toBe(true);
// Upstream-owned fields are still refreshed.
expect(alias?.['displayName']).toBe('GPT 5.5');
});

it('maps support_efforts / default_effort onto the model alias', () => {
const config: ManagedKimiConfigShape = { providers: {} };
const entry: CustomRegistryProviderEntry = {
id: 'rich',
name: 'Rich Provider',
api: 'https://rich.example/v1',
type: 'openai',
models: {
'rich-thinker': {
id: 'rich-thinker',
name: 'Rich Thinker',
reasoning: true,
support_efforts: ['low', 'high', 'max'],
default_effort: 'high',
},
},
};

applyCustomRegistryProvider(config, entry, {
kind: 'apiJson',
url: 'https://rich.example/api.json',
apiKey: 'sk-rich',
});

const alias = config.models?.['rich/rich-thinker'] as Record<string, unknown>;
expect(alias['supportEfforts']).toEqual(['low', 'high', 'max']);
expect(alias['defaultEffort']).toBe('high');
});

it('treats support_efforts as a thinking capability hint without reasoning: true', () => {
const config: ManagedKimiConfigShape = { providers: {} };
const entry: CustomRegistryProviderEntry = {
id: 'rich',
name: 'Rich Provider',
api: 'https://rich.example/v1',
type: 'openai',
models: {
'rich-effort-only': {
id: 'rich-effort-only',
name: 'Rich Effort Only',
support_efforts: ['low', 'high', 'max'],
default_effort: 'high',
},
},
};

applyCustomRegistryProvider(config, entry, {
kind: 'apiJson',
url: 'https://rich.example/api.json',
apiKey: 'sk-rich',
});

const alias = config.models?.['rich/rich-effort-only'] as Record<string, unknown>;
expect(alias['capabilities']).toContain('thinking');
expect(alias['supportEfforts']).toEqual(['low', 'high', 'max']);
});

it('drops stale effort fields when a refresh no longer declares them', () => {
const config: ManagedKimiConfigShape = {
providers: {},
models: {
'registry_chat-completions/gpt-5.5': {
provider: 'registry_chat-completions',
model: 'gpt-5.5',
maxContextSize: 131072,
supportEfforts: ['low', 'high', 'max'],
defaultEffort: 'high',
} as Record<string, unknown>,
},
};

applyCustomRegistryProvider(
config,
{
id: 'registry_chat-completions',
name: 'Sample Registry (chat completions)',
api: 'https://registry.example.test/v1',
type: 'openai',
models: {
'gpt-5.5': { id: 'gpt-5.5', name: 'GPT 5.5' },
},
},
KOKUB_SOURCE,
);

const alias = config.models?.['registry_chat-completions/gpt-5.5'];
expect(alias?.['supportEfforts']).toBeUndefined();
expect(alias?.['defaultEffort']).toBeUndefined();
});
});

describe('removeCustomRegistryProvider', () => {
Expand Down
28 changes: 26 additions & 2 deletions packages/oauth/test/model-alias-merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,37 @@ describe('mergeRefreshedModelAlias', () => {
expect(merged.supportEfforts).toBeUndefined();
});

it('keeps custom-registry supportEfforts as user data', () => {
it('refreshes custom-registry supportEfforts from upstream', () => {
const merged = mergeRefreshedModelAlias(
{
provider: 'registry',
model: 'gpt-5.5',
maxContextSize: 131072,
supportEfforts: ['low', 'high'],
},
{
provider: 'registry',
model: 'gpt-5.5',
maxContextSize: 131072,
supportEfforts: ['low', 'high', 'max'],
defaultEffort: 'high',
},
CUSTOM_REGISTRY_MODEL_FIELDS,
);

expect(merged.supportEfforts).toEqual(['low', 'high', 'max']);
expect(merged.defaultEffort).toBe('high');
});

it('drops custom-registry effort fields when upstream stops declaring them', () => {
const merged = mergeRefreshedModelAlias(
{
provider: 'registry',
model: 'gpt-5.5',
maxContextSize: 131072,
supportEfforts: ['low', 'high'],
defaultEffort: 'high',
},
{
provider: 'registry',
model: 'gpt-5.5',
Expand All @@ -64,6 +87,7 @@ describe('mergeRefreshedModelAlias', () => {
CUSTOM_REGISTRY_MODEL_FIELDS,
);

expect(merged.supportEfforts).toEqual(['low', 'high']);
expect(merged.supportEfforts).toBeUndefined();
expect(merged.defaultEffort).toBeUndefined();
});
});
Loading