// Candidate-generator for Stage 05 (Logits → Sample). // // Architecture (post-autoplan, M2): // Tier 1 — LIVE (via proxy, added in A3) // fetch('/api/next-tokens', ...) → Cloudflare Worker or local Node wrapper // using the owner's ANTHROPIC_API_KEY. Hard cost caps on the proxy side. // Tier 2 — LOCAL (this file, always available, never fails) // Hand-curated bias table keyed on the last 1–2 "words" of the prompt, // with category-based fallbacks by tail shape. Deterministic (seeded). // // A1 scope: LOCAL tier only. Proxy wiring lands in A3. // SANDBOX tier (window.claude.complete) deliberately dropped — post-export // it is permanently undefined and the code path is dead. // ── Bias table ────────────────────────────────────────────────────────────── // Keyed on the last 1 or 2 whitespace-trimmed words of the prompt, lowercased. // Later words in the context dominate (last-2 beats last-1). Each entry lists // plausible next tokens. Leading space preserved — that's how real tokenizers // (BPE, SentencePiece) represent word boundaries: " the" vs "the" are different // tokens with different ids. const BIAS_TABLE = { // ── Two-word contexts (most specific) ──────────────────────────────────── // Matrix-voiced + narrative openers the starter prompt benefits from 'dreams in': [' color', ' binary', ' silence', ' code', ' the', ' numbers', ' fragments', ' static'], 'dreams of': [' electric', ' flying', ' home', ' a', ' the', ' nothing', ' light', ' waking'], 'machine dreams': [' in', ' of', ' about', ' alone', ' and'], 'the machine': [' dreams', ' thinks', ' learns', ' knows', ' is', ' was', ' has', ' became'], 'the matrix': [' is', ' has', ' was', ' knows', ' sees', ' expects'], 'reality is': [' a', ' the', ' not', ' an', ' just', ' only', ' made'], 'truth is': [' the', ' a', ' we', ' I', ' not', ' rarely', ' only'], 'nobody knows': [' what', ' why', ' how', ' the', ' if', ' when', ' where'], // Well-worn narrative / rhetorical openings 'once upon': [' a'], 'it was': [' the', ' a', ' not', ' clear', ' only', ' all', ' already'], 'he said': [',', '.', ' nothing', ' that', ' to', ' quietly', ' again'], 'she said': [',', '.', ' nothing', ' that', ' to', ' quietly', ' again'], 'they said': [',', '.', ' nothing', ' that', ' it', ' to', ' we'], 'i think': [' the', ' it', ' we', ' that', ' I', ' about', ' so'], 'i know': [' the', ' it', ' that', ' what', ' why', ' how', ' you'], 'i saw': [' the', ' a', ' it', ' something', ' him', ' her', ' that'], // Identity / state-of-being copula patterns 'to be': [' or', ' a', ' the', ' honest', ' fair', ' clear', ' sure', ' continued'], 'i am': [' a', ' not', ' the', ' here', ' trying', ' going', ' sorry', ' still'], 'you are': [' a', ' not', ' the', ' here', ' right', ' welcome', ' still', ' going'], 'we are': [' not', ' the', ' a', ' here', ' living', ' working', ' going', ' still'], 'they are': [' not', ' the', ' a', ' still', ' already', ' only', ' mostly'], 'it is': [' a', ' the', ' not', ' also', ' important', ' possible', ' clear', ' only'], 'there is': [' a', ' no', ' also', ' something', ' an', ' only', ' nothing', ' little'], 'there are': [' no', ' many', ' also', ' several', ' two', ' three', ' some', ' few'], 'this is': [' the', ' a', ' not', ' my', ' how', ' what', ' why', ' where'], 'that is': [' why', ' the', ' a', ' not', ' how', ' what', ' where', ' when'], 'what is': [' the', ' a', ' this', ' it', ' happening', ' going', ' real', ' true'], 'what are': [' the', ' we', ' you', ' they', ' these', ' those', ' people', ' some'], 'where is': [' the', ' my', ' your', ' his', ' her', ' everyone', ' it', ' he'], 'when is': [' the', ' it', ' your', ' my', ' the next', ' a'], 'who is': [' the', ' this', ' that', ' going', ' responsible', ' there', ' in'], 'why is': [' the', ' it', ' this', ' everything', ' there', ' that', ' nobody'], 'how is': [' the', ' it', ' this', ' that', ' possible', ' she', ' he'], // Negation / modality 'is not': [' a', ' the', ' always', ' really', ' just', ' only', ' clear', ' enough'], 'are not': [' the', ' always', ' just', ' only', ' really', ' always', ' yet'], 'will not': [' be', ' let', ' make', ' happen', ' forget', ' change', ' matter'], 'cannot be': [' done', ' the', ' a', ' used', ' easily', ' fully', ' certain'], 'do not': [' know', ' think', ' want', ' have', ' need', ' use', ' let', ' worry'], 'does not': [' matter', ' mean', ' work', ' make', ' want', ' know', ' have', ' need'], 'did not': [' know', ' think', ' want', ' see', ' expect', ' mean', ' come'], 'should not': [' be', ' have', ' go', ' matter', ' let', ' worry', ' forget'], 'could not': [' be', ' have', ' see', ' find', ' stop', ' help', ' tell'], // Definite-article phrase chains 'in the': [' end', ' beginning', ' world', ' middle', ' dark', ' morning', ' future', ' system', ' data'], 'on the': [' other', ' same', ' way', ' surface', ' edge', ' contrary', ' horizon', ' screen', ' table'], 'at the': [' end', ' same', ' top', ' moment', ' heart', ' edge', ' time', ' bottom'], 'of the': [' world', ' day', ' machine', ' system', ' model', ' century', ' future', ' mind'], 'from the': [' beginning', ' start', ' inside', ' edge', ' depths', ' top', ' first', ' moment'], 'to the': [' next', ' last', ' edge', ' point', ' future', ' past', ' other', ' top'], 'for the': [' first', ' last', ' next', ' next', ' record', ' world', ' moment', ' rest'], 'with the': [' same', ' rest', ' others', ' others', ' right', ' world', ' model'], 'by the': [' time', ' end', ' way', ' side', ' moment', ' hour', ' year'], 'about the': [' world', ' future', ' system', ' model', ' way', ' fact', ' problem'], 'through the': [' door', ' window', ' screen', ' mirror', ' fog', ' static', ' years'], 'across the': [' room', ' screen', ' world', ' years', ' street', ' void', ' network'], 'beyond the': [' veil', ' horizon', ' edge', ' screen', ' pale', ' reach', ' surface'], 'within the': [' system', ' model', ' network', ' world', ' structure', ' data', ' limits'], 'inside the': [' system', ' model', ' machine', ' head', ' room', ' box', ' code'], // Indefinite-article phrase chains 'in a': [' moment', ' way', ' world', ' few', ' single', ' flash', ' sense', ' dream'], 'on a': [' screen', ' day', ' hill', ' whim', ' basic', ' single', ' table'], 'at a': [' time', ' point', ' distance', ' table', ' stop', ' rate', ' glance'], 'of a': [' kind', ' sort', ' man', ' woman', ' child', ' machine', ' dream'], 'to a': [' point', ' degree', ' world', ' place', ' stop', ' man', ' woman'], 'for a': [' moment', ' while', ' long', ' second', ' reason', ' change', ' while'], 'with a': [' smile', ' sigh', ' nod', ' shrug', ' laugh', ' frown', ' gesture'], 'into a': [' room', ' dream', ' single', ' new', ' different', ' state', ' pool'], // Quick browns / test utterances 'the quick': [' brown', ' fox', ' and', ' response'], 'brown fox': [' jumps', ' ran', ' is', ' was', ' came', ' over'], // Tech & ML domain patterns 'machine learning': [' is', ' models', ' can', ' algorithms', ' techniques', ' has', ' systems'], 'artificial intelligence': [' is', ' has', ' will', ' can', ' systems', ' models', ' agents'], 'language model': [' is', ' can', ' outputs', ' predicts', ' thinks', ' generates'], 'language models': [' are', ' can', ' learn', ' predict', ' process', ' generate'], 'neural network': [' is', ' was', ' learns', ' has', ' outputs', ' trains'], 'neural networks': [' are', ' learn', ' have', ' train', ' can', ' process'], 'deep learning': [' is', ' has', ' can', ' models', ' systems', ' framework'], 'attention head': [' looks', ' focuses', ' attends', ' weights', ' captures'], 'attention heads': [' look', ' focus', ' attend', ' weight', ' specialize'], 'token embedding': [' is', ' captures', ' represents', ' lives'], 'residual stream': [' is', ' carries', ' accumulates', ' flows'], // Contemplative continuations 'how to': [' make', ' build', ' create', ' use', ' write', ' find', ' think', ' avoid'], 'when you': [' are', ' see', ' know', ' think', ' come', ' go', ' find', ' start'], 'if you': [' are', ' want', ' need', ' know', ' see', ' try', ' can', ' look'], 'because we': [' are', ' have', ' know', ' cannot', ' do', ' were', ' still'], 'and then': [' the', ' I', ' we', ' it', ' suddenly', ' everything', ' nothing'], 'but when': [' the', ' I', ' we', ' you', ' it', ' he', ' she'], 'but then': [' the', ' I', ' we', ' you', ' it', ' suddenly', ' again'], // ── One-word contexts ──────────────────────────────────────────────────── // Articles + possessives 'the': [' model', ' world', ' system', ' data', ' future', ' number', ' machine', ' signal', ' answer', ' code'], 'a': [' new', ' single', ' few', ' simple', ' model', ' system', ' small', ' kind'], 'an': [' old', ' entire', ' example', ' array', ' order', ' idea', ' answer', ' hour'], 'this': [' is', ' was', ' means', ' time', ' one', ' way', ' could', ' kind'], 'that': [' is', ' was', ' means', ' could', ' the', ' we', ' you', ' one'], 'these': [' are', ' were', ' things', ' days', ' systems', ' models', ' numbers'], 'those': [' are', ' were', ' who', ' days', ' things', ' people', ' numbers'], 'my': [' own', ' mind', ' head', ' heart', ' point', ' job', ' life', ' first'], 'your': [' own', ' mind', ' life', ' job', ' time', ' own', ' head', ' work'], 'their': [' own', ' minds', ' lives', ' work', ' data', ' own', ' systems'], 'our': [' own', ' work', ' data', ' world', ' time', ' model', ' system'], 'its': [' own', ' weight', ' output', ' input', ' shape', ' form'], // Be-verbs / aux 'is': [' a', ' the', ' not', ' also', ' an', ' still', ' only', ' never'], 'was': [' a', ' the', ' not', ' still', ' once', ' already', ' never'], 'are': [' the', ' not', ' also', ' often', ' still', ' many', ' never'], 'were': [' the', ' not', ' all', ' still', ' never', ' only', ' many'], 'be': [' a', ' the', ' able', ' used', ' more', ' in', ' on'], 'been': [' the', ' a', ' many', ' working', ' a', ' trying', ' running'], 'being': [' a', ' the', ' one', ' honest', ' human', ' used', ' able'], 'have': [' been', ' the', ' a', ' to', ' never', ' not', ' already'], 'has': [' been', ' the', ' a', ' to', ' never', ' not', ' already'], 'had': [' been', ' the', ' a', ' to', ' never', ' not', ' already'], 'will': [' be', ' not', ' the', ' have', ' never', ' always', ' happen'], 'would': [' be', ' not', ' the', ' have', ' never', ' rather'], 'could': [' be', ' not', ' have', ' see', ' find', ' use'], 'should': [' be', ' not', ' have', ' see', ' do', ' know'], 'can': [' be', ' not', ' do', ' see', ' still', ' always', ' find'], // Conjunctions 'and': [' the', ' a', ' then', ' so', ' yet', ' I', ' now', ' we'], 'but': [' the', ' it', ' I', ' not', ' also', ' when', ' if', ' we'], 'or': [' the', ' a', ' not', ' maybe', ' even', ' at', ' rather'], 'if': [' you', ' we', ' the', ' it', ' I', ' not', ' there', ' they'], 'when': [' the', ' you', ' we', ' I', ' it', ' they', ' she', ' he'], 'while': [' the', ' you', ' we', ' I', ' it', ' they', ' doing', ' waiting'], 'until': [' the', ' you', ' we', ' it', ' I', ' they', ' everything'], 'because': [' the', ' it', ' of', ' we', ' I', ' they', ' you'], 'though': [' the', ' it', ' we', ' I', ' not', ' sometimes'], 'so': [' the', ' it', ' we', ' I', ' they', ' much', ' far', ' that'], 'yet': [' the', ' no', ' another', ' still', ' to', ' also'], // Pronouns 'i': [' am', ' was', ' have', ' had', ' will', ' would', ' think', ' know'], 'you': [' are', ' were', ' have', ' had', ' will', ' would', ' think', ' know'], 'he': [' is', ' was', ' has', ' had', ' will', ' would', ' could', ' said'], 'she': [' is', ' was', ' has', ' had', ' will', ' would', ' could', ' said'], 'we': [' are', ' were', ' have', ' had', ' will', ' would', ' can', ' should'], 'they': [' are', ' were', ' have', ' had', ' will', ' would', ' can', ' say'], 'it': [' is', ' was', ' has', ' had', ' will', ' would', ' could', ' should'], // Subject-domain nouns 'dreams': [' of', ' in', ' are', ' about', ' and', ' were', ' become'], 'machine': [' learning', ' is', ' that', ' can', ' has', ' was', ' thinks'], 'model': [' is', ' can', ' has', ' was', ' learns', ' outputs', ' thinks'], 'models': [' are', ' can', ' have', ' were', ' learn', ' output', ' think'], 'data': [' is', ' are', ' from', ' about', ' shows', ' points', ' science'], 'system': [' is', ' has', ' was', ' will', ' can', ' keeps', ' runs'], 'systems': [' are', ' have', ' were', ' will', ' can', ' keep', ' run'], 'neural': [' network', ' networks', ' pathways', ' activity', ' architecture', ' firing'], 'deep': [' learning', ' inside', ' in', ' within', ' neural', ' beneath', ' down'], 'attention':[' is', ' heads', ' head', ' matters', ' flows', ' pattern'], 'token': [' is', ' by', ' after', ' in', ' that', ' which', ' embedding'], 'tokens': [' are', ' in', ' flow', ' form', ' make', ' span', ' have'], 'think': [' about', ' of', ' that', ' it', ' we', ' again', ' twice'], 'thinks': [' about', ' the', ' it', ' that', ' of', ' we'], 'says': [' the', ' it', ' he', ' she', ' they', ' a', ' nothing'], 'said': [' the', ' it', ' he', ' she', ' to', ' that', ' nothing'], 'reality': [' is', ' was', ' has', ' checks', ' itself', ' bends', ' consists'], 'code': [' is', ' that', ' the', ' runs', ' reads', ' compiles', ' defines'], 'signal': [' is', ' from', ' noise', ' and', ' that', ' flow', ' decays'], 'numbers': [' flow', ' are', ' that', ' streaming', ' as', ' become', ' fall'], }; // ── Categorical fallback pools ────────────────────────────────────────────── // Used when the bias table has no match for the tail. Chosen by the tail's // rough "shape" (article, preposition, verb-like, sentence-end, etc). const POOLS = { after_article: [' model', ' world', ' system', ' data', ' machine', ' number', ' future', ' signal', ' answer', ' code', ' thing', ' moment'], after_prep: [' the', ' a', ' its', ' their', ' our', ' my', ' every', ' each', ' some', ' this'], after_verb_past: [' the', ' a', ' it', ' me', ' him', ' her', ' them', ' us', ' everyone'], after_verb_ing: [' the', ' a', ' to', ' at', ' for', ' with', ' from', ' about', ' on'], after_verb_generic: [' a', ' the', ' to', ' it', ' that', ' in', ' with', ' into', ' for'], after_plural: [' are', ' were', ' have', ' had', ' will', ' can', ' and', ' that'], after_singular_noun: [' is', ' was', ' has', ' had', ' will', ' can', ' that', ' which'], sentence_start: [' The', ' It', ' We', ' Then', ' A', ' In', ' This', ' There', ' After', ' But'], after_comma: [' and', ' but', ' or', ' so', ' then', ' the', ' a', ' which'], after_quote: [' said', ' the', ' he', ' she', ' they', ',', '.'], generic: [' the', ' a', ' and', ' of', ' to', ' in', ' is', ' it', ' that', ' with', ' as', ' on'], punctuation: ['.', ',', ';', '—', '…'], }; // ── Category detectors ────────────────────────────────────────────────────── const ARTICLES = new Set(['the', 'a', 'an', 'this', 'that', 'these', 'those', 'my', 'our', 'your', 'their', 'its', 'his', 'her']); const PREPS = new Set(['in', 'on', 'at', 'of', 'to', 'for', 'with', 'by', 'from', 'about', 'into', 'onto', 'through', 'across', 'beyond', 'within', 'inside', 'between', 'against', 'toward', 'towards', 'under', 'over']); const PLURAL_HINTS = /s$/; const VERB_ING = /ing$/; const VERB_ED = /(ed|ght)$/; // Split raw text into whitespace-trimmed "words" for lookup (lowercased, // stripped of most punctuation). function lastWords(text, n) { const trimmed = text.replace(/\s+$/, ''); const m = trimmed.match(/\S+/g); if (!m) return []; return m.slice(-n).map(w => w.toLowerCase().replace(/[^a-z']/g, '')); } // Returns a candidate-token array given the raw text context. function localCandidates(text) { const w2 = lastWords(text, 2); const w1 = lastWords(text, 1); const trimmed = text.trim(); const last = w1[0] || ''; const prevChar = trimmed.slice(-1); // 1. Two-word exact match (strongest) if (w2.length === 2) { const key2 = w2.join(' '); if (BIAS_TABLE[key2]) return augment(BIAS_TABLE[key2]); } // 2. One-word exact match if (w1.length === 1 && BIAS_TABLE[last]) return augment(BIAS_TABLE[last]); // 3. Categorical fallback by tail shape let pool; if (prevChar === '.' || prevChar === '!' || prevChar === '?') { pool = POOLS.sentence_start; } else if (prevChar === ',') { pool = POOLS.after_comma; } else if (prevChar === '"' || prevChar === "'" || prevChar === '"' || prevChar === '"' || prevChar === "'") { pool = POOLS.after_quote; } else if (ARTICLES.has(last)) { pool = POOLS.after_article; } else if (PREPS.has(last)) { pool = POOLS.after_prep; } else if (VERB_ING.test(last)) { pool = POOLS.after_verb_ing; } else if (VERB_ED.test(last)) { pool = POOLS.after_verb_past; } else if (PLURAL_HINTS.test(last) && last.length > 3) { // "systems", "models" — plural nouns usually take plural verbs pool = POOLS.after_plural; } else if (last.length > 2) { // Fallback for unmatched word: likely singular noun pool = POOLS.after_singular_noun; } else { pool = POOLS.generic; } return augment(pool); } // Pad / diversify a candidate list up to ~12 entries by mixing in a few // generics and a punctuation mark. Returns plain strings (no probs yet). function augment(primary) { const out = [...primary]; const used = new Set(out); for (const g of POOLS.generic) { if (out.length >= 11) break; if (!used.has(g)) { out.push(g); used.add(g); } } if (out.length < 12 && !used.has('.')) out.push('.'); return out.slice(0, 12); } // Turn a ranked candidate list (strongest first) into a distribution by // assigning logits that decay with rank, then softmax-ing at temperature T. // A seeded nudge keeps identical contexts deterministic while still feeling // organic (no perfectly clean geometric decay). function distribute(candidates, temperature, seedHash) { const rand = seededRand(seedHash); const T = Math.max(0.05, temperature); const n = candidates.length; const logits = candidates.map((_, i) => { const base = 3 - (4 * i) / Math.max(1, n - 1); const jitter = (rand() - 0.5) * 0.9; return base + jitter; }); const scaled = logits.map(l => l / T); const mx = Math.max(...scaled); const exps = scaled.map(l => Math.exp(l - mx)); const sum = exps.reduce((a, b) => a + b, 0); const probs = exps.map(e => e / sum); const items = candidates.map((tok, i) => ({ tok, logit: logits[i], prob: probs[i] })); items.sort((a, b) => b.prob - a.prob); return items; } // FNV-1a hash for deterministic seeding function hashString(s) { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return Math.abs(h); } // ── Public: local (deterministic, offline) ───────────────────────────────── function computeLocalDistribution(context, temperature) { const text = context.map(t => t.text).join(''); const tail = text.slice(-80); // enough for context matching const cands = localCandidates(tail); const seed = hashString(tail); const items = distribute(cands, temperature, seed); return { items, source: 'local' }; } // ── Debug helper (exposed for A4 tests later) ────────────────────────────── // Returns the candidate array AND which lookup path matched, so tests can // assert the bias table vs. category fallback behavior without simulating // the full distribution pipeline. function debugLookup(text) { const w2 = lastWords(text, 2); const w1 = lastWords(text, 1); const last = w1[0] || ''; const prevChar = text.trim().slice(-1); if (w2.length === 2) { const key2 = w2.join(' '); if (BIAS_TABLE[key2]) return { match: 'bias-2', key: key2, tokens: BIAS_TABLE[key2] }; } if (w1.length === 1 && BIAS_TABLE[last]) { return { match: 'bias-1', key: last, tokens: BIAS_TABLE[last] }; } // Category fallback let category; if (prevChar === '.' || prevChar === '!' || prevChar === '?') category = 'sentence_start'; else if (prevChar === ',') category = 'after_comma'; else if (prevChar === '"' || prevChar === "'") category = 'after_quote'; else if (ARTICLES.has(last)) category = 'after_article'; else if (PREPS.has(last)) category = 'after_prep'; else if (VERB_ING.test(last)) category = 'after_verb_ing'; else if (VERB_ED.test(last)) category = 'after_verb_past'; else if (PLURAL_HINTS.test(last) && last.length > 3) category = 'after_plural'; else if (last.length > 2) category = 'after_singular_noun'; else category = 'generic'; return { match: 'category', key: category, tokens: POOLS[category] }; } // ── Public: proxy (LIVE, via our owner-hosted Anthropic proxy) ───────────── // Architecture: // - In dev (origin: localhost:* / 127.*), the proxy is expected at // http://127.0.0.1:8787/api/next-tokens (run with `node proxy/server.mjs`). // - In prod, the proxy is same-origin at /api/next-tokens (Cloudflare Worker). // - Configurable override: set window.LLM_MATRIX_PROXY_URL before app.jsx // loads to point at a different endpoint. // // State machine (H5 from PLAN.md): // status 200 → LIVE, no cooldown // 400 → ERROR, 5-min tier cooldown (bad input, won't recover) // 429 (our limit) → ERROR, 60s tier cooldown // 503 (budget) → BUDGET_EXHAUSTED, 10-min tier cooldown // 529 (upstream) → ERROR, 60s tier cooldown (Anthropic will recover) // timeout / network → ERROR, 30s tier cooldown // After cooldown expires, the engine tries the proxy again on the next call. const PROXY_TIMEOUT_MS = 2000; const proxyCache = new Map(); // key: `${n}|${temp.toFixed(2)}|${prompt}`, value: { items, source } // Per-session state — tier cooldown + current badge. const proxyState = { /** @type {'available'|'cooldown'} */ tier: 'available', /** @type {'LIVE'|'LOCAL'|'BUDGET_EXHAUSTED'|'ERROR'} */ badge: 'LOCAL', cooldownUntil: 0, // ms epoch; 0 = no cooldown lastError: '', // human-readable reason for last failure dayRemainingUsd: null, // from X-Budget-Day-Remaining header monthRemainingUsd: null, // from X-Budget-Month-Remaining header }; function resolveProxyUrl() { if (typeof window !== 'undefined' && typeof window.LLM_MATRIX_PROXY_URL === 'string') { return window.LLM_MATRIX_PROXY_URL; } if (typeof window === 'undefined' || !window.location) { return '/api/next-tokens'; } const host = window.location.hostname; if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1') { return 'http://127.0.0.1:8787/api/next-tokens'; } return '/api/next-tokens'; } function setCooldown(seconds, badge, reason) { proxyState.tier = 'cooldown'; proxyState.cooldownUntil = Date.now() + seconds * 1000; proxyState.badge = badge; proxyState.lastError = reason; } function clearCooldownIfElapsed() { if (proxyState.tier === 'cooldown' && Date.now() >= proxyState.cooldownUntil) { proxyState.tier = 'available'; proxyState.cooldownUntil = 0; } } /** * Fetch a LIVE distribution from the proxy, with fallback-ready semantics. * Returns { items, source: 'proxy' | 'proxy-cached' } on success, * or null on any failure (after updating proxyState accordingly). */ async function computeProxyDistribution(context, temperature, n = 10) { clearCooldownIfElapsed(); if (proxyState.tier === 'cooldown') return null; const text = context.map(t => t.text).join(''); const tempRounded = Math.round(temperature * 100) / 100; const cacheK = `${n}|${tempRounded.toFixed(2)}|${text}`; const cached = proxyCache.get(cacheK); if (cached) { proxyState.badge = 'LIVE'; return { items: cached.items, source: 'proxy-cached' }; } const ac = new AbortController(); const timeoutId = setTimeout(() => ac.abort('timeout'), PROXY_TIMEOUT_MS); try { const resp = await fetch(resolveProxyUrl(), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ prompt: text, temperature: tempRounded, n }), signal: ac.signal, }); clearTimeout(timeoutId); // Always read the budget headers if present const dayRem = resp.headers.get('x-budget-day-remaining'); const monRem = resp.headers.get('x-budget-month-remaining'); if (dayRem !== null) proxyState.dayRemainingUsd = parseFloat(dayRem); if (monRem !== null) proxyState.monthRemainingUsd = parseFloat(monRem); if (resp.status === 200) { const data = await resp.json(); const items = (data.candidates || []).map(c => ({ tok: c.tok, prob: c.prob })); if (items.length === 0) { setCooldown(60, 'ERROR', 'proxy returned empty candidates'); return null; } items.sort((a, b) => b.prob - a.prob); proxyCache.set(cacheK, { items }); proxyState.tier = 'available'; proxyState.badge = 'LIVE'; proxyState.lastError = ''; return { items, source: 'proxy' }; } // Non-200: cool down the tier per status if (resp.status === 400) { setCooldown(300, 'ERROR', 'proxy rejected input (400)'); } else if (resp.status === 429) { setCooldown(60, 'ERROR', 'proxy rate limit hit'); } else if (resp.status === 503) { setCooldown(600, 'BUDGET_EXHAUSTED', 'daily or monthly budget exhausted'); } else if (resp.status === 529) { setCooldown(60, 'ERROR', 'upstream overloaded'); } else { setCooldown(30, 'ERROR', `proxy HTTP ${resp.status}`); } return null; } catch (err) { clearTimeout(timeoutId); const reason = (err && err.name === 'AbortError') ? 'proxy timeout' : `proxy network error: ${err?.message || err}`; setCooldown(30, 'ERROR', reason); return null; } } /** Snapshot proxy state for UI display. */ function getProxyState() { clearCooldownIfElapsed(); return { tier: proxyState.tier, badge: proxyState.badge, cooldownUntil: proxyState.cooldownUntil, lastError: proxyState.lastError, dayRemainingUsd: proxyState.dayRemainingUsd, monthRemainingUsd: proxyState.monthRemainingUsd, }; } window.LogitsEngine = { computeLocalDistribution, computeProxyDistribution, getProxyState, debugLookup, };