oxedyne/daimond/dev/verify_anthropic.mjs
4.9 KiB, 1 run
created by r2519314175:233, 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 | // verify_anthropic.mjs — is Anthropic actually selectable, and does the request it |
| 2 | // produces have the shape Anthropic accepts? |
| 3 | // |
| 4 | // A direct Anthropic transport was added and nothing could reach it: the provider |
| 5 | // was not in the picker at all. Two things then went wrong the moment it was, and |
| 6 | // both had the same cause — daimond.js carried its own copy of two rules that |
| 7 | // models.js already knows properly: |
| 8 | // |
| 9 | // - the listing endpoint. Anthropic's `/v1/models` is a SIBLING of `/v1/messages`, |
| 10 | // so appending `/models` asked `/v1/messages/models`, which is nobody's |
| 11 | // endpoint and 404s. |
| 12 | // - the auth header. Anthropic refuses a bearer token; it wants `x-api-key`, a |
| 13 | // pinned version, and the header that makes its edge answer a browser at all. |
| 14 | // A hardcoded `Authorization: Bearer` got a 401 and read as a bad key. |
| 15 | // |
| 16 | // So this asserts the ADDRESS AND HEADERS ON THE WIRE, intercepted at the browser, not |
| 17 | // what the app believes it sent. Needs dev/serve.mjs (DAIMOND_PORT, default 8777). |
| 18 | import { open, errors } from './harness.mjs'; |
| 19 | |
| 20 | const ok = [], bad = []; |
| 21 | const check = (name, pass, detail) => { |
| 22 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 23 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 24 | }; |
| 25 | |
| 26 | const s = await open({ name: 'anthropic', connect: false }); |
| 27 | const p = s.page; |
| 28 | |
| 29 | // Anthropic is never actually called: the route is intercepted and answered here, |
| 30 | // so no key, no network and no cost are involved in finding out what was sent. |
| 31 | const seen = []; |
| 32 | await p.route('**://api.anthropic.com/**', async (route) => { |
| 33 | const req = route.request(); |
| 34 | seen.push({ url: req.url(), method: req.method(), headers: req.headers() }); |
| 35 | await route.fulfill({ |
| 36 | status: 200, |
| 37 | contentType: 'application/json', |
| 38 | headers: { 'access-control-allow-origin': '*' }, |
| 39 | body: JSON.stringify({ data: [{ id: 'claude-opus-5' }, { id: 'claude-haiku-5' }] }), |
| 40 | }); |
| 41 | }); |
| 42 | |
| 43 | // ── The picker offers it ─────────────────────────────────────────────────── |
| 44 | |
| 45 | await p.evaluate(() => { |
| 46 | const open = document.getElementById('settings-btn') || document.querySelector('[data-admin="settings"]'); |
| 47 | if (open) open.click(); |
| 48 | }); |
| 49 | await p.waitForTimeout(400); |
| 50 | |
| 51 | const options = await p.evaluate(() => { |
| 52 | const sel = document.getElementById('cfg-provider'); |
| 53 | return sel ? [...sel.options].map(o => ({ value: o.value, label: o.textContent })) : []; |
| 54 | }); |
| 55 | check('Anthropic is in the provider picker', |
| 56 | options.some(o => o.value === 'anthropic'), options.map(o => o.value).join(', ')); |
| 57 | |
| 58 | // ── Choosing it fills in the right endpoint ──────────────────────────────── |
| 59 | |
| 60 | await p.evaluate(() => { |
| 61 | const sel = document.getElementById('cfg-provider'); |
| 62 | sel.value = 'anthropic'; |
| 63 | sel.dispatchEvent(new Event('change', { bubbles: true })); |
| 64 | }); |
| 65 | await p.waitForTimeout(300); |
| 66 | const base = await p.evaluate(() => (document.getElementById('cfg-base-url') || {}).value || ''); |
| 67 | check('choosing it fills in the messages endpoint', /api\.anthropic\.com\/v1\/messages$/.test(base), base); |
| 68 | |
| 69 | // ── Typing a key makes the app go and list the models ────────────────────── |
| 70 | |
| 71 | await p.evaluate(() => { |
| 72 | const k = document.getElementById('cfg-api-key'); |
| 73 | k.value = 'sk-ant-not-a-real-key'; |
| 74 | k.dispatchEvent(new Event('input', { bubbles: true })); |
| 75 | k.dispatchEvent(new Event('change', { bubbles: true })); |
| 76 | }); |
| 77 | await p.waitForTimeout(2500); |
| 78 | |
| 79 | check('the app went and asked for the model list', seen.length > 0, `${seen.length} requests`); |
| 80 | const list = seen[seen.length - 1] || { url: '', headers: {} }; |
| 81 | check('at /v1/models, a sibling of /v1/messages — not /v1/messages/models', |
| 82 | /\/v1\/models$/.test(list.url), list.url); |
| 83 | check('with x-api-key, which is what Anthropic wants', |
| 84 | !!list.headers['x-api-key'], Object.keys(list.headers).filter(h => /key|author|anthropic/i.test(h)).join(', ')); |
| 85 | check('and NOT a bearer token, which it refuses', |
| 86 | !list.headers['authorization'], list.headers['authorization'] || 'absent'); |
| 87 | check('with the version pinned', !!list.headers['anthropic-version'], list.headers['anthropic-version'] || 'missing'); |
| 88 | check('and the header that makes its edge answer a page at all', |
| 89 | list.headers['anthropic-dangerous-direct-browser-access'] === 'true'); |
| 90 | |
| 91 | const models = await p.evaluate(() => { |
| 92 | const sel = document.getElementById('cfg-model'); |
| 93 | return sel ? [...sel.options].map(o => o.value) : []; |
| 94 | }); |
| 95 | check('and the models it answered with are on offer', models.includes('claude-opus-5'), models.join(', ')); |
| 96 | |
| 97 | const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e)); |
| 98 | check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 99 | |
| 100 | await s.close(); |
| 101 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 102 | if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); } |