@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +21 -268
  3. 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
- The OpenRouter driver for @namzu/sdk. Speaks OpenRouter's OpenAI-compatible
6
- Chat Completions wire over native fetch with no runtime dependencies, maps
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 [`@namzu/sdk`](https://www.npmjs.com/package/@namzu/sdk).**
17
+ **The OpenRouter model driver for [Namzu](https://github.com/cogitave/namzu).**
20
18
 
21
- [![License: FSL-1.1-MIT](https://img.shields.io/badge/license-FSL--1.1--MIT-blue.svg)](https://github.com/cogitave/namzu/blob/main/LICENSE.md)
22
- [![npm](https://img.shields.io/npm/v/@namzu/openrouter.svg?label=%40namzu%2Fopenrouter)](https://www.npmjs.com/package/@namzu/openrouter)
19
+ [![npm](https://img.shields.io/npm/v/@namzu/openrouter.svg)](https://www.npmjs.com/package/@namzu/openrouter)
20
+ [![build](https://github.com/cogitave/namzu/actions/workflows/ci.yml/badge.svg)](https://github.com/cogitave/namzu/actions/workflows/ci.yml)
21
+ [![license](https://img.shields.io/badge/license-FSL--1.1--MIT-blue.svg)](https://github.com/cogitave/namzu/blob/main/LICENSE.md)
23
22
 
24
- [Install](#install) · [Use it](#use-it) · [Models](#models-and-the-window) · [Credentials](#credentials-and-attribution) · [Config](#configuration) · [Capabilities](#capabilities) · [Health](#health) · [Errors](#errors)
23
+ [Install](#install) · [Usage](#usage) · [Documentation](#documentation)
25
24
 
26
25
  </div>
27
26
 
28
27
  ---
29
28
 
30
- ## What this is
31
-
32
- One driver, one wire. `OpenRouterProvider` implements the kernel's
33
- `LLMProvider` contract against OpenRouter's OpenAI-compatible Chat Completions
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
- ## Use it
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
- `registerOpenRouter()` also carries the module augmentation that adds
81
- `'openrouter'` to the kernel's config union, so `ProviderRegistry.create({ type:
82
- 'openrouter', … })` narrows to `OpenRouterProviderConfig` and a typo in the
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
- OpenRouter also reports some failures **after** a `200`, as an `error` field
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
- `params.signal` is composed with the request timeout and re-checked between
316
- reads, so a Stop tears the in-flight response body down mid-turn, and it is the
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. Same as
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.0",
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": "^28.0.0"
55
+ "@namzu/sdk": "^30.0.1"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsc --build",