/** * Cost / budget / trace wiring for the Runner (core 5.x) — pure, testable helpers kept out of the * auto-running main.ts: * ③ buildPricing — Model.cost catalog → injected ModelPricing (authoritative costMicroUsd). * ④ createTracer — core's content-free TraceEvents → /metrics (spend, first-token, tool calls). * ⑤ cappedCeiling — clamp a caller's requested per-task budget DOWN to the operator ceiling. */ import { type Model, type ModelPricing, type TracerHook } from "@sema-agent/core"; import type { Metrics } from "./observability/metrics.js"; import type { QuotaTracker } from "./observability/cost-quota.js"; import type { FleetUsageAccumulator } from "./fleet-client.js"; import type { FleetLeaseManager } from "./fleet-lease.js"; import { type PromptManifestTracker } from "./observability/prompt-manifest.js"; /** ③ Per-model pricing keyed by model id (core falls back to each Model.cost when a model is absent). * B-089:`m.cost` 运行期可缺席(未定价的模型不铸这个键 —— `config.ts` 的 `buildModelCostKey`),于是 * 未定价模型在本表里**没有行**;core 的 `isModelPriced` 两边都读(`pricing[id]` 或 `model.cost`), * 两边同时缺席 = 这只模型未定价 ⇒ 成本读面整键缺席,而不是编造的 0。 * * 🔴 S-231 —— 建行判据就是 core 那只谓词本身,不再同形手抄。第二参传 `undefined` 是**语义**不是省事: * 这张表正是本函数在建,建之前它不存在,于是谓词落在它的另一半「模型自报价了吗」上。表建好之后 * 「本部署配没配价目表」这一问归 {@link isPricingConfigured}(它数的是行;每一行都只由本谓词放进来, * 所以两问不可能漂成两个答案)。 */ export declare function buildPricing(models: Record): Record; /** * 缝合审 M3:`capabilities.pricingConfigured` 的判据 —— 提成函数放在 pricing 自己的文件里,因为 * **这个表是热变的**(`boot/config-center.ts` 的 `mutateInPlace(pricing, buildPricing(config.models))`, * 那行注释自己写着 `hot: cost/model changes`)。旧码在 `main.ts` 的 capabilities 字面量里把它算成一个 * **boot 时的布尔快照**,于是运维在 config center 里加上/撤掉价目表之后,`/v1/capabilities` 还报旧值 —— * 而这个键的**全部用途**就是给消费端消歧「costMicroUsd=0 是免费跑,还是根本没配价目表」,报错了就是 * 把消歧信号变成误导信号。main.ts 侧改用 getter(读时求值)。 * * 同一条论证在同一个字面量里**已经执行过一次**:紧邻的 `restartState: () => configCenter.restartState()` * 带着注释「A live getter (not a snapshot) so /health always reads current」—— 兄弟键漏了。本仓第 7 例同形。 * * 🔴 B-089(判据本身,同一条「0 不是缺席」的病):此前判的是「**有没有一项正价**」 * (`some(v > 0)`),于是一台把四项单价**显式**配成 0 的部署(= 运营者声明这个模型免费)被报成 * 「没配价目表」—— 正好是本键要消歧的两态被折成同一个 false,消费端把一个可信的 $0 渲成 unknown。 * 判据改为**表在不在**:与 core [ref] 的 `isModelPriced`(「price table 是否 EXIST」)同向,也与 * 本键在 wire 契约里的原话(「价目表是否已配」)一致。 * 三态读回:未定价 ⇒ 空表 ⇒ false(与改前同值);声明免费 ⇒ 有行(全零)⇒ true(**唯一变的一格**); * 配了真价 ⇒ true(与改前同值)。 * * 🔴 S-231 —— 本函数**不是** `isModelPriced` 的第二份实现,它问的是**部署级**的另一问:「这张表里有行吗」。 * 「某一只模型定价了吗」只有 core 那一只谓词回答,而这张表的每一行都只由它放进来(见 {@link buildPricing}), * 所以两问结构上不可能漂成两个答案。 */ export declare function isPricingConfigured(pricing: Record): boolean; /** * Bound the cardinality of a free-form metric label value (BL-11/BL-21). A label fed by untrusted/unbounded * input — MCP tool names (a misconfigured/untrusted server can emit thousands of dynamic names) or center * model ids (a malformed roster) — would otherwise mint an unbounded number of metric series: a slow memory * leak in the registry + a /metrics scrape blow-up. We track the DISTINCT values seen; once the cap is hit, * further NEW values collapse into one `__other__` bucket (already-seen values still report exactly). The * cap is made observable (no silent truncation) via `onOverflow`, fired once when the cap is first crossed. */ export declare function cardinalityGuard(cap: number, onOverflow: () => void): (value: string) => string; /** Per-model usage accumulated for one task (shell-host contract E8). Token names mirror `turn_end.usage` / * the contract's per-model entry; `costMicroUsd` is micro-USD (÷1e6 for USD). */ export interface ModelUsageDelta { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; /** 🔴 [ref](core 2.3.0 起)**可选 = 缺席表示「未知」,不是 0**。unpriced 部署(目录里该模型没配 * `cost`、也没注 `pricing`)下 core 在 `brain.call` 帧上**整键缺席**,我们如实透传;显式全零 `cost` * = 运营者声明免费 ⇒ 键在场且为 0。两者可区分,这正是 #59 折零案的源头修复。 * 与 SDK `ModelUsageDelta`(五键全可选)/ `usage-analytics.UsageModelEntry` 同轨。 */ costMicroUsd?: number; /** core 3.0.0([ref] 装车单 S10,§DESIGN-V2 V2-⑤)可选口径标记:标出这一行的 token 分量是在哪套记账 * 基准下写的(当前唯一值 = `"uncached-components-v1"`:`inputTokens` = cache-MISS 分量,`cacheRead`/ * `cacheWrite` 分列)。**写点在 `trace/project.ts` 的 durable 行构造处**(`appendModelUsageDelta`), * 不在这里 —— {@link ModelUsageTracker.record} 的累加逻辑本身零改动,永远不设置这个键(它只在 drain * 之后、落盘之前被打上)。缺席 = 落盘更早、口径未知的存量行(usage-analytics 读端据此诚实标注)。 */ usageBasis?: string; } /** * E8 (shell-host contract): per-task × per-model usage accumulator, fed (synchronously) by the `brain.call` * tracer. The `TracerHook` contract is SYNC / no-IO, so we cannot persist from the hook; instead we accumulate * here in memory and the run lifecycle DRAINS us at each turn boundary, appending the delta to the durable run * event log. The **append-only delta** model is what makes it resume-safe: a task that suspends and resumes in a * different process appends only its own post-resume deltas; the result echo SUMS all `model_usage` events across * the whole (possibly multi-process) run. Memory is bounded — a task's entry is dropped on {@link drain}/{@link clear}. */ export declare class ModelUsageTracker { private readonly byTask; private readonly active; /** Mark a TOP-LEVEL run active so its `brain.call` usage is tracked. **Required** — `record` ignores any taskId * that isn't registered. This is the leak fence: delegated SUB-TASKS (team members, * independent verifiers) run on throwaway core-minted sessionIds that the run lifecycle never registers/drains, * so without this gate their usage would accumulate under ids nothing ever clears → an unbounded process-lifetime * leak. Sub-task spend is intentionally NOT in the per-run `modelUsage` echo (it lands in `TaskStats.nested`); * `modelUsage` is the TOP-LEVEL run's own per-model usage. Call at run start; {@link clear} unregisters. */ register(taskId: string): void; /** Accumulate one `brain.call`'s usage — ONLY for a registered top-level run (else a no-op; see {@link register}). * Sync, never throws (TracerHook contract). */ record(taskId: string, model: string, d: ModelUsageDelta): void; /** Return the usage accumulated for this task SINCE the last drain (per neutral model id), and RESET it. * `undefined` when nothing accrued — callers skip the durable append then. */ drain(taskId: string): Record | undefined; /** Drop a task's pending accumulation AND unregister it (cleanup at run end / on an abandoned/errored leg). After * this, further `record(taskId,…)` no-ops until a fresh {@link register}. `drain` deliberately does NOT unregister * (the run continues across turns); only `clear` ends the run's tracking. */ clear(taskId: string): void; } export { type PromptManifestRecord, promptManifestRecordOf, configAssembledRecordOf, PromptManifestTracker, } from "./observability/prompt-manifest.js"; /** * ④ Bridge core's fire-and-forget trace events to /metrics (+ the E8 per-task model-usage accumulator). The hook * is void / never throws / does no sync IO (in-memory counters only), per the TracerHook contract. `brain.call` * fires for EVERY call (top-level + async + council sub-tasks) → `model_cost_micro_usd_total` is the authoritative * spend, and (when a {@link ModelUsageTracker} is supplied) the per-task `modelUsage` echo's source. */ export declare function createTracer(metrics: Pick, costQuota?: QuotaTracker, modelUsage?: ModelUsageTracker, fleetUsage?: Pick, fleetLease?: Pick, quotaWeightFor?: (model: string) => number, promptManifests?: PromptManifestTracker): TracerHook; /** * [ref] side-query 记账 seam(codex R3 high:sideQuery 不走 core tracer——brain.stream 直调不发 * brain.call——createTracer 的四路 sink(costQuota/model_cost 指标/fleetUsage 批报/fleetLease 本地扣减) * 全部旁路)。本 seam 与 createTracer 的 brain.call 臂**同语义**喂同一组 sink:weightedTokens 同式 * (tokens×quotaWeight,weight-at-burn 固化)、micro-USD 同轴。装配点与 createTracer 同一处(main.ts), * 传同一组实例——两臂永不分家。 * * 已知残差(codex R3 medium,记档):cost 源=core SideQueryResult.usage.cost.total(brain 层计价), * OpenAI cache-heavy 输入按全价入账(tracer 的 e.costMicroUsd 是 cache-family-aware 价)——方向=多收 * (保守,绝不少收);根修=core 在 SideQueryResult 上回权威 costMicroUsd(已提货 core)。model 标签/ * weight 键用**路由身份 r.model**(池配置有界)非 servedModel(provider 上报,无界)。 */ export declare function createSideQueryAccountant(metrics: Pick, costQuota?: QuotaTracker, fleetUsage?: Pick, fleetLease?: Pick, quotaWeightFor?: (model: string) => number): (principal: string | undefined, r: { model: string; usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; cost?: { total?: number; }; }; }) => void; /** * ⑤ Resolve a per-task budget: honor the caller's requested value but never let it EXCEED the operator * ceiling. `0`/unset ceiling = no limit (returns the caller's value, or undefined). A non-positive / * non-numeric request is ignored (falls back to the ceiling). Returns undefined = "no budget gate". */ export declare function cappedCeiling(requested: unknown, ceiling: number): number | undefined; //# sourceMappingURL=budget.d.ts.map