@dereekb/openrouter 14.7.0 → 14.9.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/README.md CHANGED
@@ -13,7 +13,8 @@ queue drained by a sweeper the app mounts on a schedule it already runs.
13
13
 
14
14
  | Entry | Purpose |
15
15
  |---|---|
16
- | `@dereekb/openrouter` | Config types, request builder, `callModel` wrapper, deferred-tool helpers, embeddings. Pure — no I/O. |
16
+ | `@dereekb/openrouter` | Config types, request builder, `callModel` wrapper, deferred-tool helpers, embeddings, **decisions**. Pure — no I/O. |
17
+ | `@dereekb/openrouter/decision` | The decision layer WITHOUT `@openrouter/sdk`: questions, validation, the request body, the answer reader, model ids. See [below](#loading-without-the-sdk). |
17
18
  | `@dereekb/openrouter/firebase` | The `OpenRouterPrompt`, `OpenRouterPromptVersion` and `OpenRouterRunTask` models. |
18
19
  | `@dereekb/openrouter/firebase-server` | Prompt service, run-task queue + sweep, Firestore `StateAccessor`, server actions. |
19
20
 
@@ -46,6 +47,162 @@ one run onto another's history.
46
47
  Short calls skip all of it: `callModelForPrompt(...)` runs inline and returns the result with no
47
48
  document.
48
49
 
50
+ ## Decisions (System One / Jev)
51
+
52
+ OpenRouter has **two** inference surfaces, and this package serves both. `/responses` asks a model for
53
+ prose and validates the reply back into a shape. **System One** (`POST /systemone`, the `typesafe/jev-*`
54
+ models) inverts that: the caller declares the answer space up front as typed questions, and the model
55
+ returns a position inside it plus a calibrated distribution. `(state, questions) → answers`. There is no
56
+ reply to parse, so there is no off-shape reply to recover from.
57
+
58
+ Three primitives, and no others:
59
+
60
+ | Primitive | Asks | Answers |
61
+ |---|---|---|
62
+ | `openRouterChoiceQuestion(instructions, options)` | which of these options? | `choice`, guaranteed one of the declared options; `probabilities` over every option summing to 1 (a free full ranking); `confidence` |
63
+ | `openRouterScoreQuestion(instructions, levels)` | which level on this rubric? | a `score` that may land BETWEEN two levels, the distribution, `confidence` |
64
+ | `openRouterNoulQuestion(instructions, means?)` | is this true? | `noul` 0..1 — the probability IS the uncertainty, so there is no separate confidence |
65
+
66
+ ```ts
67
+ const questions = {
68
+ team: openRouterChoiceQuestion('Which team should own `ticket`?', {
69
+ billing: { what: 'Charges, invoices, refunds', not_for: 'Anything about signing in' },
70
+ access: { what: 'Sign-in, passwords, permissions', not_for: 'Anything about money' },
71
+ other: { what: 'Anything the other options do not cover' }
72
+ }),
73
+ spam: openRouterNoulQuestion('`ticket` is unsolicited marketing rather than a real request.')
74
+ };
75
+
76
+ const result = await decideForPrompt({ client, promptService, promptKey: 'demo-support-triage', state: { ticket } });
77
+ result.answers.team.choice; // typed to the declared option names
78
+ result.answers.spam.noul; // 0..1
79
+ ```
80
+
81
+ **Declare every question one state could need in ONE call.** Each is evaluated independently against the
82
+ same state, so the question map is both the batching unit and the cost unit: the state is sent and billed
83
+ once, and a speculative question the caller may discard costs only its own tokens.
84
+
85
+ **Membership is the transport's guarantee.** `readOpenRouterDecisionAnswers` checks every answer against
86
+ the question that asked it, so no consumer re-checks: past that point a `choice` is one of the declared
87
+ options, a `score` is inside the declared range, a `noul` is a probability. An answer that left its space
88
+ raises `OpenRouterDecisionAnswerFaultError` — a defect in the RESPONSE, never a judgement the model made.
89
+ "None of these fits" is said through a Noul the caller declared for it, and lands as an *answer*.
90
+
91
+ A Choice is only ever RELATIVE — its probabilities are normalised over the options supplied, so something
92
+ always wins even when nothing fits. When "nothing fits" is an outcome you act on, pair it with a Noul,
93
+ which is absolute and may be low for every option.
94
+
95
+ ### Writing a question
96
+
97
+ - **One snap judgement per question.** A judgement weighing several factors is several questions combined
98
+ in your own code.
99
+ - **Write the COMPLETE question in `instructions`** — the question id is never sent, so a descriptive key
100
+ is no substitute. A blank one is refused at declaration.
101
+ - **Point at named state with backticked dot-paths** — `` `phrase` ``, `` `ticket.sender.email` ``,
102
+ `` `messages[0].text` ``. Prefer an object state so each part has a name to point at.
103
+ `openRouterDecisionStatePaths` reads them back so a spec can pin that a declaration names keys its
104
+ state has (documented and inspectable, never enforced — backticks also quote literals).
105
+ - **Score levels are SITUATIONS, not degrees.** "Broken feature, but a workaround exists" gives the model
106
+ something to match against; "moderately severe" does not. Every level is evaluated separately and the
107
+ model never sees a level's number or its neighbours, so numbers in the descriptions do not help.
108
+ - **Choice options are CONTRASTIVE** — the same keys on every option (`{what, not_for, examples}`) so the
109
+ model compares like with like. Include an explicit `other` when the set may not cover the input.
110
+ - **Filter in code first.** Accuracy falls as a state grows with material unrelated to the decision; a
111
+ wide state is not a free hedge.
112
+
113
+ Every declaration surface takes `string | object | array` and the wire carries it VERBATIM — start with
114
+ strings, and reach for structure only for guidance prose would blur or for data that is already JSON.
115
+
116
+ ### Limits, enforced at declaration
117
+
118
+ | Limit | Value | Past it |
119
+ |---|---|---|
120
+ | Choice options | ≤ 255 | Narrow in two stages. Never truncate — an option removed is one the model can never pick, and nothing reports it was missing. |
121
+ | Score levels | 2 .. 10 | MERGE the levels that cannot be told apart. The trap: a 0..8 band is nine levels and legal, a 0..10 scale is eleven and 400s at the wire. |
122
+ | Context | 64k tokens per request | Filter in code first. |
123
+ | Price | $0.042 / Mtok input; output reported but **not billed** | `usage.cost` is synchronous and final. |
124
+
125
+ `validateOpenRouterDecisionQuestions` fails at the DECLARATION, naming the question — not as a 4xx about
126
+ a request body — and runs at version create / update / seed, so a malformed question map is refused when
127
+ it is written rather than every time it is asked.
128
+
129
+ ### Routing: the model id is the discriminator
130
+
131
+ System One models are **not listed by `GET /models`**, so nothing can be learned about one from the
132
+ catalog, and a wrong guess does not produce an error anyone can read. The slug is therefore checked on
133
+ **both** arms and neither can be entered with the other's model:
134
+
135
+ - `validateOpenRouterModelConfig(config)` errors on a `typesafe/…` slug, naming `openRouterDecision`.
136
+ It already runs at publish time, so a Jev slug typed into a stored version is refused when written.
137
+ - `validateOpenRouterModelConfig(config, { decision: true })` errors on a chat slug.
138
+ - `openRouterResponsesRequestBody` throws `OpenRouterSystemOneModelOnCompletionArmError`. It is the one
139
+ point every completion dispatch path builds its body, so no route can be added later that skips it.
140
+
141
+ `OPENROUTER_JEV_1_13_MODEL_ID` is a **versioned** slug, deliberately not the moving `jev-latest` alias:
142
+ an answer is only reproducible against the model that gave it, which is the same reason versions exist.
143
+ The reply reports the model that actually served it (`typesafe/jev-1.13-20260917`), so read `result.model`
144
+ rather than assuming the slug you asked for.
145
+
146
+ ### Loading without the SDK
147
+
148
+ The root entry re-exports SDK values (`callModel`, `responsesSend`, `systemOneCreate`, …), so importing
149
+ ANY value from `@dereekb/openrouter` evaluates `@openrouter/sdk` — a real cold-start cost, and one a test
150
+ runner that isolates each spec file pays once per file. `@dereekb/openrouter/decision` carries everything a
151
+ decision is built from and read with, and loads nothing under `@openrouter/sdk`:
152
+
153
+ - `openRouterChoiceQuestion` / `openRouterScoreQuestion` / `openRouterNoulQuestion`, the answer and
154
+ confidence helpers, and `validateOpenRouterDecisionQuestions`;
155
+ - `openRouterDecisionRequest`, `openRouterDecisionRequestBody` (the `/systemone` body, wire mapping
156
+ included), `readOpenRouterDecisionAnswers` and its `OpenRouterDecisionAnswerFaultError`,
157
+ `openRouterRunUsageFromDecisionsUsage`, `validateOpenRouterDecisionRequest`;
158
+ - the model config and the model ids, `isOpenRouterSystemOneModelId` among them.
159
+
160
+ What it does NOT carry is `openRouterDecision` — the transport, and the one `systemOneCreate` caller. A
161
+ consumer that owns its transport (its own timeout, retry policy or error shape) builds the body here, sends
162
+ it through its own lazily-loaded client, and reads the reply here. The two entries share one built chunk,
163
+ so a class has one identity whichever entry it was imported through.
164
+
165
+ ### Where a decision lives
166
+
167
+ A decision prompt is an ordinary `OpenRouterPrompt` whose version carries `q` (questions) instead of
168
+ `m` (messages), and whose config names a System One model. The two are mutually exclusive — a decision
169
+ has no prose output for instructions and seed messages to shape.
170
+
171
+ Stored questions are the STATIC half of the answer space; a caller may declare further questions per call
172
+ and they merge over the stored ones by id. That is what lets a fixed taxonomy be tuned by an operator
173
+ while a per-call candidate set still comes from code.
174
+
175
+ | Call | When |
176
+ |---|---|
177
+ | `decideForPrompt(...)` | The default. No document, no sweep — a Jev call answers in about a hundred milliseconds, has no tools and nothing to defer, so the queue buys nothing on the happy path. |
178
+ | `enqueueRunTask({ state, questions, immediate: true })` | When a FAILURE has to survive this process. Same run-it-now latency, plus a document: a retryable failure (OpenRouter down, a 429) is left QUEUED for the sweep, and a deterministic one still reaches FAILED on the first attempt. |
179
+
180
+ `immediate` writes the document either way, so `readRunTask(key)` behaves the same whether or not the
181
+ inline attempt succeeded — which is the point of it being a queue flag rather than a second inline call.
182
+ It is not decisions-specific; a completion run can use it too.
183
+
184
+ ## Store-locking a prompt
185
+
186
+ `storeLocked` (`sl`) on an `OpenRouterPrompt` says the STORE owns this prompt's content: a code
187
+ definition can neither seed it nor overtake it. It is the counterpart of a version's `lk` — that locks a
188
+ version against edits, this locks a prompt against its own definition — and it applies to any prompt,
189
+ not just a decision.
190
+
191
+ Set it on a prompt whose content is maintained at runtime. A decision is the motivating case, because its
192
+ questions are exactly the thing an operator tunes, and a later `version` bump in code would otherwise
193
+ publish straight over that work.
194
+
195
+ - **Seeding** skips a locked prompt and says so in the run's `warnings`, beside the existing rule that
196
+ never resurrects an `ARCHIVED` one.
197
+ - **Resolution** stops preferring a definition whose version has moved ahead of the store.
198
+ - It is deliberately **lenient**: a definition may still STAND IN when the store holds no version at all,
199
+ which is what keeps a fresh emulator or a never-seeded project able to serve the prompt with no manual
200
+ step. The lock prevents being *overwritten*, not being served.
201
+
202
+ Turn it on through `openRouterPrompt.update`, or declare `storeLocked` on a definition to set it when the
203
+ prompt is first created. A definition cannot lock a prompt it did not create — that would let code seize
204
+ one an operator is already maintaining.
205
+
49
206
  ## Managing prompts
50
207
 
51
208
  There is deliberately no Angular UI for prompt authoring. Declaring the CRUD is what makes every prompt
@@ -224,6 +381,11 @@ Two blocks make real API calls, both skipped unless `OPENROUTER_API_KEY` is set:
224
381
 
225
382
  - `openrouter.filesearch.spike.spec.ts` — the `file_search` passthrough probes. Deliberately cheap: a
226
383
  free model by default, and the file_search probe fails at the store lookup before anything is billed.
384
+ - `openrouter.decision.spike.spec.ts` — the System One probes. Cheap for a different reason: input is
385
+ $0.042/Mtok and output is not billed at all. They pin what only a real call can settle — that all three
386
+ primitives answer in the declared shape, that `usage.cost` arrives synchronously (which is why a
387
+ decision needs no broadcast reconciliation), and that the wire is snake_case where the SDK decodes to
388
+ camelCase.
227
389
  - the `live end-to-end` block in `openrouter.runtask.emulator.spec.ts` — publishes a version, enqueues a
228
390
  run, drains it with the real sweeper against the real API, then resolves the stored generation id
229
391
  through `openRouterGeneration`. This is the plan's end-to-end bullet minus its MCP transport: no app in
@@ -236,6 +398,7 @@ Two blocks make real API calls, both skipped unless `OPENROUTER_API_KEY` is set:
236
398
  | `OPENROUTER_TEST_MODEL_ID` | Model for the general probe and the end-to-end run. Defaults to `nvidia/nemotron-nano-9b-v2:free`. |
237
399
  | `OPENROUTER_FILE_SEARCH_MODEL_ID` | Model for the file_search probe. Must be an OpenAI model. |
238
400
  | `OPENROUTER_FILE_SEARCH_VECTOR_STORE_ID` | A real `vs_…`; upgrades the probe to the full grounded assertion. |
401
+ | `OPENROUTER_TEST_DECISION_MODEL_ID` | System One model for the decision probes. Defaults to `typesafe/jev-1.13`. Its own knob because the two arms cannot share one value. |
239
402
 
240
403
  ## CJS / ESM
241
404
 
package/decision.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/decision";
@@ -0,0 +1,2 @@
1
+ export { D as DEFAULT_OPENROUTER_PDF_PARSER_ENGINE, a as DEFAULT_OPENROUTER_SYSTEM_ONE_MODEL_ID, O as OPENROUTER_DECISION_CHOICE_OPTIONS_MAX, b as OPENROUTER_DECISION_CONFIDENCE_HIGH, c as OPENROUTER_DECISION_CONFIDENCE_MEDIUM, d as OPENROUTER_DECISION_DISTRIBUTION_SUM_TOLERANCE, e as OPENROUTER_DECISION_SCORE_LEVELS_MAX, f as OPENROUTER_DECISION_SCORE_LEVELS_MIN, g as OPENROUTER_JEV_1_13_MODEL_ID, h as OPENROUTER_JEV_LATEST_MODEL_ID, i as OPENROUTER_JEV_PREVIEW_MODEL_ID, j as OPENROUTER_SYSTEM_ONE_MODEL_NAMESPACE, k as OpenRouterDecisionAnswerFaultError, l as OpenRouterDecisionDeclarationError, m as asOpenRouterDecisionConfidenceBand, n as isBlankOpenRouterDecisionEntry, o as isOpenRouterSystemOneModelId, p as mapOpenRouterDecisionChoiceRows, q as mergeOpenRouterModelConfig, r as openRouterChoiceQuestion, s as openRouterDecisionChoiceOptionNames, t as openRouterDecisionChoiceRanking, u as openRouterDecisionRequest, v as openRouterDecisionRequestBody, w as openRouterDecisionStatePaths, x as openRouterDecisionWireQuestion, y as openRouterFileParserPlugin, z as openRouterFileSearchTool, A as openRouterNoulQuestion, B as openRouterProviderPinnedTo, C as openRouterRunUsageFromDecisionsUsage, E as openRouterScoreQuestion, F as readOpenRouterDecisionAnswers, G as splitOpenRouterDecisionModelConfig, H as validateOpenRouterDecisionQuestions, I as validateOpenRouterDecisionRequest, J as validateOpenRouterModelConfig } from './openrouter.decision.esm.js';
2
+ import '@dereekb/util';
@@ -1,5 +1,5 @@
1
1
  import { MS_IN_DAY } from '@dereekb/util';
2
- import { firestoreModelIdentity, snapshotConverterFunctions, optionalFirestoreArray, firestoreNumber, optionalFirestoreNumber, firestoreEnum, optionalFirestoreString, firestoreString, optionalFirestoreDate, firestoreDate, optionalFirestoreBoolean, optionalFirestoreJsonStringField, firestoreArray, AbstractFirestoreDocument, AbstractFirestoreDocumentWithParent, inferredTargetModelParamsType, callModelFirebaseFunctionMapFactory, where, whereDateIsOnOrBefore, orderBy, limit } from '@dereekb/firebase';
2
+ import { firestoreModelIdentity, snapshotConverterFunctions, optionalFirestoreBoolean, optionalFirestoreArray, firestoreNumber, optionalFirestoreNumber, firestoreEnum, optionalFirestoreString, firestoreString, optionalFirestoreDate, firestoreDate, optionalFirestoreJsonStringField, AbstractFirestoreDocument, AbstractFirestoreDocumentWithParent, inferredTargetModelParamsType, callModelFirebaseFunctionMapFactory, where, whereDateIsOnOrBefore, orderBy, limit } from '@dereekb/firebase';
3
3
  import { type } from 'arktype';
4
4
  import { clearable } from '@dereekb/model';
5
5
 
@@ -149,7 +149,8 @@ var openRouterPromptConverter = snapshotConverterFunctions({
149
149
  t: optionalFirestoreArray({
150
150
  filterUnique: true,
151
151
  dontStoreIfEmpty: true
152
- })
152
+ }),
153
+ sl: optionalFirestoreBoolean()
153
154
  }
