@dereekb/openrouter 13.37.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/LICENSE +21 -0
- package/README.md +195 -0
- package/firebase/index.cjs.default.js +1 -0
- package/firebase/index.cjs.js +666 -0
- package/firebase/index.cjs.mjs +2 -0
- package/firebase/index.d.ts +1 -0
- package/firebase/index.esm.js +626 -0
- package/firebase/package.json +25 -0
- package/firebase/src/index.d.ts +1 -0
- package/firebase/src/lib/index.d.ts +4 -0
- package/firebase/src/lib/openrouter.api.d.ts +226 -0
- package/firebase/src/lib/openrouter.id.d.ts +56 -0
- package/firebase/src/lib/openrouter.model.d.ts +609 -0
- package/firebase/src/lib/openrouter.query.d.ts +121 -0
- package/firebase-server/index.cjs.default.js +1 -0
- package/firebase-server/index.cjs.js +4520 -0
- package/firebase-server/index.cjs.mjs +2 -0
- package/firebase-server/index.d.ts +1 -0
- package/firebase-server/index.esm.js +4466 -0
- package/firebase-server/package.json +38 -0
- package/firebase-server/src/index.d.ts +1 -0
- package/firebase-server/src/lib/index.d.ts +10 -0
- package/firebase-server/src/lib/openrouter.action.server.d.ts +196 -0
- package/firebase-server/src/lib/openrouter.broadcast.d.ts +93 -0
- package/firebase-server/src/lib/openrouter.call.inline.d.ts +57 -0
- package/firebase-server/src/lib/openrouter.file.attachment.d.ts +97 -0
- package/firebase-server/src/lib/openrouter.module.d.ts +65 -0
- package/firebase-server/src/lib/openrouter.prompt.service.d.ts +109 -0
- package/firebase-server/src/lib/openrouter.runtask.handle.d.ts +56 -0
- package/firebase-server/src/lib/openrouter.runtask.service.d.ts +380 -0
- package/firebase-server/src/lib/openrouter.runtask.sweep.d.ts +170 -0
- package/firebase-server/src/lib/openrouter.state.accessor.d.ts +106 -0
- package/firebase-server/src/test/openrouter.fake.d.ts +134 -0
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +1867 -0
- package/index.cjs.mjs +2 -0
- package/index.d.ts +1 -0
- package/index.esm.js +1771 -0
- package/package.json +32 -0
- package/src/index.d.ts +1 -0
- package/src/lib/index.d.ts +10 -0
- package/src/lib/openrouter.call.d.ts +268 -0
- package/src/lib/openrouter.config.d.ts +314 -0
- package/src/lib/openrouter.embedding.d.ts +87 -0
- package/src/lib/openrouter.generation.d.ts +46 -0
- package/src/lib/openrouter.input.d.ts +238 -0
- package/src/lib/openrouter.prompt.d.ts +79 -0
- package/src/lib/openrouter.request.d.ts +91 -0
- package/src/lib/openrouter.sdk.d.ts +37 -0
- package/src/lib/openrouter.tool.d.ts +99 -0
- package/src/lib/openrouter.type.d.ts +125 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hapier Creative LLC.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# @dereekb/openrouter
|
|
2
|
+
|
|
3
|
+
Replacement for OpenAI's deprecated reusable Prompt Objects (`v1/prompts`, removed 2026-11-30), built
|
|
4
|
+
on OpenRouter.
|
|
5
|
+
|
|
6
|
+
OpenRouter is **stateless** — `store` is type-pinned to `false`, there is no `background: true`, no
|
|
7
|
+
server-side job store, and `previous_response_id` is rejected. Everything OpenAI used to do on their
|
|
8
|
+
side is therefore done here: prompt content and model config live in Firestore (git-adjacent and
|
|
9
|
+
MCP-editable rather than hidden in a dashboard), and asynchronous execution is an app-owned run-task
|
|
10
|
+
queue drained by a sweeper the app mounts on a schedule it already runs.
|
|
11
|
+
|
|
12
|
+
## Entry points
|
|
13
|
+
|
|
14
|
+
| Entry | Purpose |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `@dereekb/openrouter` | Config types, request builder, `callModel` wrapper, deferred-tool helpers, embeddings. Pure — no I/O. |
|
|
17
|
+
| `@dereekb/openrouter/firebase` | The `OpenRouterPrompt`, `OpenRouterPromptVersion` and `OpenRouterRunTask` models. |
|
|
18
|
+
| `@dereekb/openrouter/firebase-server` | Prompt service, run-task queue + sweep, Firestore `StateAccessor`, server actions. |
|
|
19
|
+
|
|
20
|
+
The raw OpenRouter client (`OpenRouterApi`) and the OTLP broadcast webhook live in
|
|
21
|
+
`@dereekb/nestjs/openrouter`; this package builds on them rather than duplicating them.
|
|
22
|
+
|
|
23
|
+
## Execution model
|
|
24
|
+
|
|
25
|
+
1. **Enqueue** — `enqueueRunTask({ key, promptKey, input })` writes one Firestore doc with
|
|
26
|
+
`s: QUEUED` and returns. One write, nothing blocks.
|
|
27
|
+
2. **Drain** — `openRouterRunTaskSweep(...)` claims a page of `QUEUED` tasks by lease in **`qat` order**,
|
|
28
|
+
runs them `maxParallelTasks` at a time, writes results, and stops claiming new pages once its
|
|
29
|
+
`maxRunTimeMs` budget is spent. Unclaimed work stays `QUEUED` for the next tick. Mount it on a
|
|
30
|
+
per-minute-ish schedule the app already runs.
|
|
31
|
+
3. **Consume** — `readRunTask(key)` → `COMPLETE` uses the output, `QUEUED`/`RUNNING` retries later,
|
|
32
|
+
`FAILED` takes the failure path.
|
|
33
|
+
4. **Expire** — `openRouterRunTaskExpirationSweep(...)` deletes every task queued more than
|
|
34
|
+
`OPENROUTER_RUN_TASK_MAX_AGE` (**7 days**) ago, **in any state, `RUNNING` included**. Its own, far
|
|
35
|
+
slower schedule — hourly is plenty. Nothing lives past the ceiling, and there is no per-task
|
|
36
|
+
expiration field to set or forget: a queued task runs essentially immediately, so `qat` *is* its age.
|
|
37
|
+
|
|
38
|
+
Retries are classified rather than uniform: a transient failure (429, 5xx, `ECONNRESET`, a Firestore
|
|
39
|
+
`UNAVAILABLE`) spends the `maxAttempts` budget, while a deterministic one (400, 401, 402, 403, 404, a
|
|
40
|
+
prompt that does not resolve) reaches `FAILED` on its first attempt instead of burning three sweep ticks
|
|
41
|
+
to reach the same answer. Anything unrecognized is treated as transient.
|
|
42
|
+
|
|
43
|
+
There is no replay. `enqueueRunTask({ key, …, restart: true })` re-runs a key, and `continueFrom` chains
|
|
44
|
+
one run onto another's history.
|
|
45
|
+
|
|
46
|
+
Short calls skip all of it: `callModelForPrompt(...)` runs inline and returns the result with no
|
|
47
|
+
document.
|
|
48
|
+
|
|
49
|
+
## Files and PDFs
|
|
50
|
+
|
|
51
|
+
There is no upload step. A run task stores the **GCS object path** (`fp`) and nothing else — never a
|
|
52
|
+
URL, never the bytes. The attachment is resolved fresh on **every attempt**, in one of two modes:
|
|
53
|
+
|
|
54
|
+
| Mode | What goes on the wire | When |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `signedUrl` | `file_url`, a short-lived signed URL OpenRouter dereferences itself | Default. Cheap, keeps the request small. |
|
|
57
|
+
| `inlineData` | `file_data: "data:<mime>;base64,…"` | The object is not reachable from the public internet. |
|
|
58
|
+
|
|
59
|
+
**The mode comes from the environment**, not from a flag an app has to remember to set twice: give
|
|
60
|
+
`openRouterRunTaskService` a `FirebaseServerEnvService` and `isTestingEnv` selects `inlineData`. That is
|
|
61
|
+
what makes files work against the **Firebase storage emulator**, where nothing is really signed and the
|
|
62
|
+
host is `localhost`, so a `file_url` OpenRouter tries to fetch resolves to nothing. `fileAttachmentMode`
|
|
63
|
+
overrides it explicitly, and `maxInlineFileSizeBytes` (default 256 KB) caps the read — inline bloats the
|
|
64
|
+
request ~33% and is re-paid on every attempt, unlike a URL.
|
|
65
|
+
|
|
66
|
+
`openRouterFileAttachmentResolver()` is the same factory, exposed for an inline (`callModelForPrompt`)
|
|
67
|
+
caller that needs attachments without going through the queue.
|
|
68
|
+
|
|
69
|
+
Neither payload is ever persisted. Resolving per attempt is only half the fix — the other half is that
|
|
70
|
+
the conversation written back through the `StateAccessor` has its `input_file` payloads **stripped**
|
|
71
|
+
(`openRouterMessagesWithoutFileAttachmentData`), keeping only `filename` as the rejoin key, and `load()`
|
|
72
|
+
re-points them at the current attempt's attachment
|
|
73
|
+
(`openRouterMessagesWithFreshFileAttachments`). Without that, a deferred resume hours later would replay
|
|
74
|
+
a URL that expired minutes after it was minted, or carry a second copy of the whole file in a Firestore
|
|
75
|
+
document with a 1 MiB ceiling.
|
|
76
|
+
|
|
77
|
+
PDF parsing pins `engine: 'native'` so the model provider parses on our BYOK key; the default silently
|
|
78
|
+
falls back to `mistral-ocr` (8-image cap, per-page billing) with no error.
|
|
79
|
+
|
|
80
|
+
A file whose parse is already cached on the run task (`fa`) is **not re-attached** on a retry — the
|
|
81
|
+
cached text is resubmitted instead. OpenRouter's documented `annotations` echo is emitted too, but it
|
|
82
|
+
does not currently survive the SDK: `@openrouter/sdk@1.2.26` validates the `/responses` body against a
|
|
83
|
+
closed union whose message variants have no `annotations` field, so the property is stripped before the
|
|
84
|
+
request leaves the process. Not sending the document is therefore the only thing that actually prevents
|
|
85
|
+
a re-parse today.
|
|
86
|
+
|
|
87
|
+
## Hosted tools (`file_search`, `web_search`, `mcp`)
|
|
88
|
+
|
|
89
|
+
Hosted (server-executed) tools go on the model config's `tools` array and are dispatched for real:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
config: {
|
|
93
|
+
model: 'openai/gpt-5.1',
|
|
94
|
+
tools: [openRouterFileSearchTool(['vs_…'], 5)],
|
|
95
|
+
include: ['file_search_call.results'],
|
|
96
|
+
provider: openRouterProviderPinnedTo('openai') // BYOK pinning + requireParameters
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**They do not go through `callModel`.** `@openrouter/sdk`'s `callModel` destructures `tools` off the
|
|
101
|
+
request and runs every entry through its client-function converter, which reads `tool.function.name` —
|
|
102
|
+
so a hosted entry is dropped outright when no client tools are present, and throws inside the SDK at
|
|
103
|
+
dispatch when they are. `callModelForOpenRouterRequest` therefore routes by the shape of the request:
|
|
104
|
+
|
|
105
|
+
| Run | Transport |
|
|
106
|
+
|---|---|
|
|
107
|
+
| No hosted tools | `callModel`, unchanged. |
|
|
108
|
+
| Hosted tools, no client tools and no `StateAccessor` | `sendOpenRouterResponsesRequest` — a direct, **non-streaming** `POST /responses`. |
|
|
109
|
+
| Hosted tools **plus** client tools or a `StateAccessor` | `ModelResult` assembled here, with the hosted entries appended **after** client-tool conversion. |
|
|
110
|
+
|
|
111
|
+
The two are **not** mutually exclusive: a run can search a vector store and drive the client-side tool
|
|
112
|
+
loop in the same call. Only the entry point differs — the merged path is a `callModel` request in
|
|
113
|
+
everything else, including the `x-openrouter-callmodel` header and the `stopWhen` step ceiling.
|
|
114
|
+
|
|
115
|
+
Going direct for the hosted-only case is not just about getting the tool onto the wire. The request is
|
|
116
|
+
non-streaming, so the returned `OpenResponsesResult` is OpenRouter's body verbatim rather than one
|
|
117
|
+
reassembled from stream events — which is what preserves a `file_search_call` output item and the chunks
|
|
118
|
+
`include: ['file_search_call.results']` asked for.
|
|
119
|
+
|
|
120
|
+
**OpenRouter's side is verified live.** A hosted `file_search` tool is forwarded upstream to OpenAI,
|
|
121
|
+
which resolves the store id and (for a nonexistent one) answers with its own
|
|
122
|
+
`Vector store with id [...] not found.` — an error only the upstream lookup can produce. The tool comes
|
|
123
|
+
back echoed on the response intact. See `openrouter.filesearch.spike.spec.ts`.
|
|
124
|
+
|
|
125
|
+
Two details settled by the same probes:
|
|
126
|
+
|
|
127
|
+
- The wire name is `vector_store_ids`; sending `vectorStoreIds` to the API is a flat `400`. The SDK
|
|
128
|
+
takes `vectorStoreIds` and remaps it, so `openRouterFileSearchTool()` is camelCase on purpose and
|
|
129
|
+
validation rejects the wire-cased spelling (which the SDK would silently drop, leaving a tool that
|
|
130
|
+
searches nothing and a model answering ungrounded).
|
|
131
|
+
- `provider.requireParameters: true` made no difference on a single-provider model, which is the one
|
|
132
|
+
case where it cannot. Keep it for any model with more than one provider — validation warns when a
|
|
133
|
+
hosted tool is configured without it.
|
|
134
|
+
|
|
135
|
+
**OpenRouter can never create or populate a vector store** — ingestion always goes direct to OpenAI.
|
|
136
|
+
|
|
137
|
+
## Auditing a run
|
|
138
|
+
|
|
139
|
+
A run task stores its own output; `gi (generationIds)` exists so a completed run can be looked up
|
|
140
|
+
afterwards. `openRouterGeneration({ client, id })` returns finish reason, cancellation, BYOK, latency and
|
|
141
|
+
the server-finalised token/cost breakdown, and `openRouterGenerationContent(...)` returns the stored
|
|
142
|
+
prompt / completion / reasoning / output.
|
|
143
|
+
|
|
144
|
+
Treat both as **audit surfaces, never the system of record**: what they return is tied to account logging
|
|
145
|
+
settings (nothing is retained under ZDR / logging-disabled), retention is undocumented, and they are
|
|
146
|
+
keyed per generation rather than per conversation.
|
|
147
|
+
|
|
148
|
+
## Live probes
|
|
149
|
+
|
|
150
|
+
Two blocks make real API calls, both skipped unless `OPENROUTER_API_KEY` is set:
|
|
151
|
+
|
|
152
|
+
- `openrouter.filesearch.spike.spec.ts` — the `file_search` passthrough probes. Deliberately cheap: a
|
|
153
|
+
free model by default, and the file_search probe fails at the store lookup before anything is billed.
|
|
154
|
+
- the `live end-to-end` block in `openrouter.runtask.emulator.spec.ts` — publishes a version, enqueues a
|
|
155
|
+
run, drains it with the real sweeper against the real API, then resolves the stored generation id
|
|
156
|
+
through `openRouterGeneration`. This is the plan's end-to-end bullet minus its MCP transport: no app in
|
|
157
|
+
this repo consumes the models yet, so the same server actions the callModel MCP surfaces are called
|
|
158
|
+
directly instead.
|
|
159
|
+
|
|
160
|
+
| Variable | Purpose |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `OPENROUTER_API_KEY` | Enables the live probes. |
|
|
163
|
+
| `OPENROUTER_TEST_MODEL_ID` | Model for the general probe and the end-to-end run. Defaults to `nvidia/nemotron-nano-9b-v2:free`. |
|
|
164
|
+
| `OPENROUTER_FILE_SEARCH_MODEL_ID` | Model for the file_search probe. Must be an OpenAI model. |
|
|
165
|
+
| `OPENROUTER_FILE_SEARCH_VECTOR_STORE_ID` | A real `vs_…`; upgrades the probe to the full grounded assertion. |
|
|
166
|
+
|
|
167
|
+
## CJS / ESM
|
|
168
|
+
|
|
169
|
+
`@openrouter/sdk` is ESM-only (`"type": "module"`) while this package also ships a CommonJS bundle, so
|
|
170
|
+
the CJS bundle `require()`s an ESM dependency. That works on Node 22.12+ and is verified on the
|
|
171
|
+
`nodejs24` runtime `firebase.json` deploys to: `require('dist/packages/openrouter/index.cjs.js')` loads
|
|
172
|
+
and resolves every export, `responsesSend` / `ModelResult` / `convertToolsToAPIFormat` included. Re-check
|
|
173
|
+
it if the Functions runtime is ever pinned lower — below 22.12 this is an `ERR_REQUIRE_ESM`, not a
|
|
174
|
+
warning.
|
|
175
|
+
|
|
176
|
+
## Firestore indexes
|
|
177
|
+
|
|
178
|
+
`firebase/firestore.indexes.json` is generated from the `@dbxModelFirebaseIndex`-tagged query factories
|
|
179
|
+
and is the set a consuming app must merge into its own indexes file:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
dbx-cli-generate-firestore-indexes --component packages/openrouter/firebase --output packages/openrouter/firebase/firestore.indexes.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Two** composites on `orrt`: `(s, qat)` for the drain sweep and `(s, lat)` for lease reclamation. The
|
|
186
|
+
retention query needs none at all — `qat <= cutoff` ordered by `qat` is a single-field range with a
|
|
187
|
+
matching order, which Firestore serves from its automatic single-field index.
|
|
188
|
+
|
|
189
|
+
Nothing on `orp`. The prompt query filters on `s` alone and adds no ordering, so pagination rides
|
|
190
|
+
Firestore's implicit `__name__` order — which for this model is the prompt's own readable key. Adding a
|
|
191
|
+
second filter axis (an `array-contains` on `t`, say) is what would buy the first composite here.
|
|
192
|
+
|
|
193
|
+
**The emulator does not enforce composite indexes**, so a green integration run proves nothing here —
|
|
194
|
+
`openrouter.query.spec.ts` asserts the generated file against the query factories instead, and pins the
|
|
195
|
+
count at exactly two so a re-added factory cannot quietly buy a third.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exports._default = require('./index.cjs.js').default;
|