@indexyz/dsh-custom-provider-reasoning-efforts 0.0.1 → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Read a Codex-shaped relay's reasoning levels from its /backend-api/codex/models manifest when its /v1/models inventory publishes none.
8
+
9
+ ## 0.1.0 - 2026-10-17
10
+
11
+ - Initial release.
12
+ - Declare `reasoningEfforts` for a hand-written `llm-pi-ai` provider route, whose models otherwise resolve to `reasoning: false` because the installed pi-ai catalog ships no entry to inherit from.
13
+ - Carry the declarations in the plugin row's own `providers` table, keyed by provider route and exact model id, with each level mapped to the wire spelling it dispatches and `false` for a model that cannot reason; a malformed declaration fails the load, naming the route, the model, and the level.
14
+ - Write the missing field through the Harness settings service over the `llm-pi-ai` row's volatile `providers` field, so the change reaches the running adapter without a restart; only models the profile already declares are filled, and nothing is invented.
15
+ - Interrogate a custom route's `/v1/models` for the levels it advertises (`reasoning`, `supported_reasoning_levels`, and their sibling spellings) and map them onto the models the route declares, with endpoint failures contained to a warning.
16
+ - Add `/reasoning-efforts` with `status`, `apply`, `discover`, and `clear`, reporting every change, every already-current pair, and every skip with its reason.
17
+ - Run the pass once after startup, through the launcher's readiness signal when one exists, so a declaration lands without a restart and an unreachable gateway never blocks or fails boot.
package/README.md CHANGED
@@ -1,8 +1,218 @@
1
- # Bootstrap placeholder
1
+ # @indexyz/dsh-custom-provider-reasoning-efforts
2
2
 
