mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 01:51:21 +08:00
Allow reasoning for Claude models on the Kilo gateway and restore preserved reasoning
This commit is contained in:
@@ -72,29 +72,6 @@ export namespace ProviderTransform {
|
||||
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
|
||||
}
|
||||
|
||||
// kilocode_change - skip toolCallId normalization for OpenRouter/Kilo Gateway
|
||||
// OpenRouter handles tool call IDs differently and modifying messages can break
|
||||
// thinking/redacted_thinking blocks which must remain unchanged per Anthropic API
|
||||
if (
|
||||
model.api.id.includes("claude") &&
|
||||
model.api.npm !== "@openrouter/ai-sdk-provider" &&
|
||||
model.api.npm !== "@kilocode/kilo-gateway"
|
||||
) {
|
||||
return msgs.map((msg) => {
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
|
||||
return {
|
||||
...part,
|
||||
toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
}
|
||||
return msg
|
||||
})
|
||||
}
|
||||
if (
|
||||
model.providerID === "mistral" ||
|
||||
model.api.id.toLowerCase().includes("mistral") ||
|
||||
@@ -257,81 +234,38 @@ export namespace ProviderTransform {
|
||||
})
|
||||
}
|
||||
|
||||
// kilocode_change - function added
|
||||
function fixDuplicateReasoning(msgs: ModelMessage[]) {
|
||||
for (const msg of msgs) {
|
||||
if (!Array.isArray(msg.content)) {
|
||||
continue
|
||||
}
|
||||
let isFirstToolCall = true
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "reasoning") {
|
||||
// this entry is corrupt
|
||||
delete part.providerOptions?.openrouter?.reasoning_details
|
||||
}
|
||||
if (part.type === "tool-call" && isFirstToolCall) {
|
||||
isFirstToolCall = false
|
||||
continue
|
||||
}
|
||||
if (part.type == "tool-call") {
|
||||
// this is a duplicate entry
|
||||
delete part.providerOptions?.openrouter?.reasoning_details
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
|
||||
msgs = unsupportedParts(msgs, model)
|
||||
msgs = normalizeMessages(msgs, model, options)
|
||||
|
||||
// kilocode_change - identify OpenRouter/Kilo Gateway for thinking block stripping
|
||||
const isOpenRouterOrKilo =
|
||||
model.api.npm === "@openrouter/ai-sdk-provider" || model.api.npm === "@kilocode/kilo-gateway"
|
||||
|
||||
// kilocode_change - strip thinking/reasoning blocks for OpenRouter/Kilo Gateway
|
||||
// Anthropic's API requires thinking blocks to be EXACTLY unchanged, but our storage
|
||||
// reconstructs them which counts as modification. Stripping them is safe because
|
||||
// the reasoning was already shown to the user and doesn't need to be sent back.
|
||||
if (isOpenRouterOrKilo) {
|
||||
// Helper to strip reasoning-related data from provider options/metadata
|
||||
const stripReasoningData = (opts: Record<string, any> | undefined) => {
|
||||
if (!opts) return undefined // Return undefined instead of empty object to clean up
|
||||
const result = { ...opts }
|
||||
// Strip from openrouter namespace
|
||||
if (result.openrouter) {
|
||||
result.openrouter = { ...result.openrouter }
|
||||
delete result.openrouter.reasoning_details
|
||||
delete result.openrouter.reasoning
|
||||
delete result.openrouter.thinking
|
||||
}
|
||||
// Strip from kilo namespace
|
||||
if (result.kilo) {
|
||||
result.kilo = { ...result.kilo }
|
||||
delete result.kilo.reasoning_details
|
||||
delete result.kilo.reasoning
|
||||
delete result.kilo.thinking
|
||||
}
|
||||
// Strip from anthropic namespace
|
||||
if (result.anthropic) {
|
||||
result.anthropic = { ...result.anthropic }
|
||||
delete result.anthropic.thinking
|
||||
delete result.anthropic.reasoning
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
msgs = msgs.flatMap((msg): ModelMessage[] => {
|
||||
// Handle string content (just strip metadata)
|
||||
if (!Array.isArray(msg.content)) {
|
||||
const result = { ...msg, providerOptions: stripReasoningData(msg.providerOptions) }
|
||||
if ("experimental_providerMetadata" in msg) {
|
||||
;(result as any).experimental_providerMetadata = stripReasoningData(
|
||||
(msg as any).experimental_providerMetadata,
|
||||
)
|
||||
}
|
||||
return [result]
|
||||
}
|
||||
// Filter out reasoning parts from content and strip metadata from remaining parts
|
||||
const filtered = msg.content
|
||||
.filter(
|
||||
(part: any) => part.type !== "thinking" && part.type !== "redacted_thinking" && part.type !== "reasoning",
|
||||
)
|
||||
.map((part: any) => ({
|
||||
...part,
|
||||
providerOptions: stripReasoningData(part.providerOptions),
|
||||
providerMetadata: stripReasoningData(part.providerMetadata),
|
||||
experimental_providerMetadata: stripReasoningData(part.experimental_providerMetadata),
|
||||
}))
|
||||
|
||||
// Providers may reject empty array content; drop empty messages.
|
||||
if (filtered.length === 0) return []
|
||||
|
||||
// Also strip from message-level options/metadata
|
||||
const result = { ...msg, content: filtered, providerOptions: stripReasoningData(msg.providerOptions) }
|
||||
if ("experimental_providerMetadata" in msg) {
|
||||
;(result as any).experimental_providerMetadata = stripReasoningData(
|
||||
(msg as any).experimental_providerMetadata,
|
||||
)
|
||||
}
|
||||
return [result as ModelMessage]
|
||||
})
|
||||
// kilocode_change - workaround for @openrouter/ai-sdk-provider v1 duplicating reasoning
|
||||
// fixed in https://github.com/OpenRouterTeam/ai-sdk-provider/pull/344/
|
||||
if (model.api.npm === "@kilocode/kilo-gateway") {
|
||||
fixDuplicateReasoning(msgs)
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -448,57 +382,19 @@ export namespace ProviderTransform {
|
||||
|
||||
// kilocode_change start
|
||||
case "@kilocode/kilo-gateway":
|
||||
// kilocode_change - adaptive thinking with effort levels
|
||||
// TODO: Enable when @ai-sdk/anthropic supports thinking.type: "adaptive"
|
||||
const ADAPTIVE_THINKING_ENABLED = false
|
||||
if (ADAPTIVE_THINKING_ENABLED && id.includes("claude-opus-4-6")) {
|
||||
if (model.id.includes("claude")) {
|
||||
// for models that support adaptive thinking, effort is ignored
|
||||
// for models that don't support adaptive thinking, effort is translated into a token budget
|
||||
return {
|
||||
low: {
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort: "low" },
|
||||
},
|
||||
medium: {
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort: "medium" },
|
||||
},
|
||||
high: {
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort: "high" },
|
||||
},
|
||||
max: {
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort: "max" },
|
||||
},
|
||||
none: { reasoning: { enabled: false } },
|
||||
low: { reasoning: { enabled: true, effort: "low" }, verbosity: "low" },
|
||||
medium: { reasoning: { enabled: true, effort: "medium" }, verbosity: "medium" },
|
||||
high: { reasoning: { enabled: true, effort: "high" }, verbosity: "high" },
|
||||
max: { reasoning: { enabled: true, effort: "xhigh" }, verbosity: "max" },
|
||||
}
|
||||
}
|
||||
// kilocode_change - Claude models via Kilo Gateway: no reasoning variants
|
||||
// (reasoning is broken due to OpenRouter SDK duplicating reasoning_details)
|
||||
if (
|
||||
model.id.includes("claude") ||
|
||||
model.id.includes("anthropic") ||
|
||||
model.api.id.includes("claude") ||
|
||||
model.api.id.includes("anthropic")
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
// GPT models via Kilo need encrypted reasoning content to avoid org_id mismatch
|
||||
if (!model.id.includes("gpt") && !model.id.includes("gemini-3")) return {}
|
||||
// kilocode_change - Codex models use object-based reasoning format for OpenRouter
|
||||
// OpenRouter expects { reasoning: { effort: "high" } } format
|
||||
// See: https://openrouter.ai/docs/api/api-reference/chat/send-chat-completion-request#request.body.reasoning
|
||||
if (model.id.includes("codex")) {
|
||||
return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
|
||||
}
|
||||
return Object.fromEntries(
|
||||
OPENAI_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
]),
|
||||
)
|
||||
return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
|
||||
// kilocode_change end
|
||||
|
||||
// TODO: YOU CANNOT SET max_tokens if this is set!!!
|
||||
@@ -842,8 +738,7 @@ export namespace ProviderTransform {
|
||||
result["textVerbosity"] = "low"
|
||||
}
|
||||
|
||||
// kilocode_change - include kilo provider for encrypted reasoning content
|
||||
if (input.model.providerID.startsWith("opencode") || input.model.api.npm === "@kilocode/kilo-gateway") {
|
||||
if (input.model.providerID.startsWith("opencode")) {
|
||||
result["promptCacheKey"] = input.sessionID
|
||||
result["include"] = ["reasoning.encrypted_content"]
|
||||
result["reasoningSummary"] = "auto"
|
||||
|
||||
Reference in New Issue
Block a user