Download packages/oauth/test/managed-kimi-code.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 49.2 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/oauth/test/managed-kimi-code.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/oauth/test/managed-kimi-code.test.ts
-
curl -L -o managed-kimi-code.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/oauth/test/managed-kimi-code.test.ts
49.2 kB
| import { describe, expect, it, vi } from 'vitest'; | |
| import { | |
| applyManagedApiKeyProviderModels, | |
| 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', | |
| supports_dynamic_tools: true, | |
| 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 and supports_dynamic_tools 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'); | |
| expect(models[0]?.supportsDynamicTools).toBe(true); | |
| expect(models[1]?.supportsDynamicTools).toBe(false); | |
| }); | |
| 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; supports_dynamic_tools adds dynamically_loaded_tools. | |
| expect(config.models?.['kimi-code/kimi-for-coding']?.capabilities).toEqual([ | |
| 'thinking', | |
| 'always_thinking', | |
| 'image_in', | |
| 'video_in', | |
| 'tool_use', | |
| 'dynamically_loaded_tools', | |
| ]); | |
| // '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(); | |
| }); | |
| }); | |
| describe('applyManagedApiKeyProviderModels', () => { | |
| it('merges upstream models without touching provider, services, or defaults', () => { | |
| const config: ManagedKimiConfigShape = { | |
| providers: { | |
| 'my-kimi': { | |
| type: 'kimi', | |
| baseUrl: 'https://api.example.test/coding/v1', | |
| apiKey: 'sk-distributed-key', | |
| }, | |
| }, | |
| models: { | |
| 'my-kimi/kimi-k2': { | |
| provider: 'my-kimi', | |
| model: 'kimi-k2', | |
| maxContextSize: 262144, | |
| displayName: 'Old K2', | |
| maxOutputSize: 4096, | |
| } as Record<string, unknown>, | |
| 'my-kimi/kimi-old': { | |
| provider: 'my-kimi', | |
| model: 'kimi-old', | |
| maxContextSize: 128000, | |
| }, | |
| 'other/m1': { | |
| provider: 'other', | |
| model: 'm1', | |
| maxContextSize: 128000, | |
| }, | |
| }, | |
| defaultModel: 'my-kimi/kimi-k2', | |
| thinking: { enabled: false }, | |
| }; | |
| applyManagedApiKeyProviderModels( | |
| config, | |
| 'my-kimi', | |
| [makeModelInfo('kimi-k2', { displayName: 'Fresh K2' }), makeModelInfo('kimi-k2.5')], | |
| 'my-kimi/', | |
| ); | |
| // The provider record is user-owned: no rewrite, no oauth, no apiKey reset. | |
| expect(config.providers['my-kimi']).toEqual({ | |
| type: 'kimi', | |
| baseUrl: 'https://api.example.test/coding/v1', | |
| apiKey: 'sk-distributed-key', | |
| }); | |
| // Defaults and services are the orchestrator's / OAuth branch's business. | |
| expect(config.defaultModel).toBe('my-kimi/kimi-k2'); | |
| expect(config.thinking).toEqual({ enabled: false }); | |
| expect(config.services).toBeUndefined(); | |
| // Upstream-owned fields merge; hand-written extras survive. | |
| const alias = config.models?.['my-kimi/kimi-k2']; | |
| expect(alias?.['displayName']).toBe('Fresh K2'); | |
| expect(alias?.['maxOutputSize']).toBe(4096); | |
| // New upstream model added; dropped one removed; other providers untouched. | |
| expect(config.models?.['my-kimi/kimi-k2.5']).toBeDefined(); | |
| expect(config.models?.['my-kimi/kimi-old']).toBeUndefined(); | |
| expect(config.models?.['other/m1']).toBeDefined(); | |
| }); | |
| it('writes protocol routing fields for anthropic-protocol models', () => { | |
| const config: ManagedKimiConfigShape = { providers: {}, models: {} }; | |
| applyManagedApiKeyProviderModels( | |
| config, | |
| 'my-kimi', | |
| [makeModelInfo('kimi-for-coding', { protocol: 'anthropic', supportsReasoning: true })], | |
| 'my-kimi/', | |
| ); | |
| const alias = config.models?.['my-kimi/kimi-for-coding']; | |
| expect(alias?.['provider']).toBe('my-kimi'); | |
| expect(alias?.['protocol']).toBe('anthropic'); | |
| expect(alias?.['betaApi']).toBe(true); | |
| expect(alias?.['adaptiveThinking']).toBe(true); | |
| expect(alias?.['capabilities']).toEqual(['thinking', 'tool_use']); | |
| }); | |
| it('rejects models without a positive context length', () => { | |
| const config: ManagedKimiConfigShape = { providers: {}, models: {} }; | |
| expect(() => { | |
| applyManagedApiKeyProviderModels( | |
| config, | |
| 'my-kimi', | |
| [makeModelInfo('bad', { contextLength: 0 })], | |
| 'my-kimi/', | |
| ); | |
| }).toThrow('context_length'); | |
| }); | |
| }); | |
| 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('maps the server "response" protocol value to openai_responses', async () => { | |
| const fetchImpl = vi.fn( | |
| async () => | |
| new Response( | |
| JSON.stringify({ | |
| data: [{ id: 'k3', context_length: 1048576, protocol: 'response' }], | |
| }), | |
| { 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('openai_responses'); | |
| }); | |
| it('records openai_responses protocol without anthropic routing fields', () => { | |
| const config: ManagedKimiConfigShape = { providers: {} }; | |
| applyManagedKimiCodeConfig(config, { | |
| baseUrl: KIMI_BASE_URL, | |
| models: [makeModelInfo('k3', { protocol: 'openai_responses', supportsReasoning: true })], | |
| }); | |
| expect(config.providers[KIMI_CODE_PROVIDER_NAME]).toMatchObject({ | |
| type: 'kimi', | |
| baseUrl: KIMI_BASE_URL, | |
| apiKey: '', | |
| }); | |
| const alias = config.models?.['kimi-code/k3']; | |
| expect(alias?.protocol).toBe('openai_responses'); | |
| expect(alias?.betaApi).toBeUndefined(); | |
| expect(alias?.adaptiveThinking).toBeUndefined(); | |
| }); | |
| 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(); | |
| }); | |
| }); | |