@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/firebase/index.cjs.default.js +1 -0
  4. package/firebase/index.cjs.js +666 -0
  5. package/firebase/index.cjs.mjs +2 -0
  6. package/firebase/index.d.ts +1 -0
  7. package/firebase/index.esm.js +626 -0
  8. package/firebase/package.json +25 -0
  9. package/firebase/src/index.d.ts +1 -0
  10. package/firebase/src/lib/index.d.ts +4 -0
  11. package/firebase/src/lib/openrouter.api.d.ts +226 -0
  12. package/firebase/src/lib/openrouter.id.d.ts +56 -0
  13. package/firebase/src/lib/openrouter.model.d.ts +609 -0
  14. package/firebase/src/lib/openrouter.query.d.ts +121 -0
  15. package/firebase-server/index.cjs.default.js +1 -0
  16. package/firebase-server/index.cjs.js +4520 -0
  17. package/firebase-server/index.cjs.mjs +2 -0
  18. package/firebase-server/index.d.ts +1 -0
  19. package/firebase-server/index.esm.js +4466 -0
  20. package/firebase-server/package.json +38 -0
  21. package/firebase-server/src/index.d.ts +1 -0
  22. package/firebase-server/src/lib/index.d.ts +10 -0
  23. package/firebase-server/src/lib/openrouter.action.server.d.ts +196 -0
  24. package/firebase-server/src/lib/openrouter.broadcast.d.ts +93 -0
  25. package/firebase-server/src/lib/openrouter.call.inline.d.ts +57 -0
  26. package/firebase-server/src/lib/openrouter.file.attachment.d.ts +97 -0
  27. package/firebase-server/src/lib/openrouter.module.d.ts +65 -0
  28. package/firebase-server/src/lib/openrouter.prompt.service.d.ts +109 -0
  29. package/firebase-server/src/lib/openrouter.runtask.handle.d.ts +56 -0
  30. package/firebase-server/src/lib/openrouter.runtask.service.d.ts +380 -0
  31. package/firebase-server/src/lib/openrouter.runtask.sweep.d.ts +170 -0
  32. package/firebase-server/src/lib/openrouter.state.accessor.d.ts +106 -0
  33. package/firebase-server/src/test/openrouter.fake.d.ts +134 -0
  34. package/index.cjs.default.js +1 -0
  35. package/index.cjs.js +1867 -0
  36. package/index.cjs.mjs +2 -0
  37. package/index.d.ts +1 -0
  38. package/index.esm.js +1771 -0
  39. package/package.json +32 -0
  40. package/src/index.d.ts +1 -0
  41. package/src/lib/index.d.ts +10 -0
  42. package/src/lib/openrouter.call.d.ts +268 -0
  43. package/src/lib/openrouter.config.d.ts +314 -0
  44. package/src/lib/openrouter.embedding.d.ts +87 -0
  45. package/src/lib/openrouter.generation.d.ts +46 -0
  46. package/src/lib/openrouter.input.d.ts +238 -0
  47. package/src/lib/openrouter.prompt.d.ts +79 -0
  48. package/src/lib/openrouter.request.d.ts +91 -0
  49. package/src/lib/openrouter.sdk.d.ts +37 -0
  50. package/src/lib/openrouter.tool.d.ts +99 -0
  51. 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;