File size: 11,127 Bytes
a875dec
 
 
 
 
 
22a56cb
 
a875dec
 
 
 
22a56cb
 
 
 
 
 
a875dec
b814c28
 
 
 
 
 
a875dec
b814c28
a875dec
 
 
 
 
 
 
 
 
 
 
 
 
22a56cb
a875dec
 
 
 
 
 
 
 
 
 
 
 
 
b814c28
a875dec
 
 
 
 
 
 
 
 
 
b814c28
a875dec
 
 
 
22a56cb
a875dec
 
 
 
b814c28
a875dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b814c28
a875dec
 
 
b814c28
a875dec
 
 
 
 
b814c28
a875dec
b814c28
a875dec
 
b814c28
a875dec
 
 
 
 
 
 
 
 
 
b814c28
 
a875dec
 
 
 
 
 
 
 
 
b814c28
a875dec
 
 
 
b814c28
a875dec
 
b814c28
 
 
 
 
 
 
a875dec
 
 
 
 
 
 
 
22a56cb
a875dec
 
 
 
 
 
 
 
 
 
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import { createServer, type IncomingMessage } from 'node:http';
import { readFile, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { pathToFileURL } from 'node:url';
import { computeForecast, forecastObject } from '../forecast-core/src/lib/forecastEngine.ts';
import { freezeForecastSpec } from '../forecast-core/src/lib/forecastSnapshot.ts';
import { MeteredRuntime, openRouterModel, sha256 } from '../forecast-core/src/lib/runtime.ts';
import { runForecast, validateQuestion, type ForecastQuestion } from '../forecast-core/src/lib/workflow.ts';
import { packet, type Evidence } from '../forecast-core/src/lib/evidence.ts';
import { OPENROUTER_DEFAULT_MODEL } from '../forecast-core/src/lib/byokContext.ts';

const assets = new Map([
  ['/', ['index.html', 'text/html; charset=utf-8']],
  ['/app.js', ['app.js', 'text/javascript; charset=utf-8']],
  ['/style.css', ['style.css', 'text/css; charset=utf-8']],
]);
const choices = [
  { id: OPENROUTER_DEFAULT_MODEL, name: 'MiMo 2.6 Flash', tier: 'Budget' },
  { id: 'xiaomi/mimo-v2.6-pro', name: 'MiMo 2.6 Pro', tier: 'Value' },
  { id: 'openai/gpt-5.6-luna', name: 'GPT-5.6 Luna', tier: 'Budget' },
  { id: 'google/gemini-3.8-flash', name: 'Gemini 3.8 Flash', tier: 'Mid-range' },
  { id: 'openai/gpt-6-astra', name: 'GPT-6 Astra', tier: 'Premium' },
  { id: 'anthropic/claude-fable-5.1', name: 'Claude Fable 5.1', tier: 'Premium' },
];

class InputError extends Error {
  readonly status: number;
  constructor(message: string, status = 400) { super(message); this.status = status; }
}
async function body(req: IncomingMessage): Promise<Record<string, any>> {
  if (!req.headers['content-type']?.startsWith('application/json')) throw new InputError('Send application/json.', 415);
  const chunks: Buffer[] = [];
  let size = 0;
  for await (const chunk of req) {
    size += chunk.length;
    if (size > 32768) throw new InputError('Keep the request below 32 KiB.', 413);
    chunks.push(chunk);
  }
  try {
    const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
    if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error();
    return value;
  } catch { throw new InputError('Enter valid JSON.'); }
}
function text(value: unknown, label: string, max: number): string {
  if (typeof value !== 'string' || !value.trim() || value.length > max) throw new InputError(`${label} is required (up to ${max} characters).`);
  return value.trim();
}

/** Personal keys only. Injected transport is for offline acceptance, never a client-selected endpoint. */
export function createWorkbenchServer(request: typeof fetch = fetch) {
  let active = 0;
  let catalog: Promise<Array<{ id: string; name: string; tier: string; rates: { input: number; output: number }; reasoning: boolean; context: number }>> | undefined;
  let catalogAt = 0;
  const models = () => {
    if (!catalog || Date.now() - catalogAt > 300000) {
      catalogAt = Date.now();
      catalog = (async () => {
        const response = await request('https://openrouter.ai/api/v1/models', { signal: AbortSignal.timeout(20000) });
        if (!response.ok) throw new Error('Model catalog unavailable. Try again later.');
        const raw = await response.text();
        if (raw.length > 8000000) throw new Error('Model catalog exceeded its size limit.');
        const discovered = await Promise.all(choices.map(async choice => {
          try { return { ...choice, ...await openRouterModel(choice.id, async () => new Response(raw, { status: 200 }), { textOnly: true }) }; }
          catch { return null; }
        }));
        return discovered.filter((value): value is NonNullable<typeof value> => value !== null);
      })().catch(error => { catalog = undefined; throw error; });
    }
    return catalog;
  };
  const server = createServer(async (req, res) => {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'");
    res.setHeader('Referrer-Policy', 'no-referrer');
    res.setHeader('Cache-Control', 'no-store');
    const send = (status: number, value: unknown) => {
      if (res.destroyed) return;
      res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify(value));
    };
    try {
      const asset = assets.get(req.url ?? '');
      if (req.method === 'GET' && asset) {
        const content = await readFile(new URL(asset[0], import.meta.url));
        res.writeHead(200, { 'Content-Type': asset[1] }); res.end(content); return;
      }
      if (req.method === 'GET' && req.url === '/api/models') { send(200, { models: await models() }); return; }
      if (req.method !== 'POST' || !['/api/compute', '/api/forecast'].includes(req.url ?? '')) { send(404, { error: 'Not found' }); return; }
      // No cross-origin API use, no cookies, no caller-selected provider URL.
      const origin = req.headers.origin;
      if (origin && new URL(origin).host !== req.headers.host) throw new InputError('Use this Space to submit a forecast.', 403);
      const input = await body(req);
      if (req.url === '/api/compute') {
        const spec = forecastObject(input);
        if (!['binary', 'conditional', 'bayes', 'normal'].includes(String(spec.kind))) throw new InputError('Choose a binary, conditional, Bayesian or normal model.');
        const result = computeForecast(spec);
        send(200, { result, snapshot: freezeForecastSpec(spec, result) }); return;
      }
      const apiKey = text(input.apiKey, 'Your OpenRouter key', 512);
      if (!/^sk-or-[A-Za-z0-9_-]+$/.test(apiKey)) throw new InputError('Enter a valid OpenRouter API key.');
      const q: ForecastQuestion = {
        id: randomUUID(), question: text(input.question, 'Question', 1000),
        resolution_date: text(input.resolutionDate, 'Resolution date', 10),
        dated_metric: input.resolutionRule ? text(input.resolutionRule, 'Resolution rule', 2000) : 'YES if the event described in the question occurs by 23:59 UTC on the deadline, according to a primary public source. Otherwise NO. Missing or disputed evidence remains unresolved.',
        event_type: 'occurrence', conditions: [],
      };
      validateQuestion(q);
      const now = new Date();
      if (q.resolution_date <= now.toISOString().slice(0, 10)) throw new InputError('Choose a future resolution date.');
      const evidenceText = input.evidence === undefined || input.evidence === '' ? 'No additional context supplied. Use model knowledge, state its limits, and do not invent current facts, source URLs, or claim to have searched the web.' : text(input.evidence, 'Context', 12000);
      if (JSON.stringify(q).includes(apiKey) || evidenceText.includes(apiKey)) throw new InputError('Remove your API key from the question and evidence.');
      const arm = input.arm ?? 'direct';
      if (arm !== 'direct' && arm !== 'harness') throw new InputError('Choose direct or reviewed forecasting.');
      const maxCost = input.maxCost;
      if (maxCost !== undefined && (typeof maxCost !== 'number' || !Number.isFinite(maxCost) || maxCost < 0.01 || maxCost > 0.5)) throw new InputError('Set a per-forecast ceiling between $0.01 and $0.50.');
      if (active >= 4) throw new InputError('The shared workbench is busy. No model call was made; try again shortly.', 429);
      active += 1;
      let directory: string | undefined;
      let runtime: MeteredRuntime | undefined;
      const cancelled = new AbortController();
      res.on('close', () => { if (!res.writableEnded) cancelled.abort(); });
      try {
        const model = (await models()).find(value => value.id === input.model);
        if (!model) throw new InputError('Choose an available model from the list.');
        if (cancelled.signal.aborted) return;
        // One bounded call by default; size the internal reservation to the selected model.
        const budget = maxCost ?? Math.ceil(((60000 + Buffer.byteLength(evidenceText) + Buffer.byteLength(JSON.stringify(q))) * model.rates.input + 8400 * model.rates.output) / 10000) / 100;
        const captured = new Date().toISOString();
        const source: Evidence = {
          id: 'user-context', url: 'urn:vaticinus:user-provided-context', title: 'User-provided evidence and assumptions; not independently verified',
          text: evidenceText, fetched_at: captured, published_at: null, publication_basis: 'unknown',
          content_sha256: sha256(evidenceText), snapshot_sha256: sha256(evidenceText), kind: 'page',
        };
        directory = await mkdtemp(join(tmpdir(), 'vati-space-'));
        const lifetime = AbortSignal.timeout(240000);
        runtime = new MeteredRuntime({
          apiKey, model: model.id, rates: model.rates, reasoning: model.reasoning, maxCost: budget,
          maxOutputTokens: 8400, timeoutMs: 180000, idleTimeoutMs: 45000, ledger: join(directory, 'budget.json'),
          fetch: (url, options) => request(url, { ...options, signal: AbortSignal.any([cancelled.signal, lifetime, ...(options?.signal ? [options.signal] : [])]) }),
        });
        const run = await runForecast(q, packet([source]), runtime.complete, { model: model.id, arm, asOf: captured });
        const cost = { accounted_usd: runtime.spent, ceiling_usd: budget, calls: runtime.traces.length,
          unknown_charge_calls: runtime.traces.filter(trace => !trace.usage).length };
        // Provider diagnostics may include request content. Never reflect the authorization secret.
        const diagnostic = run.issues.join(' ');
        const message = /HTTP 401|HTTP 403/.test(diagnostic) ? 'OpenRouter rejected this key. Check that it is active and allowed to use the selected model.'
          : /HTTP 402/.test(diagnostic) ? 'This OpenRouter key has no available credits. Add credits or check its spending limit.'
          : /HTTP 429/.test(diagnostic) ? 'OpenRouter is rate-limiting this key. Wait a moment before trying again.'
          : /HTTP 404|No endpoints/.test(diagnostic) ? 'This model is temporarily unavailable on OpenRouter. Choose another model.'
          : undefined;
        const safe = JSON.stringify({ run, cost, message }).split(apiKey).join('[REDACTED]');
        send(200, JSON.parse(safe));
      } finally {
        runtime?.close();
        if (directory) await rm(directory, { recursive: true, force: true });
        active -= 1;
      }
    } catch (error) {
      send(error instanceof InputError ? error.status : 400, { error: error instanceof Error ? error.message : 'The request could not be completed.' });
    }
  });
  server.requestTimeout = 30000;
  server.headersTimeout = 10000;
  return server;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  createWorkbenchServer().listen(Number(process.env.PORT ?? 7860), process.env.HOST ?? '0.0.0.0', () => {
    console.log('Vaticinus workbench ready; personal OpenRouter keys only.');
  });
}