266 lines
7.7 KiB
TypeScript
266 lines
7.7 KiB
TypeScript
import { z } from "zod"
|
|
export const ZAIConfigSchema = z.object({
|
|
apiKey: z.string().optional(),
|
|
endpoint: z.string().default("https://api.z.ai/api"),
|
|
enabled: z.boolean().default(false),
|
|
timeout: z.number().default(300000)
|
|
})
|
|
|
|
export type ZAIConfig = z.infer<typeof ZAIConfigSchema>
|
|
|
|
export const ZAIMessageSchema = z.object({
|
|
role: z.enum(["user", "assistant", "system", "tool"]),
|
|
content: z.string().optional(),
|
|
tool_calls: z.array(z.object({
|
|
id: z.string(),
|
|
type: z.literal("function"),
|
|
function: z.object({
|
|
name: z.string(),
|
|
arguments: z.string()
|
|
})
|
|
})).optional(),
|
|
tool_call_id: z.string().optional()
|
|
})
|
|
|
|
export type ZAIMessage = z.infer<typeof ZAIMessageSchema>
|
|
|
|
// Tool Definition Schema (OpenAI-compatible)
|
|
export const ZAIToolSchema = z.object({
|
|
type: z.literal("function"),
|
|
function: z.object({
|
|
name: z.string(),
|
|
description: z.string(),
|
|
parameters: z.object({
|
|
type: z.literal("object"),
|
|
properties: z.record(z.object({
|
|
type: z.string(),
|
|
description: z.string().optional()
|
|
})),
|
|
required: z.array(z.string()).optional()
|
|
})
|
|
})
|
|
})
|
|
|
|
export type ZAITool = z.infer<typeof ZAIToolSchema>
|
|
|
|
export const ZAIChatRequestSchema = z.object({
|
|
model: z.string().default("glm-4.7"),
|
|
messages: z.array(ZAIMessageSchema),
|
|
max_tokens: z.number().default(8192),
|
|
stream: z.boolean().default(true),
|
|
temperature: z.number().optional(),
|
|
tools: z.array(ZAIToolSchema).optional(),
|
|
tool_choice: z.union([
|
|
z.literal("auto"),
|
|
z.literal("none"),
|
|
z.object({
|
|
type: z.literal("function"),
|
|
function: z.object({ name: z.string() })
|
|
})
|
|
]).optional(),
|
|
thinking: z.object({
|
|
type: z.enum(["enabled", "disabled"]).optional()
|
|
}).optional()
|
|
})
|
|
|
|
export type ZAIChatRequest = z.infer<typeof ZAIChatRequestSchema>
|
|
|
|
export const ZAIChatResponseSchema = z.object({
|
|
id: z.string(),
|
|
object: z.string(),
|
|
created: z.number(),
|
|
model: z.string(),
|
|
choices: z.array(z.object({
|
|
index: z.number(),
|
|
message: z.object({
|
|
role: z.string(),
|
|
content: z.string().optional().nullable(),
|
|
reasoning_content: z.string().optional(),
|
|
tool_calls: z.array(z.object({
|
|
id: z.string(),
|
|
type: z.literal("function"),
|
|
function: z.object({
|
|
name: z.string(),
|
|
arguments: z.string()
|
|
})
|
|
})).optional()
|
|
}),
|
|
finish_reason: z.string()
|
|
})),
|
|
usage: z.object({
|
|
prompt_tokens: z.number(),
|
|
completion_tokens: z.number(),
|
|
total_tokens: z.number()
|
|
})
|
|
})
|
|
|
|
export type ZAIChatResponse = z.infer<typeof ZAIChatResponseSchema>
|
|
|
|
export const ZAIStreamChunkSchema = z.object({
|
|
id: z.string(),
|
|
object: z.string(),
|
|
created: z.number(),
|
|
model: z.string(),
|
|
choices: z.array(z.object({
|
|
index: z.number(),
|
|
delta: z.object({
|
|
role: z.string().optional(),
|
|
content: z.string().optional().nullable(),
|
|
reasoning_content: z.string().optional(),
|
|
tool_calls: z.array(z.object({
|
|
index: z.number().optional(),
|
|
id: z.string().optional(),
|
|
type: z.literal("function").optional(),
|
|
function: z.object({
|
|
name: z.string().optional(),
|
|
arguments: z.string().optional()
|
|
}).optional()
|
|
})).optional()
|
|
}),
|
|
finish_reason: z.string().nullable().optional()
|
|
}))
|
|
})
|
|
|
|
export type ZAIStreamChunk = z.infer<typeof ZAIStreamChunkSchema>
|
|
|
|
export const ZAI_MODELS = [
|
|
"glm-4.7",
|
|
"glm-4.6",
|
|
"glm-4.5",
|
|
"glm-4.5-air",
|
|
"glm-4.5-flash",
|
|
"glm-4.5-long"
|
|
] as const
|
|
|
|
export type ZAIModelName = typeof ZAI_MODELS[number]
|
|
|
|
export class ZAIClient {
|
|
private config: ZAIConfig
|
|
private baseUrl: string
|
|
|
|
constructor(config: ZAIConfig) {
|
|
this.config = config
|
|
const trimmed = config.endpoint.replace(/\/$/, "")
|
|
this.baseUrl = trimmed.replace(/\/(?:api\/coding\/)?paas\/v4$/, "")
|
|
}
|
|
|
|
async testConnection(): Promise<boolean> {
|
|
if (!this.config.apiKey) {
|
|
return false
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/paas/v4/chat/completions`, {
|
|
method: "POST",
|
|
headers: this.getHeaders(),
|
|
body: JSON.stringify({
|
|
model: "glm-4.7",
|
|
max_tokens: 1,
|
|
messages: [{ role: "user", content: "test" }]
|
|
})
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text()
|
|
console.error(`Z.AI connection failed (${response.status}): ${text}`)
|
|
}
|
|
|
|
return response.ok
|
|
} catch (error) {
|
|
console.error("Z.AI connection test failed:", error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
async listModels(): Promise<string[]> {
|
|
return [...ZAI_MODELS]
|
|
}
|
|
|
|
async *chatStream(request: ZAIChatRequest): AsyncGenerator<ZAIStreamChunk> {
|
|
if (!this.config.apiKey) {
|
|
throw new Error("Z.AI API key is required")
|
|
}
|
|
|
|
const response = await fetch(`${this.baseUrl}/paas/v4/chat/completions`, {
|
|
method: "POST",
|
|
headers: this.getHeaders(),
|
|
body: JSON.stringify({
|
|
...request,
|
|
stream: true
|
|
})
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
throw new Error(`Z.AI API error (${response.status}): ${errorText}`)
|
|
}
|
|
|
|
if (!response.body) {
|
|
throw new Error("Response body is missing")
|
|
}
|
|
|
|
const reader = response.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let buffer = ""
|
|
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
const lines = buffer.split("\n")
|
|
buffer = lines.pop() || ""
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith("data: ")) {
|
|
const data = line.slice(6).trim()
|
|
if (data === "[DONE]") return
|
|
|
|
try {
|
|
const parsed = JSON.parse(data)
|
|
yield parsed as ZAIStreamChunk
|
|
} catch (e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
reader.releaseLock()
|
|
}
|
|
}
|
|
|
|
async chat(request: ZAIChatRequest): Promise<ZAIChatResponse> {
|
|
if (!this.config.apiKey) {
|
|
throw new Error("Z.AI API key is required")
|
|
}
|
|
|
|
const response = await fetch(`${this.baseUrl}/paas/v4/chat/completions`, {
|
|
method: "POST",
|
|
headers: this.getHeaders(),
|
|
body: JSON.stringify({
|
|
...request,
|
|
stream: false
|
|
})
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
throw new Error(`Z.AI API error (${response.status}): ${errorText}`)
|
|
}
|
|
|
|
return await response.json()
|
|
}
|
|
|
|
private getHeaders(): Record<string, string> {
|
|
return {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${this.config.apiKey!}`
|
|
}
|
|
}
|
|
|
|
static validateApiKey(apiKey: string): boolean {
|
|
return typeof apiKey === "string" && apiKey.length > 0
|
|
}
|
|
}
|