Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/ocr.js

4.8 KiB, 1 run

created by r2519314175:1518, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* ocr.js — window.DaimondOcr: the authenticated POSTs that turn a PDF or an image into text.
2 *
3 * THE MODEL NEVER RECEIVES PIXELS. A PDF or a photograph is parsed on the far side of one
4 * throwaway request and only its TEXT comes back, so the transcript costs the turn words and
5 * not image tokens. The Rust tools (`file_read` on a `.pdf`, and `ocr`) build the request
6 * bodies and parse the replies; this file does the one thing that must happen in the page —
7 * the authenticated fetch — because the provider KEY lives here, in `DaimondModels`, and must
8 * not cross into the wasm.
9 *
10 * Two endpoints, both verified against the live docs on 2026-08-31:
11 *
12 * PDF OpenRouter chat/completions with the `file-parser` plugin in the BODY. The parsed
13 * text comes back as `annotations` on the assistant message. This needs an OpenRouter
14 * key specifically: the plugin is OpenRouter's. A credits key routed through
15 * OpenRouter counts.
16 *
17 * IMAGE Mistral's own `/v1/ocr` (returns markdown text, the cheap path) when a `mistral`
18 * key is configured, else a vision model on OpenRouter reading an `image_url` part.
19 *
20 * Everything here returns the RAW response body to the wasm, which parses it — so the defensive
21 * parsing is one place, in Rust, and tested there.
22 */
23(function () {
24 'use strict';
25
26 var OPENROUTER_CHAT = 'https://openrouter.ai/api/v1/chat/completions';
27 var MISTRAL_OCR = 'https://api.mistral.ai/v1/ocr';
28
29 function M() { return window.DaimondModels; }
30
31 /// The OpenRouter endpoint and key, or null when none can be found.
32 ///
33 /// An explicit `openrouter` provider first; failing that, the running default provider WHEN
34 /// its endpoint is OpenRouter — which is how a Daimond-credits key, minted to run models
35 /// through OpenRouter, pays for the file parser without the user configuring a second thing.
36 function openrouter() {
37 var m = M();
38 if (!m) return null;
39 if (m.hasKey && m.hasKey('openrouter') && m.keyFor('openrouter')) {
40 var provs = m.providers ? m.providers() : {};
41 var known = m.known ? m.known() : {};
42 var url = (provs.openrouter && provs.openrouter.url)
43 || (known.openrouter && known.openrouter.url)
44 || OPENROUTER_CHAT;
45 return { url: url, key: m.keyFor('openrouter') };
46 }
47 var d = m.resolve ? m.resolve('', '') : null;
48 if (d && d.apiKey && d.baseUrl) {
49 var host = '';
50 try { host = new URL(d.baseUrl).host; } catch (e) { host = ''; }
51 if (/(^|\.)openrouter\.ai$/.test(host)) return { url: d.baseUrl, key: d.apiKey };
52 }
53 return null;
54 }
55
56 /// The Mistral OCR endpoint and key, or null when no `mistral` provider is configured.
57 function mistral() {
58 var m = M();
59 if (!m || !m.hasKey || !m.hasKey('mistral') || !m.keyFor('mistral')) return null;
60 return { url: MISTRAL_OCR, key: m.keyFor('mistral') };
61 }
62
63 /// POST a JSON body with the right auth for the endpoint. Returns {ok, status, body}.
64 async function post(url, key, body) {
65 var headers = { 'content-type': 'application/json' };
66 var auth = (M() && M().authHeaders)
67 ? M().authHeaders(url, key)
68 : { authorization: 'Bearer ' + key };
69 for (var k in auth) { if (auth.hasOwnProperty(k)) headers[k] = auth[k]; }
70 var r = await fetch(url, { method: 'POST', headers: headers, body: body });
71 var text = '';
72 try { text = await r.text(); } catch (e) { text = ''; }
73 return { ok: r.ok, status: r.status, body: text };
74 }
75
76 /// POST a file-parser body to OpenRouter; resolve the raw response body.
77 async function chat(body) {
78 var o = openrouter();
79 if (!o) {
80 throw new Error(
81 'Reading a PDF needs an OpenRouter key: its file parser does the work. '
82 + 'Add one in the models panel, or run your models through OpenRouter.');
83 }
84 var res = await post(o.url, o.key, body);
85 if (!res.ok && !res.body) {
86 throw new Error('OpenRouter returned HTTP ' + res.status + ' with no body.');
87 }
88 return res.body;
89 }
90
91 /// Transcribe an image. Mistral OCR first (returns text, cheapest), a vision model on
92 /// OpenRouter as the fallback. Resolves '{"route":...,"body":...}' as a JSON string.
93 async function image(mistralBody, visionBody) {
94 var ms = mistral();
95 if (ms) {
96 var r = await post(ms.url, ms.key, mistralBody);
97 // A Mistral hiccup falls through to the vision model rather than failing the call.
98 if (r.ok && r.body) return JSON.stringify({ route: 'mistral', body: r.body });
99 }
100 var o = openrouter();
101 if (o) {
102 var v = await post(o.url, o.key, visionBody);
103 if (!v.ok && !v.body) {
104 throw new Error('OpenRouter returned HTTP ' + v.status + ' with no body.');
105 }
106 return JSON.stringify({ route: 'vision', body: v.body });
107 }
108 throw new Error(
109 'Reading an image needs a provider key: a Mistral key (its OCR returns text cheaply) '
110 + 'or an OpenRouter key (a vision model reads it). Add one in the models panel.');
111 }
112
113 window.DaimondOcr = { chat: chat, image: image };
114})();