diff --git a/packages/types/src/providers/kimi-code.ts b/packages/types/src/providers/kimi-code.ts index c9a458ed1f..0837e25832 100644 --- a/packages/types/src/providers/kimi-code.ts +++ b/packages/types/src/providers/kimi-code.ts @@ -7,7 +7,7 @@ export const kimiCodeReasoningEfforts = ["low", "high", "max"] as const export const kimiCodeDefaultModelInfo: ModelInfo = { contextWindow: 262_144, - maxTokens: 32_768, + maxTokens: 131_072, supportsImages: false, supportsPromptCache: false, supportsReasoningEffort: [...kimiCodeReasoningEfforts], @@ -16,6 +16,13 @@ export const kimiCodeDefaultModelInfo: ModelInfo = { description: "Kimi Code's coding model for subscription and API-key access.", } +export const kimiCodeModelDefaults: Record = { + k3: { maxTokens: 131_072 }, + "k3-256k": { maxTokens: 131_072 }, + "kimi-for-coding": { maxTokens: 131_072 }, + "kimi-for-coding-highspeed": { maxTokens: 131_072 }, +} + export const kimiCodeModels = { [kimiCodeDefaultModelId]: kimiCodeDefaultModelInfo, } as const satisfies Record diff --git a/src/api/providers/fetchers/__tests__/kimi-code.spec.ts b/src/api/providers/fetchers/__tests__/kimi-code.spec.ts index 3fbe995752..a96e760253 100644 --- a/src/api/providers/fetchers/__tests__/kimi-code.spec.ts +++ b/src/api/providers/fetchers/__tests__/kimi-code.spec.ts @@ -1,4 +1,4 @@ -import { getKimiCodeModels, mapKimiCodeModel } from "../kimi-code" +import { getKimiCodeModels, kimiCodeModelSchema, mapKimiCodeModel } from "../kimi-code" describe("Kimi Code model discovery", () => { beforeEach(() => vi.restoreAllMocks()) @@ -97,4 +97,57 @@ describe("Kimi Code model discovery", () => { expect(vi.mocked(fetch).mock.calls[0][1]?.signal?.aborted).toBe(true) expect(vi.getTimerCount()).toBe(0) }) + + it("overrides maxTokens from server max_tokens in mapKimiCodeModel", () => { + const mapped = mapKimiCodeModel({ + id: "kimi-for-coding", + max_tokens: 200_000, + }) + expect(mapped.maxTokens).toBe(200_000) + }) + + it("overrides maxTokens from server max_tokens at fetcher level in getKimiCodeModels", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + data: [{ id: "kimi-for-coding", context_length: 262144, max_tokens: 200_000 }], + }), + { status: 200 }, + ), + ) + const models = await getKimiCodeModels("token") + expect(models["kimi-for-coding"].maxTokens).toBe(200_000) + }) + + it("falls back to per-model defaults for known model ids", () => { + const expected: Record = { + k3: 131_072, + "k3-256k": 131_072, + "kimi-for-coding": 131_072, + "kimi-for-coding-highspeed": 131_072, + } + for (const [modelId, maxTokens] of Object.entries(expected)) { + const mapped = mapKimiCodeModel({ id: modelId }) + expect(mapped.maxTokens).toBe(maxTokens) + } + }) + + it("falls back to kimiCodeDefaultModelInfo.maxTokens for unknown model ids", () => { + const mapped = mapKimiCodeModel({ id: "unknown-model" }) + expect(mapped.maxTokens).toBe(131_072) + }) + + it("rejects fractional max_tokens from server response", () => { + const mapped = mapKimiCodeModel({ + id: "kimi-for-coding", + max_tokens: 131072.5, + }) + // Zod .int() would reject at schema level, but mapKimiCodeModel receives + // already-parsed data. Verify the schema rejects fractional values. + const result = kimiCodeModelSchema.safeParse({ + id: "kimi-for-coding", + max_tokens: 131072.5, + }) + expect(result.success).toBe(false) + }) }) diff --git a/src/api/providers/fetchers/kimi-code.ts b/src/api/providers/fetchers/kimi-code.ts index 10b93e26cf..02bf9180f7 100644 --- a/src/api/providers/fetchers/kimi-code.ts +++ b/src/api/providers/fetchers/kimi-code.ts @@ -3,14 +3,16 @@ import { z } from "zod" import { KIMI_CODE_BASE_URL, kimiCodeDefaultModelInfo, + kimiCodeModelDefaults, kimiCodeReasoningEfforts, type ModelInfo, type ModelRecord, } from "@roo-code/types" -const kimiCodeModelSchema = z.object({ +export const kimiCodeModelSchema = z.object({ id: z.string().min(1), context_length: z.number().positive().optional(), + max_tokens: z.number().int().positive().optional(), supports_reasoning: z.boolean().optional(), supports_image_in: z.boolean().optional(), display_name: z.string().optional(), @@ -22,9 +24,11 @@ const KIMI_CODE_MODELS_TIMEOUT_MS = 10_000 export function mapKimiCodeModel(model: z.infer): ModelInfo { const supportsReasoning = model.supports_reasoning ?? false + const defaults = kimiCodeModelDefaults[model.id] ?? {} return { ...kimiCodeDefaultModelInfo, contextWindow: model.context_length ?? kimiCodeDefaultModelInfo.contextWindow, + maxTokens: model.max_tokens ?? defaults.maxTokens ?? kimiCodeDefaultModelInfo.maxTokens, supportsReasoningEffort: supportsReasoning ? [...kimiCodeReasoningEfforts] : false, requiredReasoningEffort: supportsReasoning, reasoningEffort: supportsReasoning ? "max" : undefined,