Enable tool streaming for newer Z.ai models (#2732)
This commit is contained in:
@@ -55,6 +55,7 @@ const COPILOT_STATIC_HEADERS = {
|
|||||||
|
|
||||||
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
|
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
|
||||||
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
|
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>[]> {
|
async function fetchOpenRouterModels(): Promise<Model<any>[]> {
|
||||||
try {
|
try {
|
||||||
@@ -379,28 +380,29 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
for (const [modelId, model] of Object.entries(data.zai.models)) {
|
for (const [modelId, model] of Object.entries(data.zai.models)) {
|
||||||
const m = model as ModelsDevModel;
|
const m = model as ModelsDevModel;
|
||||||
if (m.tool_call !== true) continue;
|
if (m.tool_call !== true) continue;
|
||||||
const supportsImage = m.modalities?.input?.includes("image")
|
const supportsImage = m.modalities?.input?.includes("image");
|
||||||
|
|
||||||
models.push({
|
models.push({
|
||||||
id: modelId,
|
id: modelId,
|
||||||
name: m.name || modelId,
|
name: m.name || modelId,
|
||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
provider: "zai",
|
provider: "zai",
|
||||||
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
||||||
reasoning: m.reasoning === true,
|
reasoning: m.reasoning === true,
|
||||||
input: supportsImage ? ["text", "image"] : ["text"],
|
input: supportsImage ? ["text", "image"] : ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: m.cost?.input || 0,
|
input: m.cost?.input || 0,
|
||||||
output: m.cost?.output || 0,
|
output: m.cost?.output || 0,
|
||||||
cacheRead: m.cost?.cache_read || 0,
|
cacheRead: m.cost?.cache_read || 0,
|
||||||
cacheWrite: m.cost?.cache_write || 0,
|
cacheWrite: m.cost?.cache_write || 0,
|
||||||
},
|
},
|
||||||
compat: {
|
compat: {
|
||||||
supportsDeveloperRole: false,
|
supportsDeveloperRole: false,
|
||||||
thinkingFormat: "zai",
|
thinkingFormat: "zai",
|
||||||
},
|
...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}),
|
||||||
contextWindow: m.limit?.context || 4096,
|
},
|
||||||
maxTokens: m.limit?.output || 4096,
|
contextWindow: m.limit?.context || 4096,
|
||||||
|
maxTokens: m.limit?.output || 4096,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,6 +395,9 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio
|
|||||||
|
|
||||||
if (context.tools) {
|
if (context.tools) {
|
||||||
params.tools = convertTools(context.tools, compat);
|
params.tools = convertTools(context.tools, compat);
|
||||||
|
if (compat.zaiToolStream) {
|
||||||
|
(params as any).tool_stream = true;
|
||||||
|
}
|
||||||
} else if (hasToolHistory(context.messages)) {
|
} else if (hasToolHistory(context.messages)) {
|
||||||
// Anthropic (via LiteLLM/proxy) requires tools param when conversation has tool_calls/tool_results
|
// Anthropic (via LiteLLM/proxy) requires tools param when conversation has tool_calls/tool_results
|
||||||
params.tools = [];
|
params.tools = [];
|
||||||
@@ -835,6 +838,7 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
|
|||||||
: "openai",
|
: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
zaiToolStream: false,
|
||||||
supportsStrictMode: true,
|
supportsStrictMode: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -861,6 +865,7 @@ function getCompat(model: Model<"openai-completions">): Required<OpenAICompletio
|
|||||||
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
||||||
openRouterRouting: model.compat.openRouterRouting ?? {},
|
openRouterRouting: model.compat.openRouterRouting ?? {},
|
||||||
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
||||||
|
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
|
||||||
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ export interface OpenAICompletionsCompat {
|
|||||||
openRouterRouting?: OpenRouterRouting;
|
openRouterRouting?: OpenRouterRouting;
|
||||||
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
||||||
vercelGatewayRouting?: VercelGatewayRouting;
|
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. */
|
/** Whether the provider supports the `strict` field in tool definitions. Default: true. */
|
||||||
supportsStrictMode?: boolean;
|
supportsStrictMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,159 @@ describe("openai-completions tool_choice", () => {
|
|||||||
expect(params.reasoning_effort).toBe("medium");
|
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 () => {
|
it("maps non-standard provider finish_reason values to stopReason error", async () => {
|
||||||
mockState.chunks = [
|
mockState.chunks = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const compat: Required<OpenAICompletionsCompat> = {
|
|||||||
thinkingFormat: "openai",
|
thinkingFormat: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
zaiToolStream: false,
|
||||||
supportsStrictMode: true,
|
supportsStrictMode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user