Skip to content

Commit 68bfdb4

Browse files
committed
fix(agents): repair codex responses tool args (#75281)
1 parent edca8c7 commit 68bfdb4

3 files changed

Lines changed: 93 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
3939
- Model commands: clarify direct and inline `/model` acknowledgements for non-default selections as session-scoped. Thanks @addu2612.
4040
- Doctor/gateway: stop warning that non-existent, unconfigured user-bin directories are required in the Gateway service PATH. Fixes #76017. Thanks @xiphis.
4141
- TUI/chat: skip full provider model normalization during context-window warmup while preserving provider-owned context metadata, avoiding cold-start stalls with large model registries. Thanks @547895019.
42+
- Agents: enable malformed tool-call argument repair for Codex and Azure OpenAI Responses transports while keeping generic OpenAI Responses paths out of the repair gate. Fixes #75154. Thanks @Nimraakram22.
4243
- Memory Wiki: accept relative Markdown links that include the `.md` suffix during broken-wikilink validation, avoiding false positives for native render-mode links. Thanks @Kenneth8128.
4344
- OpenAI Codex: show the device-pairing code in the interactive SSH/headless prompt while keeping the short-lived code out of persistent runtime logs. Fixes #74212. Thanks @da22le123.
4445
- QA Lab: stop gateway children when the suite parent disappears, so interrupted local QA runs cannot leave hot orphaned gateways behind.

src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.test.ts

Lines changed: 84 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -83,63 +83,94 @@ describe("shouldRepairMalformedToolCallArguments", () => {
8383
}),
8484
).toBe(false);
8585
});
86+
87+
it("does not enable the repair for direct OpenAI responses", () => {
88+
expect(
89+
shouldRepairMalformedToolCallArguments({
90+
provider: "openai",
91+
modelApi: "openai-responses",
92+
}),
93+
).toBe(false);
94+
});
95+
96+
it("enables the repair for Codex and Azure Responses transports", () => {
97+
expect(
98+
shouldRepairMalformedToolCallArguments({
99+
provider: "openai-codex",
100+
modelApi: "openai-codex-responses",
101+
}),
102+
).toBe(true);
103+
expect(
104+
shouldRepairMalformedToolCallArguments({
105+
provider: "azure-openai-responses",
106+
modelApi: "azure-openai-responses",
107+
}),
108+
).toBe(true);
109+
});
86110
});
87111

