@namzu/openrouter 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,126 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 935b8f3: Every driver can now see.
8
+
9
+ These three dropped `attachments` outright, so a user who attached a
10
+ screenshot got a turn about nothing. `supportsVision: false` said so, which
11
+ made the declaration honest and the driver useless — and it was the last
12
+ place in the estate where a namzu capability existed on one driver and
13
+ silently did not on another.
14
+
15
+ Each wire carries an image differently, so this is one intent and three
16
+ mappings:
17
+
18
+ - **Converse**: raw bytes in an image content block beside the text. The
19
+ tool-result path already did this; the user path never looked at
20
+ `attachments`.
21
+ - **The two-dialect HTTP driver**: a `data:` URI content part on one
22
+ dialect, a base64 source block on the other.
23
+ - **The gateway driver**: a `data:` URI content part.
24
+
25
+ Across all three: a media type the endpoint cannot decode is named in the
26
+ text rather than sent, because a payload it rejects fails the whole request
27
+ and losing the turn is worse than losing sight of one image. A message with
28
+ no attachments keeps its plain-string content, so nothing about an ordinary
29
+ request changes shape. Several attachments on one message are carried in
30
+ order.
31
+
32
+ An image inside a **tool result** still degrades to a text placeholder on
33
+ the HTTP and gateway drivers: a tool message is text-only in those dialects,
34
+ so there is nowhere to put it. Converse carries it, and always did.
35
+
36
+ ### Patch Changes
37
+
38
+ - 935b8f3: Stop dumping a tool result's base64 payload into the prompt.
39
+
40
+ Four drivers mishandled a tool result carrying content blocks, each in its
41
+ own way: bedrock and the http driver's anthropic dialect `JSON.stringify`d
42
+ the whole array, putting a screenshot's base64 into the prompt as JSON
43
+ text; openrouter and the http driver's openai dialect passed the array
44
+ through raw to an endpoint expecting a string; lmstudio folded it into a
45
+ template literal, producing `[object Object]` per block. The model cannot
46
+ decode any of it, and it costs a fortune in tokens.
47
+
48
+ - The three text-only wires now flatten with the SDK's existing helper,
49
+ which names a non-text block and its size instead of inlining it. The
50
+ openai and ollama drivers already did this — the helper was there and
51
+ four callers were missing.
52
+ - **bedrock sends the image as an image.** That wire carries images
53
+ natively, so a placeholder would be a downgrade the other drivers accept
54
+ only because their format has no room for one. Text and image survive as
55
+ separate blocks in order, and a media type the format does not accept
56
+ still degrades to a named placeholder rather than being smuggled through
57
+ as text.
58
+
59
+ - 935b8f3: A turn that asked for tools no longer ends because the provider said it
60
+ didn't.
61
+
62
+ The iteration loop ended the turn on `finishReason === 'stop'` **before**
63
+ looking at whether the model had asked for tools. Endpoints on the OpenAI
64
+ wire shape — gateways and local servers especially — routinely report `stop`
65
+ on the same response that carries a populated `tool_calls`, and three of
66
+ this repo's drivers passed that value straight through.
67
+
68
+ The damage was total and silent: every requested call skipped, an assistant
69
+ turn left carrying `tool_use` blocks nothing ever answered, and the run
70
+ settling as though it had finished the work it never started.
71
+
72
+ - **The runtime now treats tool calls as the fact and the finish reason as
73
+ the summary.** When they disagree, the calls win. This is the load-bearing
74
+ fix: it protects every driver, including ones this repo does not ship.
75
+ - **The three drivers that cast the reason raw now report it honestly** —
76
+ a stream that produced a tool call reports `tool_calls`, whatever the
77
+ endpoint called it. Defence in depth, and it makes the reported reason
78
+ true for anyone else reading it.
79
+
80
+ The existing suite could not catch this: the scripted mock reports
81
+ `tool_calls` whenever it emits one, which is what an honest provider does
82
+ and therefore never the case that breaks.
83
+
84
+ - 935b8f3: A user message can carry a document
85
+
86
+ Documents existed in the type system only in the tool-result direction, and both first-party drivers mapped images only on the input side. So "here is the contract, answer questions about it" — a mainstream workload — was reachable only by having a tool read the file and stringify it. That loses the provider's native document handling (page structure, built-in OCR, citations) and pays the text cost instead.
87
+
88
+ `UserMessage.attachments` is now `MessageAttachment[]`: an image or a document. The discriminant is optional and stays optional — an attachment without one is an image, which is what every attachment was before, so no existing caller changes.
89
+
90
+ `supportsDocuments` sits beside `supportsVision` in the driver capability declaration, and the runtime checks it the same way: a document sent to a driver that declares `false` warns before the request, or throws under `strictCapabilities`, instead of letting the model answer about a file it never saw. The two are counted separately because they are separate wire shapes and a driver can map one without the other.
91
+
92
+ The two first-party drivers map documents natively. The remaining five map images only and now say so; a document reaching them degrades to a named placeholder that says which kind was dropped, rather than one that calls a document an image.
93
+
94
+ ## 1.0.3
95
+
96
+ ### Patch Changes
97
+
98
+ - 3fd2524: Normalize request-start and mid-stream failures across all seven provider
99
+ drivers with the new public `ProviderRequestError` taxonomy. Errors expose
100
+ `kind` (`throttle`, `network`, `auth`, `context_overflow`, `bad_request`, or
101
+ `server`), `providerId`, and optional `status` / `retryAfterMs`, with
102
+ `isProviderRequestError` available for structural narrowing across package
103
+ copies.
104
+
105
+ Provider error messages and metadata deliberately omit vendor response bodies,
106
+ URLs, messages, and causes because upstream errors can echo credentials. HTTP
107
+ dialect-mismatch diagnostics now keep only the endpoint origin and status.
108
+ Caller-owned aborts remain unchanged instead of being reclassified.
109
+
110
+ The runtime preserves the classified error through streaming and publishes its
111
+ safe metadata as `Run.lastProviderError` and
112
+ `run_failed.providerError`. Bedrock stream-exception events and provider
113
+ iterator/SSE failures no longer appear as clean end-of-stream.
114
+
115
+ `retryAfterMs` is metadata only; this change does not add retries or alter vendor
116
+ SDK retry settings. Provider packages now require `@namzu/sdk >=1.3.0`, the
117
+ first SDK release containing these runtime helpers and types.
118
+
119
+ Ollama now maps `done_reason: "length"` truthfully so runtime continuation can
120
+ run. LM Studio treats content-free `contextLengthReached` as context overflow,
121
+ while preserving `"length"` after partial content, and creates its WebSocket
122
+ client lazily on first use.
123
+
3
124
  ## 1.0.2
