Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions packages/types/src/providers/kimi-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,57 @@ export const kimiCodeDefaultModelId = "kimi-for-coding"

export const kimiCodeReasoningEfforts = ["low", "high", "max"] as const

export const kimiCodeDefaultModelInfo: ModelInfo = {
const kimiCodeK3ModelIds = new Set(["k3", "k3-256k"])
const kimiCodeK27ModelIds = new Set(["kimi-for-coding", "kimi-for-coding-highspeed"])

export type KimiCodeRequestProtocol = "reasoning-effort" | "thinking"

const kimiCodeBaseModelInfo: ModelInfo = {
contextWindow: 262_144,
maxTokens: 131_072,
supportsImages: false,
supportsPromptCache: false,
supportsReasoningEffort: [...kimiCodeReasoningEfforts],
requiredReasoningEffort: true,
reasoningEffort: "max",
description: "Kimi Code's coding model for subscription and API-key access.",
supportsReasoningEffort: false,
requiredReasoningEffort: false,
supportsTemperature: false,
description: "Kimi Code model for subscription and API-key access.",
}

export function getKimiCodeRequestProtocol(modelId: string): KimiCodeRequestProtocol | undefined {
if (kimiCodeK3ModelIds.has(modelId)) return "reasoning-effort"
if (kimiCodeK27ModelIds.has(modelId)) return "thinking"
return undefined
}

export const kimiCodeModelDefaults: Record<string, { maxTokens: number }> = {
k3: { maxTokens: 131_072 },
"k3-256k": { maxTokens: 131_072 },
"kimi-for-coding": { maxTokens: 131_072 },
"kimi-for-coding-highspeed": { maxTokens: 131_072 },
export function getKimiCodeModelInfo(modelId: string): ModelInfo {
const protocol = getKimiCodeRequestProtocol(modelId)

if (protocol === "reasoning-effort") {
return {
...kimiCodeBaseModelInfo,
supportsImages: true,
supportsReasoningEffort: [...kimiCodeReasoningEfforts],
requiredReasoningEffort: true,
reasoningEffort: "high",
preserveReasoning: true,
description: "Kimi K3 coding model with configurable reasoning effort.",
}
}

if (protocol === "thinking") {
return {
...kimiCodeBaseModelInfo,
supportsImages: true,
preserveReasoning: true,
description: "Kimi K2.7 Code model with preserved thinking.",
}
}

return { ...kimiCodeBaseModelInfo }
}

export const kimiCodeDefaultModelInfo = getKimiCodeModelInfo(kimiCodeDefaultModelId)

export const kimiCodeModels = {
[kimiCodeDefaultModelId]: kimiCodeDefaultModelInfo,
} as const satisfies Record<string, ModelInfo>
Expand Down
114 changes: 110 additions & 4 deletions src/api/providers/__tests__/kimi-code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ describe("KimiCodeHandler", () => {
await expect(handler.completePrompt("test")).resolves.toBe("retried")
expect(mockForceRefreshAccessToken).toHaveBeenCalledOnce()
expect(createCompletion).toHaveBeenCalledTimes(2)
expect(createCompletion).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ thinking: { type: "enabled", keep: "all" } }),
expect.anything(),
)
})