154
155
  });
155
156
  /**
@@ -214,6 +215,7 @@ var openRouterPromptVersionConverter = snapshotConverterFunctions({
214
215
  dontStoreIfEmpty: true
215
216
  }),
216
217
  c: optionalFirestoreJsonStringField(),
218
+ q: optionalFirestoreJsonStringField(),
217
219
  nt: optionalFirestoreString(),
218
220
  by: optionalFirestoreString(),
219
221
  lk: optionalFirestoreBoolean()
@@ -383,7 +385,7 @@ var openRouterRunTaskConverter = snapshotConverterFunctions({
383
385
  pv: firestoreNumber({
384
386
  default: 0
385
387
  }),
386
- in: firestoreArray({}),
388
+ in: optionalFirestoreArray({}),
387
389
  fp: optionalFirestoreArray({
388
390
  dontStoreIfEmpty: true
389
391
  }),
@@ -391,8 +393,11 @@ var openRouterRunTaskConverter = snapshotConverterFunctions({
391
393
  dontStoreIfEmpty: true
392
394
  }),
393
395
  co: optionalFirestoreJsonStringField(),
396
+ st: optionalFirestoreJsonStringField(),
397
+ q: optionalFirestoreJsonStringField(),
394
398
  o: optionalFirestoreString(),
395
399
  j: optionalFirestoreJsonStringField(),
400
+ an: optionalFirestoreJsonStringField(),
396
401
  gi: optionalFirestoreArray({
397
402
  filterUnique: true,
398
403
  dontStoreIfEmpty: true
@@ -457,7 +462,8 @@ var openRouterRunTaskConverter = snapshotConverterFunctions({
457
462
  content: c
458
463
  };
459
464
  }),
460
- config: (_version_c = version.c) !== null && _version_c !== void 0 ? _version_c : {}
465
+ config: (_version_c = version.c) !== null && _version_c !== void 0 ? _version_c : {},
466
+ questions: version.q
461
467
  };
462
468
  }
463
469
  /**
@@ -474,7 +480,8 @@ var updateOpenRouterPromptParamsType = /* @__PURE__ */ inferredTargetModelParams
474
480
  'description?': clearable('string'),
