@namzu/openrouter 1.0.3 → 2.0.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 +133 -0
- package/dist/__tests__/openrouter.test.js +22 -0
- package/dist/__tests__/openrouter.test.js.map +1 -1
- package/dist/__tests__/thinking-refused.test.d.ts +2 -0
- package/dist/__tests__/thinking-refused.test.d.ts.map +1 -0
- package/dist/__tests__/thinking-refused.test.js +35 -0
- package/dist/__tests__/thinking-refused.test.js.map +1 -0
- package/dist/__tests__/vision.test.d.ts +2 -0
- package/dist/__tests__/vision.test.d.ts.map +1 -0
- package/dist/__tests__/vision.test.js +42 -0
- package/dist/__tests__/vision.test.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -1
- package/dist/client.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/openrouter.test.ts +34 -0
- package/src/__tests__/thinking-refused.test.ts +44 -0
- package/src/__tests__/vision.test.ts +48 -0
- package/src/client.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,138 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 1500973: Every driver that cannot think now says so instead of dropping the request.
|
|
8
|
+
|
|
9
|
+
`thinking` sits on `ChatCompletionParams`, so every driver accepts it. Five of
|
|
10
|
+
them — Bedrock, OpenRouter, HTTP, Ollama, LM Studio — implemented none of it
|
|
11
|
+
and dropped the field: the caller got an ordinary completion with an empty
|
|
12
|
+
`reasoning` array, which is indistinguishable from a model that simply chose
|
|
13
|
+
not to reason. The request looked honoured and the answer looked like an
|
|
14
|
+
answer.
|
|
15
|
+
|
|
16
|
+
The OpenAI driver already refused instead, with the reasoning written out
|
|
17
|
+
beside it. So the rule had been decided once and applied once, while five
|
|
18
|
+
siblings went on being silent. It moves to `@namzu/sdk` as
|
|
19
|
+
`assertThinkingUnsupported(driverName, params)`, and a new driver now inherits
|
|
20
|
+
it rather than re-deciding it.
|
|
21
|
+
|
|
22
|
+
The error names the driver, which in a multi-provider setup is the difference
|
|
23
|
+
between a bug report about the model and a one-line configuration fix.
|
|
24
|
+
|
|
25
|
+
**Turning thinking off stays a no-op** on all of them, because that is the
|
|
26
|
+
state a driver without thinking is already in — a config shared across
|
|
27
|
+
providers saying `{ type: 'disabled' }` should not fail on the ones that were
|
|
28
|
+
never going to think.
|
|
29
|
+
|
|
30
|
+
`assertThinkingSupported` in `@namzu/openai` is unchanged as an export and now
|
|
31
|
+
delegates to the shared helper. Its message changed: it no longer says
|
|
32
|
+
"extended thinking", because `adaptive` is refused too and calling that
|
|
33
|
+
extended would be wrong.
|
|
34
|
+
|
|
35
|
+
**Migration.** If you passed `thinking` to any of the five and relied on it
|
|
36
|
+
being ignored, remove it — you were receiving a non-thinking answer either way,
|
|
37
|
+
and now you find out at the call instead of by inspecting an empty array.
|
|
38
|
+
|
|
39
|
+
Not in this change: implementing thinking natively on Bedrock, which serves the
|
|
40
|
+
same Claude models through a different wire and deserves the per-model
|
|
41
|
+
resolution the Anthropic driver just gained. That needs the Converse request
|
|
42
|
+
and response shapes verified against the reference first, and is not something
|
|
43
|
+
to guess at.
|
|
44
|
+
|
|
45
|
+
## 1.1.0
|
|
46
|
+
|
|
47
|
+
### Minor Changes
|
|
48
|
+
|
|
49
|
+
- 935b8f3: Every driver can now see.
|
|
50
|
+
|
|
51
|
+
These three dropped `attachments` outright, so a user who attached a
|
|
52
|
+
screenshot got a turn about nothing. `supportsVision: false` said so, which
|
|
53
|
+
made the declaration honest and the driver useless — and it was the last
|
|
54
|
+
place in the estate where a namzu capability existed on one driver and
|
|
55
|
+
silently did not on another.
|
|
56
|
+
|
|
57
|
+
Each wire carries an image differently, so this is one intent and three
|
|
58
|
+
mappings:
|
|
59
|
+
|
|
60
|
+
- **Converse**: raw bytes in an image content block beside the text. The
|
|
61
|
+
tool-result path already did this; the user path never looked at
|
|
62
|
+
`attachments`.
|
|
63
|
+
- **The two-dialect HTTP driver**: a `data:` URI content part on one
|
|
64
|
+
dialect, a base64 source block on the other.
|
|
65
|
+
- **The gateway driver**: a `data:` URI content part.
|
|
66
|
+
|
|
67
|
+
Across all three: a media type the endpoint cannot decode is named in the
|
|
68
|
+
text rather than sent, because a payload it rejects fails the whole request
|
|
69
|
+
and losing the turn is worse than losing sight of one image. A message with
|
|
70
|
+
no attachments keeps its plain-string content, so nothing about an ordinary
|
|
71
|
+
request changes shape. Several attachments on one message are carried in
|
|
72
|
+
order.
|
|
73
|
+
|
|
74
|
+
An image inside a **tool result** still degrades to a text placeholder on
|
|
75
|
+
the HTTP and gateway drivers: a tool message is text-only in those dialects,
|
|
76
|
+
so there is nowhere to put it. Converse carries it, and always did.
|
|
77
|
+
|
|
78
|
+
### Patch Changes
|
|
79
|
+
|
|
80
|
+
- 935b8f3: Stop dumping a tool result's base64 payload into the prompt.
|
|
81
|
+
|
|
82
|
+
Four drivers mishandled a tool result carrying content blocks, each in its
|
|
83
|
+
own way: bedrock and the http driver's anthropic dialect `JSON.stringify`d
|
|
84
|
+
the whole array, putting a screenshot's base64 into the prompt as JSON
|
|
85
|
+
text; openrouter and the http driver's openai dialect passed the array
|
|
86
|
+
through raw to an endpoint expecting a string; lmstudio folded it into a
|
|
87
|
+
template literal, producing `[object Object]` per block. The model cannot
|
|
88
|
+
decode any of it, and it costs a fortune in tokens.
|
|
89
|
+
|
|
90
|
+
- The three text-only wires now flatten with the SDK's existing helper,
|
|
91
|
+
which names a non-text block and its size instead of inlining it. The
|
|
92
|
+
openai and ollama drivers already did this — the helper was there and
|
|
93
|
+
four callers were missing.
|
|
94
|
+
- **bedrock sends the image as an image.** That wire carries images
|
|
95
|
+
natively, so a placeholder would be a downgrade the other drivers accept
|
|
96
|
+
only because their format has no room for one. Text and image survive as
|
|
97
|
+
separate blocks in order, and a media type the format does not accept
|
|
98
|
+
still degrades to a named placeholder rather than being smuggled through
|
|
99
|
+
as text.
|
|
100
|
+
|
|
101
|
+
- 935b8f3: A turn that asked for tools no longer ends because the provider said it
|
|
102
|
+
didn't.
|
|
103
|
+
|
|
104
|
+
The iteration loop ended the turn on `finishReason === 'stop'` **before**
|
|
105
|
+
looking at whether the model had asked for tools. Endpoints on the OpenAI
|
|
106
|
+
wire shape — gateways and local servers especially — routinely report `stop`
|
|
107
|
+
on the same response that carries a populated `tool_calls`, and three of
|
|
108
|
+
this repo's drivers passed that value straight through.
|
|
109
|
+
|
|
110
|
+
The damage was total and silent: every requested call skipped, an assistant
|
|
111
|
+
turn left carrying `tool_use` blocks nothing ever answered, and the run
|
|
112
|
+
settling as though it had finished the work it never started.
|
|
113
|
+
|
|
114
|
+
- **The runtime now treats tool calls as the fact and the finish reason as
|
|
115
|
+
the summary.** When they disagree, the calls win. This is the load-bearing
|
|
116
|
+
fix: it protects every driver, including ones this repo does not ship.
|
|
117
|
+
- **The three drivers that cast the reason raw now report it honestly** —
|
|
118
|
+
a stream that produced a tool call reports `tool_calls`, whatever the
|
|
119
|
+
endpoint called it. Defence in depth, and it makes the reported reason
|
|
120
|
+
true for anyone else reading it.
|
|
121
|
+
|
|
122
|
+
The existing suite could not catch this: the scripted mock reports
|
|
123
|
+
`tool_calls` whenever it emits one, which is what an honest provider does
|
|
124
|
+
and therefore never the case that breaks.
|
|
125
|
+
|
|
126
|
+
- 935b8f3: A user message can carry a document
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
`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.
|
|
131
|
+
|
|
132
|
+
`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.
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
3
136
|
## 1.0.3
|
|
4
137
|
|
|
5
138
|
### Patch Changes
|
|
@@ -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":"AAAA,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACzD,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAE7F,6EAA6E;AAC7E,4EAA4E;AAC5E,8CAA8C;AAC9C,UAAU,CAAC,GAAG,EAAE;IACf,IAAI,gBAAgB,CAAC,WAAW,CAAC,YAAY,CAAC,EAAE,CAAC;QAChD,gBAAgB,CAAC,UAAU,CAAC,YAAY,CAAC,CAAA;IAC1C,CAAC;AACF,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IAClC,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;QACrC,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;YACpD,MAAM,CAAC,gBAAgB,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAC9D,kBAAkB,EAAE,CAAA;YACpB,MAAM,CAAC,gBAAgB,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC7D,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;QAC7D,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;YAC1E,kBAAkB,EAAE,CAAA;YACpB,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,EAAE,CAAC,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAA;QACxE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;YAClE,kBAAkB,EAAE,CAAA;YACpB,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;YACjE,MAAM,CAAC,gBAAgB,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9D,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;YACvE,kBAAkB,EAAE,CAAA;YACpB,MAAM,IAAI,GAAG,gBAAgB,CAAC,eAAe,CAAC,YAAY,CAAC,CAAA;YAC3D,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAA;QAC9C,CAAC,CAAC,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACxC,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YACjD,MAAM,CAAC,uBAAuB,CAAC,CAAC,OAAO,CAAC;gBACvC,aAAa,EAAE,IAAI;gBACnB,iBAAiB,EAAE,IAAI;gBACvB,uBAAuB,EAAE,IAAI;gBAC7B,cAAc,EAAE,KAAK;
|
|
1
|
+
{"version":3,"file":"openrouter.test.js","sourceRoot":"","sources":["../../src/__tests__/openrouter.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thinking-refused.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/thinking-refused.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { OpenRouterProvider } from '../client.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shared guard's own tests pass whether or not this driver calls it, so
|
|
5
|
+
* this drives the real chatStream. The defect being fixed was a declared
|
|
6
|
+
* parameter no code path consumed — asserting the helper works would have
|
|
7
|
+
* reproduced that shape one level up.
|
|
8
|
+
*
|
|
9
|
+
* The guard runs before any request, so the unreachable host below is never
|
|
10
|
+
* dialled on the throwing cases.
|
|
11
|
+
*/
|
|
12
|
+
const provider = new OpenRouterProvider({ apiKey: 'test-key' });
|
|
13
|
+
async function run(thinking) {
|
|
14
|
+
for await (const _chunk of provider.chatStream({
|
|
15
|
+
model: 'anthropic/claude-sonnet-4.5',
|
|
16
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
17
|
+
...(thinking ? { thinking } : {}),
|
|
18
|
+
})) {
|
|
19
|
+
// drain
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
describe('OpenRouterProvider refuses a thinking request rather than dropping it', () => {
|
|
23
|
+
it('throws on a manual thinking request', async () => {
|
|
24
|
+
await expect(run({ type: 'enabled', budgetTokens: 10_000 })).rejects.toThrow(/OpenRouterProvider does not implement thinking/);
|
|
25
|
+
});
|
|
26
|
+
it('throws on an adaptive one', async () => {
|
|
27
|
+
await expect(run({ type: 'adaptive' })).rejects.toThrow(/does not implement thinking/);
|
|
28
|
+
});
|
|
29
|
+
it('does not throw for an explicit disable', async () => {
|
|
30
|
+
// Fails on the unreachable host instead, which is the point: the
|
|
31
|
+
// guard let it past.
|
|
32
|
+
await expect(run({ type: 'disabled' })).rejects.not.toThrow(/does not implement thinking/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=thinking-refused.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thinking-refused.test.js","sourceRoot":"","sources":["../../src/__tests__/thinking-refused.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEjD;;;;;;;;GAQG;AAEH,MAAM,QAAQ,GAAG,IAAI,kBAAkB,CAAC,EAAE,MAAM,EAAE,UAAU,EAAW,CAAC,CAAA;AAExE,KAAK,UAAU,GAAG,CAAC,QAA2C;IAC7D,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,QAAQ,CAAC,UAAU,CAAC;QAC9C,KAAK,EAAE,6BAA6B;QACpC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3C,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACT,CAAC,EAAE,CAAC;QAC5B,QAAQ;IACT,CAAC;AACF,CAAC;AAED,QAAQ,CAAC,uEAAuE,EAAE,GAAG,EAAE;IACtF,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC3E,gDAAgD,CAChD,CAAA;IACF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAA;IACvF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACvD,iEAAiE;QACjE,qBAAqB;QACrB,MAAM,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAA;IAC3F,CAAC,CAAC,CAAA;AACH,CAAC,CAAC,CAAA"}
|
|
@@ -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"}
|
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;AASnB,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;IAmIrE,UAAU,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IA+BlC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAWrC"}
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProviderRequestError, isCallerAbortError, isProviderRequestError, providerHttpError, providerVendorError, } from '@namzu/sdk';
|
|
1
|
+
import { ProviderRequestError, assertThinkingUnsupported, isCallerAbortError, isProviderRequestError, providerHttpError, providerVendorError, } from '@namzu/sdk';
|
|
2
2
|
const OPENROUTER_BASE_URL = process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1';
|
|
3
3
|
function parseUsage(raw) {
|
|
4
4
|
if (!raw) {
|
|
@@ -34,6 +34,8 @@ export const OPENROUTER_CAPABILITIES = {
|
|
|
34
34
|
supportsStreaming: true,
|
|
35
35
|
supportsFunctionCalling: true,
|
|
36
36
|
supportsVision: false,
|
|
37
|
+
// Images only. A document degrades to a named placeholder.
|
|
38
|
+
supportsDocuments: false,
|
|
37
39
|
};
|
|
38
40
|
export class OpenRouterProvider {
|
|
39
41
|
id = 'openrouter';
|
|
@@ -124,6 +126,7 @@ export class OpenRouterProvider {
|
|
|
124
126
|
return body;
|
|
125
127
|
}
|
|
126
128
|
async *chatStream(params) {
|
|
129
|
+
assertThinkingUnsupported('OpenRouterProvider', params);
|
|
127
130
|
const body = this.buildRequestBody(params, true);
|
|
128
131
|
const timeout = AbortSignal.timeout(this.config.timeout ?? 120_000);
|
|
129
132
|
// Compose the caller abort with the request timeout so a Stop cancels the
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AASA,OAAO,EACN,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACtB,iBAAiB,EACjB,mBAAmB,GACnB,MAAM,YAAY,CAAA;AAGnB,MAAM,mBAAmB,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,8BAA8B,CAAA;AAc7F,SAAS,UAAU,CAAC,GAAc;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACV,OAAO;YACN,YAAY,EAAE,CAAC;YACf,gBAAgB,EAAE,CAAC;YACnB,WAAW,EAAE,CAAC;YACd,YAAY,EAAE,CAAC;YACf,gBAAgB,EAAE,CAAC;SACnB,CAAA;IACF,CAAC;IACD,OAAO;QACN,YAAY,EAAE,GAAG,CAAC,aAAa;QAC/B,gBAAgB,EAAE,GAAG,CAAC,iBAAiB;QACvC,WAAW,EAAE,GAAG,CAAC,YAAY;QAC7B,YAAY,EAAE,GAAG,CAAC,qBAAqB,EAAE,aAAa,IAAI,GAAG,CAAC,uBAAuB,IAAI,CAAC;QAC1F,gBAAgB,EAAE,GAAG,CAAC,2BAA2B,IAAI,CAAC;KACtD,CAAA;AACF,CAAC;AAED,SAAS,gBAAgB,CAAC,EAAc;IACvC,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IACrC,OAAO,EAAE,CAAA;AACV,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAyB;IAC5D,aAAa,EAAE,IAAI;IACnB,iBAAiB,EAAE,IAAI;IACvB,uBAAuB,EAAE,IAAI;IAC7B,cAAc,EAAE,KAAK;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AASA,OAAO,EACN,oBAAoB,EACpB,yBAAyB,EACzB,kBAAkB,EAClB,sBAAsB,EACtB,iBAAiB,EACjB,mBAAmB,GACnB,MAAM,YAAY,CAAA;AAGnB,MAAM,mBAAmB,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,8BAA8B,CAAA;AAc7F,SAAS,UAAU,CAAC,GAAc;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACV,OAAO;YACN,YAAY,EAAE,CAAC;YACf,gBAAgB,EAAE,CAAC;YACnB,WAAW,EAAE,CAAC;YACd,YAAY,EAAE,CAAC;YACf,gBAAgB,EAAE,CAAC;SACnB,CAAA;IACF,CAAC;IACD,OAAO;QACN,YAAY,EAAE,GAAG,CAAC,aAAa;QAC/B,gBAAgB,EAAE,GAAG,CAAC,iBAAiB;QACvC,WAAW,EAAE,GAAG,CAAC,YAAY;QAC7B,YAAY,EAAE,GAAG,CAAC,qBAAqB,EAAE,aAAa,IAAI,GAAG,CAAC,uBAAuB,IAAI,CAAC;QAC1F,gBAAgB,EAAE,GAAG,CAAC,2BAA2B,IAAI,CAAC;KACtD,CAAA;AACF,CAAC;AAED,SAAS,gBAAgB,CAAC,EAAc;IACvC,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IACrC,OAAO,EAAE,CAAA;AACV,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAyB;IAC5D,aAAa,EAAE,IAAI;IACnB,iBAAiB,EAAE,IAAI;IACvB,uBAAuB,EAAE,IAAI;IAC7B,cAAc,EAAE,KAAK;IACrB,2DAA2D;IAC3D,iBAAiB,EAAE,KAAK;CACxB,CAAA;AAED,MAAM,OAAO,kBAAkB;IACrB,EAAE,GAAG,YAAY,CAAA;IACjB,IAAI,GAAG,YAAY,CAAA;IACnB,YAAY,GAAG,uBAAuB,CAAA;IAEvC,MAAM,CAAkB;IACxB,OAAO,CAAQ;IAEvB,YAAY,MAAwB;QACnC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAA;QACxF,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,mBAAmB,CAAA;IACrD,CAAC;IAEO,UAAU;QACjB,MAAM,OAAO,GAA2B;YACvC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;YAC7C,cAAc,EAAE,kBAAkB;SAClC,CAAA;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAA;QAC9C,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC1B,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAA;QAC1C,CAAC;QACD,OAAO,OAAO,CAAA;IACf,CAAC;IAEO,cAAc,CAAC,QAA0C;QAChE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAC3B,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACzB,OAAO;oBACN,IAAI,EAAE,MAAM;oBACZ,OAAO,EAAE,GAAG,CAAC,OAAO;oBACpB,YAAY,EAAG,GAA+B,CAAC,UAAU;iBACzD,CAAA;YACF,CAAC;YACD,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,IAAI,WAAW,IAAI,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;gBACrE,OAAO;oBACN,IAAI,EAAE,WAAW;oBACjB,OAAO,EAAE,GAAG,CAAC,OAAO;oBACpB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;wBACtC,EAAE,EAAE,EAAE,CAAC,EAAE;wBACT,IAAI,EAAE,EAAE,CAAC,IAAI;wBACb,QAAQ,EAAE,EAAE,CAAC,QAAQ;qBACrB,CAAC,CAAC;iBACH,CAAA;YACF,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAA;QAChD,CAAC,CAAC,CAAA;IACH,CAAC;IAEO,gBAAgB,CAAC,MAA4B,EAAE,MAAe;QACrE,MAAM,IAAI,GAA4B;YACrC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC;YAC9C,MAAM;SACN,CAAA;QAED,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;QAC1B,CAAC;QACD,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;QACvD,CAAC;QACD,IAAI,MAAM,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;YAC5C,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,iBAAiB,CAAA;QACpD,CAAC;QAED,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAA;QAC3E,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;YAAE,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,SAAS,CAAA;QACtE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAA;QACvD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAA;QACvD,IAAI,MAAM,CAAC,gBAAgB,KAAK,SAAS;YAAE,IAAI,CAAC,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,CAAA;QAC3F,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS;YAAE,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,eAAe,CAAA;QACxF,IAAI,MAAM,CAAC,iBAAiB,KAAK,SAAS;YAAE,IAAI,CAAC,kBAAkB,GAAG,MAAM,CAAC,iBAAiB,CAAA;QAC9F,IAAI,MAAM,CAAC,IAAI;YAAE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;QAExC,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACzB,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,YAAY,CAAA;QACzC,CAAC;QAED,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;YAC3B,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,cAAc,CAAA;QAC7C,CAAC;QAED,OAAO,IAAI,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,CAAC,UAAU,CAAC,MAA4B;QAC7C,yBAAyB,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAA;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QAEhD,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,CAAA;QACnE,0EAA0E;QAC1E,2EAA2E;QAC3E,gEAAgE;QAChE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;QAElF,IAAI,QAAkB,CAAA;QACtB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,mBAAmB,EAAE;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE;gBAC1B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,MAAM;aACN,CAAC,CAAA;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC;gBAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,CAAA;YAC9E,MAAM,mBAAmB,CAAC,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;QACpE,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAClB,uEAAuE;YACvE,yEAAyE;YACzE,oEAAoE;YACpE,sEAAsE;YACtE,kDAAkD;YAClD,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;YACvD,MAAM,iBAAiB,CAAC;gBACvB,UAAU,EAAE,YAAY;gBACxB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,IAAI,EAAE,SAAS;gBACf,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;aAC/C,CAAC,CAAA;QACH,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACpB,MAAM,IAAI,oBAAoB,CAAC;gBAC9B,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,YAAY;gBACxB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,MAAM,EAAE,uCAAuC;aAC/C,CAAC,CAAA;QACH,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAA;QACxC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;QACjC,IAAI,MAAM,GAAG,EAAE,CAAA;QAEf,IAAI,CAAC;YACJ,OAAO,IAAI,EAAE,CAAC;gBACb,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,CAAA;gBAC/B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;gBAC3C,IAAI,IAAI;oBAAE,MAAK;gBAEf,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;gBACjD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBAChC,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;gBAE1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;oBAC3B,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;wBAAE,SAAQ;oBACvD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;oBAC7B,IAAI,IAAI,KAAK,QAAQ;wBAAE,OAAM;oBAE7B,IAAI,CAAC;wBACJ,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAgB7B,CAAA;wBAED,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;4BAChC,MAAM,mBAAmB,CAAC;gCACzB,UAAU,EAAE,YAAY;gCACxB,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC;6BACtB,CAAC,CAAA;wBACH,CAAC;wBAED,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;wBAChC,IAAI,CAAC,MAAM;4BAAE,SAAQ;wBAErB,MAAM;4BACL,EAAE,EAAE,MAAM,CAAC,EAAE;4BACb,KAAK,EAAE;gCACN,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO;gCAC7B,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;oCAChD,KAAK,EAAE,EAAE,CAAC,KAAK;oCACf,EAAE,EAAE,EAAE,CAAC,EAAE;oCACT,IAAI,EAAE,EAAE,CAAC,IAA8B;oCACvC,QAAQ,EAAE,EAAE,CAAC,QAAQ;iCACrB,CAAC,CAAC;6BACH;4BACD,YAAY,EAAE,MAAM,CAAC,aAA4C;4BACjE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;yBAC1D,CAAA;oBACF,CAAC;oBAAC,OAAO,QAAQ,EAAE,CAAC;wBACnB,IAAI,sBAAsB,CAAC,QAAQ,CAAC;4BAAE,MAAM,QAAQ,CAAA;wBACpD,0DAA0D;wBAC1D,yDAAyD;wBACzD,MAAM,IAAI,oBAAoB,CAAC;4BAC9B,IAAI,EAAE,QAAQ;4BACd,UAAU,EAAE,YAAY;4BACxB,MAAM,EAAE,6CAA6C;yBACrD,CAAC,CAAA;oBACH,CAAC;gBACF,CAAC;YACF,CAAC;QACF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC;gBAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,CAAA;YAC9E,IAAI,sBAAsB,CAAC,GAAG,CAAC;gBAAE,MAAM,GAAG,CAAA;YAC1C,MAAM,mBAAmB,CAAC,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;QACpE,CAAC;gBAAS,CAAC;YACV,MAAM,CAAC,WAAW,EAAE,CAAA;QACrB,CAAC;IACF,CAAC;IAED,KAAK,CAAC,UAAU;QACf,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,SAAS,EAAE;YACtD,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE;SAC1B,CAAC,CAAA;QAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;QAC7D,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAQlC,CAAA;QAED,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5B,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,aAAa,EAAE,CAAC,CAAC,cAAc;YAC/B,eAAe,EAAE,CAAC,CAAC,YAAY,EAAE,qBAAqB,IAAI,IAAI;YAC9D,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,CAAC,GAAG,SAAS;YACnE,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,UAAU,IAAI,GAAG,CAAC,GAAG,SAAS;YACxE,eAAe,EAAE,IAAI;YACrB,iBAAiB,EAAE,IAAI;SACvB,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,WAAW;QAChB,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,SAAS,EAAE;gBACtD,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE;gBAC1B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;aACjC,CAAC,CAAA;YACF,OAAO,QAAQ,CAAC,EAAE,CAAA;QACnB,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAA;QACb,CAAC;IACF,CAAC;CACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@namzu/openrouter",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@biomejs/biome": "^1.9.4",
|
|
42
42
|
"@types/node": "^22.19.17",
|
|
43
43
|
"typescript": "^5.5.0",
|
|
44
|
-
"vitest": "^2.
|
|
45
|
-
"@namzu/sdk": "^
|
|
44
|
+
"vitest": "^3.2.6",
|
|
45
|
+
"@namzu/sdk": "^5.0.0"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc --build",
|
|
@@ -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,44 @@
|
|
|
1
|
+
import type { ChatCompletionParams } from '@namzu/sdk'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { OpenRouterProvider } from '../client.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shared guard's own tests pass whether or not this driver calls it, so
|
|
8
|
+
* this drives the real chatStream. The defect being fixed was a declared
|
|
9
|
+
* parameter no code path consumed — asserting the helper works would have
|
|
10
|
+
* reproduced that shape one level up.
|
|
11
|
+
*
|
|
12
|
+
* The guard runs before any request, so the unreachable host below is never
|
|
13
|
+
* dialled on the throwing cases.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const provider = new OpenRouterProvider({ apiKey: 'test-key' } as never)
|
|
17
|
+
|
|
18
|
+
async function run(thinking?: ChatCompletionParams['thinking']): Promise<void> {
|
|
19
|
+
for await (const _chunk of provider.chatStream({
|
|
20
|
+
model: 'anthropic/claude-sonnet-4.5',
|
|
21
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
22
|
+
...(thinking ? { thinking } : {}),
|
|
23
|
+
} as ChatCompletionParams)) {
|
|
24
|
+
// drain
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('OpenRouterProvider refuses a thinking request rather than dropping it', () => {
|
|
29
|
+
it('throws on a manual thinking request', async () => {
|
|
30
|
+
await expect(run({ type: 'enabled', budgetTokens: 10_000 })).rejects.toThrow(
|
|
31
|
+
/OpenRouterProvider does not implement thinking/,
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('throws on an adaptive one', async () => {
|
|
36
|
+
await expect(run({ type: 'adaptive' })).rejects.toThrow(/does not implement thinking/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('does not throw for an explicit disable', async () => {
|
|
40
|
+
// Fails on the unreachable host instead, which is the point: the
|
|
41
|
+
// guard let it past.
|
|
42
|
+
await expect(run({ type: 'disabled' })).rejects.not.toThrow(/does not implement thinking/)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -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
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from '@namzu/sdk'
|
|
10
10
|
import {
|
|
11
11
|
ProviderRequestError,
|
|
12
|
+
assertThinkingUnsupported,
|
|
12
13
|
isCallerAbortError,
|
|
13
14
|
isProviderRequestError,
|
|
14
15
|
providerHttpError,
|
|
@@ -65,6 +66,8 @@ export const OPENROUTER_CAPABILITIES: ProviderCapabilities = {
|
|
|
65
66
|
supportsStreaming: true,
|
|
66
67
|
supportsFunctionCalling: true,
|
|
67
68
|
supportsVision: false,
|
|
69
|
+
// Images only. A document degrades to a named placeholder.
|
|
70
|
+
supportsDocuments: false,
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export class OpenRouterProvider implements LLMProvider {
|
|
@@ -159,6 +162,7 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
|
|
165
|
+
assertThinkingUnsupported('OpenRouterProvider', params)
|
|
162
166
|
const body = this.buildRequestBody(params, true)
|
|
163
167
|
|
|
164
168
|
const timeout = AbortSignal.timeout(this.config.timeout ?? 120_000)
|