Migrate AI runtime to SDK and hardcode Zhipu coding endpoint
This commit is contained in:
190
lib/server/ai.ts
Normal file
190
lib/server/ai.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { generateText } from 'ai';
|
||||
import { createZhipu } from 'zhipu-ai-provider';
|
||||
|
||||
type AiConfig = {
|
||||
apiKey?: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
temperature: number;
|
||||
};
|
||||
|
||||
type EnvSource = Record<string, string | undefined>;
|
||||
|
||||
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<AiGenerateOutput>;
|
||||
};
|
||||
|
||||
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<AiGenerateOutput> {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user