@coreplane/switchboard 1.250.0 → 1.252.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/dist/assets/config/config.example.yaml +13 -2
- package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
- package/dist/assets/deploy/cloudflare/worker.ts +6 -3
- package/dist/assets/deploy/cloudflare-memory/worker.ts +108 -12
- package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +3 -2
- package/dist/assets/project.json +13 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +5 -5
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/resource.ts +6 -2
- package/dist/assets/src/core/authz/types.ts +2 -0
- package/dist/assets/src/core/budgets.ts +22 -0
- package/dist/assets/src/core/coordinator/contract.ts +42 -0
- package/dist/assets/src/core/coordinator/driver.ts +150 -12
- package/dist/assets/src/core/drain.ts +50 -0
- package/dist/assets/src/core/memory/engine.ts +98 -0
- package/dist/assets/src/core/memory/scorer.ts +12 -4
- package/dist/assets/src/core/memory/types.ts +69 -12
- package/dist/assets/src/core/modelCard.ts +51 -7
- package/dist/assets/src/core/modelPricing.ts +111 -1
- package/dist/assets/src/core/modelProxy/usage.ts +88 -0
- package/dist/assets/src/core/modelRegistry.ts +15 -1
- package/dist/assets/src/core/provider.ts +49 -0
- package/dist/assets/src/core/refusal.ts +6 -6
- package/dist/assets/src/core/reviewVerdict.ts +4 -0
- package/dist/assets/src/core/runEvents.ts +51 -2
- package/dist/assets/src/core/runFriction.ts +7 -2
- package/dist/assets/src/core/runLedger/types.ts +23 -4
- package/dist/assets/src/core/runRecord.ts +16 -0
- package/dist/assets/src/core/runUsage.ts +67 -13
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +45 -5
- package/dist/assets/src/core/ship/coordinator.ts +441 -48
- package/dist/assets/src/core/ship/renewal.ts +10 -5
- package/dist/assets/src/core/trace/attrs.ts +24 -0
- package/dist/assets/src/core/types.ts +5 -5
- package/dist/assets/src/core/verbosity.ts +48 -0
- package/dist/assets/src/deploy/liveGate.ts +40 -13
- package/dist/assets/src/deploy/restart.ts +11 -12
- package/dist/assets/src/execution/residentDepCache.ts +50 -1
- package/dist/assets/src/execution/residentDepsStore.ts +40 -2
- package/dist/assets/src/execution/residentRefresh.ts +55 -3
- package/dist/assets/src/execution/residentSteps.ts +4 -0
- package/dist/assets/src/execution/sandboxErrors.ts +8 -0
- package/dist/assets/web/dist/.vite/manifest.json +58 -52
- package/dist/assets/web/dist/assets/DeliveryPage-3ELQWM0r.js +1 -0
- package/dist/assets/web/dist/assets/{HomePage-AnycA57D.js → HomePage-BG_ok-K2.js} +2 -2
- package/dist/assets/web/dist/assets/{PendingTurnRow-BuRre8it.js → PendingTurnRow-ChCQOLgZ.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Cb3sFkfj.js → ResidentDetailPage-C9y3nbo8.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BZ6n6UxF.js → ResidentsIndexPage-i1RG9e7g.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-CBUxbeqN.js → ScheduledPage-DvYwM2TE.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CBTnZ9Qv.js → SettingsPage-Bo6yCyXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BnRjWzFN.js → StatusDot-CAfS1AUi.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-Brge0wnd.js → Tooltip-tZoum_T-.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-o6sLju16.js → UnitRoutePage-BmdOHwNn.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
- package/dist/assets/web/dist/assets/{dist-rgAhsmE-.js → dist-DfbEpHXR.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +1 -0
- package/dist/assets/web/dist/assets/{main-CeRuGONy.js → main-5Gm_1Gv8.js} +2 -2
- package/dist/assets/web/dist/assets/sseReplay-DmyMXfRC.js +11 -0
- package/dist/cli.js +3319 -1015
- package/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
- package/dist/assets/web/dist/assets/DeliveryPage-CIfBiINK.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-BRXkjkgO.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-psSMI3fN.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-68YT_RWt.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DXC7kGbN.js +0 -9
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// catalog lives in ./modelRegistry.ts.
|
|
11
11
|
|
|
12
12
|
import { EFFORT_LEVELS, type Effort } from "../effort.js";
|
|
13
|
-
import { vendorOf, wireOf, type ProviderConfig, type Wire } from "./provider.js";
|
|
13
|
+
import { billerHarnessProvider, vendorOf, wireOf, type ProviderConfig, type Wire } from "./provider.js";
|
|
14
14
|
import type { RegistryCard } from "./modelRegistry.js";
|
|
15
15
|
|
|
16
16
|
/** Which layer named a field: the operator's block, the registry card, or the
|
|
@@ -32,11 +32,23 @@ export type LevelMap = Record<Effort, LevelWord | "refused"> | "unknown";
|
|
|
32
32
|
export type InputSupport = boolean | "unknown";
|
|
33
33
|
export type CacheRule = "automatic" | "markers" | "none" | "unknown";
|
|
34
34
|
|
|
35
|
+
/** One long-context tier of a card's rate (pi's rule, `calculateCost`): when
|
|
36
|
+
* the request's input side (input + cache reads + cache writes) exceeds
|
|
37
|
+
* `inputTokensAbove`, the WHOLE request re-rates at the tier. */
|
|
38
|
+
export interface CardPriceTier {
|
|
39
|
+
inputTokensAbove: number;
|
|
40
|
+
input: number;
|
|
41
|
+
output: number;
|
|
42
|
+
cacheRead: number;
|
|
43
|
+
cacheWrite: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export interface CardPrice {
|
|
36
47
|
input: number;
|
|
37
48
|
output: number;
|
|
38
49
|
cacheRead: number;
|
|
39
50
|
cacheWrite: number;
|
|
51
|
+
tiers?: readonly CardPriceTier[];
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
export interface ModelCard {
|
|
@@ -123,14 +135,30 @@ function levelMapOf(map: Record<string, string | null> | undefined, reasoning: b
|
|
|
123
135
|
return out;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
type RawPrice = {
|
|
139
|
+
input?: number;
|
|
140
|
+
output?: number;
|
|
141
|
+
cacheRead?: number;
|
|
142
|
+
cacheWrite?: number;
|
|
143
|
+
tiers?: Array<{ inputTokensAbove?: number } & Omit<RawPrice, "tiers">>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function priceOf(raw: RawPrice | undefined): CardPrice | undefined {
|
|
129
147
|
if (!raw) return undefined;
|
|
130
148
|
const { input, output, cacheRead, cacheWrite } = raw;
|
|
131
149
|
if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined)
|
|
132
150
|
return undefined;
|
|
133
|
-
|
|
151
|
+
// A tier missing a field cannot re-rate the whole request; it is dropped,
|
|
152
|
+
// never guessed at the base rate.
|
|
153
|
+
const tiers = (raw.tiers ?? []).filter(
|
|
154
|
+
(t): t is CardPriceTier =>
|
|
155
|
+
t.inputTokensAbove !== undefined &&
|
|
156
|
+
t.input !== undefined &&
|
|
157
|
+
t.output !== undefined &&
|
|
158
|
+
t.cacheRead !== undefined &&
|
|
159
|
+
t.cacheWrite !== undefined,
|
|
160
|
+
);
|
|
161
|
+
return { input, output, cacheRead, cacheWrite, ...(tiers.length > 0 ? { tiers } : {}) };
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
/**
|
|
@@ -335,13 +363,29 @@ export function decideControls(card: ModelCard, asked: AskedControls): ControlDe
|
|
|
335
363
|
why: windowVouched ? "" : `no layer names the window; compacting at ${card.window}`,
|
|
336
364
|
});
|
|
337
365
|
|
|
338
|
-
|
|
366
|
+
// A `markers` rule needs a harness-side write that places the markers
|
|
367
|
+
// (record 0052's amendment: the harness write names the biller's own
|
|
368
|
+
// provider). The Anthropic wire's own packages place per-block breakpoints;
|
|
369
|
+
// on the chat wire a biller the table names caches the aggregator's own way
|
|
370
|
+
// — the pinned OpenCode binary exempts the openrouter route from per-block
|
|
371
|
+
// placement, so the write carries OpenRouter's top-level
|
|
372
|
+
// `cache_control: { type: "ephemeral" }` via `settings.extraBody` (measured
|
|
373
|
+
// in `opencode/testing/realDriver.test.ts`), and pi's compat sends the
|
|
374
|
+
// per-block markers. A biller served generically degrades, never a silent
|
|
375
|
+
// `native`.
|
|
376
|
+
const markersUnplaced =
|
|
377
|
+
card.cache === "markers" && card.wire === "openai-chat" && billerHarnessProvider(card.block) === undefined;
|
|
378
|
+
const cacheNative = card.cache !== "unknown" && !markersUnplaced;
|
|
339
379
|
decisions.push({
|
|
340
380
|
control: "cache",
|
|
341
381
|
outcome: cacheNative ? "native" : "degraded",
|
|
342
382
|
applied: card.cache,
|
|
343
383
|
vouched: cacheNative,
|
|
344
|
-
why: cacheNative
|
|
384
|
+
why: cacheNative
|
|
385
|
+
? ""
|
|
386
|
+
: markersUnplaced
|
|
387
|
+
? `no harness-side provider vouches for the "${card.block}" biller's cache markers; the rule goes out unvouched`
|
|
388
|
+
: `no layer names ${card.model}'s cache rule`,
|
|
345
389
|
});
|
|
346
390
|
|
|
347
391
|
return decisions;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { parseModelRef } from "./provider.js";
|
|
1
|
+
import { parseModelRef, type TokenUsage } from "./provider.js";
|
|
2
|
+
import type { CardPrice } from "./modelCard.js";
|
|
3
|
+
import type { ReportedCost } from "./modelProxy/usage.js";
|
|
2
4
|
import type { ModelUsage, RunUsage } from "./runUsage.js";
|
|
3
5
|
|
|
4
6
|
// The price of a model's tokens (docs/reference/specs/costs.md): one table
|
|
@@ -148,6 +150,105 @@ export function modelPriceOf(ref: string, prices: ModelPriceTable = NO_PRICES):
|
|
|
148
150
|
return { input: list.input, output: list.output, cacheRead: list.cacheRead, cacheWrite: list.cacheWrite5m };
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
// ---- one turn, priced at the proxy (model-proxy.md item 6) -----------------------
|
|
154
|
+
|
|
155
|
+
/** Which layer priced the turn: the provider's own reported cost, the
|
|
156
|
+
* operator's table (`costs.prices` or a block's `models.<id>.price`), the
|
|
157
|
+
* registry card (the Anthropic family list folds into this layer), or none —
|
|
158
|
+
* an unpriced turn is never $0. */
|
|
159
|
+
export type PriceSource = "provider" | "operator" | "registry" | "none";
|
|
160
|
+
|
|
161
|
+
/** The meter row's dollars: `usd` when a layer priced the turn, `feeUsd` on a
|
|
162
|
+
* BYOK turn (the aggregator's fee — inside `usd` when the vendor's charge was
|
|
163
|
+
* reported beside it, otherwise beside the layer that priced the tokens),
|
|
164
|
+
* and the layer. */
|
|
165
|
+
export interface TurnPrice {
|
|
166
|
+
usd?: number;
|
|
167
|
+
feeUsd?: number;
|
|
168
|
+
priceSource: PriceSource;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** What `priceTurn` reads of the run's model card: the ref the spans name,
|
|
172
|
+
* the card's rate when a layer named one, and which layer did
|
|
173
|
+
* (`ModelCard.provenance.price`). */
|
|
174
|
+
export interface TurnPriceCard {
|
|
175
|
+
ref: string;
|
|
176
|
+
price?: CardPrice;
|
|
177
|
+
pricedBy?: "operator" | "registry" | "wire";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const turnTokensUsd = (u: TokenUsage, p: { input: number; output: number; cacheRead: number; cacheWrite: number }) =>
|
|
181
|
+
(u.inputTokens * p.input +
|
|
182
|
+
u.outputTokens * p.output +
|
|
183
|
+
(u.cacheReadTokens ?? 0) * p.cacheRead +
|
|
184
|
+
(u.cacheWriteTokens ?? 0) * p.cacheWrite) /
|
|
185
|
+
1_000_000;
|
|
186
|
+
|
|
187
|
+
/** A card rate over one turn's counts, tiers by pi's rule (`calculateCost`):
|
|
188
|
+
* the input side is input + cache reads + cache writes, and the WHOLE request
|
|
189
|
+
* re-rates at the highest tier whose threshold it exceeds. */
|
|
190
|
+
export function cardPriceUsd(usage: TokenUsage, price: CardPrice): number {
|
|
191
|
+
const inputSide = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
192
|
+
let rates: { input: number; output: number; cacheRead: number; cacheWrite: number } = price;
|
|
193
|
+
let matched = -1;
|
|
194
|
+
for (const tier of price.tiers ?? []) {
|
|
195
|
+
if (inputSide > tier.inputTokensAbove && tier.inputTokensAbove > matched) {
|
|
196
|
+
rates = tier;
|
|
197
|
+
matched = tier.inputTokensAbove;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return turnTokensUsd(usage, rates);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* One turn's price, by record 0052's precedence: a provider-reported cost is
|
|
205
|
+
* stored as reported (`provider`; on BYOK the fee and the vendor's upstream
|
|
206
|
+
* charge sum into `usd` with `feeUsd` beside), else the operator's layer
|
|
207
|
+
* (`costs.prices` for the exact ref, or the card's operator-named rate), else
|
|
208
|
+
* the registry layer (the card's rate with its tiers, or the Anthropic family
|
|
209
|
+
* list — costs.md item 4b's fallback, folded here), else `none` — a turn no
|
|
210
|
+
* layer prices, and a stream broken before its usage arrived, stays unpriced.
|
|
211
|
+
*/
|
|
212
|
+
export function priceTurn(
|
|
213
|
+
card: TurnPriceCard,
|
|
214
|
+
reported: ReportedCost | undefined,
|
|
215
|
+
usage: TokenUsage | undefined,
|
|
216
|
+
prices: ModelPriceTable = NO_PRICES,
|
|
217
|
+
): TurnPrice {
|
|
218
|
+
if (reported) {
|
|
219
|
+
if (!reported.byok) return { usd: reported.cost, priceSource: "provider" };
|
|
220
|
+
if (reported.upstreamCost !== undefined) {
|
|
221
|
+
return { usd: reported.cost + reported.upstreamCost, priceSource: "provider", feeUsd: reported.cost };
|
|
222
|
+
}
|
|
223
|
+
// A BYOK chunk without the vendor's charge: the fee is not the turn's
|
|
224
|
+
// dollars, so it rides beside whatever layer below prices the tokens —
|
|
225
|
+
// never as an authoritative figure that understates the turn.
|
|
226
|
+
return { ...priceTurn(card, undefined, usage, prices), feeUsd: reported.cost };
|
|
227
|
+
}
|
|
228
|
+
if (!usage) return { priceSource: "none" };
|
|
229
|
+
const operator = prices[card.ref];
|
|
230
|
+
if (operator) return { usd: turnTokensUsd(usage, operator), priceSource: "operator" };
|
|
231
|
+
if (card.price) {
|
|
232
|
+
return {
|
|
233
|
+
usd: cardPriceUsd(usage, card.price),
|
|
234
|
+
priceSource: card.pricedBy === "operator" ? "operator" : "registry",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const list = anthropicPriceOf(modelIdOf(card.ref));
|
|
238
|
+
if (list) {
|
|
239
|
+
return {
|
|
240
|
+
usd: turnTokensUsd(usage, {
|
|
241
|
+
input: list.input,
|
|
242
|
+
output: list.output,
|
|
243
|
+
cacheRead: list.cacheRead,
|
|
244
|
+
cacheWrite: list.cacheWrite5m,
|
|
245
|
+
}),
|
|
246
|
+
priceSource: "registry",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { priceSource: "none" };
|
|
250
|
+
}
|
|
251
|
+
|
|
151
252
|
/** What a model's counted tokens cost at a price, USD. */
|
|
152
253
|
export function modelUsageUsd(m: ModelUsage, p: ModelPrice): number {
|
|
153
254
|
return (
|
|
@@ -211,6 +312,15 @@ export function llmUsdOfUsage(
|
|
|
211
312
|
let unpricedTokens = 0;
|
|
212
313
|
const byModel: Record<string, PricedModelUsage> = {};
|
|
213
314
|
for (const [ref, m] of Object.entries(usage.byModel)) {
|
|
315
|
+
// A model whose spans priced their own turns (model-proxy item 6) keeps
|
|
316
|
+
// the recorded figure — a provider-reported cost beats any table — and
|
|
317
|
+
// null (a turn without a figure) reads as unpriced, never re-priced here.
|
|
318
|
+
if (m.usd !== undefined) {
|
|
319
|
+
if (m.usd === null) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
|
|
320
|
+
else usd += m.usd;
|
|
321
|
+
byModel[ref] = { ...m, usd: m.usd };
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
214
324
|
const price = modelPriceOf(ref, prices);
|
|
215
325
|
const priced = price ? modelUsageUsd(m, price) : undefined;
|
|
216
326
|
if (priced === undefined) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// A provider's usage object as the proxy's meter reads it
|
|
2
|
+
// (docs/reference/specs/model-proxy.md item 6): the three wire shapes the proxy
|
|
3
|
+
// forwards — Anthropic's `message.usage`, the Chat Completions `usage` and the
|
|
4
|
+
// Responses API's `usage` — each
|
|
5
|
+
// normalized to the one `TokenUsage` every `model.turn` span carries. Undefined
|
|
6
|
+
// unless both core counts are numbers: a malformed or absent usage never fails
|
|
7
|
+
// a completion, it only leaves the turn unmetered. The first two readers were the
|
|
8
|
+
// native adapters' until record 0032's series deleted the adapters; the proxy
|
|
9
|
+
// is their one reader now.
|
|
10
|
+
|
|
11
|
+
import type { TokenUsage } from "../provider.js";
|
|
12
|
+
|
|
13
|
+
/** Anthropic `message.usage` → TokenUsage. */
|
|
14
|
+
export function usageFromAnthropic(u: unknown): TokenUsage | undefined {
|
|
15
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
16
|
+
const o = u as Record<string, unknown>;
|
|
17
|
+
if (typeof o.input_tokens !== "number" || typeof o.output_tokens !== "number") return undefined;
|
|
18
|
+
const usage: TokenUsage = { inputTokens: o.input_tokens, outputTokens: o.output_tokens };
|
|
19
|
+
if (typeof o.cache_read_input_tokens === "number") usage.cacheReadTokens = o.cache_read_input_tokens;
|
|
20
|
+
if (typeof o.cache_creation_input_tokens === "number") usage.cacheWriteTokens = o.cache_creation_input_tokens;
|
|
21
|
+
return usage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** OpenAI-style `usage` → TokenUsage. `prompt_tokens_details.cached_tokens` is
|
|
25
|
+
* the cache-read count where the server reports one. */
|
|
26
|
+
export function usageFromOpenAI(u: unknown): TokenUsage | undefined {
|
|
27
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
28
|
+
const o = u as Record<string, unknown>;
|
|
29
|
+
if (typeof o.prompt_tokens !== "number" || typeof o.completion_tokens !== "number") return undefined;
|
|
30
|
+
const usage: TokenUsage = { inputTokens: o.prompt_tokens, outputTokens: o.completion_tokens };
|
|
31
|
+
const details = o.prompt_tokens_details;
|
|
32
|
+
if (typeof details === "object" && details !== null) {
|
|
33
|
+
const cached = (details as Record<string, unknown>).cached_tokens;
|
|
34
|
+
if (typeof cached === "number") usage.cacheReadTokens = cached;
|
|
35
|
+
}
|
|
36
|
+
return usage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A provider-reported cost beside the counters (model-proxy.md item 6): the
|
|
40
|
+
* OpenRouter final chunk's `usage.cost` in USD — on a BYOK turn (`is_byok`)
|
|
41
|
+
* the aggregator's fee alone, with what the vendor billed the operator's own
|
|
42
|
+
* key in `cost_details.upstream_inference_cost`. Read off the same usage
|
|
43
|
+
* object the counters come from; a usage without a finite `cost` reports
|
|
44
|
+
* none (Anthropic's wire never carries one). */
|
|
45
|
+
export interface ReportedCost {
|
|
46
|
+
/** The figure as reported, stored as reported (record 0052). */
|
|
47
|
+
cost: number;
|
|
48
|
+
/** OpenRouter's `is_byok`: the turn billed the operator's own vendor key. */
|
|
49
|
+
byok?: boolean;
|
|
50
|
+
/** `cost_details.upstream_inference_cost`: the vendor's own charge on a BYOK turn. */
|
|
51
|
+
upstreamCost?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The cost fields of one wire `usage` object, or undefined when it carries none. */
|
|
55
|
+
export function reportedCostOf(u: unknown): ReportedCost | undefined {
|
|
56
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
57
|
+
const o = u as Record<string, unknown>;
|
|
58
|
+
if (typeof o.cost !== "number" || !Number.isFinite(o.cost)) return undefined;
|
|
59
|
+
const details = o.cost_details;
|
|
60
|
+
const upstream =
|
|
61
|
+
typeof details === "object" && details !== null
|
|
62
|
+
? (details as Record<string, unknown>).upstream_inference_cost
|
|
63
|
+
: undefined;
|
|
64
|
+
return {
|
|
65
|
+
cost: o.cost,
|
|
66
|
+
...(o.is_byok === true ? { byok: true } : {}),
|
|
67
|
+
...(typeof upstream === "number" && Number.isFinite(upstream) ? { upstreamCost: upstream } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The Responses API's `usage` → TokenUsage. `input_tokens_details.cached_tokens`
|
|
72
|
+
* is the cache-read count and `input_tokens_details.cache_write_tokens` the
|
|
73
|
+
* cache-write count where a server reports one (OpenAI itself never does; a
|
|
74
|
+
* gateway on the wire may). `output_tokens_details.reasoning_tokens` rides
|
|
75
|
+
* inside `output_tokens` — the shape is read, never a fifth counter. */
|
|
76
|
+
export function usageFromResponses(u: unknown): TokenUsage | undefined {
|
|
77
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
78
|
+
const o = u as Record<string, unknown>;
|
|
79
|
+
if (typeof o.input_tokens !== "number" || typeof o.output_tokens !== "number") return undefined;
|
|
80
|
+
const usage: TokenUsage = { inputTokens: o.input_tokens, outputTokens: o.output_tokens };
|
|
81
|
+
const details = o.input_tokens_details;
|
|
82
|
+
if (typeof details === "object" && details !== null) {
|
|
83
|
+
const { cached_tokens, cache_write_tokens } = details as Record<string, unknown>;
|
|
84
|
+
if (typeof cached_tokens === "number") usage.cacheReadTokens = cached_tokens;
|
|
85
|
+
if (typeof cache_write_tokens === "number") usage.cacheWriteTokens = cache_write_tokens;
|
|
86
|
+
}
|
|
87
|
+
return usage;
|
|
88
|
+
}
|
|
@@ -23,7 +23,21 @@ export interface RegistryCard {
|
|
|
23
23
|
* not named (the card's reader applies pi's rule). */
|
|
24
24
|
thinkingLevelMap?: Record<string, string | null>;
|
|
25
25
|
input?: string[];
|
|
26
|
-
cost?: {
|
|
26
|
+
cost?: {
|
|
27
|
+
input?: number;
|
|
28
|
+
output?: number;
|
|
29
|
+
cacheRead?: number;
|
|
30
|
+
cacheWrite?: number;
|
|
31
|
+
/** pi's long-context tiers: the whole request re-rates at the highest tier
|
|
32
|
+
* whose threshold the input side (input + cache reads + cache writes) exceeds. */
|
|
33
|
+
tiers?: Array<{
|
|
34
|
+
inputTokensAbove?: number;
|
|
35
|
+
input?: number;
|
|
36
|
+
output?: number;
|
|
37
|
+
cacheRead?: number;
|
|
38
|
+
cacheWrite?: number;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
27
41
|
contextWindow?: number;
|
|
28
42
|
maxTokens?: number;
|
|
29
43
|
compat?: Record<string, unknown>;
|
|
@@ -209,6 +209,55 @@ export function vendorOf(
|
|
|
209
209
|
return { block, model, vendor: block, vendorId: model, vendorSource: "block" };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/** The harness-side provider a block's biller implies (record 0052's
|
|
213
|
+
* amendment: the harness write names the biller's own provider, never a
|
|
214
|
+
* generic alias). Keyed by the biller — the block's name — for the billers
|
|
215
|
+
* whose protocol a harness bundles a provider for. The wires with a package
|
|
216
|
+
* of their own (`anthropic-messages` → `@ai-sdk/anthropic`,
|
|
217
|
+
* `openai-responses` → `@ai-sdk/openai`) need no entry: the wire names the
|
|
218
|
+
* package. A chat-wire biller not here is served generically
|
|
219
|
+
* (`@ai-sdk/openai-compatible`), under which a `markers` cache rule cannot be
|
|
220
|
+
* vouched for (`decideControls`): the generic provider places no cache
|
|
221
|
+
* breakpoints. */
|
|
222
|
+
export interface BillerHarnessProvider {
|
|
223
|
+
/** The AI SDK package OpenCode's configuration names for the biller
|
|
224
|
+
* (`openCodeProviderPackage` adds the `aisdk:` prefix). */
|
|
225
|
+
openCodePackage: string;
|
|
226
|
+
/** The compat words pi keys on the biller's identity, copied once from pi's
|
|
227
|
+
* own completions detection of that biller and never inferred from a URL at
|
|
228
|
+
* run time: through the proxy pi sees the bot's URL, so `piModelsJson` must
|
|
229
|
+
* say the words the biller's own base URL would have made pi detect. */
|
|
230
|
+
piCompat: {
|
|
231
|
+
/** How the wire spells reasoning: `reasoning: { effort }` under
|
|
232
|
+
* `"openrouter"`, never the completions shape's flat `reasoning_effort`. */
|
|
233
|
+
thinkingFormat: string;
|
|
234
|
+
/** How a session id would ride the headers, were affinity ever turned on. */
|
|
235
|
+
sessionAffinityFormat: string;
|
|
236
|
+
/** The vendor-qualified id prefixes the biller grants the developer role:
|
|
237
|
+
* any other id is told `supportsDeveloperRole: false`, as pi's own
|
|
238
|
+
* detection would say against the biller directly. */
|
|
239
|
+
developerRoleIdPrefixes: readonly string[];
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The biller-to-provider table: one row per biller a harness speaks natively
|
|
244
|
+
* — OpenCode's package (U44) and pi's compat words (U45) side by side. */
|
|
245
|
+
export const BILLER_HARNESS_PROVIDERS: Readonly<Record<string, BillerHarnessProvider>> = {
|
|
246
|
+
openrouter: {
|
|
247
|
+
openCodePackage: "@openrouter/ai-sdk-provider",
|
|
248
|
+
piCompat: {
|
|
249
|
+
thinkingFormat: "openrouter",
|
|
250
|
+
sessionAffinityFormat: "openrouter",
|
|
251
|
+
developerRoleIdPrefixes: ["anthropic/", "openai/"],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/** The biller's harness-side provider, or undefined when it is served generically. */
|
|
257
|
+
export function billerHarnessProvider(biller: string | undefined): BillerHarnessProvider | undefined {
|
|
258
|
+
return biller === undefined ? undefined : BILLER_HARNESS_PROVIDERS[biller];
|
|
259
|
+
}
|
|
260
|
+
|
|
212
261
|
/** The env var Anthropic's own SDK reads when an `anthropic` provider block names none. */
|
|
213
262
|
export const ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
|
|
214
263
|
|
|
@@ -56,6 +56,8 @@ const CAUSE_OF = {
|
|
|
56
56
|
which_branch: "request",
|
|
57
57
|
workspace_lost: "system",
|
|
58
58
|
ship_budget: "request",
|
|
59
|
+
// one pipeline per thread (record 0060): the host key's claim answered thread-live
|
|
60
|
+
ship_thread_live: "request",
|
|
59
61
|
setup_failed: "system",
|
|
60
62
|
// the click on a confirmation (confirm.ts)
|
|
61
63
|
confirmation_used: "request",
|
|
@@ -85,6 +87,7 @@ const CAUSE_OF = {
|
|
|
85
87
|
ship_preflight_head_unknown: "system",
|
|
86
88
|
ship_preflight_closed_resume: "request",
|
|
87
89
|
ship_preflight_no_task: "request",
|
|
90
|
+
ship_preflight_base_missing: "request",
|
|
88
91
|
// the plan hand-off's fifteen sentences (two share `plan_history_unavailable`)
|
|
89
92
|
plan_base_unknown: "request",
|
|
90
93
|
plan_routed_seed: "request",
|
|
@@ -100,12 +103,9 @@ const CAUSE_OF = {
|
|
|
100
103
|
plan_runner_conflict: "system",
|
|
101
104
|
plan_instance_orphaned: "system",
|
|
102
105
|
plan_start_failed: "system",
|
|
103
|
-
// the
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
directive_budget: "request",
|
|
107
|
-
directive_severity: "request",
|
|
108
|
-
directive_renewals: "request",
|
|
106
|
+
// the resolve parser's thrown errors (A6 of the inventory); the directive
|
|
107
|
+
// parser no longer refuses — a token whose value is outside its vocabulary
|
|
108
|
+
// is text (the interim grammar, src/directives.ts)
|
|
109
109
|
provider_unknown: "request",
|
|
110
110
|
// the model card's refusal (record 0052, model-proxy item 11): a control the
|
|
111
111
|
// resolved card does not take, named before any card or model call
|
|
@@ -108,6 +108,10 @@ export interface Finding {
|
|
|
108
108
|
line?: number;
|
|
109
109
|
/** One line naming the issue; the full explanation lives in the prose. */
|
|
110
110
|
title: string;
|
|
111
|
+
/** Machine provenance: true only on a check finding the ship round's checks
|
|
112
|
+
* step itself appended (ship/coordinator.ts `checkFinding`) — never set from
|
|
113
|
+
* a reviewer's input, whatever id the reviewer chose. */
|
|
114
|
+
check?: true;
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
export interface ReviewVerdict {
|
|
@@ -397,6 +397,10 @@ export type ShipRoundOutcome =
|
|
|
397
397
|
| "approve"
|
|
398
398
|
| "request_changes"
|
|
399
399
|
| "no_verdict"
|
|
400
|
+
/** The round's checks step read a failed check at the reviewed head (record
|
|
401
|
+
* 0055): the failures become check findings and the findings step runs as
|
|
402
|
+
* for any changes-requested round. */
|
|
403
|
+
| "checks_failed"
|
|
400
404
|
| "aborted"
|
|
401
405
|
| "stopped"
|
|
402
406
|
/** The coding round ended at its lease with the unit unfinished and the row
|
|
@@ -536,6 +540,11 @@ export type RunEvent =
|
|
|
536
540
|
callId?: string;
|
|
537
541
|
spanId?: string;
|
|
538
542
|
logIndex?: number;
|
|
543
|
+
/** The bound the call itself declared, in ms (a bash `timeout`, clamped
|
|
544
|
+
* by nothing here — pi runs an unbounded call until the loop's end).
|
|
545
|
+
* The stall signal (docs/reference/specs/live-view.md item 32) marks a
|
|
546
|
+
* call past it; absent when the call declared none. */
|
|
547
|
+
boundMs?: number;
|
|
539
548
|
seq?: number;
|
|
540
549
|
at?: number;
|
|
541
550
|
}
|
|
@@ -671,8 +680,12 @@ export type RunEvent =
|
|
|
671
680
|
* and — for a repo run — the repo, ref, PR number and PR head as resolved
|
|
672
681
|
* BEFORE the first model turn (`RepoContext`). Published by the dispatcher
|
|
673
682
|
* right after `input`, once per run, so the run page can head its Request
|
|
674
|
-
* block with linked `owner/repo · ref · #PR · sha`.
|
|
675
|
-
*
|
|
683
|
+
* block with linked `owner/repo · ref · #PR · sha`. A HOSTED ship parent
|
|
684
|
+
* (record 0060) carries a second one at the hand-off naming its runner
|
|
685
|
+
* instance (`instanceId`): `run_meta` may repeat on such a run, and every
|
|
686
|
+
* reader of a run's instance id resolves it from the LAST `run_meta`
|
|
687
|
+
* carrying one. Additive: every consumer that only knows the other types
|
|
688
|
+
* keeps working. */
|
|
676
689
|
| {
|
|
677
690
|
type: "run_meta";
|
|
678
691
|
agent: string;
|
|
@@ -865,6 +878,23 @@ export type RunEvent =
|
|
|
865
878
|
* `coordinator_tag` is — so the thread's owner rule can find the instance
|
|
866
879
|
* from the page's ship run. Additive: unknown → ignored. */
|
|
867
880
|
| { type: "ship_handoff"; instanceId: string; seq?: number; at?: number }
|
|
881
|
+
/** A coordinator child interrupted under a deploy roll (docs/reference/specs/run-history.md
|
|
882
|
+
* item 47a): the run closes `interrupted` for a restart from its request —
|
|
883
|
+
* a workspace lost across a bot roll, the resident's container replaced
|
|
884
|
+
* with the relaunch refused — and the record says so as a typed fact
|
|
885
|
+
* beside the `resumed`/`sandbox_restarted` notes, `reason` the refusal or
|
|
886
|
+
* note in one line. Published straight to the registry by the reattach
|
|
887
|
+
* path and the run loop's interruption; the parent's wait settles on its
|
|
888
|
+
* Workflow twin (`child-interrupted-<runId>`) beside `run-finished-<runId>`
|
|
889
|
+
* and confirms by `read-record`. Additive: unknown → ignored. */
|
|
890
|
+
| { type: "child_interrupted"; parentInstanceId: string; reason: string; seq?: number; at?: number }
|
|
891
|
+
/** A coordinator child resumed across a deploy roll (run-history item 47a):
|
|
892
|
+
* the same run re-attached its workspace and carries on under its record,
|
|
893
|
+
* tag and budget. Published straight to the registry by the reattach path;
|
|
894
|
+
* the Workflow twin (`child-resumed-<runId>`) tells the parent's wait to
|
|
895
|
+
* keep waiting rather than walk out the chunk asking. Additive: unknown →
|
|
896
|
+
* ignored. */
|
|
897
|
+
| { type: "child_resumed"; parentInstanceId: string; summary: string; seq?: number; at?: number }
|
|
868
898
|
/** The review post-step's outcome when the verdict landed
|
|
869
899
|
* (docs/reference/specs/agent-review.md item 18): the pull request it was
|
|
870
900
|
* posted to, the head it was pinned to (the carried head after a rebase,
|
|
@@ -905,6 +935,25 @@ export type RunEvent =
|
|
|
905
935
|
seq?: number;
|
|
906
936
|
at?: number;
|
|
907
937
|
}
|
|
938
|
+
/** A hosted ship parent's unit fact (record 0060; docs/reference/specs/agent-ship.md
|
|
939
|
+
* item 17): the runner's routes write it to the parent run through the
|
|
940
|
+
* coordinator's `hostPublish` — `unit-start` publishes state `started` with
|
|
941
|
+
* the unit's thread key and lead, `round` the round's outcome as the unit's
|
|
942
|
+
* state, `unit-end` the ending's kind with its report — carrying the pull
|
|
943
|
+
* request number when the unit's row knows one. The run page draws it as a
|
|
944
|
+
* step whose detail is the report; the friction analyzer counts it as a
|
|
945
|
+
* side fact, never activity. Additive: unknown → ignored. */
|
|
946
|
+
| {
|
|
947
|
+
type: "ship_unit";
|
|
948
|
+
unit: string;
|
|
949
|
+
state: string;
|
|
950
|
+
threadKey?: string;
|
|
951
|
+
lead?: string;
|
|
952
|
+
report?: string;
|
|
953
|
+
pr?: number;
|
|
954
|
+
seq?: number;
|
|
955
|
+
at?: number;
|
|
956
|
+
}
|
|
908
957
|
/** The request router's decision (docs/reference/specs/routing-and-config.md
|
|
909
958
|
* item 21): the preset a plain message was routed to, the one-line reason
|
|
910
959
|
* the router gave (redacted, capped — the same text the card's `routed:`
|
|
@@ -413,8 +413,10 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
413
413
|
// call that already produced its own tool pair, and artifact beside the
|
|
414
414
|
// attach_file call (or the dispatcher's staging) that moved the file;
|
|
415
415
|
// review_artifact, pr_description, pr_opened, review_posted, the ship_round
|
|
416
|
-
// boundaries
|
|
417
|
-
//
|
|
416
|
+
// boundaries, the hosted parent's ship_unit facts (record 0060 — written by
|
|
417
|
+
// the runner's routes, not the model) and the router's route are published
|
|
418
|
+
// by the dispatcher/pipeline outside the model loop entirely. Counting any
|
|
419
|
+
// of them would distort the story.
|
|
418
420
|
if (
|
|
419
421
|
ev.type === "skill_use" ||
|
|
420
422
|
ev.type === "artifact" ||
|
|
@@ -423,8 +425,11 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
423
425
|
ev.type === "pr_opened" ||
|
|
424
426
|
ev.type === "coordinator_tag" ||
|
|
425
427
|
ev.type === "ship_handoff" ||
|
|
428
|
+
ev.type === "child_interrupted" ||
|
|
429
|
+
ev.type === "child_resumed" ||
|
|
426
430
|
ev.type === "review_posted" ||
|
|
427
431
|
ev.type === "ship_round" ||
|
|
432
|
+
ev.type === "ship_unit" ||
|
|
428
433
|
ev.type === "route" ||
|
|
429
434
|
ev.type === "refusal" ||
|
|
430
435
|
ev.type === "reference" ||
|
|
@@ -59,6 +59,14 @@ export interface LiveRunMeta {
|
|
|
59
59
|
/** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
|
|
60
60
|
postedBy?: string;
|
|
61
61
|
effort?: string;
|
|
62
|
+
/** A ship pipeline's parent (record 0060): claimed under the host key
|
|
63
|
+
* (`hostKey.ts`) while `threadKey` here names the thread itself, so every
|
|
64
|
+
* record, notice and rebuilt handle files by the metadata's thread and the
|
|
65
|
+
* occupancy readers that key on the ledger's key column never see it. */
|
|
66
|
+
hosted?: true;
|
|
67
|
+
/** The run's index label (the registry sets it at create): on the row so a
|
|
68
|
+
* hosted run listed from the ledger reads as its registry view does. */
|
|
69
|
+
label?: string;
|
|
62
70
|
ref?: string;
|
|
63
71
|
headSha?: string;
|
|
64
72
|
pr?: number;
|
|
@@ -96,10 +104,10 @@ export interface LiveRunMeta {
|
|
|
96
104
|
request?: Record<string, unknown>;
|
|
97
105
|
/** The router's decision when it chose the run's preset (routing-and-config
|
|
98
106
|
* item 21) — the same fields the record's `route` event carries. On the row
|
|
99
|
-
* so a resume repaints the card as it was (`
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
107
|
+
* so a resume repaints the card as it was (the `route reason:` note at
|
|
108
|
+
* debug, the parts) and a record built from the row knows it was routed
|
|
109
|
+
* without reading the events. Absent for a preset a person, a scope or the
|
|
110
|
+
* default chose. */
|
|
103
111
|
route?: {
|
|
104
112
|
preset: string;
|
|
105
113
|
reason: string;
|
|
@@ -113,6 +121,17 @@ export interface LiveRunMeta {
|
|
|
113
121
|
* the checklist, the pushed branch, the review head, the infra counters). */
|
|
114
122
|
export type RunState = Record<string, unknown>;
|
|
115
123
|
|
|
124
|
+
/** A hosted ship parent's hosting fact on the row's state (record 0060): the
|
|
125
|
+
* runner instance the run hosts and the deadline past which a reclaim closes
|
|
126
|
+
* the row `interrupted` instead of re-hosting it. Set by the ship branch once
|
|
127
|
+
* the instance exists; read by the boot reclaim. */
|
|
128
|
+
export interface HostingState {
|
|
129
|
+
instanceId: string;
|
|
130
|
+
/** Epoch ms: the hand-off time plus the instance's `caps.maxMinutes` plus
|
|
131
|
+
* `HOSTED_DEADLINE_MARGIN_MINUTES`. */
|
|
132
|
+
until: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
export interface LiveRunRow {
|
|
117
136
|
runId: string;
|
|
118
137
|
threadKey: string;
|
|
@@ -39,6 +39,12 @@ import {
|
|
|
39
39
|
// reached `finish`.
|
|
40
40
|
export type RunStatus = "completed" | "stopped_soft" | "stopped_hard" | "failed" | "interrupted";
|
|
41
41
|
|
|
42
|
+
/** How every store-only reader renders a record still in its provisional
|
|
43
|
+
* window (run-history.md item 27's third state): not `interrupted` (a real
|
|
44
|
+
* terminal state) and not `running` (the store cannot know). One string for
|
|
45
|
+
* the CLI's `runs list`/`runs get` and the web's index row and run page. */
|
|
46
|
+
export const PROVISIONAL_LABEL = "unfinished — no finish recorded";
|
|
47
|
+
|
|
42
48
|
const RUN_STATUSES: readonly RunStatus[] = ["completed", "stopped_soft", "stopped_hard", "failed", "interrupted"];
|
|
43
49
|
|
|
44
50
|
/** Every `runs.*` id: checked before any store call. */
|
|
@@ -115,6 +121,13 @@ export interface RunRecord {
|
|
|
115
121
|
stepCount?: number;
|
|
116
122
|
schema?: number;
|
|
117
123
|
status: RunStatus;
|
|
124
|
+
/** Present on a tombstone record still in its provisional window — the
|
|
125
|
+
* start-of-run `interrupted` or the drain-deadline upgrade — before the
|
|
126
|
+
* run's final write lands. Absent on every final (finished) record. A
|
|
127
|
+
* store-only reader must render a provisional record as "unfinished — no
|
|
128
|
+
* finish recorded" rather than as `interrupted`, because the run may still
|
|
129
|
+
* be live in a registry the reader cannot see (run-history.md item 27). */
|
|
130
|
+
provisional?: true;
|
|
118
131
|
/** The failure by name, when a `failed` run has one (item 57):
|
|
119
132
|
* `policy_refusal`, the provider refused the run's model call under its
|
|
120
133
|
* usage policy. Absent on a run that did not fail, on one that failed for
|
|
@@ -944,6 +957,9 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
944
957
|
return false;
|
|
945
958
|
}
|
|
946
959
|
if (!RUN_STATUSES.includes(r.status as RunStatus)) return false;
|
|
960
|
+
// A provisional tombstone carries `provisional: true`; any other value (false,
|
|
961
|
+
// a string, etc.) is a malformed record — only the presence of the flag matters.
|
|
962
|
+
if (r.provisional !== undefined && r.provisional !== true) return false;
|
|
947
963
|
if (!isFiniteNumber(r.eventCount) || !isFiniteNumber(r.storedEventCount)) return false;
|
|
948
964
|
if (typeof r.truncated !== "boolean") return false;
|
|
949
965
|
if (!Array.isArray(r.events)) return false;
|