702 lines
23 KiB
TypeScript
702 lines
23 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { getExecutor } from "../../open-sse/executors/index.ts";
|
|
import { GlmExecutor } from "../../open-sse/executors/glm.ts";
|
|
|
|
function makeSseResponse(lines: string[]): Response {
|
|
return new Response(lines.join("\n\n") + "\n\n", {
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
});
|
|
}
|
|
|
|
test("GlmExecutor normalizes GLM coding and Anthropic URLs without duplicating endpoints", () => {
|
|
const executor = new GlmExecutor("glm");
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
}),
|
|
"https://api.z.ai/api/coding/paas/v4/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4/" },
|
|
}),
|
|
"https://api.z.ai/api/coding/paas/v4/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.z.ai/api/coding/paas/v4/chat/completions",
|
|
},
|
|
}),
|
|
"https://api.z.ai/api/coding/paas/v4/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com/api/coding/paas/v4/v1/messages",
|
|
},
|
|
}),
|
|
"https://proxy.example.com/api/coding/paas/v4/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic" },
|
|
}),
|
|
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1" },
|
|
}),
|
|
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
|
|
);
|
|
|
|
assert.equal(
|
|
new GlmExecutor("glm-cn").buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: {
|
|
anthropicBaseUrl: "https://open.bigmodel.cn/api/anthropic/v1",
|
|
primaryTransport: "anthropic",
|
|
},
|
|
}),
|
|
"https://open.bigmodel.cn/api/anthropic/v1/messages?beta=true"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 1, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic" },
|
|
}),
|
|
"https://api.z.ai/api/coding/paas/v4/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
|
|
}),
|
|
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 1, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
|
|
}),
|
|
"https://api.z.ai/api/coding/paas/v4/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.z.ai/api/anthropic/v1/messages?beta=true",
|
|
},
|
|
}),
|
|
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildCountTokensUrl("glm-5.1", {
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com/api/anthropic/v1/messages/count_tokens",
|
|
},
|
|
}),
|
|
"https://proxy.example.com/api/anthropic/v1/messages/count_tokens?beta=true"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildUrl("glm-5.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl:
|
|
"https://proxy.example.com/api/coding/paas/v4/chat/completions?tenant=alpha&route=glm",
|
|
},
|
|
}),
|
|
"https://proxy.example.com/api/coding/paas/v4/chat/completions?tenant=alpha&route=glm"
|
|
);
|
|
|
|
assert.equal(
|
|
executor.buildCountTokensUrl("glm-5.1", {
|
|
providerSpecificData: {
|
|
anthropicBaseUrl:
|
|
"https://proxy.example.com/api/anthropic/v1/messages/count_tokens?tenant=alpha&route=glm",
|
|
},
|
|
}),
|
|
"https://proxy.example.com/api/anthropic/v1/messages/count_tokens?tenant=alpha&route=glm&beta=true"
|
|
);
|
|
});
|
|
|
|
test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic headers", () => {
|
|
assert.equal(getExecutor("glm") instanceof GlmExecutor, true);
|
|
assert.equal(getExecutor("glm-cn") instanceof GlmExecutor, true);
|
|
assert.equal(getExecutor("glmt") instanceof GlmExecutor, true);
|
|
|
|
const executor = new GlmExecutor("glm");
|
|
const codingHeaders = executor.buildHeaders(
|
|
{
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
true
|
|
);
|
|
|
|
assert.equal(codingHeaders.Authorization, "Bearer glm-key");
|
|
assert.equal(codingHeaders["x-api-key"], undefined);
|
|
assert.equal(codingHeaders["anthropic-version"], undefined);
|
|
assert.equal(codingHeaders["anthropic-beta"], undefined);
|
|
assert.equal(codingHeaders["anthropic-dangerous-direct-browser-access"], undefined);
|
|
assert.equal(codingHeaders.Accept, "text/event-stream");
|
|
|
|
const countTokensHeaders = executor.buildHeaders(
|
|
{
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
false,
|
|
null,
|
|
undefined,
|
|
"anthropic"
|
|
);
|
|
assert.equal(countTokensHeaders["x-api-key"], "glm-key");
|
|
assert.equal(countTokensHeaders.Authorization, undefined);
|
|
assert.equal(countTokensHeaders["anthropic-version"], "2023-06-01");
|
|
|
|
const anthropicHeaders = executor.buildHeaders(
|
|
{
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
|
|
},
|
|
true,
|
|
null,
|
|
undefined,
|
|
"anthropic"
|
|
);
|
|
|
|
assert.equal(anthropicHeaders["x-api-key"], "glm-key");
|
|
assert.equal(anthropicHeaders.Authorization, undefined);
|
|
assert.equal(anthropicHeaders.Accept, "text/event-stream");
|
|
assert.equal(anthropicHeaders["anthropic-version"], "2023-06-01");
|
|
assert.match(anthropicHeaders["anthropic-beta"], /claude-code-20250219/);
|
|
assert.equal(anthropicHeaders["anthropic-dangerous-direct-browser-access"], "true");
|
|
assert.match(anthropicHeaders["User-Agent"], /^claude-cli\/2\.1\.195 \(external, sdk-cli\)$/);
|
|
assert.equal(anthropicHeaders["X-Stainless-Lang"], "js");
|
|
assert.equal(anthropicHeaders["X-Stainless-Runtime"], "node");
|
|
});
|
|
|
|
test("GlmExecutor preserves extra API key rotation", () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const headers = executor.buildHeaders(
|
|
{
|
|
apiKey: "primary-key",
|
|
connectionId: "glm-rotation-test",
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
|
|
extraApiKeys: ["extra-key"],
|
|
},
|
|
},
|
|
true,
|
|
null,
|
|
undefined,
|
|
"anthropic"
|
|
);
|
|
|
|
assert.ok(["primary-key", "extra-key"].includes(headers["x-api-key"]));
|
|
assert.equal(headers.Authorization, undefined);
|
|
});
|
|
|
|
test("GlmExecutor applies GLMT adaptive thinking defaults without mutating caller body", () => {
|
|
const executor = new GlmExecutor("glmt");
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
|
|
const transformed = executor.transformRequest("glm-5.1", body, true, {
|
|
apiKey: "glm-key",
|
|
}) as any;
|
|
|
|
assert.notEqual(transformed, body);
|
|
assert.equal((body as any).max_tokens, undefined);
|
|
assert.equal(transformed.max_tokens, 65_536);
|
|
assert.equal(transformed.temperature, 0.2);
|
|
assert.deepEqual(transformed.thinking, { type: "adaptive", budget_tokens: 24_576 });
|
|
});
|
|
|
|
test("GlmExecutor applies conservative GLM defaults without mutating caller body", () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
|
|
const transformed = executor.transformRequest("glm-5.1", body, false, {
|
|
apiKey: "glm-key",
|
|
}) as any;
|
|
|
|
assert.notEqual(transformed, body);
|
|
assert.equal((body as any).max_tokens, undefined);
|
|
assert.equal(transformed.max_tokens, 16_384);
|
|
assert.equal(transformed.temperature, undefined);
|
|
assert.equal(transformed.thinking, undefined);
|
|
});
|
|
|
|
test("GlmExecutor preserves caller max token settings over GLM defaults", () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const body = {
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_output_tokens: 512,
|
|
};
|
|
|
|
const transformed = executor.transformRequest("glm-5.1", body, false, {
|
|
apiKey: "glm-key",
|
|
}) as any;
|
|
|
|
assert.deepEqual(transformed, body);
|
|
assert.equal((transformed as any).max_tokens, undefined);
|
|
assert.equal((transformed as any).max_output_tokens, 512);
|
|
});
|
|
|
|
test("GlmExecutor count_tokens is best-effort and timeout bounded", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
|
|
assert.equal(
|
|
executor.buildCountTokensUrl("glm-5.1", {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
}),
|
|
"https://api.z.ai/api/anthropic/v1/messages/count_tokens?beta=true"
|
|
);
|
|
assert.equal(executor.getCountTokensTimeoutMs(), 3_000);
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
let captured: { url: string; body: any; headers: any } | null = null;
|
|
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
headers: init.headers,
|
|
};
|
|
return Response.json({ input_tokens: 42 });
|
|
};
|
|
|
|
try {
|
|
const result = await executor.countTokens({
|
|
model: "glm-5.1",
|
|
body: { messages: [{ role: "user", content: "hello" }] },
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
});
|
|
|
|
assert.equal(result?.input_tokens, 42);
|
|
assert.ok(captured);
|
|
assert.equal(captured.url, "https://api.z.ai/api/anthropic/v1/messages/count_tokens?beta=true");
|
|
assert.equal(captured.body.model, "glm-5.1");
|
|
assert.equal(captured.headers["x-api-key"], "glm-key");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor translates Anthropic streaming fallback to OpenAI SSE", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(
|
|
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"glm-5.1","content":[],"stop_reason":null,"usage":{"input_tokens":1,"output_tokens":0}}}\n\nevent: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}\n\nevent: message_stop\ndata: {"type":"message_stop"}\n\n',
|
|
{
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
}
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: { messages: [{ role: "user", content: "hello" }] },
|
|
stream: true,
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
|
|
primaryTransport: "anthropic",
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(result.targetFormat, "openai");
|
|
assert.equal(result.response.headers.get("content-type"), "text/event-stream");
|
|
const text = await result.response.text();
|
|
assert.match(text, /chat\.completion\.chunk/);
|
|
assert.doesNotMatch(text, /message_start/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor sends OpenAI coding payload first and enables streaming tool chunks", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
let captured: { url: string; body: any; headers: any } | null = null;
|
|
|
|
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
headers: init.headers,
|
|
};
|
|
return makeSseResponse([
|
|
'data: {"id":"chatcmpl-glm","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"ok"}}]}',
|
|
"data: [DONE]",
|
|
]);
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: {
|
|
messages: [{ role: "user", content: "weather" }],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "get_weather",
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
stream: true,
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
});
|
|
|
|
assert.equal(result.response.status, 200);
|
|
assert.equal(captured?.url, "https://api.z.ai/api/coding/paas/v4/chat/completions");
|
|
assert.equal(captured?.headers.Authorization, "Bearer glm-key");
|
|
assert.equal(captured?.headers["x-api-key"], undefined);
|
|
assert.equal(captured?.headers["anthropic-version"], undefined);
|
|
assert.equal(captured?.body.tool_stream, true);
|
|
assert.equal(captured?.body.tools[0].function.name, "get_weather");
|
|
assert.match(await result.response.text(), /chatcmpl-glm/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor falls back internally to Anthropic transport and returns OpenAI JSON", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: Array<{ url: string; body: any; headers: any }> = [];
|
|
|
|
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
|
calls.push({
|
|
url: String(url),
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
headers: init.headers,
|
|
});
|
|
|
|
if (calls.length === 1) {
|
|
return new Response(JSON.stringify({ error: "not found" }), {
|
|
status: 404,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
return Response.json({
|
|
id: "msg_glm",
|
|
type: "message",
|
|
role: "assistant",
|
|
model: "glm-5.1",
|
|
content: [{ type: "text", text: "fallback ok" }],
|
|
stop_reason: "end_turn",
|
|
usage: { input_tokens: 3, output_tokens: 2 },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: { messages: [{ role: "user", content: "hello" }] },
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
});
|
|
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(calls[0].url, "https://api.z.ai/api/coding/paas/v4/chat/completions");
|
|
assert.equal(calls[0].headers.Authorization, "Bearer glm-key");
|
|
assert.equal(calls[1].url, "https://api.z.ai/api/anthropic/v1/messages?beta=true");
|
|
assert.equal(calls[1].headers["x-api-key"], "glm-key");
|
|
assert.equal(calls[1].headers.Authorization, undefined);
|
|
assert.equal(calls[1].body.messages[0].role, "user");
|
|
assert.equal(calls[1].body._disableToolPrefix, undefined);
|
|
assert.equal(result.targetFormat, "openai");
|
|
|
|
const json = await result.response.json();
|
|
assert.equal(json.object, "chat.completion");
|
|
assert.equal(json.choices[0].message.content, "fallback ok");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor falls back when primary stream ends before useful content", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: string[] = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
calls.push(String(url));
|
|
if (calls.length === 1) {
|
|
return makeSseResponse(["event: ping", "data: {}"]);
|
|
}
|
|
return makeSseResponse([
|
|
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"glm-5.1","content":[],"stop_reason":null,"usage":{"input_tokens":1,"output_tokens":0}}}',
|
|
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"fallback stream ok"}}',
|
|
'event: message_stop\ndata: {"type":"message_stop"}',
|
|
]);
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: { messages: [{ role: "user", content: "hello" }] },
|
|
stream: true,
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
"https://api.z.ai/api/coding/paas/v4/chat/completions",
|
|
"https://api.z.ai/api/anthropic/v1/messages?beta=true",
|
|
]);
|
|
assert.equal(result.response.status, 200);
|
|
assert.equal(result.targetFormat, "openai");
|
|
const text = await result.response.text();
|
|
assert.match(text, /chat\.completion\.chunk/);
|
|
assert.match(text, /fallback stream ok/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor uses readiness timeout for OpenAI-compatible stream handoff", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => makeSseResponse(["event: ping", "data: {}"]);
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: { messages: [{ role: "user", content: "hello" }] },
|
|
stream: true,
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
});
|
|
|
|
assert.equal(result.response.status, 502);
|
|
assert.match(await result.response.text(), /STREAM_EARLY_EOF/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor preserves non-OK streaming upstream status before readiness", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: string[] = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
calls.push(String(url));
|
|
return new Response(JSON.stringify({ error: "invalid api key" }), {
|
|
status: 401,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: { messages: [{ role: "user", content: "hello" }] },
|
|
stream: true,
|
|
credentials: {
|
|
apiKey: "bad-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(calls, ["https://api.z.ai/api/coding/paas/v4/chat/completions"]);
|
|
assert.equal(result.response.status, 401);
|
|
assert.match(await result.response.text(), /invalid api key/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor translates Anthropic JSON errors to OpenAI-shaped fallback responses", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
type: "error",
|
|
error: { type: "invalid_request_error", message: "bad anthropic fallback" },
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: { messages: [{ role: "user", content: "hello" }] },
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",
|
|
primaryTransport: "anthropic",
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(result.targetFormat, "openai");
|
|
assert.equal(result.response.status, 400);
|
|
const json = await result.response.json();
|
|
assert.equal(json.error.message, "bad anthropic fallback");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("GlmExecutor Anthropic fallback keeps tool names unprefixed", async () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: Array<{ url: string; body: any; headers: any }> = [];
|
|
|
|
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
|
calls.push({
|
|
url: String(url),
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
headers: init.headers,
|
|
});
|
|
if (calls.length === 1) return new Response("upstream down", { status: 502 });
|
|
return Response.json({
|
|
id: "msg_tool",
|
|
type: "message",
|
|
role: "assistant",
|
|
model: "glm-5.1",
|
|
content: [
|
|
{ type: "tool_use", id: "toolu_1", name: "get_weather", input: { location: "Madrid" } },
|
|
],
|
|
stop_reason: "tool_use",
|
|
usage: { input_tokens: 4, output_tokens: 1 },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "glm-5.1",
|
|
body: {
|
|
messages: [{ role: "user", content: "weather" }],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "get_weather",
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "glm-key",
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
|
},
|
|
});
|
|
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(calls[1].body.tools[0].name, "get_weather");
|
|
assert.equal(calls[1].body.tools[0].name.startsWith("proxy_"), false);
|
|
assert.equal(calls[1].body._disableToolPrefix, undefined);
|
|
|
|
const json = await result.response.json();
|
|
assert.equal(json.choices[0].finish_reason, "tool_calls");
|
|
assert.equal(json.choices[0].message.tool_calls[0].function.name, "get_weather");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
// Regression for #4255 — GLM-5.2+ thinking models share a single max_tokens
|
|
// budget for reasoning + response. When the client omits max_tokens, the
|
|
// executor must default to the model's full output capacity (131072) so deep
|
|
// reasoning isn't truncated by the generic GLM default (16_384). Scoped to
|
|
// GLM-5.2+ via transformForTransport — non-thinking GLM models are untouched.
|
|
test("GlmExecutor defaults GLM-5.2+ max_tokens to 131072 when the client omits it", () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
|
|
const transformed = executor.transformForTransport(
|
|
"glm-5.2",
|
|
body,
|
|
false,
|
|
{
|
|
apiKey: "glm-key",
|
|
},
|
|
"openai"
|
|
) as any;
|
|
|
|
assert.equal((body as any).max_tokens, undefined, "caller body must not be mutated");
|
|
assert.equal(transformed.max_tokens, 131072);
|
|
});
|
|
|
|
test("GlmExecutor preserves a client-supplied max_tokens for GLM-5.2+ (no override)", () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const body = { messages: [{ role: "user", content: "hi" }], max_tokens: 4096 };
|
|
|
|
const transformed = executor.transformForTransport(
|
|
"glm-5.2",
|
|
body,
|
|
false,
|
|
{
|
|
apiKey: "glm-key",
|
|
},
|
|
"openai"
|
|
) as any;
|
|
|
|
assert.equal(transformed.max_tokens, 4096);
|
|
});
|
|
|
|
test("GlmExecutor does NOT bump max_tokens for non-thinking GLM (glm-4.6)", () => {
|
|
const executor = new GlmExecutor("glm");
|
|
const body = { messages: [{ role: "user", content: "hi" }] };
|
|
|
|
const transformed = executor.transformForTransport(
|
|
"glm-4.6",
|
|
body,
|
|
false,
|
|
{
|
|
apiKey: "glm-key",
|
|
},
|
|
"openai"
|
|
) as any;
|
|
|
|
// Stays at the generic GLM default (16_384) — never the 131072 thinking budget.
|
|
assert.notEqual(transformed.max_tokens, 131072);
|
|
assert.equal(transformed.max_tokens, 16_384);
|
|
});
|