4
125
 
5
126
  ### Patch Changes
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Provider error taxonomy — OpenRouter driver.
3
+ *
4
+ * Every provider failure must reach the caller as a classified
5
+ * `ProviderRequestError` (kind + status + retryAfterMs + providerId), not
6
+ * as an opaque vendor string. The message must be built from the status
7
+ * line and the classified kind ONLY — never from the response body, which
8
+ * is where a credential echoed back by the upstream would leak into a log.
9
+ *
10
+ * Transport seam: the driver calls the global `fetch` directly, so the
11
+ * existing `vi.stubGlobal('fetch', …)` harness from `http.test.ts` applies
12
+ * verbatim.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=error-taxonomy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-taxonomy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/error-taxonomy.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Provider error taxonomy — OpenRouter driver.
3
+ *
4
+ * Every provider failure must reach the caller as a classified
5
+ * `ProviderRequestError` (kind + status + retryAfterMs + providerId), not
6
+ * as an opaque vendor string. The message must be built from the status
7
+ * line and the classified kind ONLY — never from the response body, which
8
+ * is where a credential echoed back by the upstream would leak into a log.
9
+ *
10
+ * Transport seam: the driver calls the global `fetch` directly, so the
11
+ * existing `vi.stubGlobal('fetch', …)` harness from `http.test.ts` applies
12
+ * verbatim.
13
+ */
14
+ import { afterEach, describe, expect, it, vi } from 'vitest';
15
+ import { OpenRouterProvider } from '../client.js';
16
+ /**
17
+ * An obviously fake, non-functional token. Present ONLY to prove the
18
+ * driver does not interpolate a response body into a thrown message.
19
+ */
20
+ const FAKE_CREDENTIAL = 'sk-ant-FAKE-DO-NOT-USE-0000';
21
+ afterEach(() => {
22
+ vi.unstubAllGlobals();
23
+ });
24
+ function respond(status, body, headers = {}) {
25
+ return new Response(body, {
26
+ status,
27
+ headers: { 'Content-Type': 'application/json', ...headers },
28
+ });
29
+ }
30
+ function sseResponse(frames) {
31
+ return new Response(`${frames.join('\n\n')}\n\n`, {
32
+ status: 200,
33
+ headers: { 'Content-Type': 'text/event-stream' },
34
+ });
35
+ }
36
+ async function captureChatStreamError(provider, signal) {
37
+ try {
38
+ for await (const _chunk of provider.chatStream({
39
+ model: 'anthropic/claude-sonnet-4-5',
40
+ messages: [{ role: 'user', content: 'hi' }],
41
+ signal,
42
+ })) {
43
+ // drain
44
+ }
45
+ }
46
+ catch (err) {
47
+ return err;
48
+ }
49
+ throw new Error('expected chatStream to throw');
50
+ }
51
+ function makeProvider() {
52
+ return new OpenRouterProvider({
53
+ apiKey: 'test-key',
54
+ baseUrl: 'https://example.test/api/v1',
55
+ });
56
+ }
57
+ describe('@namzu/openrouter — provider error taxonomy', () => {
58
+ it('(a) 429 with `retry-after: 2` throws a throttle-classified error carrying retryAfterMs', async () => {
59
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(respond(429, JSON.stringify({ error: { message: 'rate limit exceeded' } }), {
60
+ 'retry-after': '2',
61
+ })));
62
+ const err = await captureChatStreamError(makeProvider());
63
+ expect(err).toMatchObject({
64
+ name: 'ProviderRequestError',
65
+ kind: 'throttle',
66
+ status: 429,
67
+ retryAfterMs: 2000,
68
+ providerId: 'openrouter',
69
+ });
70
+ });
71
+ it("(c) 400 whose body says the prompt is too long classifies as 'context_overflow'", async () => {
72
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(respond(400, JSON.stringify({
73
+ error: {
74
+ type: 'invalid_request_error',
75
+ message: 'prompt is too long: 305231 tokens > 200000 maximum',
76
+ },
77
+ }))));
78
+ const err = await captureChatStreamError(makeProvider());
79
+ expect(err).toMatchObject({
80
+ name: 'ProviderRequestError',
81
+ kind: 'context_overflow',
82
+ status: 400,
83
+ providerId: 'openrouter',
84
+ });
85
+ });
86
+ it("(c) 400 that is not an overflow classifies as 'bad_request'", async () => {
87
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(respond(400, JSON.stringify({
88
+ error: {
89
+ type: 'invalid_request_error',
90
+ message: "unknown field 'temperture'",
91
+ },
92
+ }))));
93
+ const err = await captureChatStreamError(makeProvider());
94
+ expect(err).toMatchObject({
95
+ name: 'ProviderRequestError',
96
+ kind: 'bad_request',
97
+ status: 400,
98
+ providerId: 'openrouter',
99
+ });
100
+ });
101
+ it("(a/auth) 401 classifies as 'auth'", async () => {
102
+ vi.stubGlobal('fetch', vi
103
+ .fn()
104
+ .mockResolvedValue(respond(401, JSON.stringify({ error: { message: 'No auth credentials found' } }))));
105
+ const err = await captureChatStreamError(makeProvider());
106
+ expect(err).toMatchObject({
107
+ name: 'ProviderRequestError',
108
+ kind: 'auth',
109
+ status: 401,
110
+ providerId: 'openrouter',
111
+ });
112
+ });
113
+ it('(d) SECURITY: a credential echoed back in a 401 body never reaches the thrown message', async () => {
114
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(respond(401, JSON.stringify({
115
+ error: { message: `invalid api key: ${FAKE_CREDENTIAL}` },
116
+ }))));
117
+ const err = await captureChatStreamError(makeProvider());
118
+ expect(err).toBeInstanceOf(Error);
119
+ const message = err.message;
120
+ expect(message).not.toContain(FAKE_CREDENTIAL);
121
+ expect(JSON.stringify(err)).not.toContain(FAKE_CREDENTIAL);
122
+ expect('cause' in err).toBe(false);
123
+ });
124
+ it('(d) SECURITY: a credential echoed back in a 400 body never reaches the thrown message', async () => {
125
+ vi.stubGlobal('fetch', vi
126
+ .fn()
127
+ .mockResolvedValue(respond(400, `bad request for key ${FAKE_CREDENTIAL}: model not permitted`)));
128
+ const err = await captureChatStreamError(makeProvider());
129
+ expect(err.message).not.toContain(FAKE_CREDENTIAL);
130
+ });
131
+ it("classifies a fetch rejection as 'network' without retaining its cause", async () => {
132
+ const vendor = new Error(`socket failed for ${FAKE_CREDENTIAL}`);
133
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(vendor));
134
+ const err = await captureChatStreamError(makeProvider());
135
+ expect(err).toMatchObject({
136
+ name: 'ProviderRequestError',
137
+ kind: 'network',
138
+ providerId: 'openrouter',
139
+ });
140
+ expect(err.message).not.toContain(FAKE_CREDENTIAL);
141
+ expect('cause' in err).toBe(false);
142
+ });
143
+ it('preserves the caller abort reason when fetch replaces the error object', async () => {
144
+ const controller = new AbortController();
145
+ const reason = new Error('user stopped');
146
+ const fetchAbort = Object.assign(new Error('request aborted'), {
147
+ name: 'AbortError',
148
+ });
149
+ controller.abort(reason);
150
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(fetchAbort));
151
+ const err = await captureChatStreamError(makeProvider(), controller.signal);
152
+ expect(fetchAbort).not.toBe(reason);
153
+ expect(err).toBe(reason);
154
+ });
155
+ it('classifies and sanitizes an SSE error after the 200 response', async () => {
156
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(sseResponse([
157
+ `data: ${JSON.stringify({
158
+ error: {
159
+ type: 'overloaded_error',
160
+ message: `echoed ${FAKE_CREDENTIAL}`,
161
+ },
162
+ })}`,
163
+ ])));
164
+ const err = await captureChatStreamError(makeProvider());
165
+ expect(err).toMatchObject({
166
+ name: 'ProviderRequestError',
167
+ kind: 'server',
168
+ providerId: 'openrouter',
169
+ });
170
+ expect(err.message).not.toContain(FAKE_CREDENTIAL);
171
+ expect(JSON.stringify(err)).not.toContain(FAKE_CREDENTIAL);
172
+ expect('cause' in err).toBe(false);
173
+ });
174
+ it('classifies malformed SSE without retaining the credential-bearing frame', async () => {
175
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(sseResponse([`data: ${FAKE_CREDENTIAL} not-json`])));
176
+ const err = await captureChatStreamError(makeProvider());
177
+ expect(err).toMatchObject({
178
+ name: 'ProviderRequestError',
179
+ kind: 'server',
180
+ providerId: 'openrouter',
181
+ });
182
+ expect(err.message).not.toContain(FAKE_CREDENTIAL);
183
+ expect(JSON.stringify(err)).not.toContain(FAKE_CREDENTIAL);
184
+ expect('cause' in err).toBe(false);
185
+ });
186
+ });
187
+ //# sourceMappingURL=error-taxonomy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-taxonomy.test.js","sourceRoot":"","sources":["../../src/__tests__/error-taxonomy.test.ts"],"names":[],"mappings":"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"}
@@ -39,6 +39,7 @@ describe('@namzu/openrouter', () => {
39
39
  supportsStreaming: true,
40
40
  supportsFunctionCalling: true,
41
41
  supportsVision: false,
42
+ supportsDocuments: false,
42
43
  });
