Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.
Closed
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
181 changes: 38 additions & 143 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") ||
Expand Down Expand Up @@ -257,81 +234,38 @@ export namespace ProviderTransform {
})
}

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
// 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
}
// Strip from anthropic namespace
if (result.anthropic) {
result.anthropic = { ...result.anthropic }
delete result.anthropic.thinking
delete result.anthropic.reasoning
if (part.type == "tool-call") {
// this is a duplicate entry
delete part.providerOptions?.openrouter?.reasoning_details
}
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]
})
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
msgs = unsupportedParts(msgs, model)
msgs = normalizeMessages(msgs, model, options)

// 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 (
Expand Down Expand Up @@ -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!!!
Expand Down Expand Up @@ -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"
Expand Down
Loading