Enable tool streaming for newer Z.ai models (#2732)

This commit is contained in:
Kao Félix
2026-03-31 13:28:24 +01:00
committed by GitHub
parent d86122cbd3
commit 758ede4da0
5 changed files with 183 additions and 20 deletions

View File

@@ -55,6 +55,7 @@ const COPILOT_STATIC_HEADERS = {
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);
async function fetchOpenRouterModels(): Promise<Model<any>[]> {
try {
@@ -379,28 +380,29 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
for (const [modelId, model] of Object.entries(data.zai.models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
const supportsImage = m.modalities?.input?.includes("image")
const supportsImage = m.modalities?.input?.includes("image");
models.push({
id: modelId,
name: m.name || modelId,
api: "openai-completions",
provider: "zai",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
reasoning: m.reasoning === true,
input: supportsImage ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
cacheRead: m.cost?.cache_read || 0,
cacheWrite: m.cost?.cache_write || 0,
},
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
},
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
id: modelId,
name: m.name || modelId,
api: "openai-completions",
provider: "zai",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
reasoning: m.reasoning === true,
input: supportsImage ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
cacheRead: m.cost?.cache_read || 0,
cacheWrite: m.cost?.cache_write || 0,
},
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}),
},
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
});
}
}

View File

@@ -395,6 +395,9 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio
if (context.tools) {
params.tools = convertTools(context.tools, compat);
if (compat.zaiToolStream) {
(params as any).tool_stream = true;
}
} else if (hasToolHistory(context.messages)) {
// Anthropic (via LiteLLM/proxy) requires tools param when conversation has tool_calls/tool_results
params.tools = [];
@@ -835,6 +838,7 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
: "openai",
openRouterRouting: {},
vercelGatewayRouting: {},
zaiToolStream: false,
supportsStrictMode: true,
};
}
@@ -861,6 +865,7 @@ function getCompat(model: Model<"openai-completions">): Required<OpenAICompletio
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
openRouterRouting: model.compat.openRouterRouting ?? {},
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
};
}

View File

@@ -277,6 +277,8 @@ export interface OpenAICompletionsCompat {
openRouterRouting?: OpenRouterRouting;
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
vercelGatewayRouting?: VercelGatewayRouting;
/** Whether z.ai supports top-level `tool_stream: true` for streaming tool call deltas. Default: false. */
zaiToolStream?: boolean;
/** Whether the provider supports the `strict` field in tool definitions. Default: true. */
supportsStrictMode?: boolean;
}

View File

@@ -200,6 +200,159 @@ describe("openai-completions tool_choice", () => {
expect(params.reasoning_effort).toBe("medium");
});
it("enables tool_stream for supported z.ai models with tools", async () => {
const model = getModel("zai", "glm-5")!;
const tools: Tool[] = [
{
name: "ping",
description: "Ping tool",
parameters: Type.Object({
ok: Type.Boolean(),
}),
},
];
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Call ping with ok=true",
timestamp: Date.now(),
},
],
tools,
},
{
apiKey: "test",
onPayload: (params: unknown) => {
payload = params;
},
},
).result();
const params = (payload ?? mockState.lastParams) as { tool_stream?: boolean };
expect(params.tool_stream).toBe(true);
});
it("stores z.ai tool_stream support in model compat metadata", () => {
expect(getModel("zai", "glm-5")?.compat?.zaiToolStream).toBe(true);
expect(getModel("zai", "glm-4.7")?.compat?.zaiToolStream).toBe(true);
expect(getModel("zai", "glm-4.7-flash")?.compat?.zaiToolStream).toBe(true);
expect(getModel("zai", "glm-4.6v")?.compat?.zaiToolStream).toBe(true);
expect(getModel("zai", "glm-4.5-air")?.compat?.zaiToolStream).toBeUndefined();
});
it("omits tool_stream for unsupported z.ai models", async () => {
const model = getModel("zai", "glm-4.5-air")!;
const tools: Tool[] = [
{
name: "ping",
description: "Ping tool",
parameters: Type.Object({
ok: Type.Boolean(),
}),
},
];
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Call ping with ok=true",
timestamp: Date.now(),
},
],
tools,
},
{
apiKey: "test",
onPayload: (params: unknown) => {
payload = params;
},
},
).result();
const params = (payload ?? mockState.lastParams) as { tool_stream?: boolean };
expect(params.tool_stream).toBeUndefined();
});
it("respects explicit z.ai tool_stream compat override", async () => {
const baseModel = getModel("zai", "glm-4.5-air")!;
const model = {
...baseModel,
compat: {
...baseModel.compat,
zaiToolStream: true,
},
} as const;
const tools: Tool[] = [
{
name: "ping",
description: "Ping tool",
parameters: Type.Object({
ok: Type.Boolean(),
}),
},
];
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Call ping with ok=true",
timestamp: Date.now(),
},
],
tools,
},
{
apiKey: "test",
onPayload: (params: unknown) => {
payload = params;
},
},
).result();
const params = (payload ?? mockState.lastParams) as { tool_stream?: boolean };
expect(params.tool_stream).toBe(true);
});
it("omits tool_stream when no tools are provided", async () => {
const model = getModel("zai", "glm-5")!;
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Hi",
timestamp: Date.now(),
},
],
},
{
apiKey: "test",
onPayload: (params: unknown) => {
payload = params;
},
},
).result();
const params = (payload ?? mockState.lastParams) as { tool_stream?: boolean };
expect(params.tool_stream).toBeUndefined();
});
it("maps non-standard provider finish_reason values to stopReason error", async () => {
mockState.chunks = [
{

View File

@@ -32,6 +32,7 @@ const compat: Required<OpenAICompletionsCompat> = {
thinkingFormat: "openai",
openRouterRouting: {},
vercelGatewayRouting: {},
zaiToolStream: false,
supportsStrictMode: true,
};