43
44
  });
44
45
  it('is exposed on the provider instance for runtime negotiation', () => {
@@ -59,5 +60,26 @@ describe('@namzu/openrouter', () => {
59
60
  expect(capabilities).toEqual(OPENROUTER_CAPABILITIES);
60
61
  });
61
62
  });
63
+ it('keeps the Namzu enforcement hint out of the OpenRouter request body', () => {
64
+ const provider = new OpenRouterProvider({ apiKey: 'test-key' });
65
+ const tools = [
66
+ {
67
+ type: 'function',
68
+ function: {
69
+ name: 'edit',
70
+ description: 'Edit',
71
+ parameters: { type: 'object' },
72
+ },
73
+ },
74
+ ];
75
+ const body = provider.buildRequestBody({
76
+ model: 'anthropic/claude-sonnet-5',
77
+ messages: [{ role: 'user', content: 'edit' }],
78
+ tools,
79
+ enforceToolInputSchema: ['edit'],
80
+ }, true);
81
+ expect(body).not.toHaveProperty('enforceToolInputSchema');
82
+ expect(body.tools).toEqual(tools);
83
+ });
62
84
  });
63
85
  //# sourceMappingURL=openrouter.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"openrouter.test.js","sourceRoot":"","sources":["../../src/__tests__/openrouter.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"openrouter.test.js","sourceRoot":"","sources":["../../src/__tests__/openrouter.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vision.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vision.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/vision.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { OPENROUTER_CAPABILITIES } from '../client.js';