88112
describe("openai-completions malformed tool-call argument repair", () => {
89-
it("repairs fragmented OpenAI-compatible function-call args before tool execution", async () => {
90-
const partialToolCall = { type: "functionCall", name: "read", arguments: {} };
91-
const streamedToolCall = { type: "functionCall", name: "read", arguments: {} };
92-
const endMessageToolCall = { type: "functionCall", name: "read", arguments: {} };
93-
const finalToolCall = { type: "functionCall", name: "read", arguments: {} };
94-
const partialMessage = { role: "assistant", content: [partialToolCall] };
95-
const endMessage = { role: "assistant", content: [endMessageToolCall] };
96-
const finalMessage = { role: "assistant", content: [finalToolCall] };
113+
it.each([
114+
["openai-completions", "sglang"],
115+
["openai-codex-responses", "openai-codex"],
116+
["azure-openai-responses", "azure-openai-responses"],
117+
])(
118+
"repairs fragmented %s function-call args before tool execution",
119+
async (modelApi, provider) => {
120+
const partialToolCall = { type: "functionCall", name: "read", arguments: {} };
121+
const streamedToolCall = { type: "functionCall", name: "read", arguments: {} };
122+
const endMessageToolCall = { type: "functionCall", name: "read", arguments: {} };
123+
const finalToolCall = { type: "functionCall", name: "read", arguments: {} };
124+
const partialMessage = { role: "assistant", content: [partialToolCall] };
125+
const endMessage = { role: "assistant", content: [endMessageToolCall] };
126+
const finalMessage = { role: "assistant", content: [finalToolCall] };
97127

98-
const stream = await invokeProviderStream({
99-
provider: "sglang",
100-
modelApi: "openai-completions",
101-
baseFn: () =>
102-
createFakeStream({
103-
events: [
104-
{
105-
type: "toolcall_delta",
106-
contentIndex: 0,
107-
delta: ".functions.read:0 ",
108-
partial: partialMessage,
109-
},
110-
{
111-
type: "toolcall_delta",
112-
contentIndex: 0,
113-
delta: '{"path":"/tmp/report.txt"',
114-
partial: partialMessage,
115-
},
116-
{
117-
type: "toolcall_delta",
118-
contentIndex: 0,
119-
delta: "}x",
120-
partial: partialMessage,
121-
},
122-
{
123-
type: "toolcall_end",
124-
contentIndex: 0,
125-
toolCall: streamedToolCall,
126-
partial: partialMessage,
127-
message: endMessage,
128-
},
129-
],
130-
resultMessage: finalMessage,
131-
}),
132-
});
128+
const stream = await invokeProviderStream({
129+
provider,
130+
modelApi,
131+
baseFn: () =>
132+
createFakeStream({
133+
events: [
134+
{
135+
type: "toolcall_delta",
136+
contentIndex: 0,
137+
delta: ".functions.read:0 ",
138+
partial: partialMessage,
139+
},
140+
{
141+
type: "toolcall_delta",
142+
contentIndex: 0,
143+
delta: '{"path":"/tmp/report.txt"',
144+
partial: partialMessage,
145+
},
146+
{
147+
type: "toolcall_delta",
148+
contentIndex: 0,
149+
delta: "}x",
150+
partial: partialMessage,
151+
},
152+
{
153+
type: "toolcall_end",
154+
contentIndex: 0,
155+
toolCall: streamedToolCall,
156+
partial: partialMessage,
157+
message: endMessage,
158+
},
159+
],
160+
resultMessage: finalMessage,
161+
}),
162+
});
133163

134-
for await (const _item of stream) {
135-
// drain
136-
}
137-
const result = await stream.result();
164+
for await (const _item of stream) {
165+
// drain
166+
}
167+
const result = await stream.result();
138168

139-
expect(partialToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
140-
expect(streamedToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
141-
expect(endMessageToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
142-
expect(finalToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
143-
expect(result).toBe(finalMessage);
144-
});
169+
expect(partialToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
170+
expect(streamedToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
171+
expect(endMessageToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
172+
expect(finalToolCall.arguments).toEqual({ path: "/tmp/report.txt" });
173+
expect(result).toBe(finalMessage);
174+
},
175+
);
145176
});

src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const MAX_TOOLCALL_REPAIR_LEADING_CHARS = 96;
1818
const MAX_TOOLCALL_REPAIR_TRAILING_CHARS = 3;
1919
const TOOLCALL_REPAIR_ALLOWED_LEADING_RE = /^[a-z0-9\s"'`.:/_\\-]+$/i;
2020
const TOOLCALL_REPAIR_ALLOWED_TRAILING_RE = /^[^\s{}[\]":,\\]{1,3}$/;
21+
const TOOLCALL_REPAIR_RESPONSES_APIS = new Set([
22+
"azure-openai-responses",
23+
"openai-codex-responses",
24+
]);
2125

2226
function shouldAttemptMalformedToolCallRepair(partialJson: string, delta: string): boolean {
2327
if (/[}\]]/.test(delta)) {
@@ -298,10 +302,11 @@ export function shouldRepairMalformedToolCallArguments(params: {
298302
provider?: string;
299303
modelApi?: string | null;
300304
}): boolean {
305+
const modelApi = params.modelApi ?? "";
301306
return (
302-
(normalizeProviderId(params.provider ?? "") === "kimi" &&
303-
params.modelApi === "anthropic-messages") ||
304-
params.modelApi === "openai-completions"
307+
(normalizeProviderId(params.provider ?? "") === "kimi" && modelApi === "anthropic-messages") ||
308+
modelApi === "openai-completions" ||
309+
TOOLCALL_REPAIR_RESPONSES_APIS.has(modelApi)
305310
);
306311
}
307312

0 commit comments

Comments
 (0)