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