3
+ /**
4
+ * This driver does not map attachments, and says so.
5
+ *
6
+ * Shipping without a wire shape is legitimate; claiming one you do not have
7
+ * is not. The runtime reads these flags and warns — or fails under
8
+ * `strictCapabilities` — before the request is built, so the flags are the
9
+ * contract, and what this pins is drift in either direction: flipping a
10
+ * flag without writing the mapping makes the runtime stop warning about
11
+ * images that still vanish, and writing the mapping without flipping the
12
+ * flag makes it keep warning about images that now arrive.
13
+ */
14
+ describe('the declared capabilities are the honest ones', () => {
15
+ it('does not claim vision', () => {
16
+ expect(OPENROUTER_CAPABILITIES.supportsVision).toBe(false);
17
+ });
18
+ it('does not claim documents', () => {
19
+ expect(OPENROUTER_CAPABILITIES.supportsDocuments).toBe(false);
20
+ });
21
+ it('claims the tool support it does have', () => {
22
+ expect(OPENROUTER_CAPABILITIES.supportsTools).toBe(true);
23
+ expect(OPENROUTER_CAPABILITIES.supportsFunctionCalling).toBe(true);
24
+ });
25
+ it('claims streaming', () => {
26
+ expect(OPENROUTER_CAPABILITIES.supportsStreaming).toBe(true);
27
+ });
28
+ it('answers every flag the negotiation reads', () => {
29
+ // An absent flag defaults to permissive, so a missing one is not a
30
+ // neutral omission — it is a claim.
31
+ for (const flag of [
32
+ 'supportsTools',
33
+ 'supportsStreaming',
34
+ 'supportsFunctionCalling',
35
+ 'supportsVision',
36
+ 'supportsDocuments',
37
+ ]) {
38
+ expect(typeof OPENROUTER_CAPABILITIES[flag]).toBe('boolean');
39
+ }
40
+ });
41
+ });
42
+ //# sourceMappingURL=vision.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vision.test.js","sourceRoot":"","sources":["../../src/__tests__/vision.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAEtD;;;;;;;;;;GAUG;AAEH,QAAQ,CAAC,+CAA+C,EAAE,GAAG,EAAE;IAC9D,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,uBAAuB,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,uBAAuB,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,uBAAuB,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACxD,MAAM,CAAC,uBAAuB,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,uBAAuB,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACnD,mEAAmE;QACnE,oCAAoC;QACpC,KAAK,MAAM,IAAI,IAAI;YAClB,eAAe;YACf,mBAAmB;YACnB,yBAAyB;YACzB,gBAAgB;YAChB,mBAAmB;SACV,EAAE,CAAC;YACZ,MAAM,CAAC,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAC7D,CAAC;IACF,CAAC,CAAC,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,oBAAoB,EACpB,WAAW,EACX,SAAS,EACT,oBAAoB,EACpB,WAAW,EAGX,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAwClD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,oBAKrC,CAAA;AAED,qBAAa,kBAAmB,YAAW,WAAW;IACrD,QAAQ,CAAC,EAAE,gBAAe;IAC1B,QAAQ,CAAC,IAAI,gBAAe;IAC5B,QAAQ,CAAC,YAAY,uBAA0B;IAE/C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAQ;gBAEX,MAAM,EAAE,gBAAgB;IAQpC,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,cAAc;IAwBtB,OAAO,CAAC,gBAAgB;IAqCjB,UAAU,CAAC,MAAM,EAAE,oBAAoB,GAAG,aAAa,CAAC,WAAW,CAAC;IAgGrE,UAAU,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IA+BlC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAWrC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,oBAAoB,EACpB,WAAW,EACX,SAAS,EACT,oBAAoB,EACpB,WAAW,EAGX,MAAM,YAAY,CAAA;AAQnB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAwClD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,oBAOrC,CAAA;AAED,qBAAa,kBAAmB,YAAW,WAAW;IACrD,QAAQ,CAAC,EAAE,gBAAe;IAC1B,QAAQ,CAAC,IAAI,gBAAe;IAC5B,QAAQ,CAAC,YAAY,uBAA0B;IAE/C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAQ;gBAEX,MAAM,EAAE,gBAAgB;IAQpC,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,cAAc;IAwBtB,OAAO,CAAC,gBAAgB;IAqCjB,UAAU,CAAC,MAAM,EAAE,oBAAoB,GAAG,aAAa,CAAC,WAAW,CAAC;IAkIrE,UAAU,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IA+BlC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAWrC"}
package/dist/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ProviderRequestError, isCallerAbortError, isProviderRequestError, providerHttpError, providerVendorError, } from '@namzu/sdk';
1
2
  const OPENROUTER_BASE_URL = process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1';
