@namzu/openrouter 2.2.0 → 2.2.1
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 +12 -0
- package/README.md +21 -268
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5394981: Make each driver's README an npm package page rather than its manual.
|
|
8
|
+
|
|
9
|
+
Every driver README carried its full reference — configuration tables, capability matrices, error surfaces — between 167 and 392 lines of it. That is a reasonable shape for a single-package repository, where the README _is_ the documentation, and the wrong one for a package in a monorepo that has a `docs/` tree: it duplicates what the docs say, and nothing checks that the two agree.
|
|
10
|
+
|
|
11
|
+
The README is now what a reader needs in the first minute — what the driver is, install, one working example, links. The reference moved to `docs/providers/<name>.md`, whole, and its code samples are now compiled against the built SDK by the doc-fence gate on every CI run. They never were before; several did not compile.
|
|
12
|
+
|
|
13
|
+
No API change.
|
|
14
|
+
|
|
3
15
|
## 2.2.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "@namzu/openrouter"
|
|
4
4
|
description: >-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
tool use, and answers the kernel's context-window question from the vendor's
|
|
8
|
-
own catalogue rather than from a hand-kept table.
|
|
5
|
+
Implements the kernel's `LLMProvider` interface over OpenRouter, which
|
|
6
|
+
fronts many vendors behind one key and one wire format.
|
|
9
7
|
tags: [readme, package, provider, openrouter]
|
|
10
8
|
timestamp: 2026-08-17T00:00:00Z
|
|
11
9
|
status: active
|
|
@@ -16,37 +14,22 @@ diataxis: reference
|
|
|
16
14
|
|
|
17
15
|
<h1>@namzu/openrouter</h1>
|
|
18
16
|
|
|
19
|
-
**The OpenRouter driver for [
|
|
17
|
+
**The OpenRouter model driver for [Namzu](https://github.com/cogitave/namzu).**
|
|
20
18
|
|
|
21
|
-
[](https://www.npmjs.com/package/@namzu/openrouter)
|
|
20
|
+
[](https://github.com/cogitave/namzu/actions/workflows/ci.yml)
|
|
21
|
+
[](https://github.com/cogitave/namzu/blob/main/LICENSE.md)
|
|
23
22
|
|
|
24
|
-
[Install](#install) · [
|
|
23
|
+
[Install](#install) · [Usage](#usage) · [Documentation](#documentation)
|
|
25
24
|
|
|
26
25
|
</div>
|
|
27
26
|
|
|
28
27
|
---
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
endpoint, and it sends exactly one request: `POST /chat/completions` with
|
|
35
|
-
`stream: true`. There is no non-streaming path, because the contract has no
|
|
36
|
-
non-streaming method — `chatStream` is the single model entry point, and a
|
|
37
|
-
caller who wants the whole answer collects the stream.
|
|
38
|
-
|
|
39
|
-
The transport is native `fetch` and nothing else. This package declares no
|
|
40
|
-
runtime dependencies at all: no vendor SDK, no HTTP client, no JSON tooling.
|
|
41
|
-
`@namzu/sdk` is a peer dependency (`>=1.3.0`), so your lockfile owns the kernel
|
|
42
|
-
version rather than this package.
|
|
43
|
-
|
|
44
|
-
What it buys over a driver bound to one vendor is the aggregation: OpenRouter
|
|
45
|
-
fronts hundreds of models from a dozen vendors behind one key and one bill, and
|
|
46
|
-
switching between them is a change to the `model` string rather than to which
|
|
47
|
-
package is installed. That also makes it the driver where a static model table
|
|
48
|
-
goes stale fastest, which is why nothing about the catalogue is hardcoded here
|
|
49
|
-
— see [Models and the window](#models-and-the-window).
|
|
29
|
+
Implements the kernel's `LLMProvider` interface over OpenRouter, which
|
|
30
|
+
fronts many vendors behind one key and one wire format. Installed only if
|
|
31
|
+
you use it — the kernel has no preferred vendor and no driver is a
|
|
32
|
+
dependency of it.
|
|
50
33
|
|
|
51
34
|
## Install
|
|
52
35
|
|
|
@@ -54,7 +37,9 @@ goes stale fastest, which is why nothing about the catalogue is hardcoded here
|
|
|
54
37
|
pnpm add @namzu/sdk @namzu/openrouter
|
|
55
38
|
```
|
|
56
39
|
|
|
57
|
-
|
|
40
|
+
`@namzu/sdk` is a peer dependency. Install both.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
58
43
|
|
|
59
44
|
```ts
|
|
60
45
|
import { ProviderRegistry } from '@namzu/sdk'
|
|
@@ -77,247 +62,15 @@ for await (const chunk of provider.chatStream({
|
|
|
77
62
|
}
|
|
78
63
|
```
|
|
79
64
|
|
|
80
|
-
`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
config is a compile error. Call it twice and it throws `DuplicateProviderError`;
|
|
84
|
-
pass `{ replace: true }` when you mean to take the slot over.
|
|
85
|
-
|
|
86
|
-
`create()` hands back the driver's capability declaration alongside it, which is
|
|
87
|
-
the same object as [`OPENROUTER_CAPABILITIES`](#capabilities):
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
const { provider, capabilities } = ProviderRegistry.create({
|
|
91
|
-
type: 'openrouter',
|
|
92
|
-
apiKey: process.env.OPENROUTER_API_KEY ?? '',
|
|
93
|
-
})
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
When you want the aggregated response rather than the deltas, collect the same
|
|
97
|
-
stream:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
import { collectChatCompletion } from '@namzu/sdk'
|
|
101
|
-
|
|
102
|
-
const response = await collectChatCompletion(
|
|
103
|
-
provider.chatStream({
|
|
104
|
-
model: 'anthropic/claude-opus-5',
|
|
105
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
106
|
-
}),
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
console.log(response.message.content)
|
|
110
|
-
console.log(response.usage.totalTokens)
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
The terminal agent constructs this driver for you: `@namzu/cli` lists
|
|
114
|
-
`openrouter` among the provider types it can build, reads `OPENROUTER_API_KEY`
|
|
115
|
-
from the environment, and defaults to `https://openrouter.ai/api/v1` and the
|
|
116
|
-
model `anthropic/claude-opus-5`.
|
|
117
|
-
|
|
118
|
-
`listModels`, `resolveContextWindow`, `probeCredential` and `healthCheck` are
|
|
119
|
-
**optional** members of `LLMProvider`, so a caller holding the interface has to
|
|
120
|
-
guard them. The snippets below hold the class instead, where they are ordinary
|
|
121
|
-
methods:
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
import { OpenRouterProvider } from '@namzu/openrouter'
|
|
125
|
-
|
|
126
|
-
const driver = new OpenRouterProvider({
|
|
127
|
-
apiKey: process.env.OPENROUTER_API_KEY ?? '',
|
|
128
|
-
})
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Models and the window
|
|
132
|
-
|
|
133
|
-
Model ids follow OpenRouter's `vendor/model-name` pattern —
|
|
134
|
-
`anthropic/claude-opus-5` — and the authority on which ones exist today is the
|
|
135
|
-
vendor, not this package. There is no hardcoded catalogue here to go stale.
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
await driver.listModels()
|
|
139
|
-
// id, name, contextWindow, maxOutputTokens, inputPrice, outputPrice,
|
|
140
|
-
// supportsToolUse, supportsStreaming
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`listModels()` returns exactly what `GET /models` sent, with no fallback list
|
|
144
|
-
behind it: `contextWindow` is the catalogue's `context_length`,
|
|
145
|
-
`maxOutputTokens` is `top_provider.max_completion_tokens` where the vendor
|
|
146
|
-
states one and `4096` where it does not, and `inputPrice` / `outputPrice` are
|
|
147
|
-
the catalogue's per-token prices scaled to per-million. If the listing call
|
|
148
|
-
fails, it throws rather than substituting a menu.
|
|
149
|
-
|
|
150
|
-
`resolveContextWindow(model)` answers the kernel's own question — how large is
|
|
151
|
-
this model's context — from that same listing. It matters because the fallback
|
|
152
|
-
below it is a hand-maintained prefix table, and this is the driver a hand-kept
|
|
153
|
-
table is least able to keep up with. The kernel's precedence is: a window you
|
|
154
|
-
configured, then the driver's answer, then the table, then the default.
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
await driver.resolveContextWindow('anthropic/claude-opus-5')
|
|
158
|
-
// the vendor's own context_length, or undefined
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
`undefined` for a model the listing does not contain, deliberately, rather than
|
|
162
|
-
a guess: "I asked and it is not there" leaves the table exactly as authoritative
|
|
163
|
-
as it was, while a substituted number would present a guess as a vendor answer.
|
|
164
|
-
The listing is resolved once per process, because several hundred models is a
|
|
165
|
-
real payload and a window does not change under a running run. A **failure** is
|
|
166
|
-
not cached — the next run asks again rather than inheriting one bad minute
|
|
167
|
-
forever.
|
|
168
|
-
|
|
169
|
-
## Credentials and attribution
|
|
170
|
-
|
|
171
|
-
`apiKey` is required and the constructor throws when it is empty. This driver
|
|
172
|
-
does not read the key from the environment itself — pass it, from wherever your
|
|
173
|
-
host keeps secrets. (`@namzu/cli` is the thing in this repository that reads
|
|
174
|
-
`OPENROUTER_API_KEY`; the library leaves that choice to you.)
|
|
175
|
-
|
|
176
|
-
`siteUrl` and `siteName` are optional and are sent verbatim as the
|
|
177
|
-
`HTTP-Referer` and `X-Title` headers, which is how OpenRouter attributes traffic
|
|
178
|
-
to an application. Omit them and the headers are not sent at all.
|
|
179
|
-
|
|
180
|
-
Every request also carries one `User-Agent` from the kernel's
|
|
181
|
-
`attributionHeaders()` — `namzu/<version> (+https://github.com/cogitave/namzu)`
|
|
182
|
-
— so a provider reading its own logs can tell this kernel's traffic apart from a
|
|
183
|
-
browser's. It is one header and only one, by design.
|
|
184
|
-
|
|
185
|
-
`probeCredential()` asks whether the key works, and it asks the endpoint that
|
|
186
|
-
can answer:
|
|
187
|
-
|
|
188
|
-
```ts
|
|
189
|
-
await driver.probeCredential() // resolves if the key is accepted, throws if not
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
It calls `GET /key`, which requires the credential. The obvious alternative was
|
|
193
|
-
measured and does not work: `/models` does not authenticate, so **any** string
|
|
194
|
-
whatsoever — a typo, the wrong clipboard entry, a revoked key — came back with
|
|
195
|
-
the full catalogue and was reported as verified. Nothing was wrong with the
|
|
196
|
-
menu; a menu was simply never evidence about a key. The thrown error carries the
|
|
197
|
-
HTTP `status`, so a caller can separate a genuine `401` from a timeout that
|
|
198
|
-
taught it nothing.
|
|
199
|
-
|
|
200
|
-
## Configuration
|
|
201
|
-
|
|
202
|
-
| Option | Default | Notes |
|
|
203
|
-
|---|---|---|
|
|
204
|
-
| `apiKey` | — | required; the constructor throws on an empty string |
|
|
205
|
-
| `baseUrl` | `OPENROUTER_BASE_URL`, else `https://openrouter.ai/api/v1` | any OpenAI-compatible endpoint |
|
|
206
|
-
| `siteUrl` | — | sent as `HTTP-Referer` |
|
|
207
|
-
| `siteName` | — | sent as `X-Title` |
|
|
208
|
-
| `timeout` | `120_000` | per-request timeout in ms |
|
|
209
|
-
|
|
210
|
-
That is the whole of `OpenRouterConfig`. `OpenRouterProviderConfig` is the same
|
|
211
|
-
shape plus the `type: 'openrouter'` discriminator the registry narrows on.
|
|
212
|
-
|
|
213
|
-
Two details about the defaults are worth knowing before you debug them. The
|
|
214
|
-
`OPENROUTER_BASE_URL` fallback is read **once, at module load**, into a module
|
|
215
|
-
constant — setting it after this package has been imported changes nothing, so
|
|
216
|
-
set it before the process starts or pass `baseUrl` explicitly. And `timeout`
|
|
217
|
-
does not replace the caller's cancellation: it is composed with
|
|
218
|
-
`params.signal`, so whichever fires first tears the request down and a Stop
|
|
219
|
-
still stops the turn mid-stream.
|
|
220
|
-
|
|
221
|
-
Because the wire is the OpenAI Chat Completions dialect, pointing `baseUrl` at
|
|
222
|
-
another server that speaks it works. If that is the whole reason you are here —
|
|
223
|
-
a self-hosted inference server rather than OpenRouter — `@namzu/http` is the
|
|
224
|
-
driver built for it and ships today.
|
|
225
|
-
|
|
226
|
-
## Capabilities
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
import { OPENROUTER_CAPABILITIES } from '@namzu/openrouter'
|
|
230
|
-
|
|
231
|
-
// {
|
|
232
|
-
// supportsTools: true,
|
|
233
|
-
// supportsStreaming: true,
|
|
234
|
-
// supportsFunctionCalling: true,
|
|
235
|
-
// supportsVision: false,
|
|
236
|
-
// supportsDocuments: false,
|
|
237
|
-
// }
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
These describe what this **driver** does, not what OpenRouter could do, and the
|
|
241
|
-
runtime reads them before the request is built — it warns, or fails under
|
|
242
|
-
`strictCapabilities`, rather than letting content vanish quietly. So the two
|
|
243
|
-
`false` entries are load-bearing: this driver's message translation maps role
|
|
244
|
-
and content, plus the tool-call fields on assistant and tool turns, and nothing
|
|
245
|
-
else. A user message's image or document `attachments` are not mapped and never
|
|
246
|
-
reach the model.
|
|
247
|
-
|
|
248
|
-
Tool schemas pass through as the kernel built them, along with `toolChoice` and
|
|
249
|
-
`parallelToolCalls`. `enforceToolInputSchema` is a kernel-internal hint rather
|
|
250
|
-
than a wire field, and it is deliberately dropped instead of being serialised
|
|
251
|
-
into the request body.
|
|
252
|
-
|
|
253
|
-
The rest of `ChatCompletionParams` maps straight onto the OpenAI-dialect names:
|
|
254
|
-
`temperature`, `maxTokens` → `max_tokens`, `topP`, `topK`, `frequencyPenalty`,
|
|
255
|
-
`presencePenalty`, `repetitionPenalty`, `stop`, `responseFormat` →
|
|
256
|
-
`response_format`, and `cacheControl` → `cache_control`. A field you do not set
|
|
257
|
-
is not sent.
|
|
258
|
-
|
|
259
|
-
Token usage comes back on the chunk that carries it, and the cache counts are
|
|
260
|
-
read from whichever shape the upstream model used: `cachedTokens` from
|
|
261
|
-
`prompt_tokens_details.cached_tokens` or `cache_read_input_tokens`, and
|
|
262
|
-
`cacheWriteTokens` from `cache_creation_input_tokens`. Both are `0` when the
|
|
263
|
-
vendor reports neither, which is the honest answer rather than an absent one.
|
|
264
|
-
|
|
265
|
-
Extended thinking and `effort` are not implemented here, and they are
|
|
266
|
-
**refused** rather than dropped — setting `thinking: { type: 'enabled' }`,
|
|
267
|
-
`thinking: { type: 'adaptive' }` or any `effort` throws before the request is
|
|
268
|
-
built. `thinking: { type: 'disabled' }` passes, because asking for nothing is
|
|
269
|
-
something this driver can honour. A silently ignored `effort` would return an
|
|
270
|
-
ordinary completion indistinguishable from the model's default, including on the
|
|
271
|
-
bill.
|
|
272
|
-
|
|
273
|
-
## Health
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
await driver.healthCheck() // boolean
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
One `GET /models` with a five-second timeout, reduced to a single bit, and it
|
|
280
|
-
never throws — an unreachable service returns `false` rather than raising. Read
|
|
281
|
-
it as "is this endpoint answering", not as "is my key good": that endpoint does
|
|
282
|
-
not authenticate. [`probeCredential()`](#credentials-and-attribution) is the one
|
|
283
|
-
that answers the credential question.
|
|
284
|
-
|
|
285
|
-
## Errors
|
|
286
|
-
|
|
287
|
-
Every failure this driver raises is a `ProviderRequestError` from `@namzu/sdk`,
|
|
288
|
-
classified into the kernel's own kinds — `throttle`, `network`, `auth`,
|
|
289
|
-
`context_overflow`, `bad_request`, `server` — so a caller can decide whether to
|
|
290
|
-
retry, compact or give up without parsing a vendor payload.
|
|
291
|
-
|
|
292
|
-
| What happened | `kind` |
|
|
293
|
-
|---|---|
|
|
294
|
-
| `401` / `403` | `auth` |
|
|
295
|
-
| `429` | `throttle`, with `retryAfterMs` parsed from the `retry-after` header |
|
|
296
|
-
| `5xx` | `server` |
|
|
297
|
-
| `4xx` whose body says the prompt is too long | `context_overflow` |
|
|
298
|
-
| any other `4xx` | `bad_request` |
|
|
299
|
-
| `fetch` itself rejected | `network`, unless the rejection names its own kind |
|
|
300
|
-
|
|
301
|
-
The error body is read to classify and is never interpolated into the thrown
|
|
302
|
-
message. That is not hygiene, it is a fix: the body used to be pasted into the
|
|
303
|
-
message verbatim, which is how a credential the upstream echoed back reached
|
|
304
|
-
every log that recorded the failure — proven with a planted fake token. What
|
|
305
|
-
survives is the vendor's own `error.message`, truncated and with
|
|
306
|
-
credential-shaped substrings replaced by a `[REDACTED:…]` marker, on the error's
|
|
307
|
-
`detail` field. The vendor error is not attached as `cause` either.
|
|
65
|
+
`chatStream` is the only model entry point; a non-streaming call is that
|
|
66
|
+
stream collected. In practice the kernel's run loop calls it and hands you
|
|
67
|
+
events.
|
|
308
68
|
|
|
309
|
-
|
|
310
|
-
inside a stream frame. Those are read and raised too, because ignoring them
|
|
311
|
-
makes a failed stream look like a clean end of turn. A frame that will not parse
|
|
312
|
-
raises as `server` without keeping the frame, since a JSON parse error quotes
|
|
313
|
-
the source text it choked on.
|
|
69
|
+
## Documentation
|
|
314
70
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
caller's own abort reason that surfaces — not a `network` classification
|
|
318
|
-
manufactured from it.
|
|
71
|
+
- [The OpenRouter driver — models, the window, credentials and attribution](https://github.com/cogitave/namzu/blob/main/docs/providers/openrouter.md)
|
|
72
|
+
- [Namzu docs](https://github.com/cogitave/namzu/tree/main/docs)
|
|
319
73
|
|
|
320
74
|
## License
|
|
321
75
|
|
|
322
|
-
FSL-1.1-MIT, converting to MIT two years after each release.
|
|
323
|
-
`@namzu/sdk`.
|
|
76
|
+
FSL-1.1-MIT, converting to MIT two years after each release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@namzu/openrouter",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@types/node": "^22.19.17",
|
|
53
53
|
"typescript": "^5.5.0",
|
|
54
54
|
"vitest": "^3.2.6",
|
|
55
|
-
"@namzu/sdk": "^
|
|
55
|
+
"@namzu/sdk": "^30.0.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsc --build",
|