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 | })(); |