@indexyz/dsh-custom-provider-reasoning-efforts 0.0.1 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-10-17
4
+
5
+ - Initial release.
6
+ - 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.
7
+ - 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.
8
+ - 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.
9
+ - 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.
10
+ - Add `/reasoning-efforts` with `status`, `apply`, `discover`, and `clear`, reporting every change, every already-current pair, and every skip with its reason.
11
+ - 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,179 @@
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
+ Discovery is on by default and runs once after startup (see `applyOnLoad` and
115
+ `discover` below); `/reasoning-efforts discover` runs it on demand. Every
116
+ interrogation failure is contained to a warning and one line in the command's
117
+ answer — a gateway that is down never blocks startup or a declaration that does
118
+ not need the network.
119
+
120
+ ## Configuration
121
+
122
+ | Field | Default | Meaning |
123
+ | ---------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
124
+ | `providers` | `{}` | Declarations keyed by provider route and then by exact model id; each value is an effort table or `false`. |
125
+ | `discover` | `true` | Whether the load-time pass interrogates endpoints as well as applying declarations. |
126
+ | `discoverRoutes` | `[]` | Routes to interrogate instead of the heuristic's own set; empty means the heuristic. |
127
+ | `namespace` | `llm-pi-ai` | The configurable namespace whose `providers` field is written. |
128
+ | `applyOnLoad` | `true` | Whether the plugin writes at load. Off leaves `/reasoning-efforts` as the only writer. |
129
+
130
+ ## Behavior and limitations
131
+
132
+ - The write goes through the Harness settings service
133
+ (`settings.describe()` / `update()`), the same seam the Models page uses, so
134
+ the Loader validates the change and reconciles the running `llm-pi-ai` row.
135
+ `providers` is a `volatile` field, so the change is live: the next
136
+ `resolveModelInfo()` — and the model-selection menu's next read — sees the
137
+ levels. No restart is involved.
138
+ - Because that write is a configuration write, the `llm-pi-ai` config node is
139
+ re-serialized: comments inside that node do not survive a write, and the plugin
140
+ writes only when it has an actual change to make. The declarations themselves
141
+ are persisted, so they keep working if the plugin is later removed;
142
+ `/reasoning-efforts clear` is the way back.
143
+ - Only models the profile's own patch layer declares can be filled. A route that
144
+ comes from a lower layer, or a model the route does not list, is reported and
145
+ skipped.
146
+ - A gateway that advertises `reasoning: true` with no level list gets no
147
+ declaration: there is no level to offer, and inventing one would claim a
148
+ capability the endpoint did not declare.
149
+ - A level name is used as its own wire spelling. A gateway that accepts a
150
+ different spelling for a level needs that spelling written explicitly in the
151
+ declaration.
152
+ - Discovery reads `openai-completions`, `openai-responses`, and
153
+ `anthropic-messages` listings; any other protocol is reported as unsupported
154
+ and left to explicit declarations.
155
+ - The load-time pass waits for the launcher's readiness signal when the
156
+ composition provides one, runs asynchronously, and contains every failure to a
157
+ log line. It never throws out of the plugin's load and never delays startup.
158
+
159
+ ## Development
160
+
161
+ The host half is TypeScript compiled to `lib/` with `tsc`; there is no browser
162
+ half.
163
+
164
+ ```bash
165
+ npm run check # typecheck, build, then test
166
+ npm run build # lib/ (Host half)
167
+ npm test # 96 Vitest cases
168
+ ```
169
+
170
+ `test/smoke-release.ts` is the package's declared `release.smoke` check: the
171
+ release engine runs it against the packed and installed tarball, verifying the
172
+ manifest targets, the built host entry, and the bundle patch the `dsh` manifest
173
+ points at.
174
+
175
+ See the repository root for the workspace layout and release process.
176
+
177
+ ## License
178
+
179
+ 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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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Interrogate a hand-written provider route for the reasoning levels its
3
+ * endpoint advertises.
4
+ *
5
+ * The request follows the Harness's own discovery contract — the same listing
6
+ * URL, the same attribution headers, and the same authentication per protocol
7
+ * — so a deployment whose Models page can list a route's models gets the same
8
+ * request here, only with the reasoning half of each entry kept.
9
+ *
10
+ * @module @indexyz/dsh-custom-provider-reasoning-efforts/discovery
11
+ */
12
+ import { type ListedModel } from "./reasoning.ts";
13
+ /**
14
+ * Join an endpoint base with its protocol's listing path.
15
+ *
16
+ * The base is treated as a prefix rather than a URL to resolve against, so a
17
+ * deployment path survives; the Anthropic listing lives at the root `/v1`
18
+ * whether or not the base's `/v1` is spelled out.
19
+ *
20
+ * @param baseURL - the route's configured endpoint.
21
+ * @param api - the route's wire protocol.
22
+ * @returns the listing URL.
23
+ */
24
+ export declare function listingUrl(baseURL: string, api: string): string;
25
+ /**
26
+ * Whether this build can read a protocol's model listing.
27
+ *
28
+ * @param api - the route's wire protocol.
29
+ * @returns true for a protocol {@link probeRoute} interrogates.
30
+ */
31
+ export declare function isListableApi(api: string): boolean;
32
+ /** One route interrogation. */
33
+ export interface RouteProbe {
34
+ /** Provider route key, for diagnostics. */
35
+ readonly route: string;
36
+ /** The route's configured endpoint. */
37
+ readonly baseURL: string;
38
+ /** The route's wire protocol. */
39
+ readonly api: string;
40
+ /** The route's own configured request headers. */
41
+ readonly headers?: Readonly<Record<string, string>>;
42
+ /** The credential resolved for the route, when one was found. */
43
+ readonly apiKey?: string;
44
+ /** Caller cancellation. */
45
+ readonly signal?: AbortSignal;
46
+ /** Transport override, for tests; the process `fetch` by default. */
47
+ readonly fetchImpl?: typeof fetch;
48
+ }
49
+ /**
50
+ * Interrogate one route's endpoint for the models it advertises and the levels
51
+ * each one publishes.
52
+ *
53
+ * @param probe - the endpoint, protocol, headers, and credential to use.
54
+ * @returns the advertised models in endpoint order.
55
+ * @throws Error when the protocol has no readable listing, the endpoint refuses
56
+ * or fails the request, or the reply is not a model listing.
57
+ */
58
+ export declare function probeRoute(probe: RouteProbe): Promise<ListedModel[]>;
59
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAkBpE;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACnC;AAaD;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA4C1E"}