@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/config.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration — schema + cross-field validation.
|
|
3
|
+
*
|
|
4
|
+
* The harness plugin-config contract (develop/basic/config) asks for an
|
|
5
|
+
* exported `Config` that is BOTH a TypeScript type and a runtime schema:
|
|
6
|
+
* "Do not export a plain object as Config; it does not implement the Standard
|
|
7
|
+
* Schema interface required by Cordis." Cordis validates it while the plugin
|
|
8
|
+
* loads, so bad config fails the load "with a precise error — the plugin never
|
|
9
|
+
* starts half-configured".
|
|
10
|
+
*
|
|
11
|
+
* ## Why the cross-field check matters more here than in a normal plugin
|
|
12
|
+
*
|
|
13
|
+
* A cost-enforcement plugin that loads with unusable configuration does not
|
|
14
|
+
* merely misbehave — it enforces NOTHING, silently, while the deployment
|
|
15
|
+
* believes it is protected. Configuration that cannot be acted on is refused
|
|
16
|
+
* at load time (the documented rule: "A plugin should also reject
|
|
17
|
+
* schema-valid config that names an unavailable resource or provider"), and
|
|
18
|
+
* the shape that produces a silent no-op is rejected explicitly:
|
|
19
|
+
*
|
|
20
|
+
* 1. exactly one of `baseUrl` / `orgKey`. Half a sync target is not a
|
|
21
|
+
* degraded sync, it is no sync (plus no `GET /v1/policy`, so the panel
|
|
22
|
+
* cannot reach the local fuse at all).
|
|
23
|
+
*
|
|
24
|
+
* Prices are NOT required here: the plugin resolves them generically by model
|
|
25
|
+
* id (see pricing.ts) and reports an unpriced call visibly rather than
|
|
26
|
+
* refusing to load. The old "budgets require a price source" gate was removed
|
|
27
|
+
* because it forced every deployment to hand-maintain a price table — exactly
|
|
28
|
+
* the maintenance burden the generic resolver exists to eliminate; a missing
|
|
29
|
+
* price is a visible state, not a boot failure and not a silent zero.
|
|
30
|
+
*/
|
|
31
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
32
|
+
/**
|
|
33
|
+
* The runtime schema Cordis validates before `apply` runs.
|
|
34
|
+
*
|
|
35
|
+
* Annotated as `Schema<Partial<DshPluginConfig>, DshPluginConfig>`: the first
|
|
36
|
+
* parameter is what a `cordis.yml` entry supplies (every field optional, filled
|
|
37
|
+
* by the schema defaults) and the second is what `apply` receives (complete).
|
|
38
|
+
* An explicit annotation is required because the inferred form cannot be named
|
|
39
|
+
* in the emitted declarations — it reaches into Schemastery's own type helpers
|
|
40
|
+
* and their vendored dependencies, which breaks the build of a package that
|
|
41
|
+
* ships `.d.ts`.
|
|
42
|
+
*/
|
|
43
|
+
export const Config = Schema.object({
|
|
44
|
+
// Empty storeUrl means "the harness-home-anchored default store"
|
|
45
|
+
// (store.ts) — a stable ledger that does not wander with the CWD the
|
|
46
|
+
// harness was started from.
|
|
47
|
+
storeUrl: Schema.string().default(""),
|
|
48
|
+
project: Schema.string().default("default"),
|
|
49
|
+
dev: Schema.string().default("unknown"),
|
|
50
|
+
budgets: Schema.array(Schema.object({
|
|
51
|
+
limitUsd: Schema.number().min(0).required(),
|
|
52
|
+
window: Schema.union(["month", "day"]).default("month"),
|
|
53
|
+
})).default([]),
|
|
54
|
+
// "Unset" is encoded as an empty value rather than an absent key so the
|
|
55
|
+
// validated config always has a definite shape (a half-present policy
|
|
56
|
+
// object is exactly the kind of state that silently disables enforcement).
|
|
57
|
+
policies: Schema.object({
|
|
58
|
+
maxReasoningEffort: Schema.string().default(""),
|
|
59
|
+
allowedModels: Schema.array(Schema.string()).default([]),
|
|
60
|
+
denylistedProjects: Schema.array(Schema.string()).default([]),
|
|
61
|
+
}).default({
|
|
62
|
+
maxReasoningEffort: "",
|
|
63
|
+
allowedModels: [],
|
|
64
|
+
denylistedProjects: [],
|
|
65
|
+
}),
|
|
66
|
+
cascade: Schema.array(Schema.string()).default([]),
|
|
67
|
+
baseUrl: Schema.string(),
|
|
68
|
+
orgKey: Schema.string().role("secret"),
|
|
69
|
+
syncIntervalMs: Schema.number().min(1000).default(60_000),
|
|
70
|
+
policyRefreshMs: Schema.number().min(1000).default(300_000),
|
|
71
|
+
budgetStatusTool: Schema.boolean().default(true),
|
|
72
|
+
pricingTable: Schema.dict(Schema.object({
|
|
73
|
+
inputCentsPerM: Schema.number().min(0).required(),
|
|
74
|
+
outputCentsPerM: Schema.number().min(0).required(),
|
|
75
|
+
cacheReadCentsPerM: Schema.number().min(0),
|
|
76
|
+
cacheWriteCentsPerM: Schema.number().min(0),
|
|
77
|
+
})).default({}),
|
|
78
|
+
pricingAliases: Schema.dict(Schema.string()).default({}),
|
|
79
|
+
pricingRegistryUrl: Schema.string().default("https://models.dev/api.json"),
|
|
80
|
+
pricingGatewayUrl: Schema.string().default(""),
|
|
81
|
+
pricingGatewayProvider: Schema.string().default(""),
|
|
82
|
+
pricingGatewayApiKeyEnv: Schema.string().default(""),
|
|
83
|
+
unpricedFallback: Schema.object({
|
|
84
|
+
inputCentsPerM: Schema.number().min(0),
|
|
85
|
+
outputCentsPerM: Schema.number().min(0),
|
|
86
|
+
cacheReadCentsPerM: Schema.number().min(0),
|
|
87
|
+
cacheWriteCentsPerM: Schema.number().min(0),
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Validate raw configuration and apply defaults, exactly as Cordis does when it
|
|
92
|
+
* loads the plugin. Exposed so tests and embedders can exercise the schema
|
|
93
|
+
* without a cast: the parameter is the partially-populated shape a
|
|
94
|
+
* `cordis.yml` entry supplies, and Schemastery fills every absent field.
|
|
95
|
+
*
|
|
96
|
+
* @throws when a value has the wrong type — Cordis surfaces that as a FAILED
|
|
97
|
+
* fibre ("invalid configuration fails the load with an actionable error").
|
|
98
|
+
*/
|
|
99
|
+
export function resolveConfig(raw) {
|
|
100
|
+
// A schema's whole job is to validate untrusted input, so this is the one
|
|
101
|
+
// place where the boundary is stated rather than assumed: `raw` is whatever
|
|
102
|
+
// a `cordis.yml` entry (or a test) contains, and Schemastery throws on a
|
|
103
|
+
// value whose type is wrong.
|
|
104
|
+
return Config(raw);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Cross-field constraints the schema cannot express, evaluated on the resolved
|
|
108
|
+
* config (defaults applied) so they see exactly what `apply` will receive.
|
|
109
|
+
*
|
|
110
|
+
* @throws Error with an actionable message — Cordis turns it into a FAILED
|
|
111
|
+
* fiber, which is the documented outcome for config naming what the plugin
|
|
112
|
+
* cannot serve.
|
|
113
|
+
*/
|
|
114
|
+
export function assertUsableConfig(config) {
|
|
115
|
+
if (Boolean(config.baseUrl) !== Boolean(config.orgKey)) {
|
|
116
|
+
throw new Error("baseUrl and orgKey must be configured together: without both, the plugin would report nothing and the panel could not reach the local fuse (local-only mode omits both).");
|
|
117
|
+
}
|
|
118
|
+
if (config.cascade.length > 0 && config.policies.allowedModels.length > 0) {
|
|
119
|
+
const allowed = new Set(config.policies.allowedModels);
|
|
120
|
+
const usable = config.cascade.some((model) => allowed.has(model));
|
|
121
|
+
if (!usable) {
|
|
122
|
+
throw new Error("cascade and policies.allowedModels do not intersect: the router could never pick a permitted model.");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
package/dist/fuse.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local fuse — the PRIMARY enforcement (<100ms, offline). Decides
|
|
3
|
+
* whether a call may proceed; the SaaS 429 is only the secondary gate.
|
|
4
|
+
* Pure: budgets arrive with spentUsd precomputed (the store provides it) and
|
|
5
|
+
* the route's reasoning set arrives pre-resolved (the caller provides it).
|
|
6
|
+
*
|
|
7
|
+
* ## Reasoning caps are ranked by the ROUTE, never by a local table
|
|
8
|
+
*
|
|
9
|
+
* `ReasoningEffortId` is adapter-owned and opaque: the harness core "brands
|
|
10
|
+
* identifiers but does not enumerate their values; each adapter owns the
|
|
11
|
+
* ordered set, display names, and optional deployment default", and
|
|
12
|
+
* `LlmModelReasoningInfo.efforts` is published in "adapter-preferred display
|
|
13
|
+
* order". A locally invented ordinal table — or worse, an ordering derived
|
|
14
|
+
* from the id's characters — would rank an adapter id such as `none`/`off`
|
|
15
|
+
* ABOVE `high` and cut the CHEAPEST possible request: a false-positive lock,
|
|
16
|
+
* the exact failure the proposal's Phase 6 gate measures.
|
|
17
|
+
*
|
|
18
|
+
* So the cap compares indices inside the route's own ordered effort list.
|
|
19
|
+
* When that list is unavailable (no route metadata, or an id the route does
|
|
20
|
+
* not publish) the cap is UNENFORCEABLE, not satisfied: the decision reports
|
|
21
|
+
* it through {@link FuseDecision.notEnforced} so the caller can warn and the
|
|
22
|
+
* panel can show the gap. It never fabricates a verdict.
|
|
23
|
+
*/
|
|
24
|
+
export interface FuseBudget {
|
|
25
|
+
limitUsd: number;
|
|
26
|
+
spentUsd: number;
|
|
27
|
+
window: "month" | "day";
|
|
28
|
+
}
|
|
29
|
+
export interface FusePolicies {
|
|
30
|
+
/** Highest allowed reasoning effort, as an id from the route's own set. */
|
|
31
|
+
maxReasoningEffort?: string;
|
|
32
|
+
allowedModels?: string[];
|
|
33
|
+
denylistedProjects?: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface FuseDecision {
|
|
36
|
+
allowed: boolean;
|
|
37
|
+
rule: string | null;
|
|
38
|
+
resetAt: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Configured policies that could not be evaluated for this call (an
|
|
41
|
+
* observable gap, never a silent pass). Empty when every policy applied.
|
|
42
|
+
*/
|
|
43
|
+
notEnforced: string[];
|
|
44
|
+
}
|
|
45
|
+
export declare function fuseDecision(input: {
|
|
46
|
+
project: string;
|
|
47
|
+
model: string;
|
|
48
|
+
reasoningEffort?: string;
|
|
49
|
+
estimatedCostUsd: number;
|
|
50
|
+
budgets: FuseBudget[];
|
|
51
|
+
policies: FusePolicies;
|
|
52
|
+
now: Date;
|
|
53
|
+
/**
|
|
54
|
+
* The route's ordered reasoning-effort ids, cheapest first (harness
|
|
55
|
+
* `LlmModelReasoningInfo.efforts`, adapter-preferred display order).
|
|
56
|
+
* Absent/empty means the route publishes no reasoning metadata.
|
|
57
|
+
*/
|
|
58
|
+
reasoningEfforts?: readonly string[];
|
|
59
|
+
}): FuseDecision;
|
package/dist/fuse.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local fuse — the PRIMARY enforcement (<100ms, offline). Decides
|
|
3
|
+
* whether a call may proceed; the SaaS 429 is only the secondary gate.
|
|
4
|
+
* Pure: budgets arrive with spentUsd precomputed (the store provides it) and
|
|
5
|
+
* the route's reasoning set arrives pre-resolved (the caller provides it).
|
|
6
|
+
*
|
|
7
|
+
* ## Reasoning caps are ranked by the ROUTE, never by a local table
|
|
8
|
+
*
|
|
9
|
+
* `ReasoningEffortId` is adapter-owned and opaque: the harness core "brands
|
|
10
|
+
* identifiers but does not enumerate their values; each adapter owns the
|
|
11
|
+
* ordered set, display names, and optional deployment default", and
|
|
12
|
+
* `LlmModelReasoningInfo.efforts` is published in "adapter-preferred display
|
|
13
|
+
* order". A locally invented ordinal table — or worse, an ordering derived
|
|
14
|
+
* from the id's characters — would rank an adapter id such as `none`/`off`
|
|
15
|
+
* ABOVE `high` and cut the CHEAPEST possible request: a false-positive lock,
|
|
16
|
+
* the exact failure the proposal's Phase 6 gate measures.
|
|
17
|
+
*
|
|
18
|
+
* So the cap compares indices inside the route's own ordered effort list.
|
|
19
|
+
* When that list is unavailable (no route metadata, or an id the route does
|
|
20
|
+
* not publish) the cap is UNENFORCEABLE, not satisfied: the decision reports
|
|
21
|
+
* it through {@link FuseDecision.notEnforced} so the caller can warn and the
|
|
22
|
+
* panel can show the gap. It never fabricates a verdict.
|
|
23
|
+
*/
|
|
24
|
+
/** Index of `effort` inside the route's ordered effort ids, or -1. */
|
|
25
|
+
function effortIndex(efforts, effort) {
|
|
26
|
+
return efforts.indexOf(effort);
|
|
27
|
+
}
|
|
28
|
+
export function fuseDecision(input) {
|
|
29
|
+
const notEnforced = [];
|
|
30
|
+
if (input.policies.denylistedProjects?.includes(input.project)) {
|
|
31
|
+
return {
|
|
32
|
+
allowed: false,
|
|
33
|
+
rule: `denylist:${input.project}`,
|
|
34
|
+
resetAt: null,
|
|
35
|
+
notEnforced,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (input.policies.allowedModels &&
|
|
39
|
+
!input.policies.allowedModels.includes(input.model)) {
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
rule: `model_not_allowed:${input.model}`,
|
|
43
|
+
resetAt: null,
|
|
44
|
+
notEnforced,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const cap = input.policies.maxReasoningEffort;
|
|
48
|
+
const requested = input.reasoningEffort;
|
|
49
|
+
if (cap && requested) {
|
|
50
|
+
const efforts = input.reasoningEfforts ?? [];
|
|
51
|
+
const requestedIndex = effortIndex(efforts, requested);
|
|
52
|
+
const capIndex = effortIndex(efforts, cap);
|
|
53
|
+
if (requestedIndex < 0 || capIndex < 0) {
|
|
54
|
+
// Not rankable against this route — report it, never guess.
|
|
55
|
+
notEnforced.push(`reasoning_cap:${cap}`);
|
|
56
|
+
}
|
|
57
|
+
else if (requestedIndex > capIndex) {
|
|
58
|
+
return {
|
|
59
|
+
allowed: false,
|
|
60
|
+
rule: `reasoning_cap:${cap}`,
|
|
61
|
+
resetAt: null,
|
|
62
|
+
notEnforced,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const budget of input.budgets) {
|
|
67
|
+
if (budget.spentUsd + input.estimatedCostUsd > budget.limitUsd) {
|
|
68
|
+
const reset = new Date(input.now);
|
|
69
|
+
if (budget.window === "month")
|
|
70
|
+
reset.setUTCMonth(reset.getUTCMonth() + 1, 1);
|
|
71
|
+
else
|
|
72
|
+
reset.setUTCDate(reset.getUTCDate() + 1);
|
|
73
|
+
reset.setUTCHours(0, 0, 0, 0);
|
|
74
|
+
return {
|
|
75
|
+
allowed: false,
|
|
76
|
+
rule: "budget_exceeded",
|
|
77
|
+
resetAt: reset.toISOString(),
|
|
78
|
+
notEnforced,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { allowed: true, rule: null, resetAt: null, notEnforced };
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness integration — the Cordis wiring that makes the pure modules
|
|
3
|
+
* live against the DeepSeek Harness runtime.
|
|
4
|
+
*
|
|
5
|
+
* Types are the REAL `@deepseek-ai/*` packages (exact-rc devDeps): the
|
|
6
|
+
* waterfall signatures, `PreStepDecision`, `LlmCallConfig` and `SessionEvent`
|
|
7
|
+
* come from the installed declarations, never from local re-declarations.
|
|
8
|
+
*
|
|
9
|
+
* ## Wiring, and the dispatch contracts each one obeys
|
|
10
|
+
*
|
|
11
|
+
* - `session/event` → **meter**. This event is `@mode emit`: a synchronous,
|
|
12
|
+
* fire-and-forget broadcast ("returned promises and values are not awaited
|
|
13
|
+
* or collected") emitted post-commit. An `async` listener therefore leaves a
|
|
14
|
+
* floating promise that a one-shot headless run can exit before it lands.
|
|
15
|
+
* The handler is consequently SYNCHRONOUS and only enqueues; a single shared
|
|
16
|
+
* drain writes to libsql, and the plugin flushes it on `turn/end`,
|
|
17
|
+
* `session/disposed`, and its own disposal — the same non-blocking-enqueue +
|
|
18
|
+
* explicit-drain contract the harness's own telemetry seam documents for
|
|
19
|
+
* this exact hot path.
|
|
20
|
+
* - `agent/pre-step` → **fuse**. `@mode waterfall`; returning
|
|
21
|
+
* `{ kind: 'reject' }` without calling `next()` short-circuits the step
|
|
22
|
+
* before any token is spent.
|
|
23
|
+
* - `agent/request` → **router**. `@mode waterfall`; always calls `next()` and
|
|
24
|
+
* rewrites the returned config, never the messages.
|
|
25
|
+
* - interval → **sync + policy pull**, registered through `ctx.effect()` so
|
|
26
|
+
* every resource Cordis does not manage itself is released on unload, hot
|
|
27
|
+
* reload, config edit, or loss of a required service.
|
|
28
|
+
*
|
|
29
|
+
* The plugin declares **no `inject`**: `ctx.logger` is framework surface, not
|
|
30
|
+
* an injectable service, and `tokenMeter` / `llm` are optional — a hard
|
|
31
|
+
* dependency on an optional service would leave the fiber PENDING forever,
|
|
32
|
+
* silently enforcing nothing.
|
|
33
|
+
*/
|
|
34
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
35
|
+
import { type DshPluginConfig } from "./config.js";
|
|
36
|
+
/**
|
|
37
|
+
* The plugin entry points.
|
|
38
|
+
*
|
|
39
|
+
* Exported as NAMED functions (the shape the harness's own tutorials use) and
|
|
40
|
+
* re-assembled into the object form by `index.ts`, which is what the loader
|
|
41
|
+
* needs: it unwraps a module to `exports.default` when one exists
|
|
42
|
+
* (`Loader.unwrapExports`) and then reads `plugin.Config` to validate the row's
|
|
43
|
+
* config. A bare-function default export therefore arrives with `Config`
|
|
44
|
+
* undefined — the schema is skipped, defaults are never applied, and `apply`
|
|
45
|
+
* receives partial config. That failure is silent at the type level and only
|
|
46
|
+
* shows up against a real profile, which is why the smoke test boots one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function apply(ctx: Context, config: DshPluginConfig): void;
|