Skip to content

feat: support Z.AI tool_stream for real-time tool call streaming#18173

Merged
steipete merged 1 commit intoopenclaw:mainfrom
tianxiao1430-jpg:feat/zai-tool-stream-support
Feb 16, 2026
Merged

feat: support Z.AI tool_stream for real-time tool call streaming#18173
steipete merged 1 commit intoopenclaw:mainfrom
tianxiao1430-jpg:feat/zai-tool-stream-support

Conversation

@tianxiao1430-jpg
Copy link

@tianxiao1430-jpg tianxiao1430-jpg commented Feb 16, 2026

Summary

Add support for Z.AI's tool_stream parameter to enable real-time visibility into model reasoning and tool call execution.

Changes

  • Automatically inject tool_stream: true for zai/z-ai providers
  • Allow disabling via params.tool_stream: false in model config
  • Follows existing pattern of createOpenRouterHeadersWrapper and createOpenAIResponsesStoreWrapper

Technical Details

Z.AI's API supports the tool_stream parameter (along with stream: true) to return progressive tool_calls deltas during streaming. This allows users to see:

  • Real-time reasoning content (reasoning_content)
  • Progressive tool call execution (tool_calls delta)
  • Intermediate thinking steps

Reference: https://docs.z.ai/api-reference#streaming

Implementation

Uses the existing onPayload callback mechanism to inject the parameter before the API request is sent, following the same pattern as the OpenAI Responses store wrapper.

Testing

  • Code follows existing patterns in the codebase
  • Added unit test file for validation
  • Manual testing with Z.AI API pending

AI Assistance

🤖 This implementation was written with Claude assistance (OpenClaw agent). The code has been reviewed to ensure it:

  • Matches existing code patterns
  • Follows OpenClaw's contribution guidelines
  • Is safe and correct

Closes #18135

Checklist

  • Mark as AI-assisted
  • Note testing level: lightly tested (code review + pattern matching)
  • I understand what the code does
  • Follows CONTRIBUTING.md guidelines

Greptile Summary

Adds Z.AI tool_stream support by injecting tool_stream: true into the API payload for zai/z-ai providers, following the existing onPayload wrapper pattern used by createOpenAIResponsesStoreWrapper. The feature is enabled by default and can be disabled via model config params.tool_stream: false.

  • The production code in extra-params.ts is functionally correct and follows established patterns
  • The enabled parameter on createZaiToolStreamWrapper is dead code — the caller already checks toolStreamEnabled before invoking the wrapper, so it is always called with true
  • The tests don't verify core behavior: none of the three test cases invoke the wrapped streamFn or check that tool_stream: true is actually injected into payloads. Compare with the existing e2e tests in pi-embedded-runner-extraparams.e2e.test.ts which properly exercise the payload mutation
  • The "disable" test has a misleading comment and doesn't actually verify that tool_stream is absent from the payload when disabled
  • Unused type imports (Context, Model) in the test file

Confidence Score: 3/5

  • The production code is safe to merge but the tests provide minimal actual coverage of the new behavior.
  • The implementation itself follows established patterns correctly and is low-risk — it only mutates payloads for Z.AI providers. However, the test suite doesn't verify the core payload mutation behavior, has misleading comments, and unused imports. The tests would pass even if the wrapper logic was completely broken, since they only check that streamFn is defined/wrapped (which always holds due to the OpenAI Responses wrapper).
  • src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.ts needs 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:

  • Context from dashboard - CLAUDE.md (source)

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
@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: S labels Feb 16, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +184 to +185
function createZaiToolStreamWrapper(
baseStreamFn: StreamFn | undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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.

Comment on lines +15 to +44
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", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused type imports
Context and Model are imported but never used in any of the test cases.

Suggested change
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.

Comment on lines +84 to +86
},
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@steipete steipete merged commit edbc68e into openclaw:main Feb 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support Z.AI Stream Tool Call (tool_stream)

2 participants