theorum 0.1.13 → 0.1.14

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/docs/kernel.md ADDED
@@ -0,0 +1,404 @@
1
+ # Kernel (`theorum/kernel`)
2
+
3
+ Type-first contracts for profiles, turns, tools, compaction, stop/resume, and
4
+ `runTurn`. Import here when a host needs the kernel surface without pulling
5
+ provider adapters.
6
+
7
+ ## Export
8
+
9
+ | Field | Value |
10
+ | --- | --- |
11
+ | Import | `theorum/kernel` / `jsr:@theorum/core/kernel` |
12
+ | Module | `src/kernel/mod.ts` |
13
+ | Also on | Root `theorum` / `mod.ts` re-exports the same runner and many helpers |
14
+
15
+ ## Ownership
16
+
17
+ | Scope | Path |
18
+ | --- | --- |
19
+ | Tree | `src/kernel/` (engine, registry, `stop.ts`, `types.ts`) |
20
+ | Re-exports | `theorum/streaming` re-exports stop helpers; source of truth stays here |
21
+
22
+ ## Profiles
23
+
24
+ Hosts declare agents with `defineProfile` / `registerProfile` (or
25
+ `registerProfiles`). `getProfile` / `hasProfile` / `listProfiles` / `clearProfiles`
26
+ manage the in-memory registry.
27
+
28
+ A `Profile` binds:
29
+
30
+ | Block | Role |
31
+ | --- | --- |
32
+ | `identity` | `handle`, optional `chat`, `system` / `systemByRole` |
33
+ | `model` | `protocol`, `provider`, `allow`, `config`, optional `select` / `thinking` / `controls` / `maxSteps` / `key` |
34
+ | `tools` | Allowlist ceiling (`allow: ToolId[]`) |
35
+ | `inputs` | Text / attachments / voice / slots / per-mime limits |
36
+ | `outputs` | Structured, image, speech, streaming, validation, `resume` |
37
+ | `guardrails` | Quota, canary, sanitize, redact, egress |
38
+
39
+ Multimodal ingress uses provider-neutral `InteractionPart` values;
40
+ `InteractionMediaPart.type` is `MediaInputKind` (`image` | `audio` | `video` |
41
+ `document`). MIME → kind mapping lives in `mediaKindForMime` (`catalog.ts`).
42
+
43
+ `model.protocol` is `'geminiInteractions' | 'openAi'`.
44
+ `model.provider` is `'google' | 'openrouter' | 'local'`.
45
+ Every id in `allow` must exist in `config`. Each `ModelSpec` carries wire ids
46
+ (`apiId`, optional `openRouterId`), `thinking` / `summaries` maps,
47
+ `thinkingLevels`, `maxOutputTokens`, `temperature`, `keyBuiltins`, optional
48
+ vault `key`, optional `compaction`.
49
+
50
+ Defaults applied at registration (`profiles.ts`):
51
+
52
+ | Field | Default when omitted |
53
+ | --- | --- |
54
+ | `model.maxSteps` | `1` |
55
+ | `guardrails.canary` | `true` |
56
+ | `guardrails.sanitizeInput` | profile-dependent |
57
+ | `guardrails.redactSensitive` | profile-dependent |
58
+
59
+ `projectProfile` / `resolveTurn` project a registered profile + `TurnRequest`
60
+ into a `ProjectedProfile` / `ResolvedGeneration` the runner and providers consume.
61
+
62
+ ## Turn lifecycle
63
+
64
+ `runTurn(request, provider, sink?)` is the single deterministic execution path
65
+ for one agent turn. Pipeline (see `engine/runner/mod.ts`):
66
+
67
+ 1. **Resolve** — `resolveTurn` picks model, thinking, tools, structured schema,
68
+ streaming flags, canary token.
69
+ 2. **Sanitize** — `sanitizeTurnRequest` strips injection/sensitive spans per
70
+ profile guardrails (unless disabled).
71
+ 3. **Compaction (before)** — when `timing: 'before'` and threshold fires, kernel
72
+ runs the compaction profile turn synchronously, then continues with trimmed
73
+ history.
74
+ 4. **Canary bind** — `bindCanary` embeds the per-turn canary in system text when
75
+ `guardrails.canary` is enabled.
76
+ 5. **Provider stream** — `provider.complete` yields partial events; runner may
77
+ gate thoughts/media per `outputs.streaming`.
78
+ 6. **Tool loop** — while under `maxSteps`, tool calls execute via `executeTool`
79
+ / `invokeFromUi`; results feed the next step.
80
+ 7. **Validation / repair** — structured output validators (`outputs.validation`)
81
+ may trigger repair turns with `input.repair`.
82
+ 8. **Egress** — `guardrails.egress.enforce` may block, refuse, or retry with
83
+ repair guidance before releasing user-visible text.
84
+ 9. **Trace** — optional sink receives a `TraceRecord`; failures are swallowed.
85
+ 10. **Terminal `done`** — one `done` event with tokens, optional `stop`,
86
+ optional `compaction` signal (`timing: 'after'`).
87
+
88
+ `continueFrom` on `TurnRequest` prepends `CONTINUE_INSTRUCTION` and carries
89
+ partial assistant text/artifact from a resumeable stop.
90
+
91
+ Optional `compactionProvider` on `TurnRequest` when the compactor profile uses a
92
+ different transport than the primary turn.
93
+
94
+ ## Stream events
95
+
96
+ `runTurn` and adapters yield `TurnEvent`:
97
+
98
+ | `type` | Payload highlights |
99
+ | --- | --- |
100
+ | `thought` | Model reasoning stream (may be gated) |
101
+ | `text` | User-visible assistant text |
102
+ | `tool` | Tool call envelope (`ok` / `error` / `pause`) |
103
+ | `structured` | Parsed JSON object when schema enforced |
104
+ | `media` | Generated image/audio bytes + mime |
105
+ | `grounding` | Search/maps grounding metadata |
106
+ | `evidence` | Provider-native evidence attachments |
107
+ | `tokens` | `input` / `output` / `total` usage (billing; may gate `meter: 'input'`) |
108
+ | `done` | Terminal: `stop`, `tokens`, `compaction`, final text pointer |
109
+ | `error` | Public-safe failure (`toErrorEvent`) |
110
+
111
+ `TurnHistoryMessage` preserves `role`, `content`, `parts`, `tool_calls`,
112
+ `tool_call_id`, and opaque `metadata` across turns.
113
+
114
+ ## Dynamic tools
115
+
116
+ Per-turn tools sit under profile `tools.allow`:
117
+
118
+ ```ts
119
+ TurnRequest: {
120
+ dynamicTools?: DynamicToolDeclaration[];
121
+ dynamicToolLoader?: DynamicToolLoader; // T2 expansion
122
+ }
123
+ ```
124
+
125
+ | Field | Role |
126
+ | --- | --- |
127
+ | `loadTier` | `T0` / `T1` / `T2` visibility strategy (host-owned) |
128
+ | `permissionTier` | `auto` / `session_consent` / `always_confirm` |
129
+ | `parameters` | JSON Schema fragment sent to the provider |
130
+
131
+ `executeTool` runs catalogued builtins/custom tools. Conflicts (e.g. Google maps vs
132
+ search) are enforced via catalog `conflictsWith`.
133
+
134
+ ## Outputs and guardrails
135
+
136
+ Profile `outputs` pins behavior the kernel enforces before adapters run:
137
+
138
+ | Pin | Effect |
139
+ | --- | --- |
140
+ | `structured` | Schema id or slot-mapped ids; `responseFormat` vs prompt enforcement |
141
+ | `image` | Aspect ratio, size, mime, grounding allowance, max input images |
142
+ | `speech` | TTS voice + `format` (`pcm` → WAV; `mp3` OpenAI-only) |
143
+ | `streaming` | `mode`, `streamThoughts`, `gateMedia` |
144
+ | `validation` | Field validators + `maxRetries` + `repairGuidance` |
145
+ | `resume` | `allowContinue` / `autoContinue` stop kinds |
146
+
147
+ Profile `guardrails`:
148
+
149
+ | Flag | Effect |
150
+ | --- | --- |
151
+ | `quota` | Host HTTP helper only (`theorum/guardrails`); not enforced inside `runTurn` |
152
+ | `canary` | Per-turn canary token; egress checks leakage |
153
+ | `sanitizeInput` / `redactSensitive` | Pre-provider text/blob scrub |
154
+ | `egress` | Host `enforce` hook; `onBlock`: `reject_to_agent` or `refuse_to_user` |
155
+
156
+ ## Compaction
157
+
158
+ Optional per-model policy on `ModelSpec.compaction`. Kernel owns trigger, split,
159
+ and timing; host owns persistence/reassembly unless `timing: 'before'` runs the
160
+ compactor inline.
161
+
162
+ ```ts
163
+ compaction: {
164
+ maxTokens: 2000,
165
+ compactAt: 0.75,
166
+ previousExchanges: 8,
167
+ profile: "my.compactor",
168
+ timing: "after",
169
+ meter: "history",
170
+ trigger: (ctx) =>
171
+ ctx.tokens > ctx.compactAt * ctx.maxTokens || hostRamPressure(),
172
+ }
173
+ ```
174
+
175
+ ### Meter
176
+
177
+ | Value | Counts |
178
+ | --- | --- |
179
+ | `history` (default) | `input.historyTokens` or local estimate of `history` only |
180
+ | `input` | Full prompt: `input.inputTokens` (before) or `tokens.input` (after) |
181
+
182
+ Provider `tokens` events always stream; they gate compaction only when
183
+ `meter: 'input'`.
184
+
185
+ ### History estimate (`meter: 'history'`)
186
+
187
+ 1. Host `historyTokens` wins when set.
188
+ 2. Else estimate from `input.history`:
189
+ - **Text** — tiktoken `o200k_base` (`HISTORY_TEXT_ENCODING`) over content,
190
+ text parts, tool-call arguments. Loads **lazily** on first text estimate.
191
+ - **Media** — stubs when size unknown (`HISTORY_MEDIA_TOKENS`: image/document
192
+ 258, audio 32, video 263).
193
+ - Current-turn attachments/voice are **not** history.
194
+
195
+ ### `previousExchanges`
196
+
197
+ | Value | Retain |
198
+ | --- | --- |
199
+ | `≥ 1` integer | That many recent user-started exchanges |
200
+ | `(0, 1)` fraction | Tail fitting in `fraction * maxTokens` (must be `< compactAt`) |
201
+ | `0` | Compact everything |
202
+
203
+ ### Compaction profile
204
+
205
+ A compaction profile is a normal registered profile. Minimal summarizer:
206
+
207
+ ```ts
208
+ registerProfile(defineProfile({
209
+ id: "my.compactor",
210
+ identity: {
211
+ handle: "Compactor",
212
+ system: "Summarize this conversation concisely. Preserve unresolved issues, "
213
+ + "decisions, and key facts.",
214
+ },
215
+ model: { /* allow + config */, maxSteps: 1 },
216
+ tools: { allow: [] },
217
+ inputs: { text: true },
218
+ outputs: { structured: "my.summary.schema" },
219
+ guardrails: { canary: false, sanitizeInput: false, redactSensitive: false },
220
+ }));
221
+ ```
222
+
223
+ ### After-turn signal
224
+
225
+ ```ts
226
+ for await (const event of runTurn(req, provider)) {
227
+ if (event.type === "done" && event.compaction?.needed) {
228
+ const { history, tokens, meter, promptTokens } = event.compaction;
229
+ // host runs compactor async, rewrites persisted history
230
+ }
231
+ }
232
+ ```
233
+
234
+ ### Compaction exports
235
+
236
+ | Export | Role |
237
+ | --- | --- |
238
+ | `CompactionSpec` / `CompactionMeter` / `CompactionTriggerContext` | Config types |
239
+ | `CompactionSignal` | `done.compaction` payload |
240
+ | `CompactionSplit` / `CompactionTokens` | Split + resolved counts |
241
+ | `resolveHistoryTokens` / `resolveCompactionTokens` | Meter resolution |
242
+ | `estimateHistoryTokens` | Local BPE + media stubs |
243
+ | `compactionNeeded` / `shouldCompact` | Threshold / custom trigger |
244
+ | `splitForCompaction` | `{ toCompact, toRetain }` |
245
+
246
+ Register-time validation: `maxTokens > 0`, `compactAt ∈ (0,1)`, integer
247
+ `previousExchanges ≥ 1`, fractional `< compactAt`, meter ∈ `{history,input}`,
248
+ compaction profile registered first.
249
+
250
+ ## Stop and resume
251
+
252
+ Providers map native finish reasons into `TurnStop` on terminal `done` events.
253
+
254
+ | `kind` | Meaning |
255
+ | --- | --- |
256
+ | `completed` | Normal completion |
257
+ | `length` | Output / budget cut off |
258
+ | `tool` | Model requested tool use |
259
+ | `filtered` | Content filter |
260
+ | `provider_error` | Upstream failure |
261
+ | `cancelled` | User / host abort |
262
+ | `stream_incomplete` | Stream ended without terminal reason |
263
+
264
+ Mappers: `turnStopFromOpenRouter`, `turnStopFromInteractionStatus`,
265
+ `turnStopFromClientStreamEnd` (host SSE drop).
266
+
267
+ ### Resume policy
268
+
269
+ ```ts
270
+ outputs: {
271
+ resume: {
272
+ allowContinue: ['length', 'stream_incomplete', 'provider_error'],
273
+ autoContinue: ['length', 'stream_incomplete'],
274
+ },
275
+ }
276
+ ```
277
+
278
+ | Constant / helper | Value / role |
279
+ | --- | --- |
280
+ | `DEFAULT_ALLOW_CONTINUE` | length, stream_incomplete, provider_error |
281
+ | `DEFAULT_AUTO_CONTINUE` | length, stream_incomplete |
282
+ | `AUTO_CONTINUE_DELAY_MS` | `1500` — suggested pause before one-shot auto-continue |
283
+ | `CONTINUE_INSTRUCTION` | Fixed continue system text (do not replace per app) |
284
+ | `isResumeableStop` | Profile `allowContinue` or default |
285
+ | `shouldAutoContinue` | One silent resume; never for `cancelled` |
286
+ | `isUserCancelledStop` | `kind === 'cancelled'` |
287
+
288
+ ### Continue turn
289
+
290
+ ```ts
291
+ for await (const event of runTurn({
292
+ profile: "my.agent",
293
+ input: { text: "" },
294
+ continueFrom: {
295
+ stop: previousDone.stop,
296
+ partialText: bufferedAssistantText,
297
+ },
298
+ }, provider)) { /* … */ }
299
+ ```
300
+
301
+ `GenerationStopError` / `isGenerationStopError` optional throw path for hosts
302
+ that prefer exceptions over stream `done.stop`.
303
+
304
+ ## Validation
305
+
306
+ Beyond compaction rules (above), `registerProfile` asserts:
307
+
308
+ - Each `tools.allow` / `model.allow` id resolves to catalog / config entries.
309
+ - Profiles with attachments or voice set `maxFiles`, `maxBytes`, `maxTurnBytes`.
310
+
311
+ Runtime structured validation uses `outputs.validation.fields` keyed by dotted
312
+ paths; failures can trigger repair turns via `input.repair`.
313
+
314
+ ## Exported API
315
+
316
+ Live barrel: `src/kernel/mod.ts`. Type surface: `export type *` from
317
+ `types.ts` (all public kernel types).
318
+
319
+ | Group | Symbols |
320
+ | --- | --- |
321
+ | Compaction | `CompactionSplit`, `CompactionTokens`, `compactionMeter`, `compactionNeeded`, `estimateHistoryTokens`, `HISTORY_MEDIA_TOKENS`, `HISTORY_TEXT_ENCODING`, `resolveCompactionTokens`, `resolveHistoryTokens`, `shouldCompact`, `splitForCompaction` |
322
+ | Runner | `runTurn` |
323
+ | Catalog | `CATALOG`, `clampThinkingLevel`, `clampThinkingLevelForApiId`, `mediaKindForMime`, `getTool`, `listBuiltinIds`, `mimeAllowed`, `mimeEssence`, `modelEntryByApiId`, `registerTools`, `requireModelSpec`, `resetTools` |
324
+ | Profiles | `ProfileDefinition`, `clearProfiles`, `defineProfile`, `getProfile`, `hasProfile`, `listProfiles`, `registerProfile`, `registerProfiles`, `projectProfile`, `resolveTurn` |
325
+ | Structured + tools | `getStructured`, `registerStructured`, `executeTool` |
326
+ | Stop / resume | `ProfileResumeSpec`, `TurnContinueFrom`, `TurnStop`, `TurnStopKind`, `AUTO_CONTINUE_DELAY_MS`, `CONTINUE_INSTRUCTION`, `DEFAULT_AUTO_CONTINUE`, `GenerationStopError`, `isGenerationStopError`, `isResumeableStop`, `isUserCancelledStop`, `shouldAutoContinue`, `turnStopFromClientStreamEnd`, `turnStopFromInteractionStatus`, `turnStopFromOpenRouter` |
327
+
328
+ ```theorum-evidence
329
+ {
330
+ "sections": {
331
+ "Export": {
332
+ "supports": [
333
+ { "kind": "source", "path": "src/kernel/mod.ts" },
334
+ { "kind": "config", "path": "package.json" }
335
+ ]
336
+ },
337
+ "Ownership": {
338
+ "supports": [
339
+ { "kind": "source", "path": "src/kernel/mod.ts" },
340
+ { "kind": "graph", "path": "docs/_map.mjs" }
341
+ ]
342
+ },
343
+ "Profiles": {
344
+ "supports": [
345
+ { "kind": "source", "path": "src/kernel/registry/profiles.ts" },
346
+ { "kind": "source", "path": "src/kernel/types.ts" },
347
+ { "kind": "contract_test", "path": "tests/kernel/profiles.test.ts" }
348
+ ]
349
+ },
350
+ "Turn lifecycle": {
351
+ "supports": [
352
+ { "kind": "source", "path": "src/kernel/engine/runner/mod.ts" },
353
+ { "kind": "source", "path": "src/kernel/registry/resolve.ts" },
354
+ { "kind": "contract_test", "path": "tests/kernel/theorum.test.ts" }
355
+ ]
356
+ },
357
+ "Stream events": {
358
+ "supports": [
359
+ { "kind": "source", "path": "src/kernel/types.ts" },
360
+ { "kind": "contract_test", "path": "tests/kernel/theorum.test.ts" }
361
+ ]
362
+ },
363
+ "Dynamic tools": {
364
+ "supports": [
365
+ { "kind": "source", "path": "src/kernel/registry/tools.ts" },
366
+ { "kind": "source", "path": "src/kernel/engine/runner/tools.ts" },
367
+ { "kind": "contract_test", "path": "tests/kernel/theorum.test.ts" }
368
+ ]
369
+ },
370
+ "Outputs and guardrails": {
371
+ "supports": [
372
+ { "kind": "source", "path": "src/kernel/engine/runner/gates.ts" },
373
+ { "kind": "contract_test", "path": "tests/kernel/theorum.test.ts" }
374
+ ]
375
+ },
376
+ "Compaction": {
377
+ "supports": [
378
+ { "kind": "source", "path": "src/kernel/engine/compaction.ts" },
379
+ { "kind": "source", "path": "src/kernel/engine/history-tokens.ts" },
380
+ { "kind": "contract_test", "path": "tests/kernel/compaction.test.ts" }
381
+ ]
382
+ },
383
+ "Stop and resume": {
384
+ "supports": [
385
+ { "kind": "source", "path": "src/kernel/stop.ts" },
386
+ { "kind": "contract_test", "path": "tests/streaming/turnStop.test.ts" }
387
+ ]
388
+ },
389
+ "Validation": {
390
+ "supports": [
391
+ { "kind": "source", "path": "src/kernel/registry/profiles.ts" },
392
+ { "kind": "source", "path": "src/kernel/engine/runner/schema-validation.ts" },
393
+ { "kind": "contract_test", "path": "tests/kernel/profiles.test.ts" }
394
+ ]
395
+ },
396
+ "Exported API": {
397
+ "supports": [
398
+ { "kind": "source", "path": "src/kernel/mod.ts" },
399
+ { "kind": "contract_test", "path": "tests/kernel/theorum.test.ts" }
400
+ ]
401
+ }
402
+ }
403
+ }
404
+ ```
@@ -0,0 +1,105 @@
1
+ # Observability (`theorum/observability`)
2
+
3
+ Trace sinks and record helpers. THEORUM does not own a database, does not read
4
+ trace-related environment variables for destinations, and never lets tracing
5
+ fail a turn.
6
+
7
+ ## Export
8
+
9
+ | Field | Value |
10
+ | --- | --- |
11
+ | Import | `theorum/observability` / `jsr:@theorum/core/observability` |
12
+ | Module | `src/observability/mod.ts` |
13
+
14
+ ## Ownership
15
+
16
+ | Path | Role |
17
+ | --- | --- |
18
+ | `src/observability/trace.ts` | Sink implementations + `writeTrace` |
19
+ | `src/observability/trace-record.ts` | `TraceRecord` shape |
20
+ | `src/observability/trace-usage.ts` | Token usage attachment |
21
+ | `src/observability/trace-attach.ts` | Attachment helpers |
22
+ | `src/observability/spans.ts` | Span redaction (`applySpans`) |
23
+
24
+ ## Trace sinks
25
+
26
+ Pass a `TraceSink` as the optional third argument to `runTurn`:
27
+
28
+ ```ts
29
+ for await (const event of runTurn(request, provider, jsonlSink(hostTraceDir))) {
30
+ // …
31
+ }
32
+ ```
33
+
34
+ | Sink | Behavior |
35
+ | --- | --- |
36
+ | `noopSink()` | Drop records (default when omitted) |
37
+ | `memorySink(into)` | Append `TraceRecord`s to a caller-owned array |
38
+ | `jsonlSink(dir)` | Daily rotating JSONL under a host-chosen directory |
39
+ | `sinkFromDir(dir)` | Resolve a directory sink helper |
40
+ | `resolveTraceDir(...)` | Path helper for hosts assembling a trace root |
41
+
42
+ `jsonlSink` writes `turns-YYYY-MM-DD.jsonl`, rotates around 32 MiB, and prunes
43
+ files older than 14 days. Directory creation is recursive.
44
+
45
+ `writeTrace(sink, recordPromise)` awaits the record and writes it; errors from
46
+ the sink are swallowed so observability cannot abort execution.
47
+
48
+ ## Trace records
49
+
50
+ `TraceRecord` captures turn identity, timing, model selection, token usage, and
51
+ related fields for host analytics. Built by `buildRecord` in the runner path and
52
+ consumed by sinks.
53
+
54
+ | Module | Role |
55
+ | --- | --- |
56
+ | `trace-record.ts` | Record type + builder inputs |
57
+ | `trace-usage.ts` | Attach provider token events |
58
+ | `trace-attach.ts` | Correlate attachments / metadata |
59
+
60
+ ## Exported API
61
+
62
+ | Export | Kind |
63
+ | --- | --- |
64
+ | `TraceSink` | type |
65
+ | `TraceRecord` | type |
66
+ | `jsonlSink`, `memorySink`, `noopSink` | function |
67
+ | `resolveTraceDir`, `sinkFromDir` | function |
68
+ | `writeTrace` | function |
69
+
70
+ ```theorum-evidence
71
+ {
72
+ "sections": {
73
+ "Export": {
74
+ "supports": [
75
+ { "kind": "source", "path": "src/observability/mod.ts" },
76
+ { "kind": "config", "path": "package.json" }
77
+ ]
78
+ },
79
+ "Ownership": {
80
+ "supports": [
81
+ { "kind": "source", "path": "src/observability/mod.ts" },
82
+ { "kind": "graph", "path": "docs/_map.mjs" }
83
+ ]
84
+ },
85
+ "Trace sinks": {
86
+ "supports": [
87
+ { "kind": "source", "path": "src/observability/trace.ts" },
88
+ { "kind": "contract_test", "path": "tests/observability/trace.test.ts" }
89
+ ]
90
+ },
91
+ "Trace records": {
92
+ "supports": [
93
+ { "kind": "source", "path": "src/observability/trace-record.ts" },
94
+ { "kind": "contract_test", "path": "tests/observability/trace.test.ts" }
95
+ ]
96
+ },
97
+ "Exported API": {
98
+ "supports": [
99
+ { "kind": "source", "path": "src/observability/mod.ts" },
100
+ { "kind": "contract_test", "path": "tests/observability/trace.test.ts" }
101
+ ]
102
+ }
103
+ }
104
+ }
105
+ ```
@@ -0,0 +1,125 @@
1
+ # OpenRouter (`theorum/openrouter`)
2
+
3
+ Direct OpenRouter provider adapter and OpenAI-compatible payload helpers.
4
+ Prefer `createProvider(profile, { openRouter })` from `theorum` /
5
+ `theorum/providers` for turn execution. Use this entry when building payloads
6
+ or constructing the adapter outside the runner.
7
+
8
+ ## Export
9
+
10
+ | Field | Value |
11
+ | --- | --- |
12
+ | Import | `theorum/openrouter` / `jsr:@theorum/core/openrouter` |
13
+ | Module | `src/providers/openrouter-mod.ts` |
14
+
15
+ ## Ownership
16
+
17
+ | Path | Role |
18
+ | --- | --- |
19
+ | `src/providers/openrouter.ts` | Chat adapter (`createOpenRouterProvider`) |
20
+ | `src/providers/openrouter-mod.ts` | Public barrel |
21
+ | `src/providers/openrouter-payload.ts` | Payload + model resolution |
22
+
23
+ Importing this entry loads `@openrouter/ai-sdk-provider` / `ai` **immediately**.
24
+ The lazy path through `createProvider` defers that until first `complete`.
25
+
26
+ ## Configuration
27
+
28
+ `OpenRouterConfig` (host-supplied):
29
+
30
+ | Field | Role |
31
+ | --- | --- |
32
+ | `apiKey` | Bearer credential |
33
+ | `baseUrl` | Optional API base override |
34
+ | `siteUrl` / `siteName` | Optional HTTP-Referer / X-Title style metadata |
35
+ | `fetch` | Optional custom fetch |
36
+ | `modelMap` | Optional map from THEORUM model id → OpenRouter model string |
37
+
38
+ ## Model resolution
39
+
40
+ `resolveOpenRouterModel(modelId, customMap?, wire?)` precedence:
41
+
42
+ | Step | Source |
43
+ | --- | --- |
44
+ | 1 | `customMap[modelId]` |
45
+ | 2 | `wire.openRouterId` |
46
+ | 3 | `wire.apiId` when it contains `/` |
47
+ | 4 | `google/${wire.apiId}` when `apiId` set |
48
+ | 5 | Pass-through model id string |
49
+
50
+ ## Payloads
51
+
52
+ `toOpenRouterPayload` maps `ProviderCompleteRequest` → OpenAI chat-completions body:
53
+
54
+ | Area | Mapped from |
55
+ | --- | --- |
56
+ | Messages | History + multimodal parts |
57
+ | Tools | Catalog builtins + dynamic tools |
58
+ | Structured output | `responseFormat` / JSON schema |
59
+ | Thinking | `reasoning.effort` |
60
+
61
+ ## Adapter behavior
62
+
63
+ | Concern | Behavior |
64
+ | --- | --- |
65
+ | Entry | `createOpenRouterProvider(config)` → `ModelProvider` |
66
+ | Stream | Yields normalized `TurnEvent`s |
67
+ | Stop | `done.stop` via `turnStopFromOpenRouter` |
68
+
69
+ ## Exported API
70
+
71
+ | Export | Kind |
72
+ | --- | --- |
73
+ | `createOpenRouterProvider` | function |
74
+ | `OpenRouterConfig` | type |
75
+ | `resolveOpenRouterModel` | function |
76
+ | `toOpenRouterPayload` | function |
77
+
78
+ ```theorum-evidence
79
+ {
80
+ "sections": {
81
+ "Export": {
82
+ "supports": [
83
+ { "kind": "source", "path": "src/providers/openrouter-mod.ts" },
84
+ { "kind": "config", "path": "package.json" }
85
+ ]
86
+ },
87
+ "Ownership": {
88
+ "supports": [
89
+ { "kind": "source", "path": "src/providers/openrouter.ts" },
90
+ { "kind": "graph", "path": "docs/_map.mjs" }
91
+ ]
92
+ },
93
+ "Configuration": {
94
+ "supports": [
95
+ { "kind": "source", "path": "src/providers/openrouter-payload.ts" },
96
+ { "kind": "contract_test", "path": "tests/providers/openrouter-payload.test.ts" }
97
+ ]
98
+ },
99
+ "Model resolution": {
100
+ "supports": [
101
+ { "kind": "source", "path": "src/providers/openrouter-payload.ts" },
102
+ { "kind": "contract_test", "path": "tests/providers/openrouter-payload.test.ts" }
103
+ ]
104
+ },
105
+ "Payloads": {
106
+ "supports": [
107
+ { "kind": "source", "path": "src/providers/openrouter-payload.ts" },
108
+ { "kind": "contract_test", "path": "tests/providers/openrouter-payload.test.ts" }
109
+ ]
110
+ },
111
+ "Adapter behavior": {
112
+ "supports": [
113
+ { "kind": "source", "path": "src/providers/openrouter.ts" },
114
+ { "kind": "contract_test", "path": "tests/providers/openrouter.test.ts" }
115
+ ]
116
+ },
117
+ "Exported API": {
118
+ "supports": [
119
+ { "kind": "source", "path": "src/providers/openrouter-mod.ts" },
120
+ { "kind": "contract_test", "path": "tests/providers/openrouter.test.ts" }
121
+ ]
122
+ }
123
+ }
124
+ }
125
+ ```