Implement dual-model filing pipeline with Ollama extraction
This commit is contained in:
128
lib/server/ai.ts
128
lib/server/ai.ts
@@ -1,7 +1,12 @@
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { generateText } from 'ai';
|
||||
import { createZhipu } from 'zhipu-ai-provider';
|
||||
|
||||
type AiWorkload = 'report' | 'extraction';
|
||||
type AiProvider = 'zhipu' | 'ollama';
|
||||
|
||||
type AiConfig = {
|
||||
provider: AiProvider;
|
||||
apiKey?: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
@@ -27,11 +32,15 @@ type AiGenerateOutput = {
|
||||
};
|
||||
|
||||
type RunAiAnalysisOptions = GetAiConfigOptions & {
|
||||
workload?: AiWorkload;
|
||||
createModel?: (config: AiConfig) => unknown;
|
||||
generate?: (input: AiGenerateInput) => Promise<AiGenerateOutput>;
|
||||
};
|
||||
|
||||
const CODING_API_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
|
||||
const OLLAMA_BASE_URL = 'http://127.0.0.1:11434';
|
||||
const OLLAMA_MODEL = 'qwen3:8b';
|
||||
const OLLAMA_API_KEY = 'ollama';
|
||||
|
||||
let warnedIgnoredZhipuBaseUrl = false;
|
||||
|
||||
@@ -74,20 +83,47 @@ 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).',
|
||||
'AI SDK fallback mode is active (live model configuration is missing or unavailable).',
|
||||
'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 toOpenAiCompatibleBaseUrl(baseUrl: string) {
|
||||
const normalized = baseUrl.endsWith('/')
|
||||
? baseUrl.slice(0, -1)
|
||||
: baseUrl;
|
||||
|
||||
return normalized.endsWith('/v1')
|
||||
? normalized
|
||||
: `${normalized}/v1`;
|
||||
}
|
||||
|
||||
function asErrorMessage(error: unknown) {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function defaultCreateModel(config: AiConfig) {
|
||||
const zhipu = createZhipu({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.baseUrl
|
||||
if (config.provider === 'zhipu') {
|
||||
const zhipu = createZhipu({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.baseUrl
|
||||
});
|
||||
|
||||
return zhipu(config.model);
|
||||
}
|
||||
|
||||
const openai = createOpenAI({
|
||||
apiKey: config.apiKey ?? OLLAMA_API_KEY,
|
||||
baseURL: toOpenAiCompatibleBaseUrl(config.baseUrl)
|
||||
});
|
||||
|
||||
return zhipu(config.model);
|
||||
return openai.chat(config.model);
|
||||
}
|
||||
|
||||
async function defaultGenerate(input: AiGenerateInput): Promise<AiGenerateOutput> {
|
||||
@@ -102,10 +138,15 @@ async function defaultGenerate(input: AiGenerateInput): Promise<AiGenerateOutput
|
||||
}
|
||||
|
||||
export function getAiConfig(options?: GetAiConfigOptions) {
|
||||
return getReportAiConfig(options);
|
||||
}
|
||||
|
||||
export function getReportAiConfig(options?: GetAiConfigOptions) {
|
||||
const env = options?.env ?? process.env;
|
||||
warnIgnoredZhipuBaseUrl(env, options?.warn ?? console.warn);
|
||||
|
||||
return {
|
||||
provider: 'zhipu',
|
||||
apiKey: envValue('ZHIPU_API_KEY', env),
|
||||
baseUrl: CODING_API_BASE_URL,
|
||||
model: envValue('ZHIPU_MODEL', env) ?? 'glm-4.7-flashx',
|
||||
@@ -113,15 +154,30 @@ export function getAiConfig(options?: GetAiConfigOptions) {
|
||||
} satisfies AiConfig;
|
||||
}
|
||||
|
||||
export function getExtractionAiConfig(options?: GetAiConfigOptions) {
|
||||
const env = options?.env ?? process.env;
|
||||
|
||||
return {
|
||||
provider: 'ollama',
|
||||
apiKey: envValue('OLLAMA_API_KEY', env) ?? OLLAMA_API_KEY,
|
||||
baseUrl: envValue('OLLAMA_BASE_URL', env) ?? OLLAMA_BASE_URL,
|
||||
model: envValue('OLLAMA_MODEL', env) ?? OLLAMA_MODEL,
|
||||
temperature: 0
|
||||
} satisfies AiConfig;
|
||||
}
|
||||
|
||||
export function isAiConfigured(options?: GetAiConfigOptions) {
|
||||
const config = getAiConfig(options);
|
||||
const config = getReportAiConfig(options);
|
||||
return Boolean(config.apiKey);
|
||||
}
|
||||
|
||||
export async function runAiAnalysis(prompt: string, systemPrompt?: string, options?: RunAiAnalysisOptions) {
|
||||
const config = getAiConfig(options);
|
||||
const workload = options?.workload ?? 'report';
|
||||
const config = workload === 'extraction'
|
||||
? getExtractionAiConfig(options)
|
||||
: getReportAiConfig(options);
|
||||
|
||||
if (!config.apiKey) {
|
||||
if (workload === 'report' && !config.apiKey) {
|
||||
return {
|
||||
provider: 'local-fallback',
|
||||
model: config.model,
|
||||
@@ -131,25 +187,49 @@ export async function runAiAnalysis(prompt: string, systemPrompt?: string, optio
|
||||
|
||||
const createModel = options?.createModel ?? defaultCreateModel;
|
||||
const generate = options?.generate ?? defaultGenerate;
|
||||
const model = createModel(config);
|
||||
const warn = options?.warn ?? console.warn;
|
||||
|
||||
const result = await generate({
|
||||
model,
|
||||
system: systemPrompt,
|
||||
prompt,
|
||||
temperature: config.temperature
|
||||
});
|
||||
try {
|
||||
const model = createModel(config);
|
||||
|
||||
const text = result.text.trim();
|
||||
if (!text) {
|
||||
throw new Error('AI SDK returned an empty response');
|
||||
const result = await generate({
|
||||
model,
|
||||
system: systemPrompt,
|
||||
prompt,
|
||||
temperature: config.temperature
|
||||
});
|
||||
|
||||
const text = result.text.trim();
|
||||
if (!text) {
|
||||
if (workload === 'extraction') {
|
||||
return {
|
||||
provider: 'local-fallback',
|
||||
model: config.model,
|
||||
text: fallbackResponse(prompt)
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('AI SDK returned an empty response');
|
||||
}
|
||||
|
||||
return {
|
||||
provider: config.provider,
|
||||
model: config.model,
|
||||
text
|
||||
};
|
||||
} catch (error) {
|
||||
if (workload === 'extraction') {
|
||||
warn(`[AI SDK] Extraction fallback activated: ${asErrorMessage(error)}`);
|
||||
|
||||
return {
|
||||
provider: 'local-fallback',
|
||||
model: config.model,
|
||||
text: fallbackResponse(prompt)
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
provider: 'zhipu',
|
||||
model: config.model,
|
||||
text
|
||||
};
|
||||
}
|
||||
|
||||
export function __resetAiWarningsForTests() {
|
||||
|
||||
Reference in New Issue
Block a user