3
- This version exists only to register the package name on npm, so that a trusted
4
- publisher can be configured for `@indexyz/dsh-custom-provider-reasoning-efforts`
5
- before its first real release. The real plugin is released as `0.1.0` from the
6
- repository `5aaee9/pi-agent-extensions` by the `publish.yml` workflow.
3
+ Give a hand-written [dsh (DeepSeek Harness)](https://github.com/deepseek-ai/deepseek-harness)
4
+ provider route the reasoning levels its gateway actually offers.
7
5
 
8
- Use `0.1.0` or later; do not install this version.
6
+ ```bash
7
+ dsh plugin --profile web add @indexyz/dsh-custom-provider-reasoning-efforts
8
+ ```
9
+
10
+ It is built and tested against dsh `0.2.0-rc.2` and declares that range as
11
+ `engines.dsh` (`>=0.2.0-rc.2 <0.3.0-0`), which the plugin market reads before it
12
+ installs. Node `22.19.0` or newer is required as well.
13
+
14
+ To try a repository checkout instead (after `npm install` at the repository root):
15
+
16
+ ```bash
17
+ dsh plugin --profile web add ./dsh-custom-provider-reasoning-efforts
18
+ ```
19
+
20
+ ## Why a custom route loses its levels
21
+
22
+ `llm-pi-ai` resolves a model's reasoning capability from the profile entry's
23
+ `reasoningEfforts` field and, only when that field is absent, from the installed
24
+ pi-ai catalog. A route the catalog does not ship — `magpie` in the configuration
25
+ this plugin was written for — has no catalog entry at all, so:
26
+
27
+ | Step | Result |
28
+ | ----------------------------------------- | ------------------------------------------------------------ |
29
+ | `catalogModels("magpie").get(id)` | empty map, so `base` is undefined |
30
+ | `resolveModelReasoning(entry, undefined)` | `{reasoning: base?.reasoning ?? false}` → `reasoning: false` |
31
+ | `reasoningInfo(model)` | `if (!model.reasoning) return {}` |
32
+ | model-selection menu | no levels to offer |
33
+
34
+ The gateway may advertise exactly what is missing — `reasoning: true` and
35
+ `supported_reasoning_levels: [{effort: "low"}, {effort: "high"}, {effort: "max"}]`
36
+ — but the Harness endpoint probe parses only `id`, `name`, `contextWindow`, and
37
+ `maxTokens`, and it does not persist what it reads, so discovery cannot fill the
38
+ gap by itself. The one thing that fixes it is the field the profile already
39
+ supports, on every model entry of that route. This plugin writes it.
40
+
41
+ ## Declarations
42
+
43
+ Declarations live in this plugin's own row in the profile patch
44
+ (`~/.dsh/profiles/<profile>/cordis.patch.yml`):
45
+
46
+ ```yaml
47
+ - insert:
48
+ - id: custom-provider-reasoning-efforts
49
+ name: "@indexyz/dsh-custom-provider-reasoning-efforts"
50
+ config:
51
+ providers:
52
+ magpie:
53
+ ollama-cloud/kimi-k3:
54
+ low: low
55
+ high: high
56
+ max: max
57
+ ollama-cloud/deepseek-v4.1-flash:
58
+ low: low
59
+ high: high
60
+ max: max
61
+ ```
62
+
63
+ Each model's value is either the levels it offers — keyed by pi-ai thinking level
64
+ (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) and mapped to the wire
65
+ spelling that level dispatches — or `false` for a model the route serves but that
66
+ cannot reason. `off: null` is the one level that may carry no wire value, because
67
+ not thinking is the parameter's absence. A declaration that is empty, offers no
68
+ level beyond `off`, puts `null` or an empty string on a thinking level, or names a
69
+ level pi-ai does not know fails the plugin's load with the route, the model, and
70
+ the level named.
71
+
72
+ The plugin writes only models the route already declares in the profile: it
73
+ never invents a route, a `baseURL`, a protocol, a credential reference, or a
74
+ model entry. A declaration for a route or model the profile does not carry is
75
+ reported by the command and skipped.
76
+
77
+ ## The `/reasoning-efforts` command
78
+
79
+ ```text
80
+ /reasoning-efforts # what is pending; writes nothing (same as `status`)
81
+ /reasoning-efforts status # same
82
+ /reasoning-efforts apply # write the declarations above
83
+ /reasoning-efforts discover # interrogate each custom route, then write
84
+ /reasoning-efforts clear # remove the reasoningEfforts the declarations name
85
+ ```
86
+
87
+ Every mode reports the route/model pairs it changed, how many were already
88
+ current, and every pair it skipped with the reason. `status`, `apply`, and
89
+ `clear` perform no network request. Nothing is written when the plan finds no
90
+ change, so a repeat run leaves the file untouched.
91
+
92
+ ## Endpoint discovery
93
+
94
+ A route is a discovery candidate when its profile declares both an explicit,
95
+ non-empty `models` list and a `baseURL` — the shape of a hand-written route.
96
+ `discoverRoutes` names the candidates instead when a deployment wants otherwise.
97
+ Each candidate is interrogated once at `GET {baseURL}/models` (Anthropic Messages
98
+ at the root `/v1/models`), with `authorization: Bearer <key>` for the OpenAI
99
+ protocols and `x-api-key` plus `anthropic-version` for Anthropic Messages. The
100
+ key is resolved from the route's own `apiKeyEnv` through the credential store,
101
+ falling back to the process environment; when neither resolves, the probe is
102
+ sent unauthenticated rather than failing.
103
+
104
+ The reply's `data` array, or the `models` object some compatible gateways expose,
105
+ is read; reasoning is taken from `supported_reasoning_levels`,
106
+ `reasoning_levels`, `supported_reasoning_efforts`, or the `reasoning` object's
107
+ `efforts` / `supported_efforts`, with string entries or `{effort}` / `{level}` /
108
+ `{id}` / `{name}` objects. A name that is a pi-ai thinking level maps to itself
109
+ as its wire spelling and `off` maps to `null`; anything else is ignored. Only
110
+ models the route already declares are considered, so a gateway listing hundreds
111
+ of models cannot add any. An explicit declaration always wins over a discovered
112
+ one, and a model whose entry advertises no levels is left exactly as it is.
113
+
114
+ ### The Codex manifest, for a relay whose inventory says nothing
115
+
116
+ A relay that proxies a Codex backend sometimes publishes an inventory with no
117
+ capability at all — only `id`, `display_name`, and `owned_by`, the shape
118
+ [`pi-provider-sub2api`](../pi-provider-sub2api) also works around. The same relay
119
+ usually answers the backend's own model list at
120
+ `GET {root}/backend-api/codex/models` (`root` is the route's `baseURL` with a
121
+ trailing `/v1` removed), where each entry carries `slug` and the efforts the
122
+ backend accepts:
123
+
124
+ ```json
125
+ {
126
+ "models": [
127
+ {
128
+ "slug": "gpt-6.1-sol",
129
+ "supported_reasoning_levels": [
130
+ { "effort": "low" },
131
+ { "effort": "medium" },
132
+ { "effort": "high" },
133
+ { "effort": "xhigh" },
134
+ { "effort": "max" },
135
+ { "effort": "ultra" }
136
+ ]
137
+ }
138
+ ]
139
+ }
140
+ ```
141
+
142
+ That manifest is a second discovery source. It is consulted only for an
143
+ `openai-responses` route whose own listing described no level for at least one
144
+ model the route declares, so a relay that already answers costs no extra request;
145
+ its levels are merged with the listing's (the listing's own spellings win) and
146
+ only the route's declared models are considered. A relay without the endpoint
147
+ answers `403`, `404`, or something that is not a manifest, which means "nothing
148
+ more to say" rather than a failure; an endpoint that is unreachable, or that
149
+ answers with its own error status, is reported. `ultra` is not a pi-ai thinking
150
+ level, so it is dropped from a discovered declaration — declare `max: ultra` in
151
+ the route's own `providers` entry to wire the `max` level to it.
152
+
153
+ Discovery is on by default and runs once after startup (see `applyOnLoad` and
154
+ `discover` below); `/reasoning-efforts discover` runs it on demand. Every
155
+ interrogation failure is contained to a warning and one line in the command's
156
+ answer — a gateway that is down never blocks startup or a declaration that does
157
+ not need the network.
158
+
159
+ ## Configuration
160
+
161
+ | Field | Default | Meaning |
162
+ | ---------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
163
+ | `providers` | `{}` | Declarations keyed by provider route and then by exact model id; each value is an effort table or `false`. |
164
+ | `discover` | `true` | Whether the load-time pass interrogates endpoints as well as applying declarations. |
165
+ | `discoverRoutes` | `[]` | Routes to interrogate instead of the heuristic's own set; empty means the heuristic. |
166
+ | `namespace` | `llm-pi-ai` | The configurable namespace whose `providers` field is written. |
167
+ | `applyOnLoad` | `true` | Whether the plugin writes at load. Off leaves `/reasoning-efforts` as the only writer. |
168
+
169
+ ## Behavior and limitations
170
+
171
+ - The write goes through the Harness settings service
172
+ (`settings.describe()` / `update()`), the same seam the Models page uses, so
173
+ the Loader validates the change and reconciles the running `llm-pi-ai` row.
174
+ `providers` is a `volatile` field, so the change is live: the next
175
+ `resolveModelInfo()` — and the model-selection menu's next read — sees the
176
+ levels. No restart is involved.
177
+ - Because that write is a configuration write, the `llm-pi-ai` config node is
178
+ re-serialized: comments inside that node do not survive a write, and the plugin
179
+ writes only when it has an actual change to make. The declarations themselves
180
+ are persisted, so they keep working if the plugin is later removed;
181
+ `/reasoning-efforts clear` is the way back.
182
+ - Only models the profile's own patch layer declares can be filled. A route that
183
+ comes from a lower layer, or a model the route does not list, is reported and
184
+ skipped.
185
+ - A gateway that advertises `reasoning: true` with no level list gets no
186
+ declaration: there is no level to offer, and inventing one would claim a
187
+ capability the endpoint did not declare.
188
+ - A level name is used as its own wire spelling. A gateway that accepts a
189
+ different spelling for a level needs that spelling written explicitly in the
190
+ declaration.
191
+ - Discovery reads `openai-completions`, `openai-responses`, and
192
+ `anthropic-messages` listings; any other protocol is reported as unsupported
193
+ and left to explicit declarations.
194
+ - The load-time pass waits for the launcher's readiness signal when the
195
+ composition provides one, runs asynchronously, and contains every failure to a
196
+ log line. It never throws out of the plugin's load and never delays startup.
197
+
198
+ ## Development
199
+
200
+ The host half is TypeScript compiled to `lib/` with `tsc`; there is no browser
201
+ half.
202
+
203
+ ```bash
204
+ npm run check # typecheck, build, then test
205
+ npm run build # lib/ (Host half)
206
+ npm test # 96 Vitest cases
207
+ ```
208
+
209
+ `test/smoke-release.ts` is the package's declared `release.smoke` check: the
210
+ release engine runs it against the packed and installed tarball, verifying the
211
+ manifest targets, the built host entry, and the bundle patch the `dsh` manifest
212
+ points at.
213
+
214
+ See the repository root for the workspace layout and release process.
215
+
216
+ ## License
217
+
218
+ MIT
@@ -0,0 +1,9 @@
1
+ # Bundle patch layer: the row loads this package's main entry as the Host half
2
+ # (a plain Cordis plugin exporting `apply`, `inject`, and `Config`), and its
3
+ # `providers` table is where a deployment declares the reasoning levels of a
4
+ # hand-written provider route.
5
+ - insert:
6
+ - id: custom-provider-reasoning-efforts
7
+ name: "@indexyz/dsh-custom-provider-reasoning-efforts"
8
+ config:
9
+ providers: {}
package/lib/apply.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Write a planned change through the Harness settings service.
3
+ *
4
+ * This is the only writer. It reads the namespace's descriptor, plans against
5
+ * the profile patch's own `providers` value, and calls `settings.update()` with
6
+ * the whole planned value and the revision `describe()` returned, so the Loader
7
+ * validates the candidate and reconciles the running `llm-pi-ai` adapter
8
+ * exactly as it does a Models-page save — `providers` is a `volatile` field, so
9
+ * the change is live without a restart. Nothing is written when the plan finds
10
+ * no change, and a revision conflict is retried once against a fresh read.
11
+ *
12
+ * @module @indexyz/dsh-custom-provider-reasoning-efforts/apply
13
+ */
14
+ import { type Change, type Intent, type Skip } from "./plan.ts";
15
+ /** One configurable namespace as the settings service describes it. */
16
+ export interface ProviderDescriptor {
17
+ /** Profile entry id addressed by the settings service. */
18
+ readonly ns: string;
19
+ /** Revision `describe()` returned; `update()` refuses a stale one. */
20
+ readonly revision: number;
21
+ /** The live resolved value, including provider defaults. */
22
+ readonly value?: unknown;
23
+ /** The profile patch's own raw value for this entry. */
24
+ readonly user?: unknown;
25
+ }
26
+ /** The part of the settings service this plugin uses. */
27
+ export interface SettingsSeam {
28
+ /** Whether the active profile accepts configuration edits. */
29
+ readonly writable: boolean;
30
+ /** Read the active namespaces with their revisions and layer values. */
31
+ describe(): readonly ProviderDescriptor[];
32
+ /** Write the namespace's volatile fields, guarded by the revision. */
33
+ update(ns: string, patch: object, expectedRevision?: number): Promise<void>;
34
+ }
35
+ /** What one pass did. */
36
+ export interface ApplyResult {
37
+ readonly kind: "applied" | "pending" | "unchanged" | "unavailable" | "readonly";
38
+ readonly namespace: string;
39
+ readonly changes: readonly Change[];
40
+ readonly skipped: readonly Skip[];
41
+ }
42
+ /** One pass's inputs. */
43
+ export interface ApplyOptions {
44
+ /** The configurable namespace whose `providers` field is written. */
45
+ readonly namespace: string;
46
+ /** The route/model pairs to apply. */
47
+ readonly intents: readonly Intent[];
48
+ /** Plan only; report the pending changes without writing. */
49
+ readonly dryRun?: boolean;
50
+ }
51
+ /**
52
+ * Plan and, unless `dryRun`, write one namespace's declared reasoning efforts.
53
+ *
54
+ * @param seam - the settings service, or a fake with the same shape.
55
+ * @param options - the namespace, the intents, and whether to write.
56
+ * @returns what the pass did, with the changes and the skips.
57
+ * @throws Whatever `update()` rejected with, other than a revision conflict.
58
+ */
59
+ export declare function applyIntents(seam: SettingsSeam, options: ApplyOptions): Promise<ApplyResult>;
60
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAiB,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAE/E,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,wEAAwE;IACxE,QAAQ,IAAI,SAAS,kBAAkB,EAAE,CAAC;IAC1C,sEAAsE;IACtE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E;AAED,yBAAyB;AACzB,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,aAAa,GAAG,UAAU,CAAC;IAChF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,SAAS,IAAI,EAAE,CAAC;CACnC;AAED,yBAAyB;AACzB,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAaD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,WAAW,CAAC,CA6BtB"}
package/lib/apply.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Write a planned change through the Harness settings service.
3
+ *
4
+ * This is the only writer. It reads the namespace's descriptor, plans against
5
+ * the profile patch's own `providers` value, and calls `settings.update()` with
6
+ * the whole planned value and the revision `describe()` returned, so the Loader
7
+ * validates the candidate and reconciles the running `llm-pi-ai` adapter
8
+ * exactly as it does a Models-page save — `providers` is a `volatile` field, so
9
+ * the change is live without a restart. Nothing is written when the plan finds
10
+ * no change, and a revision conflict is retried once against a fresh read.
11
+ *
12
+ * @module @indexyz/dsh-custom-provider-reasoning-efforts/apply
13
+ */
14
+ import { planProviders } from "./plan.js";
15
+ /** Whether an error is the settings service's revision conflict. */
16
+ function isConflict(error) {
17
+ if (error === null || typeof error !== "object")
18
+ return false;
19
+ if (Reflect.get(error, "name") === "SettingsConflictError")
20
+ return true;
21
+ return Reflect.get(error, "code") === "SETTINGS_CONFLICT";
22
+ }
23
+ function descriptorFor(seam, namespace) {
24
+ return seam.describe().find((entry) => entry.ns === namespace);
25
+ }
26
+ /**
27
+ * Plan and, unless `dryRun`, write one namespace's declared reasoning efforts.
28
+ *
29
+ * @param seam - the settings service, or a fake with the same shape.
30
+ * @param options - the namespace, the intents, and whether to write.
31
+ * @returns what the pass did, with the changes and the skips.
32
+ * @throws Whatever `update()` rejected with, other than a revision conflict.
33
+ */
34
+ export async function applyIntents(seam, options) {
35
+ const namespace = options.namespace;
36
+ if (!seam.writable)
37
+ return { kind: "readonly", namespace, changes: [], skipped: [] };
38
+ let descriptor = descriptorFor(seam, namespace);
39
+ if (descriptor === undefined) {
40
+ return { kind: "unavailable", namespace, changes: [], skipped: [] };
41
+ }
42
+ let plan = planProviders(descriptor.user, options.intents);
43
+ if (plan.changes.length === 0) {
44
+ return { kind: "unchanged", namespace, changes: [], skipped: plan.skipped };
45
+ }
46
+ if (options.dryRun === true) {
47
+ return { kind: "pending", namespace, changes: plan.changes, skipped: plan.skipped };
48
+ }
49
+ try {
50
+ await seam.update(namespace, { providers: plan.providers }, descriptor.revision);
51
+ }
52
+ catch (error) {
53
+ if (!isConflict(error))
54
+ throw error;
55
+ descriptor = descriptorFor(seam, namespace);
56
+ if (descriptor === undefined) {
57
+ return { kind: "unavailable", namespace, changes: [], skipped: plan.skipped };
58
+ }
59
+ plan = planProviders(descriptor.user, options.intents);
60
+ if (plan.changes.length === 0) {
61
+ return { kind: "unchanged", namespace, changes: [], skipped: plan.skipped };
62
+ }
63
+ await seam.update(namespace, { providers: plan.providers }, descriptor.revision);
64
+ }
65
+ return { kind: "applied", namespace, changes: plan.changes, skipped: plan.skipped };
66
+ }
67
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.js","sourceRoot":"","sources":["../apply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The declaration model this plugin supplies to a hand-written provider route.
3
+ *
4
+ * A route the installed pi-ai catalog does not ship has no catalog entry to
5
+ * inherit reasoning capability from, so every one of its models resolves to
6
+ * `reasoning: false` unless its `llm-pi-ai` profile entry carries
7
+ * `reasoningEfforts`. This module is that field's authoring vocabulary: a
8
+ * declaration names the pi-ai thinking levels a model offers and the wire
9
+ * spelling each level dispatches, or says `false` for a model that cannot
10
+ * reason at all.
11
+ *
12
+ * @module @indexyz/dsh-custom-provider-reasoning-efforts/declarations
13
+ */
14
+ /** One level's wire spelling; `null` is legal for `off` only and means dispatch sends nothing. */
15
+ export type EffortWire = string | null;
16
+ /** The levels one model offers, keyed by pi-ai thinking level. */
17
+ export type Efforts = Record<string, EffortWire>;
18
+ /** One model's declaration: the levels it offers, or `false` for a model that cannot reason. */
19
+ export type ModelDeclaration = false | Efforts;
20
+ /** Declarations keyed by provider route and then by exact model id. */
21
+ export type Declarations = Record<string, Record<string, ModelDeclaration>>;
22
+ /** A rejected declaration, naming the route, model, and level at fault. */
23
+ export declare class DeclarationError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ /**
27
+ * Validate one model's declaration.
28
+ *
29
+ * A declaration is rejected when it is empty, offers no level beyond `off`,
30
+ * carries `null` or an empty string on a level that is not `off`, or names a
31
+ * key that is not a pi-ai thinking level. Every rejection names the route, the
32
+ * model, and — where one is at fault — the level.
33
+ *
34
+ * @param route - provider route the declaration belongs to, for diagnostics.
35
+ * @param model - exact model id the declaration belongs to, for diagnostics.
36
+ * @param raw - the declaration as the row configuration carried it.
37
+ * @returns the validated declaration.
38
+ * @throws DeclarationError when the declaration cannot be served.
39
+ */
40
+ export declare function validateModelDeclaration(route: string, model: string, raw: unknown): ModelDeclaration;
41
+ /**
42
+ * Validate every declaration the row configuration carries, so a malformed
43
+ * table fails the plugin's load instead of a later write.
44
+ *
45
+ * @param raw - the `providers` table as the row configuration carried it.
46
+ * @returns the validated table.
47
+ * @throws DeclarationError naming the first declaration that cannot be served.
48
+ */
49
+ export declare function validateDeclarations(raw: Declarations | undefined): Declarations;
50
+ /**
51
+ * Render a declaration's levels for a human-readable answer.
52
+ *
53
+ * @param value - one validated declaration.
54
+ * @returns the levels in escalation order, or the non-reasoning marker.
55
+ */
56
+ export declare function describeEfforts(value: ModelDeclaration): string;
57
+ //# sourceMappingURL=declarations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declarations.d.ts","sourceRoot":"","sources":["../declarations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,kGAAkG;AAClG,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC;AAEvC,kEAAkE;AAClE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEjD,gGAAgG;AAChG,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,CAAC;AAE/C,uEAAuE;AACvE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAE5E,2EAA2E;AAC3E,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,OAAO,GACX,gBAAgB,CA6ClB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,YAAY,CAoBhF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAI/D"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The declaration model this plugin supplies to a hand-written provider route.
3
+ *
4
+ * A route the installed pi-ai catalog does not ship has no catalog entry to
5
+ * inherit reasoning capability from, so every one of its models resolves to
6
+ * `reasoning: false` unless its `llm-pi-ai` profile entry carries
7
+ * `reasoningEfforts`. This module is that field's authoring vocabulary: a
8
+ * declaration names the pi-ai thinking levels a model offers and the wire
9
+ * spelling each level dispatches, or says `false` for a model that cannot
10
+ * reason at all.
11
+ *
12
+ * @module @indexyz/dsh-custom-provider-reasoning-efforts/declarations
13
+ */
14
+ import { THINKING_LEVELS, isThinkingLevel } from "./levels.js";
15
+ /** A rejected declaration, naming the route, model, and level at fault. */
16
+ export class DeclarationError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "DeclarationError";
20
+ }
21
+ }
22
+ function isRecord(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ /**
26
+ * Validate one model's declaration.
27
+ *
28
+ * A declaration is rejected when it is empty, offers no level beyond `off`,
29
+ * carries `null` or an empty string on a level that is not `off`, or names a
30
+ * key that is not a pi-ai thinking level. Every rejection names the route, the
31
+ * model, and — where one is at fault — the level.
32
+ *
33
+ * @param route - provider route the declaration belongs to, for diagnostics.
34
+ * @param model - exact model id the declaration belongs to, for diagnostics.
35
+ * @param raw - the declaration as the row configuration carried it.
36
+ * @returns the validated declaration.
37
+ * @throws DeclarationError when the declaration cannot be served.
38
+ */
39
+ export function validateModelDeclaration(route, model, raw) {
40
+ const where = `route "${route}" model "${model}"`;
41
+ if (raw === false)
42
+ return false;
43
+ if (!isRecord(raw)) {
44
+ throw new DeclarationError(`${where}: a declaration must be a level table or false, got ${JSON.stringify(raw)}`);
45
+ }
46
+ const entries = Object.entries(raw);
47
+ if (entries.length === 0) {
48
+ throw new DeclarationError(`${where}: the declaration is empty; name the offered levels, or set it to false`);
49
+ }
50
+ const efforts = {};
51
+ let thinking = 0;
52
+ for (const [level, wire] of entries) {
53
+ if (!isThinkingLevel(level)) {
54
+ throw new DeclarationError(`${where}: "${level}" is not a pi-ai thinking level (${THINKING_LEVELS.join(", ")})`);
55
+ }
56
+ if (wire === null) {
57
+ if (level !== "off") {
58
+ throw new DeclarationError(`${where}: level "${level}" needs the wire value dispatch should send; only "off" may stay empty`);
59
+ }
60
+ efforts[level] = null;
61
+ continue;
62
+ }
63
+ if (typeof wire !== "string" || wire.length === 0) {
64
+ throw new DeclarationError(`${where}: level "${level}" must be a non-empty string or null, got ${JSON.stringify(wire)}`);
65
+ }
66
+ efforts[level] = wire;
67
+ if (level !== "off")
68
+ thinking += 1;
69
+ }
70
+ if (thinking === 0) {
71
+ throw new DeclarationError(`${where}: the declaration offers no level beyond "off"; name a thinking level, or set it to false`);
72
+ }
73
+ return efforts;
74
+ }
75
+ /**
76
+ * Validate every declaration the row configuration carries, so a malformed
77
+ * table fails the plugin's load instead of a later write.
78
+ *
79
+ * @param raw - the `providers` table as the row configuration carried it.
80
+ * @returns the validated table.
81
+ * @throws DeclarationError naming the first declaration that cannot be served.
82
+ */
83
+ export function validateDeclarations(raw) {
84
+ const declarations = {};
85
+ for (const [route, models] of Object.entries(raw ?? {})) {
86
+ if (route.length === 0)
87
+ throw new DeclarationError("a declaration names an empty provider route");
88
+ const validated = {};
89
+ for (const [model, value] of Object.entries(models)) {
90
+ if (model.length === 0) {
91
+ throw new DeclarationError(`route "${route}" names an empty model id`);
92
+ }
93
+ validated[model] = validateModelDeclaration(route, model, value);
94
+ }
95
+ if (Object.keys(validated).length === 0) {
96
+ throw new DeclarationError(`route "${route}" declares no models; remove the empty entry, or name its models`);
97
+ }
98
+ declarations[route] = validated;
99
+ }
100
+ return declarations;
101
+ }
102
+ /**
103
+ * Render a declaration's levels for a human-readable answer.
104
+ *
105
+ * @param value - one validated declaration.
106
+ * @returns the levels in escalation order, or the non-reasoning marker.
107
+ */
108
+ export function describeEfforts(value) {
109
+ if (value === false)
110
+ return "no reasoning";
111
+ const levels = THINKING_LEVELS.filter((level) => level in value);
112
+ return levels.length === 0 ? "no levels" : levels.join("/");
113
+ }
114
+ //# sourceMappingURL=declarations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declarations.js","sourceRoot":"","sources":["../declarations.ts"],"names":[],"mappings":"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"}