@openplan/dsh-fuse 0.1.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/LICENSE +21 -0
- package/README.md +214 -0
- package/cordis.patch.yml +29 -0
- package/dist/budget-tool.d.ts +66 -0
- package/dist/budget-tool.js +108 -0
- package/dist/config.d.ts +155 -0
- package/dist/config.js +125 -0
- package/dist/fuse.d.ts +59 -0
- package/dist/fuse.js +83 -0
- package/dist/harness.d.ts +48 -0
- package/dist/harness.js +773 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +55 -0
- package/dist/meter.d.ts +71 -0
- package/dist/meter.js +73 -0
- package/dist/pricing.d.ts +193 -0
- package/dist/pricing.js +450 -0
- package/dist/router.d.ts +30 -0
- package/dist/router.js +34 -0
- package/dist/store.d.ts +168 -0
- package/dist/store.js +412 -0
- package/dist/sync.d.ts +70 -0
- package/dist/sync.js +153 -0
- package/dist/wire.d.ts +52 -0
- package/dist/wire.js +15 -0
- package/package.json +64 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dsh fuse — metering ≡ enforcement for local coding agents.
|
|
3
|
+
*
|
|
4
|
+
* A free plugin for the DeepSeek Harness that exports per-call telemetry
|
|
5
|
+
* (metrics only: model, tokens, cost — never content) and enforces the org's
|
|
6
|
+
* cost policy locally, before any token is spent. The SaaS aggregates the
|
|
7
|
+
* team's sessions and publishes the policy; the local fuse applies it offline
|
|
8
|
+
* and the batch's 429 is the secondary, central gate.
|
|
9
|
+
*
|
|
10
|
+
* ## How it extends the harness
|
|
11
|
+
*
|
|
12
|
+
* It is a **hook plugin** in the harness's own vocabulary (extension
|
|
13
|
+
* cookbook): "an ordinary Cordis plugin on an interception point". It listens
|
|
14
|
+
* on the `agent/pre-step` and `agent/request` waterfalls, and observes the
|
|
15
|
+
* durable feed through `session/event`. It is deliberately NOT a tool (the
|
|
16
|
+
* docs are explicit that deployment policy does not belong in a tool the
|
|
17
|
+
* model must choose to call) and it modifies no part of the loop.
|
|
18
|
+
*
|
|
19
|
+
* ## Local-only mode
|
|
20
|
+
*
|
|
21
|
+
* Without `baseUrl`/`orgKey` nothing ever leaves the machine: the fuse
|
|
22
|
+
* enforces the configured budgets and policies entirely offline, and the
|
|
23
|
+
* local libsql store (`$DSH_HOME/dsh-fuse/local.db` by default) is
|
|
24
|
+
* the spend source.
|
|
25
|
+
*/
|
|
26
|
+
export declare const name = "fuse";
|
|
27
|
+
import { apply as pluginApply } from "./harness.js";
|
|
28
|
+
export { assertUsableConfig, type BudgetConfig, Config, type DshPluginConfig, type PolicyConfig, resolveConfig, } from "./config.js";
|
|
29
|
+
export { type FuseBudget, type FuseDecision, type FusePolicies, fuseDecision, } from "./fuse.js";
|
|
30
|
+
export { type CallProjection, hashSessionId, type MessageProvenance, projectCall, type RequestHeaderView, type TokenUsageLike, } from "./meter.js";
|
|
31
|
+
export { createPricingCache, DEFAULT_REGISTRY_URL, estimateCostUsd, fetchPricingTable, normalizeModelId, type PricingAliases, type PricingEntry, type PricingResolution, type PricingTable, type PricingVia, parseGatewayModelEntry, parseGatewayModels, parsePricingRegistry, pricingCandidates, resolvePricingEntry, type TokenCounts, } from "./pricing.js";
|
|
32
|
+
export { type RouteDecision, type RouteReason, type RouterPolicies, routeDecision, } from "./router.js";
|
|
33
|
+
export { createLocalStore, type LocalStore, type RemotePolicy, type StoredUsage, type UsageRecord, } from "./store.js";
|
|
34
|
+
export { fetchPolicy, type OrgKeyTarget, parseRemotePolicy, type SyncResult, syncBatch, } from "./sync.js";
|
|
35
|
+
export type { BatchEvent, CutEvent, UsageEvent } from "./wire.js";
|
|
36
|
+
/**
|
|
37
|
+
* The Cordis entry.
|
|
38
|
+
*
|
|
39
|
+
* The DEFAULT export is the object form `{ name, apply, Config }` because the
|
|
40
|
+
* loader unwraps a module to `exports.default` before mounting it
|
|
41
|
+
* (`Loader.unwrapExports`) and then reads `plugin.Config` to validate the row's
|
|
42
|
+
* config. Exporting the bare `apply` function as default — the shape this
|
|
43
|
+
* package originally shipped — leaves `Config` undefined, so the schema is
|
|
44
|
+
* skipped, defaults are never applied, and `apply` receives partial config.
|
|
45
|
+
* The object form is the documented shape that keeps both together.
|
|
46
|
+
*
|
|
47
|
+
* The named exports above/below serve programmatic mounting and tests.
|
|
48
|
+
*/
|
|
49
|
+
export declare const apply: typeof pluginApply;
|
|
50
|
+
export { pluginApply as plugin };
|
|
51
|
+
declare const _default: {
|
|
52
|
+
name: string;
|
|
53
|
+
apply: typeof pluginApply;
|
|
54
|
+
Config: import("@deepseek-ai/schemastery").default<Partial<import("./config.js").DshPluginConfig>, import("./config.js").DshPluginConfig>;
|
|
55
|
+
};
|
|
56
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dsh fuse — metering ≡ enforcement for local coding agents.
|
|
3
|
+
*
|
|
4
|
+
* A free plugin for the DeepSeek Harness that exports per-call telemetry
|
|
5
|
+
* (metrics only: model, tokens, cost — never content) and enforces the org's
|
|
6
|
+
* cost policy locally, before any token is spent. The SaaS aggregates the
|
|
7
|
+
* team's sessions and publishes the policy; the local fuse applies it offline
|
|
8
|
+
* and the batch's 429 is the secondary, central gate.
|
|
9
|
+
*
|
|
10
|
+
* ## How it extends the harness
|
|
11
|
+
*
|
|
12
|
+
* It is a **hook plugin** in the harness's own vocabulary (extension
|
|
13
|
+
* cookbook): "an ordinary Cordis plugin on an interception point". It listens
|
|
14
|
+
* on the `agent/pre-step` and `agent/request` waterfalls, and observes the
|
|
15
|
+
* durable feed through `session/event`. It is deliberately NOT a tool (the
|
|
16
|
+
* docs are explicit that deployment policy does not belong in a tool the
|
|
17
|
+
* model must choose to call) and it modifies no part of the loop.
|
|
18
|
+
*
|
|
19
|
+
* ## Local-only mode
|
|
20
|
+
*
|
|
21
|
+
* Without `baseUrl`/`orgKey` nothing ever leaves the machine: the fuse
|
|
22
|
+
* enforces the configured budgets and policies entirely offline, and the
|
|
23
|
+
* local libsql store (`$DSH_HOME/dsh-fuse/local.db` by default) is
|
|
24
|
+
* the spend source.
|
|
25
|
+
*/
|
|
26
|
+
export const name = "fuse";
|
|
27
|
+
import { Config as PluginConfig } from "./config.js";
|
|
28
|
+
import { apply as pluginApply } from "./harness.js";
|
|
29
|
+
export { assertUsableConfig, Config, resolveConfig, } from "./config.js";
|
|
30
|
+
export { fuseDecision, } from "./fuse.js";
|
|
31
|
+
export { hashSessionId, projectCall, } from "./meter.js";
|
|
32
|
+
export { createPricingCache, DEFAULT_REGISTRY_URL, estimateCostUsd, fetchPricingTable, normalizeModelId, parseGatewayModelEntry, parseGatewayModels, parsePricingRegistry, pricingCandidates, resolvePricingEntry, } from "./pricing.js";
|
|
33
|
+
export { routeDecision, } from "./router.js";
|
|
34
|
+
export { createLocalStore, } from "./store.js";
|
|
35
|
+
export { fetchPolicy, parseRemotePolicy, syncBatch, } from "./sync.js";
|
|
36
|
+
/**
|
|
37
|
+
* The Cordis entry.
|
|
38
|
+
*
|
|
39
|
+
* The DEFAULT export is the object form `{ name, apply, Config }` because the
|
|
40
|
+
* loader unwraps a module to `exports.default` before mounting it
|
|
41
|
+
* (`Loader.unwrapExports`) and then reads `plugin.Config` to validate the row's
|
|
42
|
+
* config. Exporting the bare `apply` function as default — the shape this
|
|
43
|
+
* package originally shipped — leaves `Config` undefined, so the schema is
|
|
44
|
+
* skipped, defaults are never applied, and `apply` receives partial config.
|
|
45
|
+
* The object form is the documented shape that keeps both together.
|
|
46
|
+
*
|
|
47
|
+
* The named exports above/below serve programmatic mounting and tests.
|
|
48
|
+
*/
|
|
49
|
+
export const apply = pluginApply;
|
|
50
|
+
export { pluginApply as plugin };
|
|
51
|
+
export default {
|
|
52
|
+
name,
|
|
53
|
+
apply: pluginApply,
|
|
54
|
+
Config: PluginConfig,
|
|
55
|
+
};
|
package/dist/meter.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The meter — projects one completed model call from the durable session log
|
|
3
|
+
* into the metrics the fuse prices and the SaaS stores.
|
|
4
|
+
*
|
|
5
|
+
* ## Sources, and why these ones
|
|
6
|
+
*
|
|
7
|
+
* - **Model and provider** come from the assistant message's own provenance.
|
|
8
|
+
* `assistant/message.message.source` is `AssistantProvenance {provider,
|
|
9
|
+
* model}`: "Model-produced assistant messages name the provider and model
|
|
10
|
+
* that produced them", and the loop "stores the assembled assistant content
|
|
11
|
+
* with the provider and model that produced it". This is per-call truth. A
|
|
12
|
+
* session-keyed cache of the latest `request/header` — the previous
|
|
13
|
+
* implementation — resolves to "unknown" on a resumed session or a missing
|
|
14
|
+
* header, and then prices the call at zero.
|
|
15
|
+
* - **Reasoning effort** comes from the request header's `LlmCallConfig`,
|
|
16
|
+
* which is the only place the log records it. When the caller omitted an
|
|
17
|
+
* effort and the ADAPTER materialized its own default, the log records that
|
|
18
|
+
* a default was applied (`EpochHeader.adapterDefaults.reasoningEffort`) but
|
|
19
|
+
* not the value; the projection therefore reports the effort the log states
|
|
20
|
+
* and stays empty otherwise rather than guessing.
|
|
21
|
+
* - **Tokens** are the harness's own disjoint counts (`TokenUsage`: uncached
|
|
22
|
+
* input, output, cache reads, cache writes).
|
|
23
|
+
* - **Duration** is measured from the step's `step/start` to the assistant
|
|
24
|
+
* settlement, i.e. the model request's latency for that step. The durable
|
|
25
|
+
* `assistant/message.stream` is not persisted by this build (verified
|
|
26
|
+
* against real logs: `data` carries only `message/step/turn/usage`), so the
|
|
27
|
+
* step boundary is the honest available source. `null` when the step's start
|
|
28
|
+
* was not observed (a resumed session, or a delivery that began before the
|
|
29
|
+
* plugin loaded).
|
|
30
|
+
*/
|
|
31
|
+
import type { TokenCounts } from "./pricing.js";
|
|
32
|
+
/** The harness's per-call token accounting (`TokenUsage`). */
|
|
33
|
+
export interface TokenUsageLike {
|
|
34
|
+
inputTokens: number;
|
|
35
|
+
outputTokens: number;
|
|
36
|
+
cacheReadTokens?: number;
|
|
37
|
+
cacheWriteTokens?: number;
|
|
38
|
+
}
|
|
39
|
+
/** `AssistantProvenance` — the provider/model that produced one message. */
|
|
40
|
+
export interface MessageProvenance {
|
|
41
|
+
provider?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
}
|
|
44
|
+
/** The subset of `EpochHeader` the meter reads (never the system prompt). */
|
|
45
|
+
export interface RequestHeaderView {
|
|
46
|
+
provider?: string;
|
|
47
|
+
model?: string;
|
|
48
|
+
reasoningEffort?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface CallProjection {
|
|
51
|
+
model: string;
|
|
52
|
+
provider: string;
|
|
53
|
+
reasoningEffort: string;
|
|
54
|
+
counts: Required<TokenCounts>;
|
|
55
|
+
durationMs: number | null;
|
|
56
|
+
}
|
|
57
|
+
/** sha256 — the session id never leaves the machine raw. */
|
|
58
|
+
export declare function hashSessionId(sessionId: string): Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Project one completed model call. Pure — no I/O, no clock: the caller
|
|
61
|
+
* supplies the settlement time and (when observed) the step's start time.
|
|
62
|
+
*/
|
|
63
|
+
export declare function projectCall(input: {
|
|
64
|
+
provenance?: MessageProvenance;
|
|
65
|
+
header?: RequestHeaderView;
|
|
66
|
+
usage: TokenUsageLike;
|
|
67
|
+
/** Epoch ms of the assistant settlement. */
|
|
68
|
+
settledAt: number;
|
|
69
|
+
/** Epoch ms when this step opened, when observed. */
|
|
70
|
+
stepStartedAt?: number;
|
|
71
|
+
}): CallProjection;
|
package/dist/meter.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The meter — projects one completed model call from the durable session log
|
|
3
|
+
* into the metrics the fuse prices and the SaaS stores.
|
|
4
|
+
*
|
|
5
|
+
* ## Sources, and why these ones
|
|
6
|
+
*
|
|
7
|
+
* - **Model and provider** come from the assistant message's own provenance.
|
|
8
|
+
* `assistant/message.message.source` is `AssistantProvenance {provider,
|
|
9
|
+
* model}`: "Model-produced assistant messages name the provider and model
|
|
10
|
+
* that produced them", and the loop "stores the assembled assistant content
|
|
11
|
+
* with the provider and model that produced it". This is per-call truth. A
|
|
12
|
+
* session-keyed cache of the latest `request/header` — the previous
|
|
13
|
+
* implementation — resolves to "unknown" on a resumed session or a missing
|
|
14
|
+
* header, and then prices the call at zero.
|
|
15
|
+
* - **Reasoning effort** comes from the request header's `LlmCallConfig`,
|
|
16
|
+
* which is the only place the log records it. When the caller omitted an
|
|
17
|
+
* effort and the ADAPTER materialized its own default, the log records that
|
|
18
|
+
* a default was applied (`EpochHeader.adapterDefaults.reasoningEffort`) but
|
|
19
|
+
* not the value; the projection therefore reports the effort the log states
|
|
20
|
+
* and stays empty otherwise rather than guessing.
|
|
21
|
+
* - **Tokens** are the harness's own disjoint counts (`TokenUsage`: uncached
|
|
22
|
+
* input, output, cache reads, cache writes).
|
|
23
|
+
* - **Duration** is measured from the step's `step/start` to the assistant
|
|
24
|
+
* settlement, i.e. the model request's latency for that step. The durable
|
|
25
|
+
* `assistant/message.stream` is not persisted by this build (verified
|
|
26
|
+
* against real logs: `data` carries only `message/step/turn/usage`), so the
|
|
27
|
+
* step boundary is the honest available source. `null` when the step's start
|
|
28
|
+
* was not observed (a resumed session, or a delivery that began before the
|
|
29
|
+
* plugin loaded).
|
|
30
|
+
*/
|
|
31
|
+
/** sha256 — the session id never leaves the machine raw. */
|
|
32
|
+
export async function hashSessionId(sessionId) {
|
|
33
|
+
const data = new TextEncoder().encode(sessionId);
|
|
34
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
35
|
+
return [...new Uint8Array(digest)]
|
|
36
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
37
|
+
.join("");
|
|
38
|
+
}
|
|
39
|
+
/** Coerce one provider-reported count to a non-negative integer. */
|
|
40
|
+
function count(value) {
|
|
41
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
42
|
+
return 0;
|
|
43
|
+
return Math.floor(value);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Project one completed model call. Pure — no I/O, no clock: the caller
|
|
47
|
+
* supplies the settlement time and (when observed) the step's start time.
|
|
48
|
+
*/
|
|
49
|
+
export function projectCall(input) {
|
|
50
|
+
// Provenance wins: it identifies the model that actually served the call.
|
|
51
|
+
const model = input.provenance?.model ?? input.header?.model ?? "";
|
|
52
|
+
const provider = input.provenance?.provider ??
|
|
53
|
+
input.header?.provider ??
|
|
54
|
+
(model.includes("/") ? model.split("/")[0] : "") ??
|
|
55
|
+
"";
|
|
56
|
+
const durationMs = typeof input.stepStartedAt === "number" &&
|
|
57
|
+
Number.isFinite(input.stepStartedAt) &&
|
|
58
|
+
input.settledAt >= input.stepStartedAt
|
|
59
|
+
? Math.round(input.settledAt - input.stepStartedAt)
|
|
60
|
+
: null;
|
|
61
|
+
return {
|
|
62
|
+
model,
|
|
63
|
+
provider,
|
|
64
|
+
reasoningEffort: input.header?.reasoningEffort ?? "",
|
|
65
|
+
counts: {
|
|
66
|
+
inputTokens: count(input.usage.inputTokens),
|
|
67
|
+
outputTokens: count(input.usage.outputTokens),
|
|
68
|
+
cacheReadTokens: count(input.usage.cacheReadTokens),
|
|
69
|
+
cacheWriteTokens: count(input.usage.cacheWriteTokens),
|
|
70
|
+
},
|
|
71
|
+
durationMs,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pricing for the fuse and the meter — resolved generically, never hand-maintained.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this module exists at all
|
|
5
|
+
*
|
|
6
|
+
* The harness reports **tokens**, exactly and for free (`TokenUsage`), and models
|
|
7
|
+
* **no monetary cost anywhere** (the only "pricing" in the harness is per-route
|
|
8
|
+
* *image* token pricing). The proposal asks for `cost_usd` and `limitUsd`, so
|
|
9
|
+
* `USD = tokens × price` and this module owns the `price` half.
|
|
10
|
+
*
|
|
11
|
+
* Nobody is invoiced from this number: its jobs are to express the customer's own
|
|
12
|
+
* policy in the unit they wrote it in, and to rank projects/sessions against each
|
|
13
|
+
* other. An approximate price is therefore fine; a MISSING price is fatal — it
|
|
14
|
+
* prices every call at zero, so no budget ever cuts and both gates go inert. That
|
|
15
|
+
* is the defect this resolver exists to prevent.
|
|
16
|
+
*
|
|
17
|
+
* ## Resolution order (measured against real gateways)
|
|
18
|
+
*
|
|
19
|
+
* 1. `pricingAliases[model]` — explicit escape hatch, highest precedence.
|
|
20
|
+
* 2. `${provider}/${model}` — the exact route, from the registry or the gateway's
|
|
21
|
+
* own `/models`. This is the correct key: the same model costs different
|
|
22
|
+
* amounts on different routes (`deepseek/deepseek-v4.1-flash` ranges
|
|
23
|
+
* $0.15–$0.375 per 1M input across registries), so keying on model alone is
|
|
24
|
+
* wrong in principle.
|
|
25
|
+
* 3. bare `model` — the provider is absent from the registry (a private gateway).
|
|
26
|
+
* Falls back to the **consensus** price across every provider that publishes
|
|
27
|
+
* the model: the modal quote, which measurements show is the upstream list
|
|
28
|
+
* price that pass-through resellers charge "at cost".
|
|
29
|
+
* 4. progressively trimmed variants, then `${provider}/${bare}`.
|
|
30
|
+
* 5. `null` — reported as **unpriced**, never silently zero.
|
|
31
|
+
*
|
|
32
|
+
* Verified on this machine's real routes: a pass-through reseller that publishes
|
|
33
|
+
* "at cost" pricing reproduces the registry's numbers to the cent (8/8 models),
|
|
34
|
+
* and the generic resolver prices 68/69 of its catalogue with no per-provider
|
|
35
|
+
* configuration.
|
|
36
|
+
*/
|
|
37
|
+
export interface PricingEntry {
|
|
38
|
+
inputCentsPerM: number;
|
|
39
|
+
outputCentsPerM: number;
|
|
40
|
+
cacheReadCentsPerM?: number;
|
|
41
|
+
cacheWriteCentsPerM?: number;
|
|
42
|
+
}
|
|
43
|
+
export type PricingTable = Record<string, PricingEntry>;
|
|
44
|
+
/** Token counts priced by {@link estimateCostUsd}. */
|
|
45
|
+
export interface TokenCounts {
|
|
46
|
+
inputTokens: number;
|
|
47
|
+
outputTokens: number;
|
|
48
|
+
cacheReadTokens: number;
|
|
49
|
+
cacheWriteTokens?: number;
|
|
50
|
+
}
|
|
51
|
+
/** Explicit `model id → price-table key` overrides; highest precedence. */
|
|
52
|
+
export type PricingAliases = Record<string, string>;
|
|
53
|
+
/** Default registry: 200+ providers, `cost` in USD per 1M tokens. */
|
|
54
|
+
export declare const DEFAULT_REGISTRY_URL = "https://models.dev/api.json";
|
|
55
|
+
/** How a key was derived from the requested model id — feeds diagnostics. */
|
|
56
|
+
export type PricingVia = "alias" | "exact" | "route" | "model" | "suffix" | "provider";
|
|
57
|
+
/** Which candidate key resolved, and how. */
|
|
58
|
+
export interface PricingResolution {
|
|
59
|
+
entry: PricingEntry;
|
|
60
|
+
/** The table key that matched. */
|
|
61
|
+
key: string;
|
|
62
|
+
/** How the key was derived from the requested model id. */
|
|
63
|
+
via: PricingVia;
|
|
64
|
+
}
|
|
65
|
+
/** Lowercase + separator-normalized form used for every lookup. */
|
|
66
|
+
export declare function normalizeModelId(model: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Ordered candidate keys for one model id, most specific first.
|
|
69
|
+
*
|
|
70
|
+
* `provider/anthropic/claude-sonnet-5` yields the full id, then each
|
|
71
|
+
* progressively stripped suffix (`anthropic/claude-sonnet-5`,
|
|
72
|
+
* `claude-sonnet-5`) — so a table keyed by a bare model id, by a
|
|
73
|
+
* vendor-qualified id, or by the adapter's own prefixed id all resolve. When
|
|
74
|
+
* the caller knows the provider route, `${provider}/${bare}` is tried too
|
|
75
|
+
* (a table keyed `openai/gpt-4o` still prices a reported `gpt-4o`).
|
|
76
|
+
*/
|
|
77
|
+
export declare function pricingCandidates(model: string, provider?: string): string[];
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the price entry for one model id.
|
|
80
|
+
*
|
|
81
|
+
* Precedence: explicit alias → exact key → `${provider}/${bare}` → bare model →
|
|
82
|
+
* progressively stripped suffix. Returns `null` when nothing matches, so callers
|
|
83
|
+
* can report the miss (and mark the call unpriced) instead of pricing at zero.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolvePricingEntry(table: PricingTable | undefined, model: string, opts?: {
|
|
86
|
+
provider?: string;
|
|
87
|
+
aliases?: PricingAliases;
|
|
88
|
+
}): PricingResolution | null;
|
|
89
|
+
/**
|
|
90
|
+
* Cost in USD from the resolved table entry (cents per 1M tokens).
|
|
91
|
+
*
|
|
92
|
+
* Cache reads and writes price at the entry's own cache rate when the table
|
|
93
|
+
* publishes one, and at the INPUT rate when it does not — deliberately
|
|
94
|
+
* conservative: the fuse prefers overestimating spend to missing a cut. The
|
|
95
|
+
* harness reports cache counts as DISJOINT from `inputTokens` (`TokenUsage`:
|
|
96
|
+
* "billed input = sum of the three"), so nothing is double counted here, and
|
|
97
|
+
* `reasoningTokens` is deliberately ignored because the docs state it is
|
|
98
|
+
* "already included in `outputTokens`; totals must not add it again".
|
|
99
|
+
*
|
|
100
|
+
* @returns USD cost plus the key that priced it, or `null` when the model is
|
|
101
|
+
* unpriced — this function never invents a price.
|
|
102
|
+
*/
|
|
103
|
+
export declare function estimateCostUsd(table: PricingTable | undefined, model: string, counts: TokenCounts, opts?: {
|
|
104
|
+
provider?: string;
|
|
105
|
+
aliases?: PricingAliases;
|
|
106
|
+
}): {
|
|
107
|
+
costUsd: number;
|
|
108
|
+
key: string;
|
|
109
|
+
via: PricingVia;
|
|
110
|
+
} | null;
|
|
111
|
+
/**
|
|
112
|
+
* Parse the registry into a table keyed BOTH ways:
|
|
113
|
+
*
|
|
114
|
+
* - `${provider}/${model}` — the exact route (authoritative).
|
|
115
|
+
* - bare `${model}` — the **modal** (most-agreed) quote across every provider,
|
|
116
|
+
* used only when the route is absent. Measurements show this modal value is
|
|
117
|
+
* the upstream list price (e.g. 34 of 39 providers quote exactly $5.00/1M for
|
|
118
|
+
* `gpt-5.5`), which is what a pass-through reseller charges at cost.
|
|
119
|
+
*
|
|
120
|
+
* Every key is normalized so a reported `deepseek/deepseek-v4.1-flash` and a
|
|
121
|
+
* registry `deepseek-v4.1-flash` land on the same lookup.
|
|
122
|
+
*/
|
|
123
|
+
export declare function parsePricingRegistry(json: unknown): {
|
|
124
|
+
table: PricingTable;
|
|
125
|
+
routes: number;
|
|
126
|
+
models: number;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Normalize one OpenAI-compatible `/models` entry that publishes prices.
|
|
130
|
+
*
|
|
131
|
+
* The convention is real but the shape is not standardized; three layouts cover
|
|
132
|
+
* every publicly-readable gateway measured:
|
|
133
|
+
* - OpenRouter: `pricing.{prompt,completion,input_cache_read,input_cache_write}`
|
|
134
|
+
* in USD **per token** (strings).
|
|
135
|
+
* - DeepInfra: `metadata.pricing.{input_tokens,output_tokens,cache_read_tokens}`
|
|
136
|
+
* in USD **per 1M**.
|
|
137
|
+
* - Novita: `input_token_price_per_m` / `output_token_price_per_m` per 1M.
|
|
138
|
+
* A registry-shaped `cost.{input,output,cache_read,cache_write}` is accepted too.
|
|
139
|
+
*/
|
|
140
|
+
export declare function parseGatewayModelEntry(model: unknown): PricingEntry | null;
|
|
141
|
+
/**
|
|
142
|
+
* Parse a gateway `/models` response, keyed by `${provider}/${model}`.
|
|
143
|
+
*
|
|
144
|
+
* Returns an empty table when the gateway publishes no prices (e.g. command-code)
|
|
145
|
+
* — the caller then falls back to the registry by model id.
|
|
146
|
+
*/
|
|
147
|
+
export declare function parseGatewayModels(json: unknown, provider: string): {
|
|
148
|
+
table: PricingTable;
|
|
149
|
+
priced: number;
|
|
150
|
+
total: number;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Fetch + merge every configured price source. Never throws: a failed source
|
|
154
|
+
* keeps whatever the config already carries, and a config override always wins
|
|
155
|
+
* on conflicts.
|
|
156
|
+
*
|
|
157
|
+
* The registry is the primary source (200+ providers, keyed by route, carries
|
|
158
|
+
* cache read/write). A gateway `/models` is consulted first when configured,
|
|
159
|
+
* because a gateway that publishes its own prices is authoritative for its own
|
|
160
|
+
* routes.
|
|
161
|
+
*/
|
|
162
|
+
export declare function fetchPricingTable(input: {
|
|
163
|
+
/** Registry URL; pass `""` to skip. Defaults to {@link DEFAULT_REGISTRY_URL}. */
|
|
164
|
+
registryUrl?: string;
|
|
165
|
+
/** OpenAI-compatible `/models` URL that may publish prices. */
|
|
166
|
+
gatewayUrl?: string;
|
|
167
|
+
/** Provider id used to key the gateway's routes (e.g. "opencode-go"). */
|
|
168
|
+
gatewayProvider?: string;
|
|
169
|
+
/** Env var holding the gateway bearer token, when the endpoint needs auth. */
|
|
170
|
+
gatewayApiKeyEnv?: string;
|
|
171
|
+
/** User config wins over every fetched source on per-key conflicts. */
|
|
172
|
+
override?: PricingTable;
|
|
173
|
+
fetchImpl?: typeof fetch;
|
|
174
|
+
}): Promise<{
|
|
175
|
+
table: PricingTable;
|
|
176
|
+
failures: string[];
|
|
177
|
+
routes: number;
|
|
178
|
+
models: number;
|
|
179
|
+
gatewayPriced: number;
|
|
180
|
+
}>;
|
|
181
|
+
/**
|
|
182
|
+
* TTL cache for the sync (refresh hourly; the first call inside the plugin is
|
|
183
|
+
* non-blocking — the fuse starts on the config table and upgrades when the
|
|
184
|
+
* fetch lands). `onUpdate` lets the caller persist the table for offline use.
|
|
185
|
+
*/
|
|
186
|
+
export declare function createPricingCache(fetchTable: () => Promise<{
|
|
187
|
+
table: PricingTable;
|
|
188
|
+
}>, ttlMs?: number, onUpdate?: (table: PricingTable) => void): {
|
|
189
|
+
current: () => PricingTable;
|
|
190
|
+
refresh: () => void;
|
|
191
|
+
/** Seed the cache from persisted state (offline first boot). */
|
|
192
|
+
hydrate: (table: PricingTable) => void;
|
|
193
|
+
};
|