Files

368 lines
13 KiB
TypeScript

import { expect } from "bun:test"
import { ConfigProvider, Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Auth, LLM, LLMClient, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
import { ZAI, ZAICodingPlan } from "../../src/providers.js"
import { compileRequest } from "../../src/route/client.js"
import { Endpoint } from "../../src/route/endpoint.js"
import { it } from "../lib/effect.js"
import { dynamicResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
const tool = ToolDefinition.make({
name: "lookup",
description: "Look up a value",
inputSchema: { type: "object", properties: { key: { type: "string" } } },
})
it.effect("ZAI owns standard and Coding Plan endpoints and bearer credentials", () =>
Effect.gen(function* () {
expect(ZAI.provider.model).toBe(ZAI.provider.chat)
expect(ZAICodingPlan.provider.model).toBe(ZAICodingPlan.provider.chat)
for (const item of [
{ model: ZAI.chat("glm-5.3"), path: "/paas/v4/chat/completions", provider: "zai" },
{ model: ZAICodingPlan.chat("glm-5.3"), path: "/coding/paas/v4/chat/completions", provider: "zai-coding-plan" },
{ model: ZAICodingPlan.messages("glm-5.3"), path: "/anthropic/v1/messages", provider: "zai-coding-plan" },
{ model: ZAICodingPlan.responses("glm-5.3"), path: "/v1/responses", provider: "zai-coding-plan" },
]) {
const request = LLM.request({ model: item.model, prompt: "Hello" })
const compiled = yield* compileRequest(request)
const url = Endpoint.render(item.model.route.endpoint, { request, body: compiled.body }).toString()
expect(url).toBe(`https://api.z.ai/api${item.path}`)
expect(item.model.provider).toBe(item.provider)
expect(item.model.route.providerMetadataKey).toBe("zai")
expect(compiled.body.thinking).toBeUndefined()
expect(compiled.body.reasoning_effort).toBeUndefined()
expect(compiled.body.reasoning).toBeUndefined()
expect(compiled.body.output_config).toBeUndefined()
const headers = yield* item.model.route.auth.apply({
request,
method: "POST",
url,
body: "{}",
headers: Headers.empty,
})
expect(headers.authorization).toBe("Bearer fixture")
}
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { ZAI_API_KEY: "fixture" } })))),
)
it.effect("ZAI Chat retains native thinking fields and scopes tool streaming to supported models", () =>
Effect.gen(function* () {
for (const configure of [ZAI.configure, ZAICodingPlan.configure]) {
const provider = configure({
baseURL: "https://gateway.example/custom",
auth: Auth.header("x-test-key", "fixture"),
})
for (const [modelID, supported] of [
["glm-3.9", false],
["glm-4", false],
["glm-4.5", false],
["glm-4.5-air", false],
["glm-4.5-x", false],
["glm-4.6", true],
["glm-4.7-flash", true],
["glm-4.8", true],
["glm-4.10", true],
["glm-5", true],
["glm-5.3", true],
["glm-5.3-flash", true],
["glm-6", true],
["glm-10.1-new-family", true],
["zai/glm-6.1", true],
["GLM-4.6", true],
["not-glm-6", false],
["glm-4.60foo", false],
["glm-6.x", false],
["future-model", false],
] as const) {
const result = yield* compileRequest(
LLM.request({
model: provider.chat(modelID),
prompt: "Hello",
generation: { maxTokens: 123 },
tools: [tool],
providerOptions: { thinking: { type: "enabled", clear_thinking: false } },
}),
)
expect(result.body).toMatchObject({
model: modelID,
max_tokens: 123,
thinking: { type: "enabled", clear_thinking: false },
})
expect(result.body.tool_stream).toBe(supported ? true : undefined)
expect(result.body.max_completion_tokens).toBeUndefined()
expect(result.body.store).toBeUndefined()
expect(result.body.tools).toEqual([
expect.objectContaining({ function: expect.not.objectContaining({ strict: expect.anything() }) }),
])
}
for (const thinking of [
undefined,
{ type: "disabled" },
{ clear_thinking: false },
{ type: "enabled", clear_thinking: true },
]) {
const result = yield* compileRequest(
LLM.request({
model: provider.chat("glm-5.2"),
providerOptions: {
thinking,
toolStream: false,
doSample: false,
responseFormat: { type: "json_object" },
requestID: "request-1",
userID: "user-1",
},
tools: [tool],
}),
)
expect(result.body.thinking).toEqual(thinking)
expect(result.body).toMatchObject({
tool_stream: false,
do_sample: false,
response_format: { type: "json_object" },
request_id: "request-1",
user_id: "user-1",
})
}
}
}),
)
it.effect("ZAI tool streaming defaults require tools and preserve explicit overrides", () =>
Effect.gen(function* () {
for (const configure of [ZAI.configure, ZAICodingPlan.configure]) {
const provider = configure({ apiKey: "fixture" })
for (const item of [
{ id: "glm-6.1", tools: [], toolStream: undefined, expected: undefined },
{ id: "glm-4.5", tools: [tool], toolStream: true, expected: true },
{ id: "glm-6.1", tools: [tool], toolStream: false, expected: false },
{ id: "future-model", tools: [tool], toolStream: true, expected: true },
]) {
const result = yield* compileRequest(
LLM.request({
model: provider.chat(item.id),
prompt: "Hello",
tools: item.tools,
providerOptions: { toolStream: item.toolStream },
}),
)
expect(result.body.tool_stream).toBe(item.expected)
}
}
}),
)
it.effect("ZAI lowers effort using the selected native API without inventing thinking settings", () =>
Effect.gen(function* () {
for (const effort of ["none", "minimal", "low", "medium", "high", "xhigh", "max", "future-effort"]) {
const standard = yield* compileRequest(
LLM.request({
model: ZAI.configure({ apiKey: "fixture" }).chat("glm-5.3"),
providerOptions: { reasoningEffort: effort },
}),
)
const coding = ZAICodingPlan.configure({ apiKey: "fixture" })
const chat = yield* compileRequest(
LLM.request({ model: coding.chat("glm-5.3"), providerOptions: { reasoningEffort: effort } }),
)
const messages = yield* compileRequest(
LLM.request({ model: coding.messages("glm-5.3"), providerOptions: { effort } }),
)
const responses = yield* compileRequest(
LLM.request({ model: coding.responses("glm-5.3"), providerOptions: { reasoningEffort: effort } }),
)
expect(standard.body.reasoning_effort).toBe(effort)
expect(chat.body.reasoning_effort).toBe(effort)
expect(messages.body.output_config).toEqual({ effort })
expect(responses.body.reasoning).toEqual({ effort })
for (const result of [standard, chat, messages, responses]) expect(result.body.thinking).toBeUndefined()
}
for (const type of ["enabled", "adaptive", "disabled"]) {
const result = yield* compileRequest(
LLM.request({
model: ZAICodingPlan.configure({ apiKey: "fixture", providerOptions: { thinking: { type } } }).messages(
"glm-5.3",
),
}),
)
expect(result.body.thinking).toEqual({ type })
}
}),
)
it.effect("ZAI Coding Chat sends PDFs from tool results as file parts", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: ZAICodingPlan.configure({ apiKey: "fixture" }).chat("glm-5.3-flash"),
messages: [
Message.user("Read the report."),
Message.assistant({ type: "tool-call", id: "call_pdf", name: "read", input: {} }),
Message.tool({
id: "call_pdf",
name: "read",
resultType: "content",
result: [
{
type: "file",
mime: "application/pdf",
uri: "data:application/pdf;base64,JVBERi0=",
name: "report.pdf",
},
],
}),
],
}),
)
expect(prepared.body.messages.at(-1)).toEqual({
role: "user",
content: [{ type: "file", file: { filename: "report.pdf", file_data: "data:application/pdf;base64,JVBERi0=" } }],
})
}),
)
it.effect("ZAI rejects invalid typed thinking options before execution", () =>
Effect.gen(function* () {
const model = ZAI.configure({ apiKey: "fixture" })
.chat("glm-5.3")
.route.with({ providerOptions: { thinking: { clear_thinking: "false" } } })
.model({ id: "glm-5.3" })
const error = yield* compileRequest(LLM.request({ model })).pipe(Effect.flip)
expect(error.reason._tag).toBe("InvalidRequest")
expect(error.message).toContain("clear_thinking")
}),
)
it.effect("ZAI preserves unsigned and signed Messages reasoning", () =>
Effect.gen(function* () {
const result = yield* compileRequest(
LLM.request({
model: ZAICodingPlan.configure({ apiKey: "fixture" }).messages("glm-5.3"),
messages: [
Message.user("Hello"),
Message.assistant([
ReasoningPart.make({ type: "reasoning", text: "unsigned" }),
ReasoningPart.make({
type: "reasoning",
text: "signed",
providerMetadata: { zai: { signature: "opaque" } },
}),
]),
],
}),
)
expect(result.body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "assistant",
content: [
{ type: "thinking", thinking: "unsigned", signature: "" },
{ type: "thinking", thinking: "signed", signature: "opaque" },
],
}),
]),
)
}),
)
it.effect("ZAI applies configured options, request options, and final body overlays in order", () =>
LLMClient.generate(
LLM.request({
model: ZAI.configure({
apiKey: "fixture",
baseURL: "https://gateway.example/custom",
providerOptions: { thinking: { type: "enabled", clear_thinking: true }, reasoningEffort: "high" },
}).chat("glm-5.3"),
prompt: "Hello",
providerOptions: { thinking: { clear_thinking: false }, reasoningEffort: "low" },
http: { body: { reasoning_effort: "max", extension: true } },
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.sync(() => {
expect(input.request.url).toBe("https://gateway.example/custom/chat/completions")
expect(input.request.headers.authorization).toBe("Bearer fixture")
expect(JSON.parse(input.text)).toMatchObject({
thinking: { type: "enabled", clear_thinking: false },
reasoning_effort: "max",
extension: true,
})
return input.respond(
sseEvents({ choices: [{ delta: { content: "Hello" }, finish_reason: "stop" }] }, "[DONE]"),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
),
)
it.effect("ZAI Coding Responses uses HTTP with a supplied WebSocket executor", () =>
LLMClient.generate(
LLM.request({ model: ZAICodingPlan.configure({ apiKey: "fixture" }).responses("glm-5.3"), prompt: "Hello" }),
{
webSocket: { execute: () => Effect.die("Unexpected WebSocket") },
},
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.sync(() => {
expect(input.request.url).toBe("https://api.z.ai/api/v1/responses")
return input.respond(sseEvents({ type: "response.completed", response: {} }), {
headers: { "content-type": "text/event-stream" },
})
}),
),
),
),
)
it.effect("ZAI Coding Messages serializes enabled thinking without a token budget", () =>
LLMClient.generate(
LLM.request({
model: ZAICodingPlan.configure({
apiKey: "fixture",
baseURL: "https://gateway.example/messages/v1",
providerOptions: { thinking: { type: "enabled" }, effort: "max" },
}).messages("glm-5.3"),
prompt: "Hello",
providerOptions: { effort: "high" },
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.sync(() => {
expect(input.request.url).toBe("https://gateway.example/messages/v1/messages")
expect(input.request.headers.authorization).toBe("Bearer fixture")
expect(input.request.headers["anthropic-version"]).toBe("2023-06-01")
expect(JSON.parse(input.text)).toMatchObject({
thinking: { type: "enabled" },
output_config: { effort: "high" },
})
return input.respond(
sseEvents(
{
type: "message_start",
message: {
id: "msg_fixture",
role: "assistant",
model: "glm-5.3",
content: [],
usage: { input_tokens: 1, output_tokens: 0 },
},
},
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
{ type: "message_stop" },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
),
)