Skip to content

Commit 669402b

Browse files
committed
fix(opencode-go): strip Kimi reasoning replay fields
1 parent fea89cd commit 669402b

4 files changed

Lines changed: 134 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
4545
- Gateway chat: broadcast returned agent-run error payloads after an agent starts so ACP/WebChat clients receive terminal idle-timeout errors. Fixes #84945.
4646
- Dashboard/CLI: allow macOS browser launching through `open` even when SSH environment variables are present, while preserving Linux SSH no-display protection. Fixes #67088. Thanks @theglove44.
4747
- Codex app-server: keep native web search observations out of mirrored chat transcripts while preserving tool progress telemetry. Fixes #85109. Thanks @ugitmebaby.
48+
- OpenCode Go: strip unsupported Kimi reasoning replay fields before provider requests so repeated `kimi-k2.6` turns do not fail schema validation. Fixes #83812. Thanks @Sleeck.
4849
- Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures.
4950
- Doctor/Codex: point native Codex asset warnings at the canonical `openclaw migrate plan codex` preview command. Fixes #84948. Thanks @markoa.
5051
- CLI/models: make `capability model auth logout --agent` remove auth profiles from the selected non-default agent store. Fixes #85092. Thanks @islandpreneur007.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, expect, it } from "vitest";
2+
import { stripOpencodeGoKimiReasoningPayload } from "./reasoning-sanitizer.js";
3+
4+
describe("OpenCode Go Kimi reasoning payload sanitizer", () => {
5+
it("strips unsupported replay reasoning fields from messages and input", () => {
6+
const payload = {
7+
model: "kimi-k2.6",
8+
reasoning_effort: "high",
9+
reasoning: { effort: "high" },
10+
reasoningEffort: "high",
11+
messages: [
12+
{
13+
role: "assistant",
14+
content: [
15+
{ type: "text", text: "done" },
16+
{ type: "thinking", reasoning_details: [{ text: "private thought" }] },
17+
{ type: "redacted_thinking", data: "opaque" },
18+
],
19+
reasoning_details: [{ text: "private thought" }],
20+
reasoning_content: "private thought",
21+
reasoning_text: "private thought",
22+
},
23+
],
24+
input: [
25+
{
26+
role: "assistant",
27+
content: "done",
28+
reasoning_details: [{ text: "private thought" }],
29+
},
30+
{ type: "reasoning", summary: [] },
31+
{
32+
role: "assistant",
33+
content: [{ type: "thinking", reasoning_details: [{ text: "private thought" }] }],
34+
},
35+
],
36+
};
37+
38+
stripOpencodeGoKimiReasoningPayload(payload);
39+
40+
expect(payload).toEqual({
41+
model: "kimi-k2.6",
42+
messages: [
43+
{
44+
role: "assistant",
45+
content: [{ type: "text", text: "done" }],
46+
},
47+
],
48+
input: [
49+
{
50+
role: "assistant",
51+
content: "done",
52+
},
53+
{
54+
role: "assistant",
55+
content: [{ type: "text", text: "[assistant reasoning omitted]" }],
56+
},
57+
],
58+
});
59+
});
60+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const REASONING_REPLAY_FIELDS = [
2+
"reasoning_details",
3+
"reasoning_content",
4+
"reasoning",
5+
"reasoning_text",
6+
] as const;
7+
8+
const OMITTED_ASSISTANT_REASONING_TEXT = "[assistant reasoning omitted]";
9+
10+
function isReasoningReplayPart(value: unknown): boolean {
11+
if (!value || typeof value !== "object") {
12+
return false;
13+
}
14+
const type = (value as { type?: unknown }).type;
15+
return type === "thinking" || type === "redacted_thinking" || type === "reasoning";
16+
}
17+
18+
function stripReasoningReplayFields(value: unknown): void {
19+
if (!value || typeof value !== "object") {
20+
return;
21+
}
22+
23+
const record = value as Record<string, unknown>;
24+
for (const field of REASONING_REPLAY_FIELDS) {
25+
delete record[field];
26+
}
27+
28+
const content = record.content;
29+
if (Array.isArray(content)) {
30+
const nextContent = [];
31+
for (const part of content) {
32+
if (isReasoningReplayPart(part)) {
33+
continue;
34+
}
35+
stripReasoningReplayFields(part);
36+
nextContent.push(part);
37+
}
38+
record.content =
39+
nextContent.length > 0
40+
? nextContent
41+
: [{ type: "text", text: OMITTED_ASSISTANT_REASONING_TEXT }];
42+
}
43+
}
44+
45+
function stripReasoningReplayFieldsFromList(value: unknown): unknown {
46+
if (!Array.isArray(value)) {
47+
return value;
48+
}
49+
const nextItems = [];
50+
51+
for (const item of value) {
52+
if (isReasoningReplayPart(item)) {
53+
continue;
54+
}
55+
stripReasoningReplayFields(item);
56+
nextItems.push(item);
57+
}
58+
return nextItems;
59+
}
60+
61+
export function stripOpencodeGoKimiReasoningPayload(payloadObj: Record<string, unknown>): void {
62+
stripReasoningReplayFields(payloadObj);
63+
delete payloadObj.reasoning_effort;
64+
delete payloadObj.reasoningEffort;
65+
if ("messages" in payloadObj) {
66+
payloadObj.messages = stripReasoningReplayFieldsFromList(payloadObj.messages);
67+
}
68+
if ("input" in payloadObj) {
69+
payloadObj.input = stripReasoningReplayFieldsFromList(payloadObj.input);
70+
}
71+
}

extensions/opencode-go/stream.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
streamWithPayloadPatch,
55
} from "openclaw/plugin-sdk/provider-stream-shared";
66
import { isOpencodeGoKimiNoReasoningModelId } from "./provider-catalog.js";
7+
import { stripOpencodeGoKimiReasoningPayload } from "./reasoning-sanitizer.js";
78

89
function isOpencodeGoDeepSeekV4ModelId(modelId: unknown): boolean {
910
return modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro";
@@ -22,9 +23,7 @@ export function createOpencodeGoDeepSeekV4Wrapper(
2223
}
2324

2425
function stripReasoningParams(payloadObj: Record<string, unknown>): void {
25-
delete payloadObj.reasoning;
26-
delete payloadObj.reasoning_effort;
27-
delete payloadObj.reasoningEffort;
26+
stripOpencodeGoKimiReasoningPayload(payloadObj);
2827
}
2928

3029
export function createOpencodeGoKimiNoReasoningWrapper(

0 commit comments

Comments
 (0)