@dereekb/openrouter 14.7.0 → 14.8.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 +144 -1
- package/firebase/index.esm.js +14 -5
- package/firebase/package.json +7 -7
- package/firebase/src/lib/openrouter.api.d.ts +21 -0
- package/firebase/src/lib/openrouter.model.d.ts +74 -2
- package/firebase-server/index.esm.js +417 -49
- package/firebase-server/package.json +12 -12
- package/firebase-server/src/lib/openrouter.call.inline.d.ts +58 -1
- package/firebase-server/src/lib/openrouter.runtask.handle.d.ts +26 -0
- package/firebase-server/src/lib/openrouter.runtask.service.d.ts +37 -1
- package/firebase-server/src/test/openrouter.fake.d.ts +45 -0
- package/index.esm.js +1402 -52
- package/package.json +7 -7
- package/src/lib/index.d.ts +2 -0
- package/src/lib/openrouter.call.d.ts +18 -1
- package/src/lib/openrouter.config.d.ts +18 -1
- package/src/lib/openrouter.decision.d.ts +352 -0
- package/src/lib/openrouter.decision.question.d.ts +392 -0
- package/src/lib/openrouter.prompt.d.ts +25 -0
- package/src/lib/openrouter.sdk.d.ts +10 -2
- package/src/lib/openrouter.type.d.ts +51 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ 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
|
|
16
|
+
| `@dereekb/openrouter` | Config types, request builder, `callModel` wrapper, deferred-tool helpers, embeddings, **decisions**. Pure — no I/O. |
|
|
17
17
|
| `@dereekb/openrouter/firebase` | The `OpenRouterPrompt`, `OpenRouterPromptVersion` and `OpenRouterRunTask` models. |
|
|
18
18
|
| `@dereekb/openrouter/firebase-server` | Prompt service, run-task queue + sweep, Firestore `StateAccessor`, server actions. |
|
|
19
19
|
|
|
@@ -46,6 +46,143 @@ one run onto another's history.
|
|
|
46
46
|
Short calls skip all of it: `callModelForPrompt(...)` runs inline and returns the result with no
|
|
47
47
|
document.
|
|
48
48
|
|
|
49
|
+
## Decisions (System One / Jev)
|
|
50
|
+
|
|
51
|
+
OpenRouter has **two** inference surfaces, and this package serves both. `/responses` asks a model for
|
|
52
|
+
prose and validates the reply back into a shape. **System One** (`POST /systemone`, the `typesafe/jev-*`
|
|
53
|
+
models) inverts that: the caller declares the answer space up front as typed questions, and the model
|
|
54
|
+
returns a position inside it plus a calibrated distribution. `(state, questions) → answers`. There is no
|
|
55
|
+
reply to parse, so there is no off-shape reply to recover from.
|
|
56
|
+
|
|
57
|
+
Three primitives, and no others:
|
|
58
|
+
|
|
59
|
+
| Primitive | Asks | Answers |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `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` |
|
|
62
|
+
| `openRouterScoreQuestion(instructions, levels)` | which level on this rubric? | a `score` that may land BETWEEN two levels, the distribution, `confidence` |
|
|
63
|
+
| `openRouterNoulQuestion(instructions, means?)` | is this true? | `noul` 0..1 — the probability IS the uncertainty, so there is no separate confidence |
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const questions = {
|
|
67
|
+
team: openRouterChoiceQuestion('Which team should own `ticket`?', {
|
|
68
|
+
billing: { what: 'Charges, invoices, refunds', not_for: 'Anything about signing in' },
|
|
69
|
+
access: { what: 'Sign-in, passwords, permissions', not_for: 'Anything about money' },
|
|
70
|
+
other: { what: 'Anything the other options do not cover' }
|
|
71
|
+
}),
|
|
72
|
+
spam: openRouterNoulQuestion('`ticket` is unsolicited marketing rather than a real request.')
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const result = await decideForPrompt({ client, promptService, promptKey: 'demo-support-triage', state: { ticket } });
|
|
76
|
+
result.answers.team.choice; // typed to the declared option names
|
|
77
|
+
result.answers.spam.noul; // 0..1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Declare every question one state could need in ONE call.** Each is evaluated independently against the
|
|
81
|
+
same state, so the question map is both the batching unit and the cost unit: the state is sent and billed
|
|
82
|
+
once, and a speculative question the caller may discard costs only its own tokens.
|
|
83
|
+
|
|
84
|
+
**Membership is the transport's guarantee.** `readOpenRouterDecisionAnswers` checks every answer against
|
|
85
|
+
the question that asked it, so no consumer re-checks: past that point a `choice` is one of the declared
|
|
86
|
+
options, a `score` is inside the declared range, a `noul` is a probability. An answer that left its space
|
|
87
|
+
raises `OpenRouterDecisionAnswerFaultError` — a defect in the RESPONSE, never a judgement the model made.
|
|
88
|
+
"None of these fits" is said through a Noul the caller declared for it, and lands as an *answer*.
|
|
89
|
+
|
|
90
|
+
A Choice is only ever RELATIVE — its probabilities are normalised over the options supplied, so something
|
|
91
|
+
always wins even when nothing fits. When "nothing fits" is an outcome you act on, pair it with a Noul,
|
|
92
|
+
which is absolute and may be low for every option.
|
|
93
|
+
|
|
94
|
+
### Writing a question
|
|
95
|
+
|
|
96
|
+
- **One snap judgement per question.** A judgement weighing several factors is several questions combined
|
|
97
|
+
in your own code.
|
|
98
|
+
- **Write the COMPLETE question in `instructions`** — the question id is never sent, so a descriptive key
|
|
99
|
+
is no substitute. A blank one is refused at declaration.
|
|
100
|
+
- **Point at named state with backticked dot-paths** — `` `phrase` ``, `` `ticket.sender.email` ``,
|
|
101
|
+
`` `messages[0].text` ``. Prefer an object state so each part has a name to point at.
|
|
102
|
+
`openRouterDecisionStatePaths` reads them back so a spec can pin that a declaration names keys its
|
|
103
|
+
state has (documented and inspectable, never enforced — backticks also quote literals).
|
|
104
|
+
- **Score levels are SITUATIONS, not degrees.** "Broken feature, but a workaround exists" gives the model
|
|
105
|
+
something to match against; "moderately severe" does not. Every level is evaluated separately and the
|
|
106
|
+
model never sees a level's number or its neighbours, so numbers in the descriptions do not help.
|
|
107
|
+
- **Choice options are CONTRASTIVE** — the same keys on every option (`{what, not_for, examples}`) so the
|
|
108
|
+
model compares like with like. Include an explicit `other` when the set may not cover the input.
|
|
109
|
+
- **Filter in code first.** Accuracy falls as a state grows with material unrelated to the decision; a
|
|
110
|
+
wide state is not a free hedge.
|
|
111
|
+
|
|
112
|
+
Every declaration surface takes `string | object | array` and the wire carries it VERBATIM — start with
|
|
113
|
+
strings, and reach for structure only for guidance prose would blur or for data that is already JSON.
|
|
114
|
+
|
|
115
|
+
### Limits, enforced at declaration
|
|
116
|
+
|
|
117
|
+
| Limit | Value | Past it |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| 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. |
|
|
120
|
+
| 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. |
|
|
121
|
+
| Context | 64k tokens per request | Filter in code first. |
|
|
122
|
+
| Price | $0.042 / Mtok input; output reported but **not billed** | `usage.cost` is synchronous and final. |
|
|
123
|
+
|
|
124
|
+
`validateOpenRouterDecisionQuestions` fails at the DECLARATION, naming the question — not as a 4xx about
|
|
125
|
+
a request body — and runs at version create / update / seed, so a malformed question map is refused when
|
|
126
|
+
it is written rather than every time it is asked.
|
|
127
|
+
|
|
128
|
+
### Routing: the model id is the discriminator
|
|
129
|
+
|
|
130
|
+
System One models are **not listed by `GET /models`**, so nothing can be learned about one from the
|
|
131
|
+
catalog, and a wrong guess does not produce an error anyone can read. The slug is therefore checked on
|
|
132
|
+
**both** arms and neither can be entered with the other's model:
|
|
133
|
+
|
|
134
|
+
- `validateOpenRouterModelConfig(config)` errors on a `typesafe/…` slug, naming `openRouterDecision`.
|
|
135
|
+
It already runs at publish time, so a Jev slug typed into a stored version is refused when written.
|
|
136
|
+
- `validateOpenRouterModelConfig(config, { decision: true })` errors on a chat slug.
|
|
137
|
+
- `openRouterResponsesRequestBody` throws `OpenRouterSystemOneModelOnCompletionArmError`. It is the one
|
|
138
|
+
point every completion dispatch path builds its body, so no route can be added later that skips it.
|
|
139
|
+
|
|
140
|
+
`OPENROUTER_JEV_1_13_MODEL_ID` is a **versioned** slug, deliberately not the moving `jev-latest` alias:
|
|
141
|
+
an answer is only reproducible against the model that gave it, which is the same reason versions exist.
|
|
142
|
+
The reply reports the model that actually served it (`typesafe/jev-1.13-20260917`), so read `result.model`
|
|
143
|
+
rather than assuming the slug you asked for.
|
|
144
|
+
|
|
145
|
+
### Where a decision lives
|
|
146
|
+
|
|
147
|
+
A decision prompt is an ordinary `OpenRouterPrompt` whose version carries `q` (questions) instead of
|
|
148
|
+
`m` (messages), and whose config names a System One model. The two are mutually exclusive — a decision
|
|
149
|
+
has no prose output for instructions and seed messages to shape.
|
|
150
|
+
|
|
151
|
+
Stored questions are the STATIC half of the answer space; a caller may declare further questions per call
|
|
152
|
+
and they merge over the stored ones by id. That is what lets a fixed taxonomy be tuned by an operator
|
|
153
|
+
while a per-call candidate set still comes from code.
|
|
154
|
+
|
|
155
|
+
| Call | When |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `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. |
|
|
158
|
+
| `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. |
|
|
159
|
+
|
|
160
|
+
`immediate` writes the document either way, so `readRunTask(key)` behaves the same whether or not the
|
|
161
|
+
inline attempt succeeded — which is the point of it being a queue flag rather than a second inline call.
|
|
162
|
+
It is not decisions-specific; a completion run can use it too.
|
|
163
|
+
|
|
164
|
+
## Store-locking a prompt
|
|
165
|
+
|
|
166
|
+
`storeLocked` (`sl`) on an `OpenRouterPrompt` says the STORE owns this prompt's content: a code
|
|
167
|
+
definition can neither seed it nor overtake it. It is the counterpart of a version's `lk` — that locks a
|
|
168
|
+
version against edits, this locks a prompt against its own definition — and it applies to any prompt,
|
|
169
|
+
not just a decision.
|
|
170
|
+
|
|
171
|
+
Set it on a prompt whose content is maintained at runtime. A decision is the motivating case, because its
|
|
172
|
+
questions are exactly the thing an operator tunes, and a later `version` bump in code would otherwise
|
|
173
|
+
publish straight over that work.
|
|
174
|
+
|
|
175
|
+
- **Seeding** skips a locked prompt and says so in the run's `warnings`, beside the existing rule that
|
|
176
|
+
never resurrects an `ARCHIVED` one.
|
|
177
|
+
- **Resolution** stops preferring a definition whose version has moved ahead of the store.
|
|
178
|
+
- It is deliberately **lenient**: a definition may still STAND IN when the store holds no version at all,
|
|
179
|
+
which is what keeps a fresh emulator or a never-seeded project able to serve the prompt with no manual
|
|
180
|
+
step. The lock prevents being *overwritten*, not being served.
|
|
181
|
+
|
|
182
|
+
Turn it on through `openRouterPrompt.update`, or declare `storeLocked` on a definition to set it when the
|
|
183
|
+
prompt is first created. A definition cannot lock a prompt it did not create — that would let code seize
|
|
184
|
+
one an operator is already maintaining.
|
|
185
|
+
|
|
49
186
|
## Managing prompts
|
|
50
187
|
|
|
51
188
|
There is deliberately no Angular UI for prompt authoring. Declaring the CRUD is what makes every prompt
|
|
@@ -224,6 +361,11 @@ Two blocks make real API calls, both skipped unless `OPENROUTER_API_KEY` is set:
|
|
|
224
361
|
|
|
225
362
|
- `openrouter.filesearch.spike.spec.ts` — the `file_search` passthrough probes. Deliberately cheap: a
|
|
226
363
|
free model by default, and the file_search probe fails at the store lookup before anything is billed.
|
|
364
|
+
- `openrouter.decision.spike.spec.ts` — the System One probes. Cheap for a different reason: input is
|
|
365
|
+
$0.042/Mtok and output is not billed at all. They pin what only a real call can settle — that all three
|
|
366
|
+
primitives answer in the declared shape, that `usage.cost` arrives synchronously (which is why a
|
|
367
|
+
decision needs no broadcast reconciliation), and that the wire is snake_case where the SDK decodes to
|
|
368
|
+
camelCase.
|
|
227
369
|
- the `live end-to-end` block in `openrouter.runtask.emulator.spec.ts` — publishes a version, enqueues a
|
|
228
370
|
run, drains it with the real sweeper against the real API, then resolves the stored generation id
|
|
229
371
|
through `openRouterGeneration`. This is the plan's end-to-end bullet minus its MCP transport: no app in
|
|
@@ -236,6 +378,7 @@ Two blocks make real API calls, both skipped unless `OPENROUTER_API_KEY` is set:
|
|
|
236
378
|
| `OPENROUTER_TEST_MODEL_ID` | Model for the general probe and the end-to-end run. Defaults to `nvidia/nemotron-nano-9b-v2:free`. |
|
|
237
379
|
| `OPENROUTER_FILE_SEARCH_MODEL_ID` | Model for the file_search probe. Must be an OpenAI model. |
|
|
238
380
|
| `OPENROUTER_FILE_SEARCH_VECTOR_STORE_ID` | A real `vs_…`; upgrades the probe to the full grounded assertion. |
|
|
381
|
+
| `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
382
|
|
|
240
383
|
## CJS / ESM
|
|
241
384
|
|
package/firebase/index.esm.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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({
|
package/firebase/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/openrouter/firebase",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.8.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"peerDependencies": {
|
|
7
|
-
"@dereekb/date": "14.
|
|
8
|
-
"@dereekb/firebase": "14.
|
|
9
|
-
"@dereekb/model": "14.
|
|
10
|
-
"@dereekb/openrouter": "14.
|
|
11
|
-
"@dereekb/rxjs": "14.
|
|
12
|
-
"@dereekb/util": "14.
|
|
7
|
+
"@dereekb/date": "14.8.0",
|
|
8
|
+
"@dereekb/firebase": "14.8.0",
|
|
9
|
+
"@dereekb/model": "14.8.0",
|
|
10
|
+
"@dereekb/openrouter": "14.8.0",
|
|
11
|
+
"@dereekb/rxjs": "14.8.0",
|
|
12
|
+
"@dereekb/util": "14.8.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
|
|
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
|
*
|