@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenPlan Code Company
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # /dsh-fuse
2
+
3
+ Política de custo multi-agente para o DeepSeek Harness: o plugin mede cada
4
+ chamada do agente, **trava antes** de estourar o budget (fuse local, <100ms,
5
+ offline) e sincroniza com o painel SaaS — cujo 429 é o portão secundário. Só
6
+ métricas sobem: tokens, custo, modelo — o conteúdo das sessões **nunca sai da
7
+ máquina**.
8
+
9
+ Painel central: **https://dsh.openplan.cc** — gastos por organização, projeto
10
+ e pessoa, budgets que o plugin aplica localmente, e o histórico de cortes.
11
+
12
+ ## Instalar
13
+
14
+ O pacote declara `dsh.bundle`, que é o que faz `dsh plugin add` ativar a
15
+ camada. Sem essa declaração o pnpm instalaria uma biblioteca inerte.
16
+
17
+ ```bash
18
+ # npm — a forma canônica de instalação (publicado, v0.1.0)
19
+ dsh plugin --profile <perfil> add @openplan/dsh-fuse
20
+
21
+ # tarball (sem depender de registry)
22
+ pnpm pack # gera openplan-dsh-fuse-<versão>.tgz
23
+ dsh plugin --profile <perfil> add ./openplan-dsh-fuse-0.1.0.tgz
24
+
25
+ # direto do git (exige allowlist de build do pnpm >= 10 — veja a doc do harness)
26
+ dsh plugin --profile <perfil> add github:<org>/<repo>#<sha>
27
+ ```
28
+
29
+ Fonte: **https://github.com/openplancc/dsh-fuse** (espelho gerado a cada
30
+ release — cada tag `v<versão>` corresponde a um commit deste monorepo).
31
+
32
+ O plugin usa `@libsql/client` como store local — zero build nativa,
33
+ distribuível sem compilação (Node ≥ 20).
34
+
35
+ Verifique a camada sem bootar:
36
+
37
+ ```bash
38
+ dsh --profile <perfil> --dump-config # mostra '# == @openplan/dsh-fuse'
39
+ ```
40
+
41
+ ## Configurar
42
+
43
+ O bundle traz defaults seguros (modo local-only, store ancorado no harness
44
+ home, sync de 60s). Para sobrescrever, edite o `cordis.patch.yml` **do seu
45
+ profile** — a camada do usuário, aplicada depois de todas as camadas de bundle.
46
+ A sobrescrita é uma **linha direta com o mesmo `id`** (não um segundo `insert:`,
47
+ que duplicaria o id e faria o loader falhar com `duplicate loader entry id`):
48
+
49
+ ```yaml
50
+ # ~/.dsh/profiles/<perfil>/cordis.patch.yml
51
+ - id: fuse
52
+ name: '@openplan/dsh-fuse'
53
+ config:
54
+ project: meu-projeto
55
+ dev: eu@empresa.com
56
+ # Segredo nunca em texto puro no YAML — a tag !!js resolve no load.
57
+ orgKey: !!js process.env.DSH_ORG_KEY
58
+ baseUrl: https://dsh-api.openplan.cc
59
+ # Orçamento local (o fuse corta offline antes de gastar):
60
+ budgets:
61
+ - limitUsd: 50
62
+ window: month
63
+ - limitUsd: 5
64
+ window: day
65
+ # Políticas:
66
+ policies:
67
+ # maxReasoningEffort é um id da PRÓPRIA rota (adapter-owned):
68
+ maxReasoningEffort: medium
69
+ allowedModels:
70
+ - deepseek/deepseek-v4-flash
71
+ denylistedProjects:
72
+ - projeto-cliente-confidencial
73
+ # Preço (cents por 1M tokens) — OPCIONAL. Por padrão o plugin resolve o
74
+ # preço de cada modelo automaticamente (registry models.dev; veja abaixo),
75
+ # então esta tabela só existe para override explícito. As chaves podem ser
76
+ # o id do adapter (com prefixo de provider), o id nu, ou qualquer alias:
77
+ pricingTable:
78
+ deepseek/deepseek-v4.1-flash:
79
+ inputCentsPerM: 15
80
+ outputCentsPerM: 60
81
+ cacheReadCentsPerM: 3
82
+ gpt-4o:
83
+ inputCentsPerM: 250
84
+ outputCentsPerM: 1000
85
+ # Escape hatch quando o id do adapter não é adivinhável:
86
+ pricingAliases:
87
+ vendor/opaque-model-v9: gpt-4o
88
+ # Uma taxa ÚNICA opcional (cents por 1M) para modelos que nenhuma fonte
89
+ # precifica — um knob, não uma tabela. Sem ela, chamadas sem preço entram
90
+ # com custo zero mas marcadas como `unpriced` (nunca um zero silencioso).
91
+ unpricedFallback:
92
+ inputCentsPerM: 15
93
+ outputCentsPerM: 60
94
+ ```
95
+
96
+ **Config inválida falha o load**, com erro acionável, em vez de ligar um plugin
97
+ que não enforça nada: metade de um alvo de sync (só `baseUrl` ou só `orgKey`)
98
+ ou uma `cascade` que não intersecta `policies.allowedModels` são recusados no
99
+ boot. Preço **não** é exigido — o plugin resolve genericamente (abaixo); um
100
+ modelo sem preço é um estado visível (`unpriced`), não um boot falho.
101
+
102
+ ## Como o preço é resolvido
103
+
104
+ O harness reporta o modelo com o id do **adapter** (normalmente com prefixo de
105
+ provider, ex. `deepseek/deepseek-v4-flash`), enquanto os catálogos de preço vêm
106
+ keyed por rota (`provider/model`). O plugin junta **duas fontes**, ambas
107
+ baixadas e cacheadas no libsql para uso offline:
108
+
109
+ - **registry** (`pricingRegistryUrl`, default `https://models.dev/api.json`) —
110
+ catálogo de preços em USD por rota para 200+ providers. Cobre gateways que
111
+ não publicam preço (ex. command-code) resolvendo cada modelo por id.
112
+ - **gateway** (`pricingGatewayUrl` + `pricingGatewayProvider`) — quando o
113
+ gateway publica preço no próprio `/models` (OpenRouter, DeepInfra, …), esses
114
+ números são **autoritativos** para as rotas dele.
115
+
116
+ A resolução de um id de adapter procura, nesta ordem:
117
+
118
+ 1. `pricingAliases[model]` (explícito, sempre ganha)
119
+ 2. chave exata na tabela local
120
+ 3. rota `provider/model` no registry/gateway
121
+ 4. id nu no registry (o id do adapter sem prefixo)
122
+ 5. sufixos progressivos do id (ex. `provider/anthropic/x` → `anthropic/x` → `x`)
123
+
124
+ Modelo que nenhuma fonte precifica é gravado com **`unpriced: true`** (contado
125
+ e mostrado no painel) e logado — nunca um zero silencioso, porque um preço
126
+ zerado faz o fuse nunca cortar e o budget do servidor nunca disparar. Se a org
127
+ setou `unpricedFallback`, esses modelos usam a taxa única e o budget continua
128
+ cortando.
129
+
130
+ ## O agente enxerga a política (model-facing tool)
131
+
132
+ A doc do harness prescreve o par: o gate diz *não* (o fuse em `agent/pre-step`)
133
+ — e uma **tool model-facing separada** deixa o próprio agente ler o estado da
134
+ política. Quando a composição monta o serviço `tools` (`@deepseek-ai/dsh-tools`),
135
+ o plugin registra `dsh_budget_status`: o modelo pode consultar, no meio da
136
+ sessão, os budgets em vigor por escopo (org/projeto/dev), o total já gasto vs o
137
+ limite, e se há um 429 remoto ativo (e até quando). É o que torna uma trava dura
138
+ tolerável — o agente entende por que foi cortado e pode adaptar (perguntar,
139
+ parar, escolher rota mais barata) em vez de morrer às cegas.
140
+
141
+ A tool é **só leitura**, nunca uma decisão: lê o mesmo libsql que o fuse enforça
142
+ (`spentForWindow`, `remoteBlockFor`, a política publicada) e formata uma visão.
143
+ Ela não gateia nem reescreve nada. O schema viaja em toda request, então quem
144
+ quiser zero presença na wire desliga com `budgetStatusTool: false` (default
145
+ `true`); e se o serviço `tools` não estiver montado, o registro é simplesmente
146
+ pulado — o plugin declara nenhum `inject`, então um serviço opcional nunca
147
+ segura o load.
148
+
149
+ ## Modo local-only
150
+
151
+ Sem `baseUrl`/`orgKey` o plugin funciona **sozinho**: nada sobe, o fuse enforça
152
+ os budgets offline. As sessões ficam no libsql local, por padrão em
153
+ `$DSH_HOME/dsh-fuse/local.db` (`~/.dsh/...` quando `DSH_HOME` não está
154
+ definido) — ancorado no harness home, então o ledger não muda conforme o
155
+ diretório de onde o harness foi iniciado. `storeUrl` troca o caminho; um
156
+ caminho relativo só vale quando você o define explicitamente.
157
+
158
+ ## O que sobe pro painel (sync)
159
+
160
+ A cada 60s (`syncIntervalMs`), um único batch (`POST /v1/usage/batch`):
161
+
162
+ - **usage events** — projeto, dev, modelo, provider, reasoning effort, tokens
163
+ (entrada, saída, cache leitura, cache escrita), duração do step, custo e
164
+ timestamp. O session id sai **hasheado (SHA-256) no cliente**; conteúdo nunca.
165
+ - **fuse cuts** — toda vez que o fuse travou uma chamada (regra + projeto), pro
166
+ painel mostrar "quantas vezes a política cortou".
167
+
168
+ Um batch só marca as linhas como sincronizadas quando o SaaS **confirma** (2xx
169
+ com `ok`): um 500, um 502 do proxy ou uma página de erro nunca aposentam uma
170
+ linha sem ela ter sido gravada.
171
+
172
+ Um `429 BudgetExceeded { rule, reset_at }` do SaaS **engata o fuse local** até o
173
+ reset da janela — o time bloqueado centralmente fica bloqueado na máquina também.
174
+
175
+ ## O painel como control plane
176
+
177
+ Além do 429, o plugin puxa `GET /v1/policy` (key-authed) no boot e a cada
178
+ `policyRefreshMs`: budgets e políticas `hard` da org, no vocabulário do fuse.
179
+ É isso que faz uma regra editada no painel chegar na máquina do dev — sem esse
180
+ pull, o fuse local só enforçaria o que o YAML do deployment diz. O resultado
181
+ fica em cache no libsql, então enforcement **não** depende da rede no boot.
182
+
183
+ ## Alertas (50/80/95% + desvio)
184
+
185
+ Os thresholds graduados são disparados pelo backend (não pelo plugin) —
186
+ configure os canais no painel (**Alertas → Canais**): Telegram (bot token +
187
+ chat id) ou webhook (POST JSON `{kind, payload, at}`). O desvio de gasto acende
188
+ quando a última hora queima ≥ 5× a média das últimas 7 dias E ≥ US$ 1.
189
+
190
+ ## Reasoning cap: por que não há lista fixa de valores
191
+
192
+ `ReasoningEffortId` é *adapter-owned*: o core do harness "brands identifiers but
193
+ does not enumerate their values; each adapter owns the ordered set". Então o cap
194
+ é comparado **por índice na lista de efforts da própria rota**
195
+ (`ctx.llm.resolveModelInfo`). Quando a rota não publica a lista, o cap é
196
+ reportado como **não enforçável** (`notEnforced`, com aviso no log) em vez de
197
+ adivinhado — um ranking local por nome, ou por código de caractere,
198
+ classificaria um id como `none` acima de `high` e travaria justamente a
199
+ requisição mais barata.
200
+
201
+ ## GitHub Action
202
+
203
+ Veja `examples/agent-budget.yml`.
204
+
205
+ ## Verificação
206
+
207
+ `pnpm dsh:smoke` (na raiz do monorepo) empacota o plugin, instala num profile
208
+ descartável com o `dsh plugin add` real, boota o harness headless e exige que o
209
+ fuse recuse um step que não cabe no budget e que um run permitido registre custo
210
+ **diferente de zero**.
211
+
212
+ ## Licença
213
+
214
+ MIT.
@@ -0,0 +1,29 @@
1
+ # The bundle layer `dsh` applies when a profile lists this package.
2
+ #
3
+ # Every key this row needs is restated here because a patch REPLACES a row's
4
+ # config rather than deep-merging it. It deliberately carries no `config:` at
5
+ # all: the exported Config schema supplies the defaults a deployment is likely
6
+ # to keep (local-only mode, harness-home-anchored store, 60s sync), and users
7
+ # override what they need in their own profile's `cordis.patch.yml`, which is
8
+ # applied after every bundle layer.
9
+ #
10
+ # dsh plugin --profile <name> add @openplan/dsh-fuse
11
+ # dsh --profile <name> --dump-config # shows the "# == @openplan/dsh-fuse" layer
12
+ #
13
+ # Local-only mode (nothing leaves the machine) is the default. To report to a
14
+ # panel, add to the profile patch:
15
+ #
16
+ # - id: fuse
17
+ # name: '@openplan/dsh-fuse'
18
+ # config:
19
+ # project: meu-projeto
20
+ # dev: eu@empresa.com
21
+ # orgKey: !!js process.env.DSH_ORG_KEY
22
+ # baseUrl: https://dsh-api.openplan.cc
23
+ # budgets:
24
+ # - limitUsd: 50
25
+ # window: month
26
+ #
27
+ - insert:
28
+ - id: fuse
29
+ name: '@openplan/dsh-fuse'
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The model-facing budget-status tool — the documented *"separate
3
+ * model-facing ask tool"* that pairs with an enforcement gate.
4
+ *
5
+ * The harness extension cookbook prescribes the pair: a gate says *no* (the
6
+ * plugin's `agent/pre-step` fuse), and a separate tool lets the AGENT itself
7
+ * read the policy state — remaining budget per enforced scope, active remote
8
+ * blocks and their reset, the project/dev labels in force. A hard cut is only
9
+ * tolerable when the thing being cut understands it: the model can query this
10
+ * before spending in a long task (or right after being cut) and adapt —
11
+ * asking the user, stopping, or picking a cheaper route — instead of dying
12
+ * blind. This is the mitigation the proposal's Fase-6 metric
13
+ * ("falso-positivo de trava") needs.
14
+ *
15
+ * ## Status-only, never a decision
16
+ *
17
+ * The tool is deliberately inert as policy: it reads the same libsql store the
18
+ * fuse enforces (`spentForWindow`, `remoteBlockFor`, the published policy) and
19
+ * formats a view. It never gates, never rewrites, and its schema rides in the
20
+ * model's toolset only when the deployment left it enabled
21
+ * (`config.budgetStatusTool`). Registration is optional at load: it is skipped
22
+ * when the composition mounts no `tools` service (`@deepseek-ai/dsh-tools`),
23
+ * because the plugin declares no `inject` — an optional service must never
24
+ * hold the fiber PENDING.
25
+ *
26
+ * Types are the REAL `@deepseek-ai/dsh-tools` declarations (the same rule as
27
+ * the rest of the plugin): the object below is built as a literal so no
28
+ * runtime import of `dsh-tools` is needed — the plugin stays distributable
29
+ * standalone, and only the type surface is referenced.
30
+ */
31
+ import type { ToolDefinition } from "@deepseek-ai/dsh-tools";
32
+ /** One budget in force for this machine, with its governing scope. */
33
+ export interface ScopedBudgetStatus {
34
+ scope: "org" | "project" | "dev";
35
+ /** Project label / developer id for scoped entries. */
36
+ reference?: string;
37
+ window: "month" | "day";
38
+ limitUsd: number;
39
+ spentUsd: number;
40
+ }
41
+ /** The active remote 429 block governing this machine, if any. */
42
+ export interface RemoteBlockStatus {
43
+ rule: string;
44
+ resetAt: string;
45
+ }
46
+ /** The full model-facing view of the local policy state. */
47
+ export interface BudgetStatus {
48
+ project: string;
49
+ dev: string;
50
+ /** ISO timestamp of the snapshot. */
51
+ at: string;
52
+ /** Budgets in force (no ordering guaranteed). */
53
+ budgets: ScopedBudgetStatus[];
54
+ block: RemoteBlockStatus | null;
55
+ }
56
+ /** Human-readable markdown-ish summary handed straight to the model. */
57
+ export declare function budgetStatusText(status: BudgetStatus): string;
58
+ /**
59
+ * Build the registry-ready tool definition. The status read is injected so the
60
+ * module stays pure (unit-testable without the harness); the wiring in
61
+ * `harness.ts` supplies the live read over the same store the fuse uses.
62
+ *
63
+ * @param readStatus - resolve the current policy state (store-backed).
64
+ * @returns a `ToolDefinition` for `ctx.tools.register(...)`.
65
+ */
66
+ export declare function createBudgetStatusTool(readStatus: () => Promise<BudgetStatus>): ToolDefinition;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The model-facing budget-status tool — the documented *"separate
3
+ * model-facing ask tool"* that pairs with an enforcement gate.
4
+ *
5
+ * The harness extension cookbook prescribes the pair: a gate says *no* (the
6
+ * plugin's `agent/pre-step` fuse), and a separate tool lets the AGENT itself
7
+ * read the policy state — remaining budget per enforced scope, active remote
8
+ * blocks and their reset, the project/dev labels in force. A hard cut is only
9
+ * tolerable when the thing being cut understands it: the model can query this
10
+ * before spending in a long task (or right after being cut) and adapt —
11
+ * asking the user, stopping, or picking a cheaper route — instead of dying
12
+ * blind. This is the mitigation the proposal's Fase-6 metric
13
+ * ("falso-positivo de trava") needs.
14
+ *
15
+ * ## Status-only, never a decision
16
+ *
17
+ * The tool is deliberately inert as policy: it reads the same libsql store the
18
+ * fuse enforces (`spentForWindow`, `remoteBlockFor`, the published policy) and
19
+ * formats a view. It never gates, never rewrites, and its schema rides in the
20
+ * model's toolset only when the deployment left it enabled
21
+ * (`config.budgetStatusTool`). Registration is optional at load: it is skipped
22
+ * when the composition mounts no `tools` service (`@deepseek-ai/dsh-tools`),
23
+ * because the plugin declares no `inject` — an optional service must never
24
+ * hold the fiber PENDING.
25
+ *
26
+ * Types are the REAL `@deepseek-ai/dsh-tools` declarations (the same rule as
27
+ * the rest of the plugin): the object below is built as a literal so no
28
+ * runtime import of `dsh-tools` is needed — the plugin stays distributable
29
+ * standalone, and only the type surface is referenced.
30
+ */
31
+ /** Human-readable markdown-ish summary handed straight to the model. */
32
+ export function budgetStatusText(status) {
33
+ const lines = [
34
+ `dsh budget status — project "${status.project}", dev "${status.dev}" (at ${status.at})`,
35
+ ];
36
+ if (status.budgets.length === 0) {
37
+ lines.push(" no budgets in force");
38
+ }
39
+ else {
40
+ lines.push(" budgets in force:");
41
+ for (const budget of status.budgets) {
42
+ const label = budget.scope === "project"
43
+ ? `project "${budget.reference ?? status.project}"`
44
+ : budget.scope === "dev"
45
+ ? `dev "${budget.reference ?? status.dev}"`
46
+ : "org (this machine)";
47
+ const used = budget.limitUsd > 0
48
+ ? `${((budget.spentUsd / budget.limitUsd) * 100).toFixed(1)}% used`
49
+ : "no limit";
50
+ lines.push(` - ${label} · ${budget.window} · $${budget.spentUsd.toFixed(4)} / $${budget.limitUsd.toFixed(2)} (${used})`);
51
+ }
52
+ }
53
+ if (status.block) {
54
+ lines.push(` remote block: ${status.block.rule} until ${status.block.resetAt} (a SaaS 429 is engaging the local fuse)`);
55
+ }
56
+ else {
57
+ lines.push(" remote block: none");
58
+ }
59
+ lines.push(" note: figures are what the local fuse enforces; unpriced calls count at $0.");
60
+ return lines.join("\n");
61
+ }
62
+ /**
63
+ * Build the registry-ready tool definition. The status read is injected so the
64
+ * module stays pure (unit-testable without the harness); the wiring in
65
+ * `harness.ts` supplies the live read over the same store the fuse uses.
66
+ *
67
+ * @param readStatus - resolve the current policy state (store-backed).
68
+ * @returns a `ToolDefinition` for `ctx.tools.register(...)`.
69
+ */
70
+ export function createBudgetStatusTool(readStatus) {
71
+ return {
72
+ name: "dsh_budget_status",
73
+ description: "Read the local dsh cost-policy status: remaining budget per enforced scope (org/project/dev), any active remote block and its reset time. Useful before spending in a long task or right after a step was cut by policy.",
74
+ parameters: {
75
+ type: "object",
76
+ properties: {},
77
+ additionalProperties: false,
78
+ },
79
+ output: {
80
+ schema: {
81
+ type: "object",
82
+ properties: {
83
+ status: {
84
+ type: "string",
85
+ description: "Human-readable policy status summary.",
86
+ },
87
+ },
88
+ required: ["status"],
89
+ additionalProperties: false,
90
+ },
91
+ render: (_args, value) => {
92
+ const status = value.status;
93
+ return [
94
+ {
95
+ type: "text",
96
+ text: typeof status === "string" ? status : String(value),
97
+ },
98
+ ];
99
+ },
100
+ },
101
+ execute: async () => {
102
+ const status = await readStatus();
103
+ return { status: budgetStatusText(status) };
104
+ },
105
+ // Pure read of shared state; parallel sibling calls are safe.
106
+ isConcurrencySafe: () => true,
107
+ };
108
+ }
@@ -0,0 +1,155 @@
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
+ import type { PricingAliases, PricingTable } from "./pricing.js";
33
+ export interface BudgetConfig {
34
+ limitUsd: number;
35
+ window: "month" | "day";
36
+ }
37
+ export interface PolicyConfig {
38
+ /**
39
+ * Highest allowed reasoning effort, as an id from the route's own set.
40
+ * Empty string means "no cap".
41
+ */
42
+ maxReasoningEffort: string;
43
+ /** Allowed models; empty means every model the harness routes is allowed. */
44
+ allowedModels: string[];
45
+ /** Denied projects; empty means nothing is denied. */
46
+ denylistedProjects: string[];
47
+ }
48
+ export interface DshPluginConfig {
49
+ /**
50
+ * libsql URL — a file for the installed plugin, `:memory:` for tests.
51
+ * Empty means the harness-home-anchored default
52
+ * (`$DSH_HOME/dsh-fuse/local.db`, `~/.dsh` when `$DSH_HOME` is
53
+ * unset); a relative `file:` path is honored only when explicitly set.
54
+ */
55
+ storeUrl: string;
56
+ /** Project label for windowing. */
57
+ project: string;
58
+ /** Developer identity carried on every usage row. */
59
+ dev: string;
60
+ /** Local caps — the fuse's budget inputs; spent comes from the store. */
61
+ budgets: BudgetConfig[];
62
+ /** Policy gates: reasoning cap, allowed models, project denylist. */
63
+ policies: PolicyConfig;
64
+ /** Routing cascade cheap → expensive (model ids). */
65
+ cascade: string[];
66
+ /** SaaS sync (optional): baseUrl + orgKey enable the 429 + policy pull. */
67
+ baseUrl?: string;
68
+ orgKey?: string;
69
+ /** Sync cadence in ms. */
70
+ syncIntervalMs: number;
71
+ /** Policy-refresh cadence in ms (`GET /v1/policy`). */
72
+ policyRefreshMs: number;
73
+ /**
74
+ * Expose the model-facing budget-status tool (`dsh_budget_status`) to the
75
+ * agent: the docs' "separate model-facing ask tool" paired with the fuse
76
+ * gate. It is status-only — reads the same store the fuse enforces and
77
+ * never decides. The schema rides in every request, so a deployment that
78
+ * wants zero tool presence on the wire can turn it off.
79
+ */
80
+ budgetStatusTool: boolean;
81
+ /**
82
+ * Cost table (cents per 1M tokens) — YAML-serializable so the fuse has real
83
+ * numbers offline. Keys may be bare (`gpt-4o`), vendor-qualified, the
84
+ * adapter's own prefixed id, or the normalized `provider/model` route; the
85
+ * resolver tries each form (see pricing.ts). Optional: prices are resolved
86
+ * generically when this is empty.
87
+ */
88
+ pricingTable: PricingTable;
89
+ /**
90
+ * Explicit `reported model id → price-table key` overrides. The escape
91
+ * hatch for an adapter whose id nothing else can guess.
92
+ */
93
+ pricingAliases: PricingAliases;
94
+ /**
95
+ * The registry of model prices, keyed by route — auto-synced, cached by the
96
+ * plugin for offline use, never hand-maintained. Empty string disables it.
97
+ */
98
+ pricingRegistryUrl: string;
99
+ /**
100
+ * The gateway's own OpenAI-compatible `/models` URL. When it publishes
101
+ * prices, those numbers are authoritative for its routes; when it does not
102
+ * (e.g. command-code), the registry resolves each model id generically.
103
+ */
104
+ pricingGatewayUrl: string;
105
+ /**
106
+ * Provider id used to key the gateway's routes in the table.
107
+ */
108
+ pricingGatewayProvider: string;
109
+ /**
110
+ * Env var holding the gateway bearer token, when `/models` needs auth.
111
+ */
112
+ pricingGatewayApiKeyEnv: string;
113
+ /**
114
+ * ONE optional org-level rate for models nothing prices (cents per 1M) — a
115
+ * knob, not a table. Lets a USD budget still cut on an unpriced route.
116
+ * Absent = unpriced calls count $0 and are reported visibly as unpriced.
117
+ */
118
+ unpricedFallback?: {
119
+ inputCentsPerM: number;
120
+ outputCentsPerM: number;
121
+ cacheReadCentsPerM?: number;
122
+ cacheWriteCentsPerM?: number;
123
+ };
124
+ }
125
+ /**
126
+ * The runtime schema Cordis validates before `apply` runs.
127
+ *
128
+ * Annotated as `Schema<Partial<DshPluginConfig>, DshPluginConfig>`: the first
129
+ * parameter is what a `cordis.yml` entry supplies (every field optional, filled
130
+ * by the schema defaults) and the second is what `apply` receives (complete).
131
+ * An explicit annotation is required because the inferred form cannot be named
132
+ * in the emitted declarations — it reaches into Schemastery's own type helpers
133
+ * and their vendored dependencies, which breaks the build of a package that
134
+ * ships `.d.ts`.
135
+ */
136
+ export declare const Config: Schema<Partial<DshPluginConfig>, DshPluginConfig>;
137
+ /**
138
+ * Validate raw configuration and apply defaults, exactly as Cordis does when it
139
+ * loads the plugin. Exposed so tests and embedders can exercise the schema
140
+ * without a cast: the parameter is the partially-populated shape a
141
+ * `cordis.yml` entry supplies, and Schemastery fills every absent field.
142
+ *
143
+ * @throws when a value has the wrong type — Cordis surfaces that as a FAILED
144
+ * fibre ("invalid configuration fails the load with an actionable error").
145
+ */
146
+ export declare function resolveConfig(raw: unknown): DshPluginConfig;
147
+ /**
148
+ * Cross-field constraints the schema cannot express, evaluated on the resolved
149
+ * config (defaults applied) so they see exactly what `apply` will receive.
150
+ *
151
+ * @throws Error with an actionable message — Cordis turns it into a FAILED
152
+ * fiber, which is the documented outcome for config naming what the plugin
153
+ * cannot serve.
154
+ */
155
+ export declare function assertUsableConfig(config: DshPluginConfig): void;