import * as SecureStore from "expo-secure-store"; export type AIProvider = | "anthropic" | "openai" | "perplexity" | "ollama" | "openrouter" | "custom"; const KEYS = { AI_PROVIDER: "pref_ai_provider", AI_BASE_URL: "pref_ai_base_url", AI_MODEL: "pref_ai_model", AI_KEY: "pref_ai_key", }; export const AI_PROVIDER_BASE_URLS: Record = { anthropic: "https://api.anthropic.com", openai: "https://api.openai.com/v1", perplexity: "https://api.perplexity.ai", ollama: "http://localhost:11434/v1", openrouter: "https://openrouter.ai/api/v1", custom: "", }; export const AI_PROVIDER_MODEL_PLACEHOLDERS: Record = { anthropic: "claude-haiku-4-5-20251001", openai: "gpt-4o-mini", perplexity: "llama-3.1-sonar-small-128k-online", ollama: "llama3.2", openrouter: "openai/gpt-4o-mini", custom: "model-name", }; const SYSTEM_PROMPT = "You are a systems reliability engineer. Analyze the incident and respond with JSON ONLY " + "(no prose, no markdown fences):\n" + '{"root_cause":"...","fix":"..."}\n' + "Be concise and technical. Max 200 chars per field."; async function getConfig() { const [provider, baseUrl, model, key] = await Promise.all([ SecureStore.getItemAsync(KEYS.AI_PROVIDER), SecureStore.getItemAsync(KEYS.AI_BASE_URL), SecureStore.getItemAsync(KEYS.AI_MODEL), SecureStore.getItemAsync(KEYS.AI_KEY), ]); const p = (provider ?? "openai") as AIProvider; return { provider: p, baseUrl: baseUrl || AI_PROVIDER_BASE_URLS[p], model: model || AI_PROVIDER_MODEL_PLACEHOLDERS[p], key: key ?? "", }; } export async function callAI( title: string, symptom: string ): Promise<{ rootCause: string; fix: string }> { const config = await getConfig(); const userMsg = `Title: ${title}\nSymptom: ${symptom}`; const raw = config.provider === "anthropic" ? await callAnthropic(config, userMsg) : await callOpenAICompat(config, userMsg); try { const parsed = JSON.parse(raw) as { root_cause?: string; fix?: string }; return { rootCause: parsed.root_cause ?? "", fix: parsed.fix ?? "", }; } catch { throw new Error(`Unexpected AI response: ${raw.slice(0, 120)}`); } } async function callAnthropic( config: { baseUrl: string; model: string; key: string }, userMsg: string ): Promise { const res = await fetch(`${config.baseUrl}/v1/messages`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": config.key, "anthropic-version": "2023-06-01", }, body: JSON.stringify({ model: config.model, max_tokens: 512, system: SYSTEM_PROMPT, messages: [{ role: "user", content: userMsg }], }), }); if (!res.ok) throw new Error(`Anthropic ${res.status}: ${await res.text()}`); const data = (await res.json()) as { content: { text: string }[] }; return data.content[0].text; } async function callOpenAICompat( config: { baseUrl: string; model: string; key: string }, userMsg: string ): Promise { const headers: Record = { "Content-Type": "application/json" }; if (config.key) headers["Authorization"] = `Bearer ${config.key}`; const res = await fetch(`${config.baseUrl}/chat/completions`, { method: "POST", headers, body: JSON.stringify({ model: config.model, max_tokens: 512, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: userMsg }, ], }), }); if (!res.ok) throw new Error(`AI ${res.status}: ${await res.text()}`); const data = (await res.json()) as { choices: { message: { content: string } }[]; }; return data.choices[0].message.content; }