import { Logger } from '@nestjs/common'; import OpenAI from 'openai'; import type { CompletionEvent, CompletionParams, IProviderAdapter, ModelInfo, ProviderHealth, } from '@mosaicstack/types'; import { getModelCapability } from '../model-capabilities.js'; /** * Default Z.ai API base URL. * Z.ai (BigModel / Zhipu AI) exposes an OpenAI-compatible API at this endpoint. * Can be overridden via the ZAI_BASE_URL environment variable. */ const DEFAULT_ZAI_BASE_URL = 'https://open.bigmodel.cn/api/paas/v4'; /** * GLM-5 model identifier on the Z.ai platform. */ const GLM5_MODEL_ID = 'glm-5'; /** * Z.ai (Zhipu AI / BigModel) provider adapter. * * Z.ai exposes an OpenAI-compatible REST API. This adapter uses the `openai` * SDK with a custom base URL and the ZAI_API_KEY environment variable. * * Configuration: * ZAI_API_KEY — required; Z.ai API key * ZAI_BASE_URL — optional; override the default API base URL */ export class ZaiAdapter implements IProviderAdapter { readonly name = 'zai'; private readonly logger = new Logger(ZaiAdapter.name); private client: OpenAI | null = null; private registeredModels: ModelInfo[] = []; async register(): Promise { const apiKey = process.env['ZAI_API_KEY']; if (!apiKey) { this.logger.debug('Skipping Z.ai provider registration: ZAI_API_KEY not set'); return; } const baseURL = process.env['ZAI_BASE_URL'] ?? DEFAULT_ZAI_BASE_URL; this.client = new OpenAI({ apiKey, baseURL }); this.registeredModels = this.buildModelList(); this.logger.log(`Z.ai provider registered with ${this.registeredModels.length} model(s)`); } listModels(): ModelInfo[] { return this.registeredModels; } async healthCheck(): Promise { const apiKey = process.env['ZAI_API_KEY']; if (!apiKey) { return { status: 'down', lastChecked: new Date().toISOString(), error: 'ZAI_API_KEY not configured', }; } const baseURL = process.env['ZAI_BASE_URL'] ?? DEFAULT_ZAI_BASE_URL; const start = Date.now(); try { const res = await fetch(`${baseURL}/models`, { method: 'GET', headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json', }, signal: AbortSignal.timeout(5000), }); const latencyMs = Date.now() - start; if (!res.ok) { return { status: 'degraded', latencyMs, lastChecked: new Date().toISOString(), error: `HTTP ${res.status}`, }; } return { status: 'healthy', latencyMs, lastChecked: new Date().toISOString() }; } catch (err) { const latencyMs = Date.now() - start; const error = err instanceof Error ? err.message : String(err); return { status: 'down', latencyMs, lastChecked: new Date().toISOString(), error }; } } /** * Stream a completion through Z.ai's OpenAI-compatible API. */ async *createCompletion(params: CompletionParams): AsyncIterable { if (!this.client) { throw new Error('ZaiAdapter is not initialized. Ensure ZAI_API_KEY is set.'); } const stream = await this.client.chat.completions.create({ model: params.model, messages: params.messages.map((m) => ({ role: m.role, content: m.content })), temperature: params.temperature, max_tokens: params.maxTokens, stream: true, }); let inputTokens = 0; let outputTokens = 0; for await (const chunk of stream) { const choice = chunk.choices[0]; if (!choice) continue; const delta = choice.delta; if (delta.content) { yield { type: 'text_delta', content: delta.content }; } if (choice.finish_reason === 'stop') { const usage = (chunk as { usage?: { prompt_tokens?: number; completion_tokens?: number } }) .usage; if (usage) { inputTokens = usage.prompt_tokens ?? 0; outputTokens = usage.completion_tokens ?? 0; } } } yield { type: 'done', usage: { inputTokens, outputTokens }, }; } // --------------------------------------------------------------------------- // Private helpers // --------------------------------------------------------------------------- private buildModelList(): ModelInfo[] { const capability = getModelCapability(GLM5_MODEL_ID); if (!capability) { this.logger.warn(`Model capability entry not found for '${GLM5_MODEL_ID}'; using defaults`); return [ { id: GLM5_MODEL_ID, provider: 'zai', name: 'GLM-5', reasoning: false, contextWindow: 128000, maxTokens: 8192, inputTypes: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, ]; } return [ { id: capability.id, provider: 'zai', name: capability.displayName, reasoning: capability.capabilities.reasoning, contextWindow: capability.contextWindow, maxTokens: capability.maxOutputTokens, inputTypes: capability.capabilities.vision ? ['text', 'image'] : ['text'], cost: { input: capability.costPer1kInput ?? 0, output: capability.costPer1kOutput ?? 0, cacheRead: 0, cacheWrite: 0, }, }, ]; } }