@namzu/openrouter 1.0.2 → 1.0.3
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 +30 -0
- package/dist/__tests__/error-taxonomy.test.d.ts +15 -0
- package/dist/__tests__/error-taxonomy.test.d.ts.map +1 -0
- package/dist/__tests__/error-taxonomy.test.js +187 -0
- package/dist/__tests__/error-taxonomy.test.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +55 -16
- package/dist/client.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/error-taxonomy.test.ts +272 -0
- package/src/client.ts +57 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3fd2524: Normalize request-start and mid-stream failures across all seven provider
|
|
8
|
+
drivers with the new public `ProviderRequestError` taxonomy. Errors expose
|
|
9
|
+
`kind` (`throttle`, `network`, `auth`, `context_overflow`, `bad_request`, or
|
|
10
|
+
`server`), `providerId`, and optional `status` / `retryAfterMs`, with
|
|
11
|
+
`isProviderRequestError` available for structural narrowing across package
|
|
12
|
+
copies.
|
|
13
|
+
|
|
14
|
+
Provider error messages and metadata deliberately omit vendor response bodies,
|
|
15
|
+
URLs, messages, and causes because upstream errors can echo credentials. HTTP
|
|
16
|
+
dialect-mismatch diagnostics now keep only the endpoint origin and status.
|
|
17
|
+
Caller-owned aborts remain unchanged instead of being reclassified.
|
|
18
|
+
|
|
19
|
+
The runtime preserves the classified error through streaming and publishes its
|
|
20
|
+
safe metadata as `Run.lastProviderError` and
|
|
21
|
+
`run_failed.providerError`. Bedrock stream-exception events and provider
|
|
22
|
+
iterator/SSE failures no longer appear as clean end-of-stream.
|
|
23
|
+
|
|
24
|
+
`retryAfterMs` is metadata only; this change does not add retries or alter vendor
|
|
25
|
+
SDK retry settings. Provider packages now require `@namzu/sdk >=1.3.0`, the
|
|
26
|
+
first SDK release containing these runtime helpers and types.
|
|
27
|
+
|
|
28
|
+
Ollama now maps `done_reason: "length"` truthfully so runtime continuation can
|
|
29
|
+
run. LM Studio treats content-free `contextLengthReached` as context overflow,
|
|
30
|
+
while preserving `"length"` after partial content, and creates its WebSocket
|
|
31
|
+
client lazily on first use.
|
|
32
|
+
|
|
3
33
|
## 1.0.2
|
|
4
34
|
|
|
5
35
|
### 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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"}
|
package/dist/client.d.ts.map
CHANGED
|
@@ -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;
|
|
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,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;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) {
|
|
@@ -129,18 +130,41 @@ export class OpenRouterProvider {
|
|
|
129
130
|
// response body stream. When no caller signal is present this is the exact
|
|
130
131
|
// prior `AbortSignal.timeout(...)` expression (byte-identical).
|
|
131
132
|
const signal = params.signal ? AbortSignal.any([timeout, params.signal]) : timeout;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
let response;
|
|
134
|
+
try {
|
|
135
|
+
response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: this.getHeaders(),
|
|
138
|
+
body: JSON.stringify(body),
|
|
139
|
+
signal,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (isCallerAbortError(err, params.signal))
|
|
144
|
+
throw params.signal?.reason ?? err;
|
|
145
|
+
throw providerVendorError({ providerId: 'openrouter', error: err });
|
|
146
|
+
}
|
|
138
147
|
if (!response.ok) {
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
// The body is read to CLASSIFY (a 400 saying "prompt is too long" is a
|
|
149
|
+
// context overflow, any other 400 is a bad request) and then dropped. It
|
|
150
|
+
// used to be interpolated straight into the message, which is how a
|
|
151
|
+
// credential the upstream echoed back reached every log that recorded
|
|
152
|
+
// the failure — proven with a planted fake token.
|
|
153
|
+
const errorBody = await response.text().catch(() => '');
|
|
154
|
+
throw providerHttpError({
|
|
155
|
+
providerId: 'openrouter',
|
|
156
|
+
status: response.status,
|
|
157
|
+
body: errorBody,
|
|
158
|
+
retryAfter: response.headers.get('retry-after'),
|
|
159
|
+
});
|
|
141
160
|
}
|
|
142
161
|
if (!response.body) {
|
|
143
|
-
throw new
|
|
162
|
+
throw new ProviderRequestError({
|
|
163
|
+
kind: 'server',
|
|
164
|
+
providerId: 'openrouter',
|
|
165
|
+
status: response.status,
|
|
166
|
+
detail: 'the response contained no stream body',
|
|
167
|
+
});
|
|
144
168
|
}
|
|
145
169
|
const reader = response.body.getReader();
|
|
146
170
|
const decoder = new TextDecoder();
|
|
@@ -163,6 +187,12 @@ export class OpenRouterProvider {
|
|
|
163
187
|
return;
|
|
164
188
|
try {
|
|
165
189
|
const parsed = JSON.parse(data);
|
|
190
|
+
if (parsed.error !== undefined) {
|
|
191
|
+
throw providerVendorError({
|
|
192
|
+
providerId: 'openrouter',
|
|
193
|
+
error: new Error(data),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
166
196
|
const choice = parsed.choices[0];
|
|
167
197
|
if (!choice)
|
|
168
198
|
continue;
|
|
@@ -182,17 +212,26 @@ export class OpenRouterProvider {
|
|
|
182
212
|
};
|
|
183
213
|
}
|
|
184
214
|
catch (parseErr) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
215
|
+
if (isProviderRequestError(parseErr))
|
|
216
|
+
throw parseErr;
|
|
217
|
+
// JSON SyntaxError messages include a source snippet, and
|
|
218
|
+
// mapping failures may include vendor values. Drop both.
|
|
219
|
+
throw new ProviderRequestError({
|
|
220
|
+
kind: 'server',
|
|
221
|
+
providerId: 'openrouter',
|
|
222
|
+
detail: 'the provider stream returned malformed data',
|
|
223
|
+
});
|
|
192
224
|
}
|
|
193
225
|
}
|
|
194
226
|
}
|
|
195
227
|
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
if (isCallerAbortError(err, params.signal))
|
|
230
|
+
throw params.signal?.reason ?? err;
|
|
231
|
+
if (isProviderRequestError(err))
|
|
232
|
+
throw err;
|
|
233
|
+
throw providerVendorError({ providerId: 'openrouter', error: err });
|
|
234
|
+
}
|
|
196
235
|
finally {
|
|
197
236
|
reader.releaseLock();
|
|
198
237
|
}
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
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.
|
|
3
|
+
"version": "1.0.3",
|
|
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.
|
|
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
44
|
"vitest": "^2.0.0",
|
|
45
|
-
"@namzu/sdk": "^1.
|
|
45
|
+
"@namzu/sdk": "^1.4.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
|
+
})
|
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'
|
|
@@ -160,20 +167,41 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
160
167
|
// prior `AbortSignal.timeout(...)` expression (byte-identical).
|
|
161
168
|
const signal = params.signal ? AbortSignal.any([timeout, params.signal]) : timeout
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
let response: Response
|
|
171
|
+
try {
|
|
172
|
+
response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: this.getHeaders(),
|
|
175
|
+
body: JSON.stringify(body),
|
|
176
|
+
signal,
|
|
177
|
+
})
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
|
|
180
|
+
throw providerVendorError({ providerId: 'openrouter', error: err })
|
|
181
|
+
}
|
|
169
182
|
|
|
170
183
|
if (!response.ok) {
|
|
171
|
-
|
|
172
|
-
|
|
184
|
+
// The body is read to CLASSIFY (a 400 saying "prompt is too long" is a
|
|
185
|
+
// context overflow, any other 400 is a bad request) and then dropped. It
|
|
186
|
+
// used to be interpolated straight into the message, which is how a
|
|
187
|
+
// credential the upstream echoed back reached every log that recorded
|
|
188
|
+
// the failure — proven with a planted fake token.
|
|
189
|
+
const errorBody = await response.text().catch(() => '')
|
|
190
|
+
throw providerHttpError({
|
|
191
|
+
providerId: 'openrouter',
|
|
192
|
+
status: response.status,
|
|
193
|
+
body: errorBody,
|
|
194
|
+
retryAfter: response.headers.get('retry-after'),
|
|
195
|
+
})
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
if (!response.body) {
|
|
176
|
-
throw new
|
|
199
|
+
throw new ProviderRequestError({
|
|
200
|
+
kind: 'server',
|
|
201
|
+
providerId: 'openrouter',
|
|
202
|
+
status: response.status,
|
|
203
|
+
detail: 'the response contained no stream body',
|
|
204
|
+
})
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
const reader = response.body.getReader()
|
|
@@ -199,6 +227,7 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
199
227
|
try {
|
|
200
228
|
const parsed = JSON.parse(data) as {
|
|
201
229
|
id: string
|
|
230
|
+
error?: unknown
|
|
202
231
|
choices: Array<{
|
|
203
232
|
delta: {
|
|
204
233
|
content?: string
|
|
@@ -214,6 +243,13 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
214
243
|
usage?: RawUsage
|
|
215
244
|
}
|
|
216
245
|
|
|
246
|
+
if (parsed.error !== undefined) {
|
|
247
|
+
throw providerVendorError({
|
|
248
|
+
providerId: 'openrouter',
|
|
249
|
+
error: new Error(data),
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
217
253
|
const choice = parsed.choices[0]
|
|
218
254
|
if (!choice) continue
|
|
219
255
|
|
|
@@ -232,16 +268,21 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
232
268
|
usage: parsed.usage ? parseUsage(parsed.usage) : undefined,
|
|
233
269
|
}
|
|
234
270
|
} catch (parseErr) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
271
|
+
if (isProviderRequestError(parseErr)) throw parseErr
|
|
272
|
+
// JSON SyntaxError messages include a source snippet, and
|
|
273
|
+
// mapping failures may include vendor values. Drop both.
|
|
274
|
+
throw new ProviderRequestError({
|
|
275
|
+
kind: 'server',
|
|
276
|
+
providerId: 'openrouter',
|
|
277
|
+
detail: 'the provider stream returned malformed data',
|
|
278
|
+
})
|
|
242
279
|
}
|
|
243
280
|
}
|
|
244
281
|
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
|
|
284
|
+
if (isProviderRequestError(err)) throw err
|
|
285
|
+
throw providerVendorError({ providerId: 'openrouter', error: err })
|
|
245
286
|
} finally {
|
|
246
287
|
reader.releaseLock()
|
|
247
288
|
}
|