diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index 5c0aa7e..bc689e3 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -1262,7 +1262,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { case "glm": case "qwen": case "qiniu": - case "kimi": case "novita": { const envVar = PROVIDER_ENV_VARS[provider] if (!envVar) { @@ -1288,6 +1287,23 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { break } + case "kimi": { + const apiKey = resolveApiKey(overrides, "KIMI_API_KEY") + const baseURL = resolveBaseURL( + overrides?.apiKey, + overrides?.baseUrl, + resolveBaseUrlEnv(overrides, "KIMI_BASE_URL"), + PROVIDER_INFO["kimi"]?.defaultBaseUrl, + ) + // Use createDeepSeek to properly handle reasoning_content for Kimi + // thinking models (e.g., kimi-k2.6). Kimi's API uses the same + // reasoning_content field as DeepSeek, so this provider correctly + // captures and replays reasoning in multi-turn conversations. + const customProvider = createDeepSeek({ apiKey, baseURL }) + model = customProvider(modelId) + break + } + default: throw new Error( `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`, diff --git a/tests/unit/ai-providers.test.ts b/tests/unit/ai-providers.test.ts index 92b3320..338a179 100644 --- a/tests/unit/ai-providers.test.ts +++ b/tests/unit/ai-providers.test.ts @@ -245,6 +245,67 @@ vi.mock("ollama-ai-provider-v2", () => { return { createOllama: mockCreateOllama, ollama: mockOllama } }) +vi.mock("@ai-sdk/deepseek", () => { + const mockModel = { modelId: "test-model" } + const mockProviderFn = vi.fn(() => mockModel) + const mockCreateDeepSeek = vi.fn(() => mockProviderFn) + const mockDeepseek = vi.fn(() => mockModel) + return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek } +}) + +describe("Kimi provider uses createDeepSeek for reasoning_content support", () => { + let createDeepSeekMock: ReturnType + const savedEnv: Record = {} + + beforeEach(async () => { + savedEnv.KIMI_API_KEY = process.env.KIMI_API_KEY + savedEnv.KIMI_BASE_URL = process.env.KIMI_BASE_URL + delete process.env.KIMI_BASE_URL + + const mod = await import("@ai-sdk/deepseek") + createDeepSeekMock = mod.createDeepSeek as ReturnType + createDeepSeekMock.mockClear() + }) + + afterEach(() => { + process.env.KIMI_API_KEY = savedEnv.KIMI_API_KEY + process.env.KIMI_BASE_URL = savedEnv.KIMI_BASE_URL + }) + + it("uses createDeepSeek with Kimi default base URL for reasoning_content support", () => { + process.env.KIMI_API_KEY = "test-kimi-key" + + getAIModel({ + provider: "kimi", + apiKey: "test-kimi-key", + modelId: "moonshot-v1-8k", + }) + + expect(createDeepSeekMock).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://api.moonshot.cn/v1", + }), + ) + }) + + it("uses custom base URL when provided for kimi provider", () => { + process.env.KIMI_API_KEY = "test-kimi-key" + + getAIModel({ + provider: "kimi", + apiKey: "test-kimi-key", + baseUrl: "https://custom-kimi-endpoint.com/v1", + modelId: "kimi-k2.6", + }) + + expect(createDeepSeekMock).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://custom-kimi-endpoint.com/v1", + }), + ) + }) +}) + describe("Ollama API key security", () => { let createOllamaMock: ReturnType const savedEnv: Record = {}