import { generateText } from 'ai'; import { createZhipu } from 'zhipu-ai-provider'; type AiConfig = { apiKey?: string; baseUrl: string; model: string; temperature: number; }; type EnvSource = Record; type GetAiConfigOptions = { env?: EnvSource; warn?: (message: string) => void; }; type AiGenerateInput = { model: unknown; system?: string; prompt: string; temperature: number; }; type AiGenerateOutput = { text: string; }; type RunAiAnalysisOptions = GetAiConfigOptions & { createModel?: (config: AiConfig) => unknown; generate?: (input: AiGenerateInput) => Promise; }; const DEPRECATED_LEGACY_GATEWAY_ENV_KEYS = [ 'OPENCLAW_BASE_URL', 'OPENCLAW_API_KEY', 'OPENCLAW_MODEL', 'OPENCLAW_AUTH_MODE', 'OPENCLAW_BASIC_AUTH_USERNAME', 'OPENCLAW_BASIC_AUTH_PASSWORD', 'OPENCLAW_API_KEY_HEADER', 'OPENCLAW_PORT', 'OPENCLAW_IMAGE', 'OPENCLAW_BUILD_CONTEXT', 'OPENCLAW_DOCKERFILE' ] as const; const CODING_API_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'; let warnedDeprecatedGatewayEnv = false; let warnedIgnoredZhipuBaseUrl = false; function envValue(name: string, env: EnvSource = process.env) { const value = env[name]; if (!value) { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function parseTemperature(value: string | undefined) { const parsed = Number(value); if (!Number.isFinite(parsed)) { return 0.2; } return Math.min(Math.max(parsed, 0), 2); } function warnDeprecatedGatewayEnv(env: EnvSource, warn: (message: string) => void) { if (warnedDeprecatedGatewayEnv) { return; } const presentKeys = DEPRECATED_LEGACY_GATEWAY_ENV_KEYS.filter((key) => Boolean(envValue(key, env))); if (presentKeys.length === 0) { return; } warnedDeprecatedGatewayEnv = true; warn( `[AI SDK] Deprecated OPENCLAW_* variables are ignored after migration: ${presentKeys.join(', ')}. Use ZHIPU_API_KEY, ZHIPU_MODEL, and AI_TEMPERATURE.` ); } function warnIgnoredZhipuBaseUrl(env: EnvSource, warn: (message: string) => void) { if (warnedIgnoredZhipuBaseUrl) { return; } const configuredBaseUrl = envValue('ZHIPU_BASE_URL', env); if (!configuredBaseUrl) { return; } warnedIgnoredZhipuBaseUrl = true; warn( `[AI SDK] ZHIPU_BASE_URL is ignored. The Coding API endpoint is hardcoded to ${CODING_API_BASE_URL}.` ); } function fallbackResponse(prompt: string) { const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260); return [ 'AI SDK fallback mode is active (Zhipu configuration is missing).', 'Thesis: Portfolio remains analyzable with local heuristics until live model access is configured.', 'Risk scan: Concentration and filing sentiment should be monitored after each sync cycle.', `Context digest: ${clipped}` ].join('\n\n'); } function defaultCreateModel(config: AiConfig) { const zhipu = createZhipu({ apiKey: config.apiKey, baseURL: config.baseUrl }); return zhipu(config.model); } async function defaultGenerate(input: AiGenerateInput): Promise { const result = await generateText({ model: input.model as never, system: input.system, prompt: input.prompt, temperature: input.temperature }); return { text: result.text }; } export function getAiConfig(options?: GetAiConfigOptions) { const env = options?.env ?? process.env; warnDeprecatedGatewayEnv(env, options?.warn ?? console.warn); warnIgnoredZhipuBaseUrl(env, options?.warn ?? console.warn); return { apiKey: envValue('ZHIPU_API_KEY', env), baseUrl: CODING_API_BASE_URL, model: envValue('ZHIPU_MODEL', env) ?? 'glm-4.7-flashx', temperature: parseTemperature(envValue('AI_TEMPERATURE', env)) } satisfies AiConfig; } export function isAiConfigured(options?: GetAiConfigOptions) { const config = getAiConfig(options); return Boolean(config.apiKey); } export async function runAiAnalysis(prompt: string, systemPrompt?: string, options?: RunAiAnalysisOptions) { const config = getAiConfig(options); if (!config.apiKey) { return { provider: 'local-fallback', model: config.model, text: fallbackResponse(prompt) }; } const createModel = options?.createModel ?? defaultCreateModel; const generate = options?.generate ?? defaultGenerate; const model = createModel(config); const result = await generate({ model, system: systemPrompt, prompt, temperature: config.temperature }); const text = result.text.trim(); if (!text) { throw new Error('AI SDK returned an empty response'); } return { provider: 'zhipu', model: config.model, text }; } export function __resetAiWarningsForTests() { warnedDeprecatedGatewayEnv = false; warnedIgnoredZhipuBaseUrl = false; }