File size: 8,586 Bytes
20f108c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// Simulate phantom.mjs --chat flow
import { runChat } from './chat.mjs';

// Simulate the exact llm object from phantom.mjs
import fs from "fs";
const config = JSON.parse(fs.readFileSync('./config.json', 'utf-8'));

// Copy exact phantom.mjs PROVIDERS
const PROVIDERS = {
  openai:      { url: "https://opencode.ai/zen/v1",                keyEnv: "OPENCODE_ZEN_API_KEY",     defaultModel: "nemotron-3-ultra-free", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 16384 }),               parse: d => { const c = d.choices?.[0]?.message?.content?.trim(); return c || (d.choices?.[0]?.finish_reason === "length" ? "[Response truncated — increase max_tokens]" : "…"); }, auth: k => ({ "Authorization": `Bearer ${k}` }) },
  anthropic:   { url: "https://api.anthropic.com/v1",         keyEnv: "ANTHROPIC_API_KEY",   defaultModel: "claude-sonnet-4-20250514", chatPath: "/messages",         fmt: o => ({ model: o.model, messages: o.messages, max_tokens: 512 }),                                 parse: d => d.content?.[0]?.text || d.content?.toString() || "...",                                                                                                      auth: k => ({ "x-api-key": k, "anthropic-version": "2023-06-01" }) },
  gemini:      { url: "https://generativelanguage.googleapis.com/v1beta", keyEnv: "GEMINI_API_KEY", defaultModel: "gemini-2.0-flash", chatPath: "/models/{model}:generateContent", fmt: o => ({ contents: o.messages.map(m => ({ role: m.role === "assistant" ? "model" : m.role, parts: [{ text: m.content }] })) }), parse: d => d.candidates?.[0]?.content?.parts?.[0]?.text || "...",                                             auth: () => ({}), urlMod: (u, m, k) => `${u}${m}?key=${k}` },
  groq:        { url: "https://api.groq.com/openai/v1",       keyEnv: "GROQ_API_KEY",        defaultModel: "llama-3.3-70b-versatile", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }),               parse: d => d.choices?.[0]?.message?.content?.trim() || "...",                                                                                                       auth: k => ({ "Authorization": `Bearer ${k}` }) },
  deepseek:    { url: "https://api.deepseek.com/v1",          keyEnv: "DEEPSEEK_API_KEY",    defaultModel: "deepseek-chat",   chatPath: "/chat/completions",     fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }),               parse: d => d.choices?.[0]?.message?.content?.trim() || "...",                                                                                                       auth: k => ({ "Authorization": `Bearer ${k}` }) },
  mistral:     { url: "https://api.mistral.ai/v1",            keyEnv: "MISTRAL_API_KEY",     defaultModel: "mistral-large-latest", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }),               parse: d => d.choices?.[0]?.message?.content?.trim() || "...",                                                                                                       auth: k => ({ "Authorization": `Bearer ${k}` }) },
  openrouter:  { url: "https://openrouter.ai/api/v1",         keyEnv: "OPENROUTER_API_KEY",  defaultModel: "anthropic/claude-sonnet-4", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }),               parse: d => d.choices?.[0]?.message?.content?.trim() || "...",                                                                                                       auth: k => ({ "Authorization": `Bearer ${k}` }) },
  ollama:      { url: process.env.OLLAMA_HOST || "http://localhost:11434", keyEnv: "",        defaultModel: "llama3",         chatPath: "/api/chat",           fmt: o => ({ model: o.model, messages: o.messages, stream: false }),                                  parse: d => d.message?.content?.trim() || "...",                                                                                                                       auth: () => ({}) },
  opencode:    { url: "https://opencode.ai/zen/v1",                keyEnv: "OPENCODE_ZEN_API_KEY",     defaultModel: "nemotron-3-ultra-free", chatPath: "/chat/completions",     fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 16384 }),               parse: d => { const c = d.choices?.[0]?.message?.content?.trim(); return c || (d.choices?.[0]?.finish_reason === "length" ? "[Response truncated — increase max_tokens or shorten context]" : "…"); },                                                        auth: k => ({ "Authorization": `Bearer ${k}` }) },
};

