Oregami
Repositories/oxedyne/daimond

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).
18import { open, errors } from './harness.mjs';
19
20const ok = [], bad = [];
21const check = (name, pass, detail) => {
22 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
23 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
24};
25
26const s = await open({ name: 'anthropic', connect: false });
27const 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.
31const seen = [];
32await 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
45await p.evaluate(() => {
46 const open = document.getElementById('settings-btn') || document.querySelector('[data-admin="settings"]');
47 if (open) open.click();
48});
49await p.waitForTimeout(400);
50
51const 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});
55check('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
60await p.evaluate(() => {
61 const sel = document.getElementById('cfg-provider');
62 sel.value = 'anthropic';
63 sel.dispatchEvent(new Event('change', { bubbles: true }));
64});
65await p.waitForTimeout(300);
66const base = await p.evaluate(() => (document.getElementById('cfg-base-url') || {}).value || '');
67check('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
71await 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});
77await p.waitForTimeout(2500);
78
79check('the app went and asked for the model list', seen.length > 0, `${seen.length} requests`);
80const list = seen[seen.length - 1] || { url: '', headers: {} };
81check('at /v1/models, a sibling of /v1/messages — not /v1/messages/models',
82 /\/v1\/models$/.test(list.url), list.url);
83check('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(', '));
85check('and NOT a bearer token, which it refuses',
86 !list.headers['authorization'], list.headers['authorization'] || 'absent');
87check('with the version pinned', !!list.headers['anthropic-version'], list.headers['anthropic-version'] || 'missing');
88check('and the header that makes its edge answer a page at all',
89 list.headers['anthropic-dangerous-direct-browser-access'] === 'true');
90
91const models = await p.evaluate(() => {
92 const sel = document.getElementById('cfg-model');
93 return sel ? [...sel.options].map(o => o.value) : [];
94});
95check('and the models it answered with are on offer', models.includes('claude-opus-5'), models.join(', '));
96
97const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e));
98check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | '));
99
100await s.close();
101console.log(`\n${ok.length} passed, ${bad.length} failed`);
102if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }