kimi-code/packages/oauth/test/managed-kimi-code.test.ts
liruifengv c070fbedde
feat: add model alias overrides (#1262)
* feat: add model alias overrides

Preserve user model overrides across provider catalog refreshes and resolve effective model metadata for runtime, TUI, protocol, and ACP consumers.

* fix: apply model display name overrides

Show overridden model display names in the footer, welcome panel, status output, and model switch confirmations.

* fix: pass through kimi effort when undeclared

Keep support_efforts authoritative when declared, but pass requested Kimi thinking effort through when the model does not declare support_efforts.

* fix: honor model overrides in effort commands

Use effective model metadata for /effort choices and for always_thinking clamping when resolving thinking effort.
2026-07-01 19:57:13 +08:00

1391 lines
43 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
applyManagedKimiCodeLogoutConfig,
applyManagedKimiCodeConfig,
clearManagedKimiCodeConfig,
fetchManagedKimiCodeModels,
KIMI_CODE_OAUTH_KEY,
KIMI_CODE_PROVIDER_NAME,
ManagedKimiCodeModelsAuthError,
provisionManagedKimiCodeConfig,
resolveKimiCodeLoginAuth,
resolveKimiCodeOAuthKey,
resolveKimiCodeOAuthRef,
resolveKimiCodeRuntimeAuth,
type ManagedKimiCodeModelInfo,
type ManagedKimiConfigShape,
} from '../src/managed-kimi-code';
import { OAuthUnauthorizedError } from '../src/errors';
function makeModelsResponse(): Response {
return new Response(
JSON.stringify({
data: [
{
id: 'kimi-for-coding',
context_length: 262144,
supports_reasoning: true,
supports_image_in: true,
supports_video_in: true,
display_name: 'Kimi for Coding',
},
{
id: 'kimi-k2.5',
context_length: 250000,
supports_reasoning: false,
supports_image_in: false,
supports_video_in: false,
supports_tool_use: false,
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
);
}
describe('provisionManagedKimiCodeConfig', () => {
it('keeps the legacy credential key for the default production environment', () => {
expect(
resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.kimi.com/',
baseUrl: 'https://api.kimi.com/coding/v1/',
}),
).toBe(KIMI_CODE_OAUTH_KEY);
});
it('scopes credential keys for non-default OAuth hosts and API base URLs', () => {
const devKey = resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.dev.example.test',
baseUrl: 'https://api.dev.example.test/coding/v1',
});
expect(devKey).not.toBe(KIMI_CODE_OAUTH_KEY);
expect(devKey).toMatch(/^oauth\/kimi-code-env-[a-f0-9]{16}$/);
expect(
resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.dev.example.test/',
baseUrl: 'https://api.dev.example.test/coding/v1/',
}),
).toBe(devKey);
});
it('derives a full OAuth ref whose key and persisted host stay in sync', () => {
// Default environment collapses to the legacy ref (no persisted host), so
// existing production credentials keep resolving to `kimi-code.json`.
expect(
resolveKimiCodeOAuthRef({
oauthHost: 'https://auth.kimi.com/',
baseUrl: 'https://api.kimi.com/coding/v1/',
}),
).toEqual({ storage: 'file', key: KIMI_CODE_OAUTH_KEY, oauthHost: undefined });
const defaultAuthCustomApiRef = resolveKimiCodeOAuthRef({
baseUrl: 'https://api.example.test/coding/v1',
});
expect(defaultAuthCustomApiRef).toEqual({
storage: 'file',
key: resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.kimi.com',
baseUrl: 'https://api.example.test/coding/v1',
}),
oauthHost: 'https://auth.kimi.com',
});
// A non-default environment yields a scoped key AND the normalized host,
// both derived from the same input — login and runtime cannot drift apart.
const devRef = resolveKimiCodeOAuthRef({
oauthHost: 'https://auth.dev.example.test/',
baseUrl: 'https://api.dev.example.test/coding/v1',
});
expect(devRef).toEqual({
storage: 'file',
key: resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.dev.example.test',
baseUrl: 'https://api.dev.example.test/coding/v1',
}),
oauthHost: 'https://auth.dev.example.test',
});
});
it('resolves runtime auth from environment overrides over persisted config', () => {
const configuredBaseUrl = 'https://api.configured.example.test/coding/v1';
const envBaseUrl = 'https://api.env.example.test/coding/v1/';
const envOauthHost = 'https://auth.env.example.test/';
const configuredOAuthRef = resolveKimiCodeOAuthRef({
baseUrl: configuredBaseUrl,
});
const auth = resolveKimiCodeRuntimeAuth({
configuredBaseUrl,
configuredOAuthRef,
env: {
KIMI_CODE_BASE_URL: envBaseUrl,
KIMI_CODE_OAUTH_HOST: envOauthHost,
},
});
expect(auth.baseUrl).toBe('https://api.env.example.test/coding/v1');
expect(auth.oauthRef).toEqual({
storage: 'file',
key: resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.env.example.test',
baseUrl: 'https://api.env.example.test/coding/v1',
}),
oauthHost: 'https://auth.env.example.test',
});
});
it('preserves a matching configured runtime OAuth ref when env is not overridden', () => {
const baseUrl = 'https://api.dev.example.test/coding/v1';
const configuredOAuthRef = {
storage: 'keyring' as const,
key: resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.dev.example.test',
baseUrl,
}),
oauthHost: 'https://auth.dev.example.test',
};
expect(
resolveKimiCodeRuntimeAuth({
configuredBaseUrl: baseUrl,
configuredOAuthRef,
env: {},
}),
).toEqual({
baseUrl,
oauthRef: configuredOAuthRef,
});
});
it('resolves login auth without reusing persisted refs under explicit or env overrides', () => {
const configuredBaseUrl = 'https://api.configured.example.test/coding/v1';
const configuredOAuthRef = resolveKimiCodeOAuthRef({ baseUrl: configuredBaseUrl });
expect(
resolveKimiCodeLoginAuth({
configuredBaseUrl,
configuredOAuthRef,
requestedBaseUrl: 'https://api.requested.example.test/coding/v1/',
env: {},
}),
).toEqual({
baseUrl: 'https://api.requested.example.test/coding/v1',
oauthHost: undefined,
});
expect(
resolveKimiCodeLoginAuth({
configuredBaseUrl,
configuredOAuthRef,
env: {},
}),
).toEqual({
baseUrl: configuredBaseUrl,
oauthHost: undefined,
oauthRef: configuredOAuthRef,
});
});
it('writes the managed provider, models, services, and default model through an adapter', async () => {
const config: ManagedKimiConfigShape = {
providers: {
custom: {
type: 'kimi',
apiKey: 'sk-existing',
baseUrl: 'https://example.test/v1',
},
},
models: {
'kimi-code/stale': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'stale',
},
'custom-default': {
provider: 'custom',
model: 'custom-model',
},
},
};
const write = vi.fn();
const fetchMock = vi.fn(async () => makeModelsResponse());
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: fetchMock as unknown as typeof fetch,
adapter: {
configPath: '/tmp/config.toml',
read: () => config,
write,
apply: applyManagedKimiCodeConfig,
},
});
expect(result).toMatchObject({
providerName: KIMI_CODE_PROVIDER_NAME,
defaultModel: 'kimi-code/kimi-for-coding',
defaultThinking: true,
configPath: '/tmp/config.toml',
});
expect(result.models[0]?.supportsToolUse).toBe(true);
expect(result.models[1]?.supportsToolUse).toBe(false);
expect(fetchMock).toHaveBeenCalledWith(
'https://api.kimi.com/coding/v1/models',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer oauth-access-token',
Accept: 'application/json',
}),
}),
);
const calls = fetchMock.mock.calls as unknown as [string, RequestInit?][];
const init = calls[0]?.[1] ?? {};
const headers = new Headers((init.headers ?? {}) as Record<string, string>);
expect(headers.get('user-agent')).toBeNull();
expect(headers.get('x-msh-platform')).toBeNull();
expect(write).toHaveBeenCalledWith(config);
expect(config.providers['custom']).toMatchObject({
apiKey: 'sk-existing',
});
expect(config.models?.['custom-default']?.provider).toBe('custom');
expect(config.models?.['kimi-code/stale']).toBeUndefined();
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toMatchObject({
type: 'kimi',
baseUrl: 'https://api.kimi.com/coding/v1',
apiKey: '',
oauth: { storage: 'file', key: 'oauth/kimi-code' },
});
expect(config.models?.['kimi-code/kimi-for-coding']).toMatchObject({
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
maxContextSize: 262144,
capabilities: ['thinking', 'image_in', 'video_in', 'tool_use'],
displayName: 'Kimi for Coding',
});
expect(config.models?.['kimi-code/kimi-k2.5']?.capabilities).toBeUndefined();
expect(config.services?.moonshotSearch).toMatchObject({
baseUrl: 'https://api.kimi.com/coding/v1/search',
apiKey: '',
oauth: { storage: 'file', key: 'oauth/kimi-code' },
});
expect(Object.keys(config.services ?? {})).toEqual(['moonshotSearch', 'moonshotFetch']);
});
it('writes scoped OAuth refs when provisioning against a non-default environment', async () => {
const config: ManagedKimiConfigShape = {
providers: {},
};
const oauthKey = resolveKimiCodeOAuthKey({
oauthHost: 'https://auth.dev.example.test',
baseUrl: 'https://api.dev.example.test/coding/v1',
});
await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
baseUrl: 'https://api.dev.example.test/coding/v1',
oauthKey,
oauthHost: 'https://auth.dev.example.test',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toMatchObject({
baseUrl: 'https://api.dev.example.test/coding/v1',
oauth: {
storage: 'file',
key: oauthKey,
oauthHost: 'https://auth.dev.example.test',
},
});
expect(config.services?.moonshotSearch?.oauth).toEqual({
storage: 'file',
key: oauthKey,
oauthHost: 'https://auth.dev.example.test',
});
expect(config.services?.moonshotFetch?.oauth).toEqual({
storage: 'file',
key: oauthKey,
oauthHost: 'https://auth.dev.example.test',
});
});
it('persists the default OAuth host when only the API base URL is scoped', async () => {
const config: ManagedKimiConfigShape = {
providers: {},
};
const baseUrl = 'https://api.example.test/coding/v1';
const oauthKey = resolveKimiCodeOAuthKey({ baseUrl });
await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
baseUrl,
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toMatchObject({
baseUrl,
oauth: {
storage: 'file',
key: oauthKey,
oauthHost: 'https://auth.kimi.com',
},
});
});
it('preserves an existing valid default model during refresh', async () => {
const config: ManagedKimiConfigShape = {
providers: {
custom: {
type: 'kimi',
apiKey: 'sk-existing',
baseUrl: 'https://example.test/v1',
},
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
apiKey: '',
},
},
defaultModel: 'custom-default',
thinking: { enabled: false },
models: {
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 1000,
},
'kimi-code/stale': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'stale',
maxContextSize: 1000,
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('custom-default');
expect(result.defaultThinking).toBe(false);
expect(config.defaultModel).toBe('custom-default');
expect(config.thinking?.enabled).toBe(false);
expect(config.models?.['kimi-code/stale']).toBeUndefined();
expect(config.models?.['kimi-code/kimi-for-coding']?.displayName).toBe('Kimi for Coding');
});
it('infers default_thinking from fresh managed model capabilities', async () => {
const config: ManagedKimiConfigShape = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
apiKey: '',
},
},
defaultModel: 'kimi-code/kimi-for-coding',
models: {
'kimi-code/kimi-for-coding': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
maxContextSize: 1000,
capabilities: [],
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('kimi-code/kimi-for-coding');
expect(result.defaultThinking).toBe(true);
expect(config.thinking?.enabled).toBe(true);
});
it('preserves explicit default_thinking when preserving a custom default without capabilities', async () => {
const config: ManagedKimiConfigShape = {
providers: {
custom: {
type: 'kimi',
apiKey: 'sk-existing',
},
},
defaultModel: 'custom-default',
thinking: { enabled: true },
models: {
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 1000,
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('custom-default');
expect(result.defaultThinking).toBe(true);
expect(config.thinking?.enabled).toBe(true);
});
it('defaults default_thinking to false when a preserved custom default has no signal', async () => {
const config: ManagedKimiConfigShape = {
providers: {
custom: {
type: 'kimi',
apiKey: 'sk-existing',
},
},
defaultModel: 'custom-default',
models: {
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 1000,
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('custom-default');
expect(result.defaultThinking).toBe(false);
expect(config.thinking?.enabled).toBe(false);
});
it('does not infer default_thinking from preserved custom default capabilities', async () => {
const config: ManagedKimiConfigShape = {
providers: {
custom: {
type: 'kimi',
apiKey: 'sk-existing',
},
},
defaultModel: 'custom-default',
models: {
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 1000,
capabilities: [],
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('custom-default');
expect(result.defaultThinking).toBe(false);
expect(config.thinking?.enabled).toBe(false);
});
it('keeps default_thinking off even when preserved custom default has thinking capability', async () => {
const config: ManagedKimiConfigShape = {
providers: {
custom: {
type: 'kimi',
apiKey: 'sk-existing',
},
},
defaultModel: 'custom-default',
models: {
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 1000,
capabilities: ['thinking'],
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('custom-default');
expect(result.defaultThinking).toBe(false);
expect(config.thinking?.enabled).toBe(false);
});
it('falls back to the first fetched model when the preserved default was removed', async () => {
const config: ManagedKimiConfigShape = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
apiKey: '',
},
},
defaultModel: 'kimi-code/stale',
thinking: { enabled: false },
models: {
'kimi-code/stale': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'stale',
maxContextSize: 1000,
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('kimi-code/kimi-for-coding');
expect(result.defaultThinking).toBe(false);
expect(config.defaultModel).toBe('kimi-code/kimi-for-coding');
expect(config.thinking?.enabled).toBe(false);
});
it('removes managed provider, models, services, and default model on logout', () => {
const config: ManagedKimiConfigShape = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
apiKey: '',
},
custom: {
type: 'kimi',
apiKey: 'sk-existing',
},
},
defaultModel: 'kimi-code/kimi-for-coding',
thinking: { enabled: true },
models: {
'kimi-code/kimi-for-coding': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
maxContextSize: 262144,
},
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 1000,
},
},
services: {
moonshotSearch: { baseUrl: 'https://api.kimi.com/coding/v1/search' },
moonshotFetch: { baseUrl: 'https://api.kimi.com/coding/v1/fetch' },
customService: { baseUrl: 'https://service.example.test' },
},
raw: {
default_model: 'kimi-code/kimi-for-coding',
providers: {
[KIMI_CODE_PROVIDER_NAME]: { type: 'kimi' },
custom: { type: 'kimi' },
},
models: {
'kimi-code/kimi-for-coding': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
},
'custom-default': {
provider: 'custom',
model: 'custom-model',
},
},
services: {
moonshot_search: { base_url: 'https://api.kimi.com/coding/v1/search' },
moonshot_fetch: { base_url: 'https://api.kimi.com/coding/v1/fetch' },
},
},
};
applyManagedKimiCodeLogoutConfig(config);
expect(config.defaultModel).toBeUndefined();
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toBeUndefined();
expect(config.providers['custom']).toBeDefined();
expect(config.models?.['kimi-code/kimi-for-coding']).toBeUndefined();
expect(config.models?.['custom-default']).toBeDefined();
expect(config.services?.moonshotSearch).toBeUndefined();
expect(config.services?.moonshotFetch).toBeUndefined();
expect(config.services?.['customService']).toEqual({
baseUrl: 'https://service.example.test',
});
});
it('rejects managed models that do not include a positive context_length', async () => {
const fetchImpl = vi.fn(
async () =>
new Response(
JSON.stringify({
data: [{ id: 'kimi-for-coding', supports_reasoning: true }],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
) as unknown as typeof fetch;
await expect(
fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl,
}),
).rejects.toThrow(/positive context_length/);
});
it('surfaces API error messages from model listing failures', async () => {
const fetchImpl = vi.fn(
async () =>
new Response(JSON.stringify({ error: { message: 'quota exceeded' } }), {
status: 429,
headers: { 'Content-Type': 'application/json' },
}),
) as unknown as typeof fetch;
await expect(
fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl,
}),
).rejects.toThrow('quota exceeded');
});
it('classifies model listing 401 responses as OAuth unauthorized', async () => {
const fetchImpl = vi.fn(
async () =>
new Response(
JSON.stringify({
error: { message: 'The API Key appears to be invalid or may have expired.' },
}),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
},
),
) as unknown as typeof fetch;
await expect(
fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl,
}),
).rejects.toBeInstanceOf(OAuthUnauthorizedError);
});
it('classifies membership-check 402 responses as OAuth unauthorized', async () => {
const fetchImpl = vi.fn(
async () =>
new Response(
JSON.stringify({
error: {
message:
"We're unable to verify your membership benefits at this time. Please ensure your membership is active.",
},
}),
{
status: 402,
headers: { 'Content-Type': 'application/json' },
},
),
) as unknown as typeof fetch;
const promise = fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
baseUrl: 'https://api.dev.example.test/coding/v1',
fetchImpl,
});
await expect(promise).rejects.toThrow(
"Kimi Code models endpoint https://api.dev.example.test/coding/v1 rejected OAuth credentials: We're unable to verify your membership benefits at this time. Please ensure your membership is active.",
);
await expect(
fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
baseUrl: 'https://api.dev.example.test/coding/v1',
fetchImpl,
}),
).rejects.toMatchObject({
status: 402,
baseUrl: 'https://api.dev.example.test/coding/v1',
});
await expect(
fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl,
}),
).rejects.toBeInstanceOf(OAuthUnauthorizedError);
await expect(
fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl,
}),
).rejects.toBeInstanceOf(ManagedKimiCodeModelsAuthError);
});
it('clears managed provider, models, default model, and services on logout', () => {
const config: ManagedKimiConfigShape = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
apiKey: '',
oauth: { storage: 'file', key: 'oauth/kimi-code' },
},
custom: {
type: 'kimi',
apiKey: 'sk-existing',
},
},
defaultModel: 'kimi-code/kimi-for-coding',
models: {
'kimi-code/kimi-for-coding': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
maxContextSize: 262144,
},
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 128000,
},
},
services: {
moonshotSearch: {
baseUrl: 'https://api.kimi.com/coding/v1/search',
apiKey: '',
oauth: { storage: 'file', key: 'oauth/kimi-code' },
},
moonshotFetch: {
baseUrl: 'https://api.kimi.com/coding/v1/fetch',
apiKey: '',
oauth: { storage: 'file', key: 'oauth/kimi-code' },
},
otherService: { baseUrl: 'https://service.example.test' },
},
};
const result = clearManagedKimiCodeConfig(config);
expect(result).toMatchObject({
providerName: KIMI_CODE_PROVIDER_NAME,
removedProvider: true,
removedModels: ['kimi-code/kimi-for-coding'],
defaultModelCleared: true,
removedServices: ['moonshotSearch', 'moonshotFetch'],
});
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toBeUndefined();
expect(config.providers['custom']).toMatchObject({ apiKey: 'sk-existing' });
expect(config.defaultModel).toBeUndefined();
expect(config.models?.['kimi-code/kimi-for-coding']).toBeUndefined();
expect(config.models?.['custom-default']).toMatchObject({ provider: 'custom' });
expect(config.services?.moonshotSearch).toBeUndefined();
expect(config.services?.moonshotFetch).toBeUndefined();
expect(config.services?.['otherService']).toMatchObject({
baseUrl: 'https://service.example.test',
});
});
});
describe('supports_thinking_type', () => {
function makeThinkingTypeModelsResponse(): Response {
return new Response(
JSON.stringify({
data: [
{
id: 'kimi-for-coding',
context_length: 262144,
supports_reasoning: true,
supports_image_in: true,
supports_video_in: true,
supports_thinking_type: 'only',
display_name: 'Kimi For Coding',
},
{
// 'no' is the authoritative declaration and overrides the legacy
// supports_reasoning boolean.
id: 'kimi-plain',
context_length: 128000,
supports_reasoning: true,
supports_thinking_type: 'no',
},
{
id: 'kimi-toggle',
context_length: 128000,
supports_reasoning: true,
supports_thinking_type: 'both',
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
);
}
it('parses supports_thinking_type from the models endpoint', async () => {
const models = await fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeThinkingTypeModelsResponse()) as unknown as typeof fetch,
});
expect(models[0]?.supportsThinkingType).toBe('only');
expect(models[1]?.supportsThinkingType).toBe('no');
expect(models[2]?.supportsThinkingType).toBe('both');
});
it('leaves supportsThinkingType undefined when the field is absent or invalid', async () => {
const absent = await fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeModelsResponse()) as unknown as typeof fetch,
});
expect(absent[0]?.supportsThinkingType).toBeUndefined();
const invalid = await fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(
async () =>
new Response(
JSON.stringify({
data: [
{
id: 'kimi-for-coding',
context_length: 262144,
supports_reasoning: true,
supports_thinking_type: 'maybe',
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
) as unknown as typeof fetch,
});
expect(invalid[0]?.supportsThinkingType).toBeUndefined();
});
it('maps the three states onto capabilities, overriding supports_reasoning', async () => {
const config: ManagedKimiConfigShape = { providers: {} };
await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeThinkingTypeModelsResponse()) as unknown as typeof fetch,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
// 'only' → thinking locked on.
expect(config.models?.['kimi-code/kimi-for-coding']?.capabilities).toEqual([
'thinking',
'always_thinking',
'image_in',
'video_in',
'tool_use',
]);
// 'no' → no thinking capability despite supports_reasoning=true.
expect(config.models?.['kimi-code/kimi-plain']?.capabilities).toEqual(['tool_use']);
// 'both' → plain toggleable thinking.
expect(config.models?.['kimi-code/kimi-toggle']?.capabilities).toEqual([
'thinking',
'tool_use',
]);
});
it('forces default thinking on when the selected default model is thinking-only', async () => {
const config: ManagedKimiConfigShape = { providers: {}, thinking: { enabled: false } };
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeThinkingTypeModelsResponse()) as unknown as typeof fetch,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('kimi-code/kimi-for-coding');
expect(result.defaultThinking).toBe(true);
expect(config.thinking?.enabled).toBe(true);
});
it('forces default thinking on when preserving a thinking-only managed default', async () => {
const config: ManagedKimiConfigShape = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
apiKey: '',
},
},
defaultModel: 'kimi-code/kimi-for-coding',
thinking: { enabled: false },
models: {
'kimi-code/kimi-for-coding': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-for-coding',
maxContextSize: 262144,
capabilities: ['thinking'],
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeThinkingTypeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('kimi-code/kimi-for-coding');
expect(result.defaultThinking).toBe(true);
expect(config.thinking?.enabled).toBe(true);
});
it('forces default thinking off when preserving a no-thinking managed default', async () => {
const config: ManagedKimiConfigShape = {
providers: {
[KIMI_CODE_PROVIDER_NAME]: {
type: 'kimi',
apiKey: '',
},
},
defaultModel: 'kimi-code/kimi-plain',
thinking: { enabled: true },
models: {
'kimi-code/kimi-plain': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-plain',
maxContextSize: 128000,
capabilities: ['thinking'],
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeThinkingTypeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('kimi-code/kimi-plain');
expect(result.defaultThinking).toBe(false);
expect(config.thinking?.enabled).toBe(false);
});
it('keeps a preserved non-managed default thinking selection untouched', async () => {
const config: ManagedKimiConfigShape = {
providers: {
custom: {
type: 'kimi',
apiKey: 'sk-existing',
},
},
defaultModel: 'custom-default',
thinking: { enabled: false },
models: {
'custom-default': {
provider: 'custom',
model: 'custom-model',
maxContextSize: 1000,
},
},
};
const result = await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeThinkingTypeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
expect(result.defaultModel).toBe('custom-default');
expect(result.defaultThinking).toBe(false);
expect(config.thinking?.enabled).toBe(false);
});
});
describe('support_efforts / default_effort', () => {
function makeEffortModelsResponse(): Response {
return new Response(
JSON.stringify({
data: [
{
id: 'kimi-for-coding',
context_length: 262144,
supports_reasoning: true,
supports_thinking_type: 'both',
think_efforts: {
support: true,
valid_efforts: ['low', 'high', 'max'],
default_effort: 'high',
},
display_name: 'Kimi For Coding',
},
{
// Empty / non-string entries are filtered; absent fields stay undefined.
id: 'kimi-plain',
context_length: 128000,
supports_reasoning: true,
think_efforts: { support: true, valid_efforts: ['low', '', 42] },
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
);
}
it('parses think_efforts from the models endpoint', async () => {
const models = await fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeEffortModelsResponse()) as unknown as typeof fetch,
});
expect(models[0]?.supportEfforts).toEqual(['low', 'high', 'max']);
expect(models[0]?.defaultEffort).toBe('high');
// The empty string and number are filtered out of valid_efforts.
expect(models[1]?.supportEfforts).toEqual(['low']);
expect(models[1]?.defaultEffort).toBeUndefined();
});
it('ignores think_efforts entirely when support is not true', async () => {
const models = await fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl: async () =>
new Response(
JSON.stringify({
data: [
{
id: 'kimi-no-effort',
context_length: 128000,
supports_reasoning: true,
think_efforts: {
support: false,
valid_efforts: ['low', 'high'],
default_effort: 'high',
},
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
});
// support !== true gates the whole object — valid_efforts / default_effort
// are ignored.
expect(models[0]?.supportEfforts).toBeUndefined();
expect(models[0]?.defaultEffort).toBeUndefined();
});
it('ignores legacy flat fields even when think_efforts is absent', async () => {
// The legacy support_efforts / default_effort fields are no longer read;
// only the nested think_efforts object is honored.
const models = await fetchManagedKimiCodeModels({
accessToken: 'oauth-access-token',
fetchImpl: async () =>
new Response(
JSON.stringify({
data: [
{
id: 'kimi-k2',
context_length: 128000,
supports_reasoning: true,
support_efforts: ['low', 'high'],
default_effort: 'high',
},
],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
});
expect(models[0]?.supportEfforts).toBeUndefined();
expect(models[0]?.defaultEffort).toBeUndefined();
});
it('writes supportEfforts and defaultEffort onto the provisioned model entry', async () => {
const config: ManagedKimiConfigShape = { providers: {} };
await provisionManagedKimiCodeConfig({
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeEffortModelsResponse()) as unknown as typeof fetch,
adapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});
const alias = config.models?.['kimi-code/kimi-for-coding'];
expect(alias?.['supportEfforts']).toEqual(['low', 'high', 'max']);
expect(alias?.['defaultEffort']).toBe('high');
});
});
describe('selective merge', () => {
const baseOptions = {
baseUrl: 'https://api.example.test/coding/v1',
oauthKey: 'test-key',
};
it('preserves non-managed user fields but drops stale managed fields', () => {
const config: ManagedKimiConfigShape = {
providers: {},
models: {
'kimi-code/kimi-k2': {
provider: 'kimi-code',
model: 'kimi-k2',
maxContextSize: 262144,
capabilities: ['thinking'],
maxOutputSize: 4096,
supportEfforts: ['low', 'high', 'max'],
} as Record<string, unknown>,
},
};
applyManagedKimiCodeConfig(config, {
...baseOptions,
models: [
{
id: 'kimi-k2',
contextLength: 262144,
supportsReasoning: true,
supportsImageIn: false,
supportsVideoIn: false,
supportsThinkingType: 'both',
},
],
});
const alias = config.models?.['kimi-code/kimi-k2'];
expect(alias?.['maxOutputSize']).toBe(4096);
expect(alias?.['supportEfforts']).toBeUndefined();
expect(alias?.['maxContextSize']).toBe(262144);
});
it('preserves overrides when upstream declares managed fields', () => {
const config: ManagedKimiConfigShape = {
providers: {},
models: {
'kimi-code/kimi-k2': {
provider: 'kimi-code',
model: 'kimi-k2',
maxContextSize: 262144,
overrides: { supportEfforts: ['low'] },
} as Record<string, unknown>,
},
};
applyManagedKimiCodeConfig(config, {
...baseOptions,
models: [
{
id: 'kimi-k2',
contextLength: 262144,
supportsReasoning: true,
supportsImageIn: false,
supportsVideoIn: false,
supportEfforts: ['low', 'high', 'max'],
defaultEffort: 'high',
},
],
});
const alias = config.models?.['kimi-code/kimi-k2'];
expect(alias?.['supportEfforts']).toEqual(['low', 'high', 'max']);
expect(alias?.['defaultEffort']).toBe('high');
expect(alias?.['overrides']).toEqual({ supportEfforts: ['low'] });
});
it('removes managed models that upstream no longer lists', () => {
const config: ManagedKimiConfigShape = {
providers: {},
models: {
'kimi-code/kimi-k2': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'kimi-k2',
maxContextSize: 262144,
},
'kimi-code/removed': {
provider: KIMI_CODE_PROVIDER_NAME,
model: 'removed',
maxContextSize: 128000,
},
},
};
applyManagedKimiCodeConfig(config, {
...baseOptions,
models: [
{
id: 'kimi-k2',
contextLength: 262144,
supportsReasoning: true,
supportsImageIn: false,
supportsVideoIn: false,
},
],
});
expect(config.models?.['kimi-code/kimi-k2']).toBeDefined();
expect(config.models?.['kimi-code/removed']).toBeUndefined();
});
});
function makeModelInfo(
id: string,
overrides: Partial<ManagedKimiCodeModelInfo> = {},
): ManagedKimiCodeModelInfo {
return {
id,
contextLength: 200000,
supportsReasoning: false,
supportsImageIn: false,
supportsVideoIn: false,
...overrides,
};
}
const KIMI_BASE_URL = 'https://api.kimi.com/coding/v1';
describe('managed protocol routing', () => {
it('reads protocol from the /models response', async () => {
const fetchImpl = vi.fn(
async () =>
new Response(
JSON.stringify({
data: [{ id: 'kimi-for-coding', context_length: 262144, protocol: 'anthropic' }],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
) as unknown as typeof fetch;
const models = await fetchManagedKimiCodeModels({ accessToken: 't', fetchImpl });
expect(models).toHaveLength(1);
expect(models[0]?.protocol).toBe('anthropic');
});
it('keeps the provider on the kimi REST base and records the model protocol when anthropic', () => {
const config: ManagedKimiConfigShape = { providers: {} };
applyManagedKimiCodeConfig(config, {
baseUrl: KIMI_BASE_URL,
models: [makeModelInfo('kimi-for-coding', { protocol: 'anthropic' })],
});
// The provider stays on the kimi wire + REST base; the anthropic transport
// is resolved per-model at runtime, not baked into the provider config, so
// the REST base keeps flowing to OAuth key derivation and plugin env.
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toMatchObject({
type: 'kimi',
baseUrl: KIMI_BASE_URL,
apiKey: '',
});
expect(config.models?.['kimi-code/kimi-for-coding']).toMatchObject({
provider: KIMI_CODE_PROVIDER_NAME,
protocol: 'anthropic',
betaApi: true,
});
});
it('keeps the kimi protocol and baseUrl when the model has no anthropic protocol', () => {
const config: ManagedKimiConfigShape = { providers: {} };
applyManagedKimiCodeConfig(config, {
baseUrl: KIMI_BASE_URL,
models: [makeModelInfo('kimi-for-coding')],
});
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toMatchObject({
type: 'kimi',
baseUrl: KIMI_BASE_URL,
apiKey: '',
});
expect(config.models?.['kimi-code/kimi-for-coding']?.provider).toBe(KIMI_CODE_PROVIDER_NAME);
expect(config.models?.['kimi-code/kimi-for-coding']?.protocol).toBeUndefined();
});
it('drops the model protocol on refresh when the server stops declaring anthropic', () => {
const config: ManagedKimiConfigShape = { providers: {} };
applyManagedKimiCodeConfig(config, {
baseUrl: KIMI_BASE_URL,
models: [makeModelInfo('kimi-for-coding', { protocol: 'anthropic' })],
});
expect(config.models?.['kimi-code/kimi-for-coding']?.protocol).toBe('anthropic');
applyManagedKimiCodeConfig(config, {
baseUrl: KIMI_BASE_URL,
models: [makeModelInfo('kimi-for-coding')],
});
// The provider never leaves the kimi wire / REST base across refreshes —
// only the per-model protocol annotation changes.
expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toMatchObject({
type: 'kimi',
baseUrl: KIMI_BASE_URL,
});
expect(config.models?.['kimi-code/kimi-for-coding']?.protocol).toBeUndefined();
});
});