const PHANTOM_LLM_PROVIDER = "openai";
let _config = config;

function getProvider() {
  const name = PHANTOM_LLM_PROVIDER || "openai";
  const p = PROVIDERS[name];
  if (!p) return PROVIDERS.openai;
  return p;
}

function getKey(p) {
  if (p.keyEnv) {
    const k = process.env[p.keyEnv];
    if (k) return k;
    if (_config[p.keyEnv]) return _config[p.keyEnv];
  }
  return "";
}

async function detectProviders() {
  const available = {};
  for (const [name, p] of Object.entries(PROVIDERS)) {
    if (name === "ollama") {
      try {
        const r = await fetch(`${p.url}/api/tags`, { signal: AbortSignal.timeout(3000) });
        available[name] = r.ok ? "local" : "no";
      } catch { available[name] = "no"; }
    } else {
      available[name] = p.keyEnv && getKey(p) ? "key" : "no";
    }
  }
  return available;
}

function selectBest(avail) {
  const order = ["openai", "ollama", "opencode", "anthropic", "groq", "gemini", "deepseek", "mistral", "openrouter"];
  for (const name of order) {
    if (avail[name] && avail[name] !== "no") return name;
  }
  return null;
}

const phantomLLM = {
  get provider() { return PHANTOM_LLM_PROVIDER; },
  set provider(name) { if (PROVIDERS[name]) PHANTOM_LLM_PROVIDER = name; },
  get providers() { return Object.keys(PROVIDERS); },
  detectProviders,
  selectBest,
  get hasLLM() {
    const p = getProvider();
    return !!(p.keyEnv ? getKey(p) : p === PROVIDERS.ollama);
  },
  chat: async function(messages, opts = {}) {
    const p = getProvider();
    const key = getKey(p);
    if (p.keyEnv && !key) return `[${PHANTOM_LLM_PROVIDER}] No API key. Set ${p.keyEnv} env or in config.json`;
    const model = opts.model || p.defaultModel;

    // Fallback chain on rate limits / errors
    const fallbackOrder = ["openai", "anthropic", "groq", "gemini", "deepseek", "mistral", "openrouter", "opencode"];
    let currentProvider = PHANTOM_LLM_PROVIDER;
    let lastError = "";

    for (const providerName of fallbackOrder) {
      const providerObj = PROVIDERS[providerName];
      if (!providerObj) continue;

      const providerKey = getKey(providerObj);
      if (providerObj.keyEnv && !providerKey && providerName !== "ollama") continue;

      try {
        let url = `${providerObj.url}${providerObj.chatPath.replace("{model}", model)}`;
        const headers = { "Content-Type": "application/json", ...providerObj.auth(providerKey) };
        if (providerObj.urlMod) url = providerObj.urlMod(url, providerObj.chatPath.replace("{model}", model), providerKey);
        const body = JSON.stringify(providerObj.fmt({ model, messages }));

        const r = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(60000) });

        if (!r.ok) {
          const t = await r.text().catch(() => "");
          lastError = `[${providerName} ${r.status}] ${t.substring(0, 200)}`;

          // Fallback on rate limit or service errors
          if (r.status === 429 || r.status === 503 || r.status >= 500) {
            console.log(`[LLM Fallback] ${providerName} returned ${r.status}, trying next provider...`);
            continue;
          }
          return lastError;
        }

        const d = await r.json();
        const result = providerObj.parse(d) || "...";

        if (providerName !== currentProvider) {
          console.log(`[LLM Fallback] Switched to ${providerName}`);
        }
        return result;
      } catch (e) {
        lastError = `[${providerName} err] ${e.message}`;
        console.log(`[LLM Fallback] ${providerName} error: ${e.message}, trying next...`);
        continue;
      }
    }

    return `[LLM Fallback] All providers exhausted. Last error: ${lastError}`;
  },
};

console.log('phantomLLM.provider:', phantomLLM.provider);
console.log('phantomLLM.hasLLM:', phantomLLM.hasLLM);

await runChat(phantomLLM);