"use strict"; import type { VisionConfig } from "./config"; import type { ContentPart } from "./media"; interface VisionChatRequest { model: string; messages: Array<{ role: string; content: string | ContentPart[]; }>; thinking?: { type: string }; stream: boolean; temperature: number; top_p: number; max_tokens: number; } interface VisionChatResponse { choices: Array<{ message: { content: string | null; }; }>; } /** 判断是否应该重试(HTTP 429 或 5xx) */ function isRetryable(status: number): boolean { return status === 429 || status >= 500; } /** 指数退避等待 */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } class TimeoutError extends Error { constructor(seconds: number) { super(`Vision API request timed out after ${seconds}s`); this.name = "TimeoutError"; } } class CancelledError extends Error { constructor() { super("Vision API request cancelled"); this.name = "CancelledError"; } } /** * 调用 GLM-4.6V chat/completions API(非流式)。 * - 自动重试 HTTP 429 和 5xx(指数退避,最多 2 次) * - 超时和取消不重试 * - 网络错误重试 2 次 */ export async function visionCompletion( config: VisionConfig, systemPrompt: string, contentParts: ContentPart[], userPrompt: string, signal?: AbortSignal ): Promise { const url = `${config.baseUrl.replace(/\/$/, "")}/chat/completions`; const userContent: ContentPart[] = [ ...contentParts, { type: "text", text: userPrompt }, ]; const body: VisionChatRequest = { model: config.model, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userContent }, ], thinking: config.thinking ? { type: "enabled" } : undefined, stream: false, temperature: config.temperature, top_p: config.topP, max_tokens: config.maxTokens, }; let lastError: Error | null = null; for (let attempt = 0; attempt <= 2; attempt++) { // 先检查外部取消信号 if (signal?.aborted) { throw new CancelledError(); } try { const controller = new AbortController(); let timedOut = false; const timeoutId = setTimeout(() => { timedOut = true; controller.abort(new TimeoutError(config.timeoutSecs)); }, config.timeoutSecs * 1000); // 外部 signal 触发时也取消 const onExternalAbort = () => { if (!timedOut) { controller.abort(new CancelledError()); } }; signal?.addEventListener("abort", onExternalAbort, { once: true }); let response: Response; try { response = await fetch(url, { method: "POST", headers: { "Authorization": `Bearer ${config.apiKey}`, "Content-Type": "application/json", "X-Title": "pi-zai-vision", "Accept-Language": "en-US,en", }, body: JSON.stringify(body), signal: controller.signal, }); } catch (err: any) { clearTimeout(timeoutId); signal?.removeEventListener("abort", onExternalAbort); if (timedOut) { throw new TimeoutError(config.timeoutSecs); } if (signal?.aborted) { throw new CancelledError(); } // 网络错误,记下并重试 lastError = err instanceof Error ? err : new Error(String(err)); if (attempt < 2) { await sleep(1000 * Math.pow(2, attempt)); continue; } throw lastError; } clearTimeout(timeoutId); signal?.removeEventListener("abort", onExternalAbort); if (!response.ok) { const errorBody = await response.text().catch(() => ""); const statusText = response.status === 401 || response.status === 403 ? "Authentication failed — check your Z_AI_API_KEY and Z_AI_MODE" : response.status === 404 ? "Endpoint not found — check your Z_AI_MODE setting" : `HTTP ${response.status}: ${errorBody.slice(0, 500)}`; const err = new Error(`Vision API error ${statusText}`); if (isRetryable(response.status) && attempt < 2) { lastError = err; await sleep(1000 * Math.pow(2, attempt)); continue; } throw err; } const data: VisionChatResponse = await response.json(); const content = data.choices?.[0]?.message?.content; if (content == null) { throw new Error("Vision API response missing content — the model may not support vision or the image was not understood"); } return content; } catch (err: any) { // 超时/取消 → 不重试 if (err instanceof TimeoutError || err instanceof CancelledError) { throw err; } // API 错误已在上面处理 if (err?.message?.startsWith("Vision API error")) { throw err; } // 其他意外错误 → 重试 if (attempt < 2) { lastError = err instanceof Error ? err : new Error(String(err)); await sleep(1000 * Math.pow(2, attempt)); continue; } throw err; } } throw lastError || new Error("Max retries exceeded for vision API request"); }