2
3
  function parseUsage(raw) {
3
4
  if (!raw) {
@@ -33,6 +34,8 @@ export const OPENROUTER_CAPABILITIES = {
33
34
  supportsStreaming: true,
34
35
  supportsFunctionCalling: true,
35
36
  supportsVision: false,
37
+ // Images only. A document degrades to a named placeholder.
38
+ supportsDocuments: false,
36
39
  };
37
40
  export class OpenRouterProvider {
38
41
  id = 'openrouter';
@@ -129,18 +132,41 @@ export class OpenRouterProvider {
129
132
  // response body stream. When no caller signal is present this is the exact
130
133
  // prior `AbortSignal.timeout(...)` expression (byte-identical).
131
134
  const signal = params.signal ? AbortSignal.any([timeout, params.signal]) : timeout;
132
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
133
- method: 'POST',
134
- headers: this.getHeaders(),
135
- body: JSON.stringify(body),
136
- signal,
137
- });
135
+ let response;
136
+ try {
137
+ response = await fetch(`${this.baseUrl}/chat/completions`, {
138
+ method: 'POST',
139
+ headers: this.getHeaders(),
140
+ body: JSON.stringify(body),
141
+ signal,
142
+ });
143
+ }
144
+ catch (err) {
145
+ if (isCallerAbortError(err, params.signal))
146
+ throw params.signal?.reason ?? err;
147
+ throw providerVendorError({ providerId: 'openrouter', error: err });
148
+ }
138
149
  if (!response.ok) {
139
- const errorBody = await response.text();
140
- throw new Error(`OpenRouter API error (${response.status}): ${errorBody}`);
150
+ // The body is read to CLASSIFY (a 400 saying "prompt is too long" is a
151
+ // context overflow, any other 400 is a bad request) and then dropped. It
152
+ // used to be interpolated straight into the message, which is how a
153
+ // credential the upstream echoed back reached every log that recorded
154
+ // the failure — proven with a planted fake token.
155
+ const errorBody = await response.text().catch(() => '');
156
+ throw providerHttpError({
157
+ providerId: 'openrouter',
158
+ status: response.status,
159
+ body: errorBody,
160
+ retryAfter: response.headers.get('retry-after'),
161
+ });
141
162
  }
142
163
  if (!response.body) {
143
- throw new Error('OpenRouter returned no stream body');
164
+ throw new ProviderRequestError({
165
+ kind: 'server',
166
+ providerId: 'openrouter',
167
+ status: response.status,
168
+ detail: 'the response contained no stream body',
169
+ });
144
170
  }
145
171
  const reader = response.body.getReader();
146
172
  const decoder = new TextDecoder();
@@ -163,6 +189,12 @@ export class OpenRouterProvider {
163
189
  return;
164
190
  try {
165
191
  const parsed = JSON.parse(data);
192
+ if (parsed.error !== undefined) {
193
+ throw providerVendorError({
194
+ providerId: 'openrouter',
195
+ error: new Error(data),
196
+ });
197
+ }
166
198
  const choice = parsed.choices[0];
167
199
  if (!choice)
168
200
  continue;
@@ -182,17 +214,26 @@ export class OpenRouterProvider {
182
214
  };
183
215
  }
184
216
  catch (parseErr) {
185
- yield {
186
- id: '',
187
- delta: { content: undefined },
188
- finishReason: undefined,
189
- usage: undefined,
190
- error: `Stream parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
191
- };
217
+ if (isProviderRequestError(parseErr))
218
+ throw parseErr;
219
+ // JSON SyntaxError messages include a source snippet, and
220
+ // mapping failures may include vendor values. Drop both.
221
+ throw new ProviderRequestError({
222
+ kind: 'server',
223
+ providerId: 'openrouter',
224
+ detail: 'the provider stream returned malformed data',
225
+ });
192
226
  }
193
227
  }
194
228
  }
195
229
  }
230
+ catch (err) {
231
+ if (isCallerAbortError(err, params.signal))
232
+ throw params.signal?.reason ?? err;
233
+ if (isProviderRequestError(err))
234
+ throw err;
235
+ throw providerVendorError({ providerId: 'openrouter', error: err });
236
+ }
196
237
  finally {
197
238
  reader.releaseLock();
198
239
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/openrouter",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "OpenRouter LLM provider for @namzu/sdk. Access to 200+ models (Claude, GPT-4, Llama, Mixtral) via OpenRouter's unified API.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -35,14 +35,14 @@
35
35
  },
36
36
  "dependencies": {},
37
37
  "peerDependencies": {
38
- "@namzu/sdk": ">=1.0.0"
38
+ "@namzu/sdk": ">=1.3.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@biomejs/biome": "^1.9.4",
42
42
  "@types/node": "^22.19.17",
43
43
  "typescript": "^5.5.0",
44
- "vitest": "^2.0.0",
45
- "@namzu/sdk": "^1.2.0"
44
+ "vitest": "^3.2.6",
45
+ "@namzu/sdk": "^3.0.0"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc --build",
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Provider error taxonomy — OpenRouter driver.
3
+ *
4
+ * Every provider failure must reach the caller as a classified
5
+ * `ProviderRequestError` (kind + status + retryAfterMs + providerId), not
6
+ * as an opaque vendor string. The message must be built from the status
7
+ * line and the classified kind ONLY — never from the response body, which
8
+ * is where a credential echoed back by the upstream would leak into a log.
9
+ *
10
+ * Transport seam: the driver calls the global `fetch` directly, so the
11
+ * existing `vi.stubGlobal('fetch', …)` harness from `http.test.ts` applies
12
+ * verbatim.
13
+ */
14
+
15
+ import { afterEach, describe, expect, it, vi } from 'vitest'
16
+ import { OpenRouterProvider } from '../client.js'
17
+
18
+ /**
19
+ * An obviously fake, non-functional token. Present ONLY to prove the
20
+ * driver does not interpolate a response body into a thrown message.
21
+ */
22
+ const FAKE_CREDENTIAL = 'sk-ant-FAKE-DO-NOT-USE-0000'
23
+
24
+ afterEach(() => {
25
+ vi.unstubAllGlobals()
26
+ })
27
+
28
+ function respond(status: number, body: string, headers: Record<string, string> = {}): Response {
29
+ return new Response(body, {
30
+ status,
31
+ headers: { 'Content-Type': 'application/json', ...headers },
32
+ })
33
+ }
34
+
35
+ function sseResponse(frames: string[]): Response {
36
+ return new Response(`${frames.join('\n\n')}\n\n`, {
37
+ status: 200,
38
+ headers: { 'Content-Type': 'text/event-stream' },
39
+ })
40
+ }
41
+
42
+ async function captureChatStreamError(
43
+ provider: OpenRouterProvider,
44
+ signal?: AbortSignal,
45
+ ): Promise<unknown> {
46
+ try {
47
+ for await (const _chunk of provider.chatStream({
48
+ model: 'anthropic/claude-sonnet-4-5',
49
+ messages: [{ role: 'user', content: 'hi' }],
50
+ signal,
51
+ })) {
52
+ // drain
53
+ }
54
+ } catch (err) {
55
+ return err
56
+ }
57
+ throw new Error('expected chatStream to throw')
58
+ }
59
+
60
+ function makeProvider(): OpenRouterProvider {
61
+ return new OpenRouterProvider({
62
+ apiKey: 'test-key',
63
+ baseUrl: 'https://example.test/api/v1',
64
+ })
65
+ }
66
+
67
+ describe('@namzu/openrouter — provider error taxonomy', () => {
68
+ it('(a) 429 with `retry-after: 2` throws a throttle-classified error carrying retryAfterMs', async () => {
69
+ vi.stubGlobal(
70
+ 'fetch',
71
+ vi.fn().mockResolvedValue(
72
+ respond(429, JSON.stringify({ error: { message: 'rate limit exceeded' } }), {
73
+ 'retry-after': '2',
74
+ }),
75
+ ),
76
+ )
77
+
78
+ const err = await captureChatStreamError(makeProvider())
79
+
80
+ expect(err).toMatchObject({
81
+ name: 'ProviderRequestError',
82
+ kind: 'throttle',
83
+ status: 429,
84
+ retryAfterMs: 2000,
85
+ providerId: 'openrouter',
86
+ })
87
+ })
88
+
89
+ it("(c) 400 whose body says the prompt is too long classifies as 'context_overflow'", async () => {
90
+ vi.stubGlobal(
91
+ 'fetch',
92
+ vi.fn().mockResolvedValue(
93
+ respond(
94
+ 400,
95
+ JSON.stringify({
96
+ error: {
97
+ type: 'invalid_request_error',
98
+ message: 'prompt is too long: 305231 tokens > 200000 maximum',
99
+ },
100
+ }),
101
+ ),
102
+ ),
103
+ )
104
+
105
+ const err = await captureChatStreamError(makeProvider())
106
+
107
+ expect(err).toMatchObject({
108
+ name: 'ProviderRequestError',
109
+ kind: 'context_overflow',
110
+ status: 400,
111
+ providerId: 'openrouter',
112
+ })
113
+ })
114
+
115
+ it("(c) 400 that is not an overflow classifies as 'bad_request'", async () => {
116
+ vi.stubGlobal(
117
+ 'fetch',
118
+ vi.fn().mockResolvedValue(
119
+ respond(
120
+ 400,
121
+ JSON.stringify({
122
+ error: {
123
+ type: 'invalid_request_error',
124
+ message: "unknown field 'temperture'",
125
+ },
126
+ }),
127
+ ),
128
+ ),
129
+ )
130
+
131
+ const err = await captureChatStreamError(makeProvider())
132
+
133
+ expect(err).toMatchObject({
134
+ name: 'ProviderRequestError',
135
+ kind: 'bad_request',
136
+ status: 400,
137
+ providerId: 'openrouter',
138
+ })
139
+ })
140
+
141
+ it("(a/auth) 401 classifies as 'auth'", async () => {
142
+ vi.stubGlobal(
143
+ 'fetch',
144
+ vi
145
+ .fn()
146
+ .mockResolvedValue(
147
+ respond(401, JSON.stringify({ error: { message: 'No auth credentials found' } })),
148
+ ),
149
+ )
150
+
151
+ const err = await captureChatStreamError(makeProvider())
152
+
153
+ expect(err).toMatchObject({
154
+ name: 'ProviderRequestError',
155
+ kind: 'auth',
156
+ status: 401,
157
+ providerId: 'openrouter',
158
+ })
159
+ })
160
+
161
+ it('(d) SECURITY: a credential echoed back in a 401 body never reaches the thrown message', async () => {
162
+ vi.stubGlobal(
163
+ 'fetch',
164
+ vi.fn().mockResolvedValue(
165
+ respond(
166
+ 401,
167
+ JSON.stringify({
168
+ error: { message: `invalid api key: ${FAKE_CREDENTIAL}` },
169
+ }),
170
+ ),
171
+ ),
172
+ )
173
+
174
+ const err = await captureChatStreamError(makeProvider())
175
+
176
+ expect(err).toBeInstanceOf(Error)
177
+ const message = (err as Error).message
178
+ expect(message).not.toContain(FAKE_CREDENTIAL)
179
+ expect(JSON.stringify(err)).not.toContain(FAKE_CREDENTIAL)
180
+ expect('cause' in (err as object)).toBe(false)
181
+ })
182
+
183
+ it('(d) SECURITY: a credential echoed back in a 400 body never reaches the thrown message', async () => {
184
+ vi.stubGlobal(
185
+ 'fetch',
186
+ vi
187
+ .fn()
188
+ .mockResolvedValue(
189
+ respond(400, `bad request for key ${FAKE_CREDENTIAL}: model not permitted`),
190
+ ),
191
+ )
192
+
193
+ const err = await captureChatStreamError(makeProvider())
194
+
195
+ expect((err as Error).message).not.toContain(FAKE_CREDENTIAL)
196
+ })
197
+
198
+ it("classifies a fetch rejection as 'network' without retaining its cause", async () => {
199
+ const vendor = new Error(`socket failed for ${FAKE_CREDENTIAL}`)
200
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(vendor))
201
+
202
+ const err = await captureChatStreamError(makeProvider())
203
+
204
+ expect(err).toMatchObject({
205
+ name: 'ProviderRequestError',
206
+ kind: 'network',
207
+ providerId: 'openrouter',
208
+ })
209
+ expect((err as Error).message).not.toContain(FAKE_CREDENTIAL)
210
+ expect('cause' in (err as object)).toBe(false)
211
+ })
212
+
213
+ it('preserves the caller abort reason when fetch replaces the error object', async () => {
214
+ const controller = new AbortController()
215
+ const reason = new Error('user stopped')
216
+ const fetchAbort = Object.assign(new Error('request aborted'), {
217
+ name: 'AbortError',
218
+ })
219
+ controller.abort(reason)
220
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(fetchAbort))
221
+
222
+ const err = await captureChatStreamError(makeProvider(), controller.signal)
223
+
224
+ expect(fetchAbort).not.toBe(reason)
225
+ expect(err).toBe(reason)
226
+ })
227
+
228
+ it('classifies and sanitizes an SSE error after the 200 response', async () => {
229
+ vi.stubGlobal(
230
+ 'fetch',
231
+ vi.fn().mockResolvedValue(
232
+ sseResponse([
233
+ `data: ${JSON.stringify({
234
+ error: {
235
+ type: 'overloaded_error',
236
+ message: `echoed ${FAKE_CREDENTIAL}`,
237
+ },
238
+ })}`,
239
+ ]),
240
+ ),
241
+ )
242
+
243
+ const err = await captureChatStreamError(makeProvider())
244
+
245
+ expect(err).toMatchObject({
246
+ name: 'ProviderRequestError',
247
+ kind: 'server',
248
+ providerId: 'openrouter',
249
+ })
250
+ expect((err as Error).message).not.toContain(FAKE_CREDENTIAL)
251
+ expect(JSON.stringify(err)).not.toContain(FAKE_CREDENTIAL)
252
+ expect('cause' in (err as object)).toBe(false)
253
+ })
254
+
255
+ it('classifies malformed SSE without retaining the credential-bearing frame', async () => {
256
+ vi.stubGlobal(
257
+ 'fetch',
258
+ vi.fn().mockResolvedValue(sseResponse([`data: ${FAKE_CREDENTIAL} not-json`])),
259
+ )
260
+
261
+ const err = await captureChatStreamError(makeProvider())
262
+
263
+ expect(err).toMatchObject({
264
+ name: 'ProviderRequestError',
265
+ kind: 'server',
266
+ providerId: 'openrouter',
267
+ })
268
+ expect((err as Error).message).not.toContain(FAKE_CREDENTIAL)
269
+ expect(JSON.stringify(err)).not.toContain(FAKE_CREDENTIAL)
270
+ expect('cause' in (err as object)).toBe(false)
271
+ })
272
+ })
@@ -45,6 +45,7 @@ describe('@namzu/openrouter', () => {
45
45
  supportsStreaming: true,
46
46
  supportsFunctionCalling: true,
47
47
  supportsVision: false,
48
+ supportsDocuments: false,
48
49
  })
49
50
  })
50
51
 
@@ -67,4 +68,37 @@ describe('@namzu/openrouter', () => {
67
68
  expect(capabilities).toEqual(OPENROUTER_CAPABILITIES)
68
69
  })
69
70
  })
71
+
72
+ it('keeps the Namzu enforcement hint out of the OpenRouter request body', () => {
73
+ const provider = new OpenRouterProvider({ apiKey: 'test-key' })
74
+ const tools = [
75
+ {
76
+ type: 'function' as const,
77
+ function: {
78
+ name: 'edit',
79
+ description: 'Edit',
80
+ parameters: { type: 'object' },
81
+ },
82
+ },
83
+ ]
84
+ const body = (
85
+ provider as unknown as {
86
+ buildRequestBody(
87
+ params: import('@namzu/sdk').ChatCompletionParams,
88
+ stream: boolean,
89
+ ): Record<string, unknown>
90
+ }
91
+ ).buildRequestBody(
92
+ {
93
+ model: 'anthropic/claude-sonnet-5',
94
+ messages: [{ role: 'user', content: 'edit' }],
95
+ tools,
96
+ enforceToolInputSchema: ['edit'],
97
+ },
98
+ true,
99
+ )
100
+
101
+ expect(body).not.toHaveProperty('enforceToolInputSchema')
102
+ expect(body.tools).toEqual(tools)
103
+ })
70
104
  })
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { OPENROUTER_CAPABILITIES } from '../client.js'
4
+
5
+ /**
6
+ * This driver does not map attachments, and says so.
7
+ *
8
+ * Shipping without a wire shape is legitimate; claiming one you do not have
9
+ * is not. The runtime reads these flags and warns — or fails under
10
+ * `strictCapabilities` — before the request is built, so the flags are the
11
+ * contract, and what this pins is drift in either direction: flipping a
12
+ * flag without writing the mapping makes the runtime stop warning about
13
+ * images that still vanish, and writing the mapping without flipping the
14
+ * flag makes it keep warning about images that now arrive.
15
+ */
16
+
17
+ describe('the declared capabilities are the honest ones', () => {
18
+ it('does not claim vision', () => {
19
+ expect(OPENROUTER_CAPABILITIES.supportsVision).toBe(false)
20
+ })
21
+
22
+ it('does not claim documents', () => {
23
+ expect(OPENROUTER_CAPABILITIES.supportsDocuments).toBe(false)
24
+ })
25
+
26
+ it('claims the tool support it does have', () => {
27
+ expect(OPENROUTER_CAPABILITIES.supportsTools).toBe(true)
28
+ expect(OPENROUTER_CAPABILITIES.supportsFunctionCalling).toBe(true)
29
+ })
30
+
31
+ it('claims streaming', () => {
32
+ expect(OPENROUTER_CAPABILITIES.supportsStreaming).toBe(true)
33
+ })
34
+
35
+ it('answers every flag the negotiation reads', () => {
36
+ // An absent flag defaults to permissive, so a missing one is not a
37
+ // neutral omission — it is a claim.
38
+ for (const flag of [
39
+ 'supportsTools',
40
+ 'supportsStreaming',
41
+ 'supportsFunctionCalling',
42
+ 'supportsVision',
43
+ 'supportsDocuments',
44
+ ] as const) {
45
+ expect(typeof OPENROUTER_CAPABILITIES[flag]).toBe('boolean')
46
+ }
47
+ })
48
+ })
package/src/client.ts CHANGED
@@ -7,6 +7,13 @@ import type {
7
7
  TokenUsage,
8
8
  ToolChoice,
9
9
  } from '@namzu/sdk'
10
+ import {
11
+ ProviderRequestError,
12
+ isCallerAbortError,
13
+ isProviderRequestError,
14
+ providerHttpError,
15
+ providerVendorError,
16
+ } from '@namzu/sdk'
10
17
  import type { OpenRouterConfig } from './types.js'
11
18
 
12
19
  const OPENROUTER_BASE_URL = process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1'
@@ -58,6 +65,8 @@ export const OPENROUTER_CAPABILITIES: ProviderCapabilities = {
58
65
  supportsStreaming: true,
59
66
  supportsFunctionCalling: true,
60
67
  supportsVision: false,
68
+ // Images only. A document degrades to a named placeholder.
69
+ supportsDocuments: false,
61
70
  }
62
71
 
63
72
  export class OpenRouterProvider implements LLMProvider {
@@ -160,20 +169,41 @@ export class OpenRouterProvider implements LLMProvider {
160
169
  // prior `AbortSignal.timeout(...)` expression (byte-identical).
161
170
  const signal = params.signal ? AbortSignal.any([timeout, params.signal]) : timeout
162
171
 
163
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
164
- method: 'POST',
165
- headers: this.getHeaders(),
166
- body: JSON.stringify(body),
167
- signal,
168
- })
172
+ let response: Response
173
+ try {
174
+ response = await fetch(`${this.baseUrl}/chat/completions`, {
175
+ method: 'POST',
176
+ headers: this.getHeaders(),
177
+ body: JSON.stringify(body),
178
+ signal,
179
+ })
180
+ } catch (err) {
181
+ if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
182
+ throw providerVendorError({ providerId: 'openrouter', error: err })
183
+ }
169
184
 
170
185
  if (!response.ok) {
171
- const errorBody = await response.text()
172
- throw new Error(`OpenRouter API error (${response.status}): ${errorBody}`)
186
+ // The body is read to CLASSIFY (a 400 saying "prompt is too long" is a
187
+ // context overflow, any other 400 is a bad request) and then dropped. It
188
+ // used to be interpolated straight into the message, which is how a
189
+ // credential the upstream echoed back reached every log that recorded
190
+ // the failure — proven with a planted fake token.
191
+ const errorBody = await response.text().catch(() => '')
192
+ throw providerHttpError({
193
+ providerId: 'openrouter',
194
+ status: response.status,
195
+ body: errorBody,
196
+ retryAfter: response.headers.get('retry-after'),
197
+ })
173
198
  }
174
199
 
175
200
  if (!response.body) {
176
- throw new Error('OpenRouter returned no stream body')
201
+ throw new ProviderRequestError({
202
+ kind: 'server',
203
+ providerId: 'openrouter',
204
+ status: response.status,
205
+ detail: 'the response contained no stream body',
206
+ })
177
207
  }
178
208
 
179
209
  const reader = response.body.getReader()
@@ -199,6 +229,7 @@ export class OpenRouterProvider implements LLMProvider {
199
229
  try {
200
230
  const parsed = JSON.parse(data) as {
201
231
  id: string
232
+ error?: unknown
202
233
  choices: Array<{
203
234
  delta: {
204
235
  content?: string
@@ -214,6 +245,13 @@ export class OpenRouterProvider implements LLMProvider {
214
245
  usage?: RawUsage
215
246
  }
216
247
 
248
+ if (parsed.error !== undefined) {
249
+ throw providerVendorError({
250
+ providerId: 'openrouter',
251
+ error: new Error(data),
252
+ })
253
+ }
254
+
217
255
  const choice = parsed.choices[0]
218
256
  if (!choice) continue
219
257
 
@@ -232,16 +270,21 @@ export class OpenRouterProvider implements LLMProvider {
232
270
  usage: parsed.usage ? parseUsage(parsed.usage) : undefined,
233
271
  }
234
272
  } catch (parseErr) {
235
- yield {
236
- id: '',
237
- delta: { content: undefined },
238
- finishReason: undefined,
239
- usage: undefined,
240
- error: `Stream parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
241
- }
273
+ if (isProviderRequestError(parseErr)) throw parseErr
274
+ // JSON SyntaxError messages include a source snippet, and
275
+ // mapping failures may include vendor values. Drop both.
276
+ throw new ProviderRequestError({
277
+ kind: 'server',
278
+ providerId: 'openrouter',
279
+ detail: 'the provider stream returned malformed data',
280
+ })
242
281
  }
243
282
  }
244
283
  }
284
+ } catch (err) {
285
+ if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
286
+ if (isProviderRequestError(err)) throw err
287
+ throw providerVendorError({ providerId: 'openrouter', error: err })
245
288
  } finally {
246
289
  reader.releaseLock()
247
290
  }