475
481
  'tags?': clearable('string[]'),
476
482
  'state?': clearable('number'),
477
- 'activeVersion?': clearable('number')
483
+ 'activeVersion?': clearable('number'),
484
+ 'storeLocked?': clearable('boolean')
478
485
  }));
479
486
  var openRouterPromptVersionMessageParamsType = /* @__PURE__ */ type({
480
487
  role: "'user' | 'system' | 'assistant' | 'developer'",
@@ -485,6 +492,7 @@ var createOpenRouterPromptVersionParamsType = /* @__PURE__ */ type({
485
492
  'instructions?': clearable('string'),
486
493
  'messages?': clearable(openRouterPromptVersionMessageParamsType.array()),
487
494
  'config?': clearable('object'),
495
+ 'questions?': clearable('object'),
488
496
  'notes?': clearable('string'),
489
497
  'activate?': clearable('boolean')
490
498
  });
@@ -492,6 +500,7 @@ var updateOpenRouterPromptVersionParamsType = /* @__PURE__ */ inferredTargetMode
492
500
  'instructions?': clearable('string'),
493
501
  'messages?': clearable(openRouterPromptVersionMessageParamsType.array()),
494
502
  'config?': clearable('object'),
503
+ 'questions?': clearable('object'),
495
504
  'notes?': clearable('string')
496
505
  }));