it("does not retry on 401 when using API key auth", async () => {
Expand Down Expand Up @@ -214,18 +219,33 @@ describe("KimiCodeHandler", () => {
expect(model.info.maxTokens).toBe(8000)
})

it("defaults to max reasoning effort and advertises low/high/max support", () => {
it("uses preserved thinking instead of reasoning effort for the default K2.7 model", () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" })
const model = handler.getModel()
expect(model.info.supportsReasoningEffort).toBe(false)
expect(model.info.requiredReasoningEffort).toBe(false)
expect(model.info.preserveReasoning).toBe(true)
expect(model.reasoning).toBeUndefined()
})

it("defaults K3 to high reasoning effort and advertises low/high/max support", () => {
const handler = new KimiCodeHandler({
kimiCodeAuthMethod: "api-key",
kimiCodeApiKey: "key",
apiModelId: "k3",
})
const model = handler.getModel()
expect(model.info.supportsReasoningEffort).toEqual(["low", "high", "max"])
expect(model.info.requiredReasoningEffort).toBe(true)
expect(model.reasoning).toEqual({ reasoning_effort: "max" })
expect(model.info.supportsTemperature).toBe(false)
expect(model.reasoning).toEqual({ reasoning_effort: "high" })
})

it("sends the user-selected reasoning effort", () => {
it("sends the user-selected K3 reasoning effort", () => {
const handler = new KimiCodeHandler({
kimiCodeAuthMethod: "api-key",
kimiCodeApiKey: "key",
apiModelId: "k3",
reasoningEffort: "low",
})
expect(handler.getModel().reasoning).toEqual({ reasoning_effort: "low" })
Expand All @@ -235,8 +255,94 @@ describe("KimiCodeHandler", () => {
const handler = new KimiCodeHandler({
kimiCodeAuthMethod: "api-key",
kimiCodeApiKey: "key",
apiModelId: "k3",
reasoningEffort: "medium",
})
expect(handler.getModel().reasoning).toEqual({ reasoning_effort: "max" })
expect(handler.getModel().reasoning).toEqual({ reasoning_effort: "high" })
})

it.each(["k3", "k3-256k"])(
"sends the K3 reasoning protocol without temperature or thinking for %s",
async (modelId) => {
const handler = new KimiCodeHandler({
kimiCodeAuthMethod: "api-key",
kimiCodeApiKey: "key",
apiModelId: modelId,
modelTemperature: 0.7,
})
const createCompletion = vi.spyOn(handler["client"].chat.completions, "create").mockResolvedValue({
async *[Symbol.asyncIterator]() {
yield { choices: [{ delta: { content: "ok" }, finish_reason: "stop" }] }
},
} as never)

for await (const _chunk of handler.createMessage("system", [{ role: "user", content: "test" }])) {
// consume
}

expect(createCompletion).toHaveBeenCalledWith(
expect.objectContaining({ model: modelId, reasoning_effort: "high" }),
expect.anything(),
)
const request = createCompletion.mock.calls[0][0]
expect(request).not.toHaveProperty("temperature")
expect(request).not.toHaveProperty("thinking")
},
)

it.each(["kimi-for-coding", "kimi-for-coding-highspeed"])(
"sends K2.7 preserved thinking without reasoning effort or temperature for %s",
async (modelId) => {
const handler = new KimiCodeHandler({
kimiCodeAuthMethod: "api-key",
kimiCodeApiKey: "key",
apiModelId: modelId,
reasoningEffort: "low",
modelTemperature: 0.7,
})
const createCompletion = vi.spyOn(handler["client"].chat.completions, "create").mockResolvedValue({
async *[Symbol.asyncIterator]() {
yield { choices: [{ delta: { content: "ok" }, finish_reason: "stop" }] }
},
} as never)

for await (const _chunk of handler.createMessage("system", [
{ role: "user", content: "inspect the project" },
{
role: "assistant",
content: [
{ type: "reasoning", text: "I should inspect the files.", summary: [] } as never,
{ type: "tool_use", id: "call_1", name: "read_file", input: { path: "README.md" } },
],
},
{
role: "user",
content: [
{ type: "tool_result", tool_use_id: "call_1", content: "# Project" },
{ type: "text", text: "Continue with the result." },
],
},
])) {
// consume
}

expect(createCompletion).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
thinking: { type: "enabled", keep: "all" },
}),
expect.anything(),
)
const request = createCompletion.mock.calls[0][0]
expect(request).not.toHaveProperty("reasoning_effort")
expect(request).not.toHaveProperty("temperature")
expect(request.messages).toContainEqual(
expect.objectContaining({ role: "assistant", reasoning_content: "I should inspect the files." }),
)
expect(request.messages).toContainEqual(
expect.objectContaining({ role: "tool", content: "# Project\n\nContinue with the result." }),
)
expect(request.messages.filter((message) => message.role === "user")).toHaveLength(1)
},
)
})
38 changes: 33 additions & 5 deletions src/api/providers/fetchers/__tests__/kimi-code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe("Kimi Code model discovery", () => {
beforeEach(() => vi.restoreAllMocks())
afterEach(() => vi.useRealTimers())

it("maps official model fields", () => {
it("maps server-reported capacity and display fields without guessing an unknown reasoning protocol", () => {
expect(
mapKimiCodeModel({
id: "kimi-test",
Expand All @@ -15,14 +15,42 @@ describe("Kimi Code model discovery", () => {
}),
).toMatchObject({
contextWindow: 131072,
supportsReasoningEffort: ["low", "high", "max"],
requiredReasoningEffort: true,
reasoningEffort: "max",
supportsReasoningEffort: false,
requiredReasoningEffort: false,
supportsImages: true,
displayName: "Kimi Test",
})
})

it.each(["k3", "k3-256k"])("applies the K3 reasoning-effort profile to %s", (modelId) => {
const mapped = mapKimiCodeModel({ id: modelId, supports_reasoning: true })

expect(mapped).toMatchObject({
maxTokens: 131_072,
supportsReasoningEffort: ["low", "high", "max"],
requiredReasoningEffort: true,
reasoningEffort: "high",
supportsTemperature: false,
preserveReasoning: true,
})
})

it.each(["kimi-for-coding", "kimi-for-coding-highspeed"])(
"applies the K2.7 preserved-thinking profile to %s",
(modelId) => {
const mapped = mapKimiCodeModel({ id: modelId, supports_reasoning: true })

expect(mapped).toMatchObject({
maxTokens: 131_072,
supportsReasoningEffort: false,
requiredReasoningEffort: false,
supportsTemperature: false,
preserveReasoning: true,
})
expect(mapped.reasoningEffort).toBeUndefined()
},
)

it("uses bearer auth for GET /models", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ data: [{ id: "kimi-for-coding", context_length: 262144 }] }), {
Expand Down Expand Up @@ -80,7 +108,7 @@ describe("Kimi Code model discovery", () => {
const models = await getKimiCodeModels("token")
expect(Object.keys(models)).toHaveLength(2)
expect(models["model-a"].contextWindow).toBe(100000)
expect(models["model-b"].supportsReasoningEffort).toEqual(["low", "high", "max"])
expect(models["model-b"].supportsReasoningEffort).toBe(false)
})

it("aborts model discovery after its deadline", async () => {
Expand Down
23 changes: 6 additions & 17 deletions src/api/providers/fetchers/kimi-code.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { z } from "zod"

import {
KIMI_CODE_BASE_URL,
kimiCodeDefaultModelInfo,
kimiCodeModelDefaults,
kimiCodeReasoningEfforts,
type ModelInfo,
type ModelRecord,
} from "@roo-code/types"
import { KIMI_CODE_BASE_URL, getKimiCodeModelInfo, type ModelInfo, type ModelRecord } from "@roo-code/types"

export const kimiCodeModelSchema = z.object({
id: z.string().min(1),
Expand All @@ -23,16 +16,12 @@ const kimiCodeModelsResponseSchema = z.object({ data: z.array(kimiCodeModelSchem
const KIMI_CODE_MODELS_TIMEOUT_MS = 10_000

export function mapKimiCodeModel(model: z.infer<typeof kimiCodeModelSchema>): ModelInfo {
const supportsReasoning = model.supports_reasoning ?? false
const defaults = kimiCodeModelDefaults[model.id] ?? {}
const profile = getKimiCodeModelInfo(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,
supportsImages: model.supports_image_in ?? false,
...profile,
contextWindow: model.context_length ?? profile.contextWindow,
maxTokens: model.max_tokens ?? profile.maxTokens,
supportsImages: model.supports_image_in ?? profile.supportsImages,
displayName: model.display_name,
}
}
Expand Down
33 changes: 31 additions & 2 deletions src/api/providers/kimi-code.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import type OpenAI from "openai"

import {
KIMI_CODE_BASE_URL,
getKimiCodeModelInfo,
getKimiCodeRequestProtocol,
kimiCodeDefaultModelId,
kimiCodeDefaultModelInfo,
providerIdentifiers,
type KimiCodeAuthMethod,
type ModelInfo,
Expand All @@ -14,6 +16,7 @@ import type { ApiHandlerOptions } from "../../shared/api"
import { kimiCodeOAuthManager } from "../../integrations/kimi-code/oauth"

import type { ApiHandlerCreateMessageMetadata } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import type { ApiStream } from "../transform/stream"
import { getModelParams } from "../transform/model-params"

Expand Down Expand Up @@ -111,7 +114,7 @@ export class KimiCodeHandler extends OpenAiHandler {

override getModel() {
const id = this.kimiOptions.apiModelId || kimiCodeDefaultModelId
const info: ModelInfo = this.models[id] ?? kimiCodeDefaultModelInfo
const info: ModelInfo = this.models[id] ?? getKimiCodeModelInfo(id)
const params = getModelParams({
format: "openai",
modelId: id,
Expand All @@ -121,4 +124,30 @@ export class KimiCodeHandler extends OpenAiHandler {
})
return { id, info, ...params }
}

protected override transformChatCompletionRequest<T extends OpenAI.Chat.Completions.ChatCompletionCreateParams>(
requestOptions: T,
): T {
const transformed = { ...requestOptions } as T & {
thinking?: { type: "enabled"; keep: "all" }
}
delete transformed.temperature

const protocol = getKimiCodeRequestProtocol(requestOptions.model)
if (protocol === "reasoning-effort") {
const effort = this.getModel().reasoning?.reasoning_effort
if (effort) transformed.reasoning_effort = effort
} else if (protocol === "thinking") {
delete transformed.reasoning_effort
transformed.thinking = { type: "enabled", keep: "all" }
}

return transformed
}

protected override convertMessagesForRequest(
messages: Anthropic.Messages.MessageParam[],
): OpenAI.Chat.ChatCompletionMessageParam[] {
return convertToOpenAiMessages(messages, { mergeToolResultText: true })
}
}
Loading
Loading