Files
NomadArch/packages/server/src/integrations/zai-api.ts
Gemini AI 229f86c229
Some checks failed
Release Binaries / release (push) Has been cancelled
fix: add robust Z.AI config persistence with error handling and logging
2025-12-29 00:13:55 +04:00

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
}
}