497
506
  var readOpenRouterPromptParamsType = /* @__PURE__ */ inferredTargetModelParamsType.merge(type({
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/openrouter/firebase",
3
- "version": "14.7.0",
3
+ "version": "14.9.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/date": "14.7.0",
8
- "@dereekb/firebase": "14.7.0",
9
- "@dereekb/model": "14.7.0",
10
- "@dereekb/openrouter": "14.7.0",
11
- "@dereekb/rxjs": "14.7.0",
12
- "@dereekb/util": "14.7.0",
7
+ "@dereekb/date": "14.9.0",
8
+ "@dereekb/firebase": "14.9.0",
9
+ "@dereekb/model": "14.9.0",
10
+ "@dereekb/openrouter": "14.9.0",
11
+ "@dereekb/rxjs": "14.9.0",
12
+ "@dereekb/util": "14.9.0",
13
13
  "arktype": "^2.2.0"
14
14
  },
15
15
  "exports": {
@@ -26,6 +26,14 @@ export interface UpdateOpenRouterPromptParams extends InferredTargetModelParams
26
26
  * failing to resolve.
27
27
  */
28
28
  readonly activeVersion?: Maybe<OpenRouterPromptVersionNumber>;
29
+ /**
30
+ * Whether the prompt is locked to the store, so a code definition can neither seed it nor overtake it.
31
+ *
32
+ * The one write that turns the lever on and off, and it is deliberately here rather than only on a
33
+ * definition: locking a prompt is an operator's decision about who maintains its content, and an
34
+ * operator has to be able to take that decision back.
35
+ */
36
+ readonly storeLocked?: Maybe<boolean>;
29
37
  }
30
38
  export declare const updateOpenRouterPromptParamsType: Type<UpdateOpenRouterPromptParams>;
31
39
  /**
@@ -66,6 +74,14 @@ export interface CreateOpenRouterPromptVersionParams {
66
74
  * Model configuration. Passthrough JSON — validated against `OpenRouterModelConfig` but stored as-is.
67
75
  */
68
76
  readonly config?: Maybe<Record<string, unknown>>;
77
+ /**
78
+ * The questions this version declares, making it a DECISION prompt.
79
+ *
80
+ * Passthrough JSON, validated against the declaration guards before the version is written — an
81
+ * eleven-level Score or a question with blank instructions is refused here rather than at the wire.
82
+ * A version carrying questions must also name a System One model in its config.
83
+ */
84
+ readonly questions?: Maybe<Record<string, unknown>>;
69
85
  /**
70
86
  * Why this version was created.
71
87
  */
@@ -101,6 +117,11 @@ export interface UpdateOpenRouterPromptVersionParams extends InferredTargetModel
101
117
  * Model configuration. Passthrough JSON — validated against `OpenRouterModelConfig` but stored as-is.
102
118
  */
103
119
  readonly config?: Maybe<Record<string, unknown>>;
120
+ /**
121
+ * The questions this version declares, making it a DECISION prompt. See
122
+ * {@link CreateOpenRouterPromptVersionParams.questions}.
123
+ */
124
+ readonly questions?: Maybe<Record<string, unknown>>;
104
125
  /**
105
126
  * Why the version says what it says.
106
127
  */
@@ -1,6 +1,6 @@
1
1
  import { type GrantedReadRole, type GrantedUpdateRole } from '@dereekb/model';
2
2
  import { type Maybe, type Milliseconds } from '@dereekb/util';
3
- import { type OpenRouterFileAnnotation, type OpenRouterFileReference, type OpenRouterGenerationId, type OpenRouterInputMessage, type OpenRouterInputRole, type OpenRouterModelConfig, type OpenRouterPromptKey, type OpenRouterPromptVersionNumber, type OpenRouterResolvedPrompt, type OpenRouterRunError, type OpenRouterRunUsage } from '@dereekb/openrouter';
3
+ import { type OpenRouterDecisionAnswers, type OpenRouterDecisionQuestions, type OpenRouterStorableDecisionState, type OpenRouterFileAnnotation, type OpenRouterFileReference, type OpenRouterGenerationId, type OpenRouterInputMessage, type OpenRouterInputRole, type OpenRouterModelConfig, type OpenRouterPromptKey, type OpenRouterPromptVersionNumber, type OpenRouterResolvedPrompt, type OpenRouterRunError, type OpenRouterRunUsage } from '@dereekb/openrouter';
4
4
  import { AbstractFirestoreDocument, AbstractFirestoreDocumentWithParent, type CollectionGroup, type CollectionReference, type FirestoreCollection, type FirestoreCollectionGroup, type FirestoreCollectionWithParent, type FirestoreContext, type FirestoreModelKey } from '@dereekb/firebase';
5
5
  import { openRouterPromptVersionId } from './openrouter.id';
6
6
  /**
@@ -116,6 +116,25 @@ export interface OpenRouterPrompt {
116
116
  * @dbxModelVariable tags
117
117
  */
118
118
  t?: Maybe<string[]>;
119
+ /**
120
+ * Whether this prompt is locked to the store, so a code definition can neither seed it nor overtake
121
+ * it.
122
+ *
123
+ * The counterpart of {@link OpenRouterPromptVersion.lk}: that locks a VERSION against edits, this
124
+ * locks a PROMPT against its own code definition. Both exist for the same reason — something a past
125
+ * decision depended on must keep saying what it said — but they defend against different writers.
126
+ *
127
+ * Set it on a prompt whose content is maintained at RUNTIME rather than in code. A decision prompt is
128
+ * the motivating case, because its questions are the thing an operator tunes, but nothing here is
129
+ * decision-specific and any prompt may be locked.
130
+ *
131
+ * Deliberately LENIENT: a definition may still stand in when the store holds no version at all, which
132
+ * is what keeps a fresh environment — a new emulator, a test, a project that has never been seeded —
133
+ * able to serve the prompt with no manual step first. What the lock prevents is being OVERWRITTEN.
134
+ *
135
+ * @dbxModelVariable storeLocked
136
+ */
137
+ sl?: Maybe<boolean>;
119
138
  }
120
139
  /**
121
140
  * Roles for an {@link OpenRouterPrompt}. Prompts are operational configuration, so reads and writes
@@ -240,6 +259,21 @@ export interface OpenRouterPromptVersion {
240
259
  * @dbxModelVariable createdBy
241
260
  */
242
261
  by?: Maybe<FirestoreModelKey>;
262
+ /**
263
+ * The questions this version declares, when it is a DECISION prompt.
264
+ *
265
+ * Presence is what makes a version a decision: one carrying questions is asked through
266
+ * `openRouterDecision` against a System One model, and one without is a completion. The two are
267
+ * mutually exclusive — a decision has no prose output for `i` and `m` to shape.
268
+ *
269
+ * Stored as a JSON STRING for the reason `c` is, only more forcefully. A question's criteria may be
270
+ * arbitrary structured JSON, and Firestore forbids an array inside an array — so a Score whose levels
271
+ * are `{what, signals: [...]}` objects, or any criteria carrying a list, fails the write outright as a
272
+ * native map. A native map structurally cannot hold a legal question set.
273
+ *
274
+ * @dbxModelVariable questions
275
+ */
276
+ q?: Maybe<OpenRouterDecisionQuestions>;
243
277
  /**
244
278
  * Whether the version is locked against further edits.
245
279
  *
@@ -507,9 +541,34 @@ export interface OpenRouterRunTask {
507
541
  /**
508
542
  * The call input.
509
543
  *
544
+ * Optional because a DECISION run has none: it carries a state and an answer space rather than
545
+ * messages. Exactly one of `in` and `st` is meaningful on any given task.
546
+ *
510
547
  * @dbxModelVariable input
511
548
  */
512
- in: OpenRouterInputMessage[];
549
+ in?: Maybe<OpenRouterInputMessage[]>;
550
+ /**
551
+ * The content to judge, on a DECISION run.
552
+ *
553
+ * PRESENCE OF THIS FIELD IS THE DISCRIMINATOR. A task carrying a state is dispatched to
554
+ * `POST /systemone` and one without it to `/responses` — no separate kind enum, because which surface
555
+ * a request needs is a property of the request, and a second field saying so is a second thing that
556
+ * can disagree with it.
557
+ *
558
+ * Passthrough JSON for the reason {@link OpenRouterPromptVersion.q} states.
559
+ *
560
+ * @dbxModelVariable state
561
+ */
562
+ st?: Maybe<OpenRouterStorableDecisionState>;
563
+ /**
564
+ * Questions declared by the caller for THIS run, merged over the version's own stored questions.
565
+ *
566
+ * Only the dynamic half is stored: the static half already lives on the version, and copying it here
567
+ * would let a run cite a version whose questions it does not actually use.
568
+ *
569
+ * @dbxModelVariable questions
570
+ */
571
+ q?: Maybe<OpenRouterDecisionQuestions>;
513
572
  /**
514
573
  * Files to attach, as GCS object paths — never signed URLs. See {@link OpenRouterFileReference} for why.
515
574
  *
@@ -542,6 +601,19 @@ export interface OpenRouterRunTask {
542
601
  * @dbxModelVariable outputJson
543
602
  */
544
603
  j?: Maybe<Record<string, unknown>>;
604
+ /**
605
+ * The answers, on a completed DECISION run.
606
+ *
607
+ * Written instead of `o` / `j`, not alongside them: a decision produces no text, and storing an
608
+ * answer map as `outputJson` would make a reader guess which kind of run it is holding from the
609
+ * shape of a loose object.
610
+ *
611
+ * Already membership-checked against the declared questions when it is written — see
612
+ * `readOpenRouterDecisionAnswers` — so a reader does not re-check one.
613
+ *
614
+ * @dbxModelVariable answers
615
+ */
616
+ an?: Maybe<OpenRouterDecisionAnswers>;
545
617
  /**
546
618
  * Generation ids produced, for auditing via `getGeneration` / `listGenerationContent`.
547
619
  *