feat: support Z.AI tool_stream for real-time tool call streaming#18173
Conversation
Add support for Z.AI's native tool_stream parameter to enable real-time visibility into model reasoning and tool call execution. - Automatically inject tool_stream=true for zai/z-ai providers - Allow disabling via params.tool_stream: false in model config - Follows existing pattern of OpenRouter and OpenAI wrappers This enables Z.AI API features described in: https://docs.z.ai/api-reference#streaming AI-assisted: Claude (OpenClaw agent) helped write this implementation. Testing: lightly tested (code review + pattern matching existing wrappers) Closes openclaw#18135
| function createZaiToolStreamWrapper( | ||
| baseStreamFn: StreamFn | undefined, |
There was a problem hiding this comment.
Dead enabled parameter
The enabled parameter is always true at the only call site (line 251), since the caller already checks toolStreamEnabled before invoking the wrapper. The if (!enabled) branch (lines 191-193) is unreachable dead code. Consider removing the parameter and the early-return branch to simplify the function, or alternatively move the toolStreamEnabled check inside the wrapper (similar to how createOpenAIResponsesStoreWrapper handles its shouldForceResponsesStore check internally).
| function createZaiToolStreamWrapper( | |
| baseStreamFn: StreamFn | undefined, | |
| function createZaiToolStreamWrapper( | |
| baseStreamFn: StreamFn | undefined, | |
| ): StreamFn { |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/extra-params.ts
Line: 184:185
Comment:
**Dead `enabled` parameter**
The `enabled` parameter is always `true` at the only call site (line 251), since the caller already checks `toolStreamEnabled` before invoking the wrapper. The `if (!enabled)` branch (lines 191-193) is unreachable dead code. Consider removing the parameter and the early-return branch to simplify the function, or alternatively move the `toolStreamEnabled` check inside the wrapper (similar to how `createOpenAIResponsesStoreWrapper` handles its `shouldForceResponsesStore` check internally).
```suggestion
function createZaiToolStreamWrapper(
baseStreamFn: StreamFn | undefined,
): StreamFn {
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| it("should inject tool_stream=true for zai provider by default", () => { | ||
| const capturedPayloads: unknown[] = []; | ||
| const mockStreamFn: StreamFn = vi.fn((model, context, options) => { | ||
| // Capture the payload that would be sent | ||
| options?.onPayload?.({ model: model.id, messages: [] }); | ||
| return { | ||
| push: vi.fn(), | ||
| result: vi.fn().mockResolvedValue({ | ||
| role: "assistant", | ||
| content: [{ type: "text", text: "ok" }], | ||
| stopReason: "stop", | ||
| }), | ||
| } as any; | ||
| }); | ||
|
|
||
| const agent = { streamFn: mockStreamFn }; | ||
| const cfg = { | ||
| agents: { | ||
| defaults: {}, | ||
| }, | ||
| }; | ||
|
|
||
| applyExtraParamsToAgent(agent, cfg as any, "zai", "glm-5"); | ||
|
|
||
| // The streamFn should be wrapped | ||
| expect(agent.streamFn).toBeDefined(); | ||
| expect(agent.streamFn).not.toBe(mockStreamFn); | ||
| }); | ||
|
|
||
| it("should not inject tool_stream for non-zai providers", () => { |
There was a problem hiding this comment.
Tests don't verify payload mutation
This test declares capturedPayloads (line 16) but never uses it. More importantly, none of the three tests actually invoke the wrapped streamFn and verify that tool_stream: true gets injected into the payload. Compare with the existing e2e tests in pi-embedded-runner-extraparams.e2e.test.ts, which call agent.streamFn(...) and then assert on the mutated payload (e.g., expect(payload.store).toBe(true)).
As written, the first test only checks agent.streamFn !== mockStreamFn (which is true simply because createOpenAIResponsesStoreWrapper always wraps at the end), and the third test only checks agent.streamFn is defined. Consider invoking the wrapped function and verifying the payload, e.g.:
const payload: Record<string, unknown> = { model: "glm-5" };
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload);
return new AssistantMessageEventStream();
};
const agent = { streamFn: baseStreamFn };
applyExtraParamsToAgent(agent, cfg as any, "zai", "glm-5");
void agent.streamFn?.(model, { messages: [] }, {});
expect(payload.tool_stream).toBe(true);Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.ts
Line: 15:44
Comment:
**Tests don't verify payload mutation**
This test declares `capturedPayloads` (line 16) but never uses it. More importantly, none of the three tests actually invoke the wrapped `streamFn` and verify that `tool_stream: true` gets injected into the payload. Compare with the existing e2e tests in `pi-embedded-runner-extraparams.e2e.test.ts`, which call `agent.streamFn(...)` and then assert on the mutated payload (e.g., `expect(payload.store).toBe(true)`).
As written, the first test only checks `agent.streamFn !== mockStreamFn` (which is true simply because `createOpenAIResponsesStoreWrapper` always wraps at the end), and the third test only checks `agent.streamFn` is defined. Consider invoking the wrapped function and verifying the payload, e.g.:
```ts
const payload: Record<string, unknown> = { model: "glm-5" };
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload);
return new AssistantMessageEventStream();
};
const agent = { streamFn: baseStreamFn };
applyExtraParamsToAgent(agent, cfg as any, "zai", "glm-5");
void agent.streamFn?.(model, { messages: [] }, {});
expect(payload.tool_stream).toBe(true);
```
How can I resolve this? If you propose a fix, please make it concise.| @@ -0,0 +1,94 @@ | |||
| import { describe, expect, it, vi } from "vitest"; | |||
| import type { StreamFn } from "@mariozechner/pi-agent-core"; | |||
| import type { Context, Model } from "@mariozechner/pi-ai"; | |||
There was a problem hiding this comment.
Unused type imports
Context and Model are imported but never used in any of the test cases.
| import type { Context, Model } from "@mariozechner/pi-ai"; | |
| import type { StreamFn } from "@mariozechner/pi-agent-core"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.ts
Line: 3:3
Comment:
**Unused type imports**
`Context` and `Model` are imported but never used in any of the test cases.
```suggestion
import type { StreamFn } from "@mariozechner/pi-agent-core";
```
How can I resolve this? If you propose a fix, please make it concise.| }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Misleading test comment
The comment says "The tool_stream wrapper should be applied but with enabled=false" but this is incorrect. When tool_stream: false is in the config, the wrapper is never applied at all (the if (toolStreamEnabled) check on line 249 of extra-params.ts prevents it). The test should verify that tool_stream is not injected into the payload when disabled, not just that streamFn is defined (which it always will be due to the OpenAI Responses wrapper).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.ts
Line: 84:86
Comment:
**Misleading test comment**
The comment says "The tool_stream wrapper should be applied but with enabled=false" but this is incorrect. When `tool_stream: false` is in the config, the wrapper is never applied at all (the `if (toolStreamEnabled)` check on line 249 of `extra-params.ts` prevents it). The test should verify that `tool_stream` is *not* injected into the payload when disabled, not just that `streamFn` is defined (which it always will be due to the OpenAI Responses wrapper).
How can I resolve this? If you propose a fix, please make it concise.
Summary
Add support for Z.AI's
tool_streamparameter to enable real-time visibility into model reasoning and tool call execution.Changes
tool_stream: trueforzai/z-aiprovidersparams.tool_stream: falsein model configcreateOpenRouterHeadersWrapperandcreateOpenAIResponsesStoreWrapperTechnical Details
Z.AI's API supports the
tool_streamparameter (along withstream: true) to return progressivetool_callsdeltas during streaming. This allows users to see:reasoning_content)tool_callsdelta)Reference: https://docs.z.ai/api-reference#streaming
Implementation
Uses the existing
onPayloadcallback mechanism to inject the parameter before the API request is sent, following the same pattern as the OpenAI Responses store wrapper.Testing
AI Assistance
🤖 This implementation was written with Claude assistance (OpenClaw agent). The code has been reviewed to ensure it:
Closes #18135
Checklist
Greptile Summary
Adds Z.AI
tool_streamsupport by injectingtool_stream: trueinto the API payload forzai/z-aiproviders, following the existingonPayloadwrapper pattern used bycreateOpenAIResponsesStoreWrapper. The feature is enabled by default and can be disabled via model configparams.tool_stream: false.extra-params.tsis functionally correct and follows established patternsenabledparameter oncreateZaiToolStreamWrapperis dead code — the caller already checkstoolStreamEnabledbefore invoking the wrapper, so it is always called withtruestreamFnor check thattool_stream: trueis actually injected into payloads. Compare with the existing e2e tests inpi-embedded-runner-extraparams.e2e.test.tswhich properly exercise the payload mutationtool_streamis absent from the payload when disabledContext,Model) in the test fileConfidence Score: 3/5
streamFnis defined/wrapped (which always holds due to the OpenAI Responses wrapper).src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.tsneeds significant rework to actually verify payload mutation behavior.Last reviewed commit: aba438e
(2/5) Greptile learns from your feedback when you react with thumbs up/down!
Context used:
dashboard- CLAUDE.md (source)