import axios from 'axios'; import { getLLMConfig } from './config.js'; import { line, error } from './log.js'; function isOpenAIGpt55(provider, model) { return provider === 'openai' && /^gpt-5\.5(?:-|$)/i.test(model || ''); } function chatEndpoint(baseURL, provider, model) { const base = baseURL.replace(/\/$/, ''); return isOpenAIGpt55(provider, model) ? `${base}/responses` : `${base}/chat/completions`; } function chatPayload(provider, model, systemPrompt, userContent) { if (isOpenAIGpt55(provider, model)) { return { model, instructions: systemPrompt, input: userContent, temperature: 0.2 }; } return { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }; } function extractContent(provider, model, data) { if (!isOpenAIGpt55(provider, model)) return data.choices[0].message.content; if (typeof data.output_text === 'string') return data.output_text; const parts = data.output?.flatMap(item => item.content || []) || []; const text = parts .map(part => { if (typeof part.text === 'string') return part.text; if (typeof part.content === 'string') return part.content; return ''; }) .filter(Boolean) .join(''); if (text) return text; return data.choices?.[0]?.message?.content || ''; } function opencodeModelConfig(model) { const [providerID, modelID] = model.includes('/') ? model.split('/', 2) : [process.env.OPENCODE_PROVIDER || 'google', model]; return { providerID, modelID }; } function applyOpenCodeAuth(headers) { const password = process.env.OPENCODE_SERVER_PASSWORD; if (!password) return; const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode'; headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; } function extractOpenCodeContent(data) { const parts = data.parts || data.data?.parts || data.info?.content || data.data?.info?.content || []; return parts .map(part => part.text || part.content || '') .filter(Boolean) .join(''); } async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers) { const base = baseURL.replace(/\/$/, ''); const { providerID, modelID } = opencodeModelConfig(model); const session = await axios.post( `${base}/session`, { title: 'AI Code Review', model: { providerID, id: modelID } }, { headers } ); const sessionID = session.data.id || session.data.data?.id; if (!sessionID) throw new Error('OpenCode session 建立失敗:回應中沒有 session id'); const resp = await axios.post( `${base}/session/${sessionID}/message`, { model: { providerID, modelID }, system: systemPrompt, parts: [{ type: 'text', text: userContent }], }, { headers } ); return extractOpenCodeContent(resp.data); } export async function chat(systemPrompt, userContent) { const { provider, apiKeys, baseURL, model } = getLLMConfig(); if (!provider) throw new Error('未設定任何 LLM API Key'); line(`[LLM] provider=${provider} model=${model}`); const headers = { 'Content-Type': 'application/json' }; if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; const shuffled = [...apiKeys].sort(() => Math.random() - 0.5); for (let i = 0; i < shuffled.length; i++) { if (provider !== 'ollama' && provider !== 'opencode') headers['Authorization'] = `Bearer ${shuffled[i]}`; try { if (provider === 'opencode') { applyOpenCodeAuth(headers); return await chatOpenCode(baseURL, model, systemPrompt, userContent, headers); } const resp = await axios.post( chatEndpoint(baseURL, provider, model), chatPayload(provider, model, systemPrompt, userContent), { headers } ); return extractContent(provider, model, resp.data); } catch (e) { line(`[LLM] key[${i + 1}/${shuffled.length}] 失敗: ${e.message}`); } } error('[LLM] 所有 API Key 均失敗,終止流程'); process.exit(1); } export async function chatJSON(systemPrompt, userContent) { const text = await chat(systemPrompt, userContent); try { return JSON.parse(text.trim().replace(/^```[^\n]*\n?/, '').replace(/```$/, '').trim()); } catch (e) { line(`[LLM] JSON 解析失敗: ${e.message}`); return []; } }