@sagmans/dsh-tui 0.6.0 → 0.8.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/.agents/skills/dsh-tui-dogfood/SKILL.md +83 -0
- package/.agents/skills/dsh-tui-dogfood/references/home-state.md +78 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/clone-links.mjs +133 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +417 -0
- package/.agents/skills/dsh-tui-update-models/SKILL.md +199 -0
- package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +292 -0
- package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +314 -0
- package/README.md +154 -27
- package/lib/agent/present.d.ts.map +1 -1
- package/lib/agent/present.js +2 -1
- package/lib/agent/present.js.map +1 -1
- package/lib/cards/composition.d.ts +68 -0
- package/lib/cards/composition.d.ts.map +1 -0
- package/lib/cards/composition.js +117 -0
- package/lib/cards/composition.js.map +1 -0
- package/lib/cards/presenter.d.ts +23 -0
- package/lib/cards/presenter.d.ts.map +1 -0
- package/lib/cards/presenter.js +394 -0
- package/lib/cards/presenter.js.map +1 -0
- package/lib/cards/preview.d.ts +42 -0
- package/lib/cards/preview.d.ts.map +1 -0
- package/lib/cards/preview.js +51 -0
- package/lib/cards/preview.js.map +1 -0
- package/lib/cards.d.ts +12 -120
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +4 -556
- package/lib/cards.js.map +1 -1
- package/lib/compat/section.d.ts +29 -0
- package/lib/compat/section.d.ts.map +1 -0
- package/lib/compat/section.js +82 -0
- package/lib/compat/section.js.map +1 -0
- package/lib/config.d.ts +27 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +61 -1
- package/lib/config.js.map +1 -1
- package/lib/contracts.d.ts +28 -0
- package/lib/contracts.d.ts.map +1 -1
- package/lib/fold-cursor.d.ts +8 -0
- package/lib/fold-cursor.d.ts.map +1 -1
- package/lib/fold-cursor.js +18 -0
- package/lib/fold-cursor.js.map +1 -1
- package/lib/gates/question-card.d.ts +77 -0
- package/lib/gates/question-card.d.ts.map +1 -0
- package/lib/gates/question-card.js +113 -0
- package/lib/gates/question-card.js.map +1 -0
- package/lib/gates/questions.d.ts +127 -0
- package/lib/gates/questions.d.ts.map +1 -0
- package/lib/gates/questions.js +418 -0
- package/lib/gates/questions.js.map +1 -0
- package/lib/gates.d.ts +1 -113
- package/lib/gates.d.ts.map +1 -1
- package/lib/gates.js +1 -484
- package/lib/gates.js.map +1 -1
- package/lib/herdr/client.d.ts +8 -0
- package/lib/herdr/client.d.ts.map +1 -1
- package/lib/herdr/client.js +13 -1
- package/lib/herdr/client.js.map +1 -1
- package/lib/herdr/constants.d.ts +18 -0
- package/lib/herdr/constants.d.ts.map +1 -1
- package/lib/herdr/constants.js +18 -0
- package/lib/herdr/constants.js.map +1 -1
- package/lib/herdr/reporter.d.ts +20 -4
- package/lib/herdr/reporter.d.ts.map +1 -1
- package/lib/herdr/reporter.js +70 -20
- package/lib/herdr/reporter.js.map +1 -1
- package/lib/herdr/state.d.ts +10 -0
- package/lib/herdr/state.d.ts.map +1 -1
- package/lib/herdr/state.js +21 -4
- package/lib/herdr/state.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +348 -2602
- package/lib/index.js.map +1 -1
- package/lib/input/action-catalog.d.ts +74 -0
- package/lib/input/action-catalog.d.ts.map +1 -0
- package/lib/input/action-catalog.js +137 -0
- package/lib/input/action-catalog.js.map +1 -0
- package/lib/input/actions.d.ts +5 -81
- package/lib/input/actions.d.ts.map +1 -1
- package/lib/input/actions.js +4 -625
- package/lib/input/actions.js.map +1 -1
- package/lib/input/completion.d.ts +1 -1
- package/lib/input/completion.d.ts.map +1 -1
- package/lib/input/completion.js +2 -1
- package/lib/input/completion.js.map +1 -1
- package/lib/input/file-index.d.ts +40 -0
- package/lib/input/file-index.d.ts.map +1 -0
- package/lib/input/file-index.js +141 -0
- package/lib/input/file-index.js.map +1 -0
- package/lib/input/file-search.d.ts +1 -38
- package/lib/input/file-search.d.ts.map +1 -1
- package/lib/input/file-search.js +1 -487
- package/lib/input/file-search.js.map +1 -1
- package/lib/input/key-press.d.ts +57 -0
- package/lib/input/key-press.d.ts.map +1 -0
- package/lib/input/key-press.js +218 -0
- package/lib/input/key-press.js.map +1 -0
- package/lib/input/keymap-conflicts.d.ts +59 -0
- package/lib/input/keymap-conflicts.d.ts.map +1 -0
- package/lib/input/keymap-conflicts.js +292 -0
- package/lib/input/keymap-conflicts.js.map +1 -0
- package/lib/input/keymap-settings.js +1 -1
- package/lib/input/keymap-settings.js.map +1 -1
- package/lib/input/keymap.d.ts.map +1 -1
- package/lib/input/keymap.js +3 -1
- package/lib/input/keymap.js.map +1 -1
- package/lib/input/workspace-files.d.ts +10 -0
- package/lib/input/workspace-files.d.ts.map +1 -0
- package/lib/input/workspace-files.js +359 -0
- package/lib/input/workspace-files.js.map +1 -0
- package/lib/install-skills.d.ts +39 -0
- package/lib/install-skills.d.ts.map +1 -0
- package/lib/install-skills.js +273 -0
- package/lib/install-skills.js.map +1 -0
- package/lib/keys-command.d.ts +2 -1
- package/lib/keys-command.d.ts.map +1 -1
- package/lib/keys-command.js +2 -1
- package/lib/keys-command.js.map +1 -1
- package/lib/model-list.d.ts +37 -0
- package/lib/model-list.d.ts.map +1 -0
- package/lib/model-list.js +60 -0
- package/lib/model-list.js.map +1 -0
- package/lib/startup.d.ts +8 -0
- package/lib/startup.d.ts.map +1 -1
- package/lib/startup.js +120 -24
- package/lib/startup.js.map +1 -1
- package/lib/subagents.d.ts +7 -3
- package/lib/subagents.d.ts.map +1 -1
- package/lib/subagents.js +28 -4
- package/lib/subagents.js.map +1 -1
- package/lib/surface/appearance-preferences.d.ts +26 -0
- package/lib/surface/appearance-preferences.d.ts.map +1 -0
- package/lib/surface/appearance-preferences.js +139 -0
- package/lib/surface/appearance-preferences.js.map +1 -0
- package/lib/surface/appearance.d.ts +87 -0
- package/lib/surface/appearance.d.ts.map +1 -0
- package/lib/surface/appearance.js +353 -0
- package/lib/surface/appearance.js.map +1 -0
- package/lib/surface/background-work.d.ts +51 -0
- package/lib/surface/background-work.d.ts.map +1 -0
- package/lib/surface/background-work.js +172 -0
- package/lib/surface/background-work.js.map +1 -0
- package/lib/surface/commands.d.ts +82 -0
- package/lib/surface/commands.d.ts.map +1 -0
- package/lib/surface/commands.js +370 -0
- package/lib/surface/commands.js.map +1 -0
- package/lib/surface/modal-input.d.ts +59 -0
- package/lib/surface/modal-input.d.ts.map +1 -0
- package/lib/surface/modal-input.js +243 -0
- package/lib/surface/modal-input.js.map +1 -0
- package/lib/surface/model-choice.d.ts +54 -0
- package/lib/surface/model-choice.d.ts.map +1 -0
- package/lib/surface/model-choice.js +325 -0
- package/lib/surface/model-choice.js.map +1 -0
- package/lib/surface/preset-choice.d.ts +78 -0
- package/lib/surface/preset-choice.d.ts.map +1 -0
- package/lib/surface/preset-choice.js +163 -0
- package/lib/surface/preset-choice.js.map +1 -0
- package/lib/surface/prompt-input.d.ts +97 -0
- package/lib/surface/prompt-input.d.ts.map +1 -0
- package/lib/surface/prompt-input.js +215 -0
- package/lib/surface/prompt-input.js.map +1 -0
- package/lib/surface/prompt-memory.d.ts +76 -0
- package/lib/surface/prompt-memory.d.ts.map +1 -0
- package/lib/surface/prompt-memory.js +167 -0
- package/lib/surface/prompt-memory.js.map +1 -0
- package/lib/surface/session-lifecycle.d.ts +120 -0
- package/lib/surface/session-lifecycle.d.ts.map +1 -0
- package/lib/surface/session-lifecycle.js +324 -0
- package/lib/surface/session-lifecycle.js.map +1 -0
- package/lib/surface/session-picker.d.ts +39 -0
- package/lib/surface/session-picker.d.ts.map +1 -0
- package/lib/surface/session-picker.js +107 -0
- package/lib/surface/session-picker.js.map +1 -0
- package/lib/surface/session-view.d.ts +86 -0
- package/lib/surface/session-view.d.ts.map +1 -0
- package/lib/surface/session-view.js +215 -0
- package/lib/surface/session-view.js.map +1 -0
- package/lib/surface/staged-turns.d.ts +81 -0
- package/lib/surface/staged-turns.d.ts.map +1 -0
- package/lib/surface/staged-turns.js +206 -0
- package/lib/surface/staged-turns.js.map +1 -0
- package/lib/surface/terminal-lifecycle.d.ts +76 -0
- package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
- package/lib/surface/terminal-lifecycle.js +222 -0
- package/lib/surface/terminal-lifecycle.js.map +1 -0
- package/lib/terminal-text/scan.d.ts +44 -0
- package/lib/terminal-text/scan.d.ts.map +1 -0
- package/lib/terminal-text/scan.js +195 -0
- package/lib/terminal-text/scan.js.map +1 -0
- package/lib/terminal-text/sgr.d.ts +32 -0
- package/lib/terminal-text/sgr.d.ts.map +1 -0
- package/lib/terminal-text/sgr.js +203 -0
- package/lib/terminal-text/sgr.js.map +1 -0
- package/lib/terminal-text.d.ts +1 -1
- package/lib/terminal-text.d.ts.map +1 -1
- package/lib/terminal-text.js +2 -379
- package/lib/terminal-text.js.map +1 -1
- package/lib/theme-command.js +2 -1
- package/lib/theme-command.js.map +1 -1
- package/lib/theme-defaults.d.ts +64 -0
- package/lib/theme-defaults.d.ts.map +1 -0
- package/lib/theme-defaults.js +248 -0
- package/lib/theme-defaults.js.map +1 -0
- package/lib/theme-files.d.ts +1 -1
- package/lib/theme-files.d.ts.map +1 -1
- package/lib/theme-files.js +1 -1
- package/lib/theme-files.js.map +1 -1
- package/lib/theme-resolver.d.ts +36 -0
- package/lib/theme-resolver.d.ts.map +1 -0
- package/lib/theme-resolver.js +112 -0
- package/lib/theme-resolver.js.map +1 -0
- package/lib/theme-schema.d.ts.map +1 -1
- package/lib/theme-schema.js +3 -2
- package/lib/theme-schema.js.map +1 -1
- package/lib/theme-settings.d.ts +5 -7
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +37 -21
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +0 -89
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +0 -356
- package/lib/theme-tokens.js.map +1 -1
- package/lib/theme.d.ts +1 -1
- package/lib/theme.d.ts.map +1 -1
- package/lib/theme.js +2 -1
- package/lib/theme.js.map +1 -1
- package/lib/tool-display.d.ts.map +1 -1
- package/lib/tool-display.js +2 -1
- package/lib/tool-display.js.map +1 -1
- package/lib/transcript/message-content.d.ts +21 -0
- package/lib/transcript/message-content.d.ts.map +1 -0
- package/lib/transcript/message-content.js +61 -0
- package/lib/transcript/message-content.js.map +1 -0
- package/lib/transcript/tool-calls.d.ts +106 -0
- package/lib/transcript/tool-calls.d.ts.map +1 -0
- package/lib/transcript/tool-calls.js +254 -0
- package/lib/transcript/tool-calls.js.map +1 -0
- package/lib/transcript.d.ts +14 -55
- package/lib/transcript.d.ts.map +1 -1
- package/lib/transcript.js +31 -253
- package/lib/transcript.js.map +1 -1
- package/lib/ui/dock.d.ts +14 -2
- package/lib/ui/dock.d.ts.map +1 -1
- package/lib/ui/dock.js +52 -10
- package/lib/ui/dock.js.map +1 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +2 -1
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/keymap-picker.d.ts +2 -1
- package/lib/ui/keymap-picker.d.ts.map +1 -1
- package/lib/ui/keymap-picker.js +1 -0
- package/lib/ui/keymap-picker.js.map +1 -1
- package/lib/ui/view/gate-card.d.ts +21 -0
- package/lib/ui/view/gate-card.d.ts.map +1 -0
- package/lib/ui/view/gate-card.js +104 -0
- package/lib/ui/view/gate-card.js.map +1 -0
- package/lib/ui/view/tool-card.d.ts +149 -0
- package/lib/ui/view/tool-card.d.ts.map +1 -0
- package/lib/ui/view/tool-card.js +429 -0
- package/lib/ui/view/tool-card.js.map +1 -0
- package/lib/ui/view/transcript-message.d.ts +62 -0
- package/lib/ui/view/transcript-message.d.ts.map +1 -0
- package/lib/ui/view/transcript-message.js +158 -0
- package/lib/ui/view/transcript-message.js.map +1 -0
- package/lib/ui/view.d.ts +105 -223
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +132 -679
- package/lib/ui/view.js.map +1 -1
- package/package.json +3 -1
- package/themes/violet-orbit.yaml +7 -5
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Model wiring reference
|
|
2
|
+
|
|
3
|
+
Companion to [SKILL.md](../SKILL.md): exact fields, files, and checks.
|
|
4
|
+
|
|
5
|
+
## Resolution order
|
|
6
|
+
|
|
7
|
+
1. Bundle layers in `cordis.yml`, in order.
|
|
8
|
+
2. The profile patch `~/.dsh/profiles/<profile>/cordis.patch.yml`, entry by entry:
|
|
9
|
+
`id` targeted config, `disabled`, `insert`.
|
|
10
|
+
3. A patch `config` **replaces** that row's base config; fields it omits fall back
|
|
11
|
+
to the schema default, not to the bundle's value.
|
|
12
|
+
4. The model directory a profile sees is owned by whichever row is enabled:
|
|
13
|
+
`llm-pi-ai` (additive provider config), `dsh-provider-extra` (owned catalog),
|
|
14
|
+
or both, if the profile is not using exclusive ownership.
|
|
15
|
+
5. `$DSH_HOME/settings.yaml` sits over the adapter rows, not over a catalog. A
|
|
16
|
+
`llm-pi-ai:` or `llm-deepseek:` section reshapes that adapter per request; a
|
|
17
|
+
`dsh-provider-extra:` section reshapes only the legacy routes'
|
|
18
|
+
`extraModels`/`codexExtraModels`, and only where no `models`/`codexModels`
|
|
19
|
+
selection is declared. A managed catalog reads no settings overlay at all, so
|
|
20
|
+
a catalog edit lands in the profile patch or nowhere.
|
|
21
|
+
|
|
22
|
+
Exclusive ownership is a set of disable rows plus the catalog itself:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
- { "id": "agent-default-model", "disabled": true }
|
|
26
|
+
- { "id": "llm-pi-ai", "disabled": true }
|
|
27
|
+
- { "id": "llm-deepseek", "disabled": true }
|
|
28
|
+
- id: dsh-provider-extra
|
|
29
|
+
config: { catalog: { version: 1, ... } }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Disable every competing row in the **whole** profile, not only the rows that come
|
|
33
|
+
before provider-extra: on `dsh-base` those are `llm-pi-ai`, `llm-deepseek`, and
|
|
34
|
+
`agent-default-model`, and another bundle may add `llm-openai` or
|
|
35
|
+
`llm-anthropic`. Inspect the composed profile rather than trusting this list.
|
|
36
|
+
Preflight refuses to register managed routes when an adapter, a provider
|
|
37
|
+
directory, or a default owner already exists; a later registration is caught by
|
|
38
|
+
the host's `llm/adapters-updated` notification. Either way the plugin logs
|
|
39
|
+
`CATALOG_OWNER_COLLISION` once per transition and fails closed: listing,
|
|
40
|
+
selection resolution, request preparation, default reads and saves, new
|
|
41
|
+
dispatch, and starting a sign-in all refuse while the conflict exists. Requests
|
|
42
|
+
already past their credential gates finish. Removing the competing rows restores
|
|
43
|
+
service without a reload of the catalog itself.
|
|
44
|
+
|
|
45
|
+
## `llm-pi-ai` provider config
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
- id: llm-pi-ai
|
|
49
|
+
config:
|
|
50
|
+
providers:
|
|
51
|
+
qwen-token-plan:
|
|
52
|
+
apiKeyEnv: QWEN_TOKEN_PLAN_API_KEY # env var holding the key
|
|
53
|
+
baseURL: https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1
|
|
54
|
+
api: openai-completions # optional wire override
|
|
55
|
+
models:
|
|
56
|
+
- id: qwen3.8-max # served as-is
|
|
57
|
+
- id: deepseek-v4.1-flash
|
|
58
|
+
name: DeepSeek V4.1 Flash
|
|
59
|
+
contextWindow: 1000000
|
|
60
|
+
maxTokens: 384000
|
|
61
|
+
input: [text, image]
|
|
62
|
+
reasoningEfforts: { low: low, high: high, max: max }
|
|
63
|
+
compat:
|
|
64
|
+
supportsStore: false
|
|
65
|
+
supportsDeveloperRole: false
|
|
66
|
+
supportsReasoningEffort: true
|
|
67
|
+
supportsStrictMode: true
|
|
68
|
+
thinkingFormat: qwen
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
An id that the installed catalog already describes needs no restatement. An id it
|
|
72
|
+
does not describe needs the metadata (or a `template` sibling to clone).
|
|
73
|
+
|
|
74
|
+
Route fields (`providers` is a dict keyed by the route id; the key is the route):
|
|
75
|
+
`apiKeyEnv`, `displayName`, `api`, `baseURL`, `models`, `modelOverrides` (per-id
|
|
76
|
+
reshaping of an installed catalog, refused beside a `models` list), `compat`,
|
|
77
|
+
`defaultContextWindow` (262144), `defaultMaxTokens` (32768), `defaultInput`
|
|
78
|
+
(`[text]`), `headers`, `reasoning`, `thinkingBudgets`, `cacheRetention`,
|
|
79
|
+
`transport`, `timeoutMs`, `websocketConnectTimeoutMs`, `streamIdleTimeoutMs`,
|
|
80
|
+
`maxRequestImageBytes`, `requestImagePixelBudget`, `requestImageMaxBytes`,
|
|
81
|
+
`retryPolicy`. A route key that names an installed pi-ai provider inherits that
|
|
82
|
+
provider's endpoint, protocol, display name, and catalog; any other key is a
|
|
83
|
+
complete hand declaration and must name `api` and `baseURL`.
|
|
84
|
+
|
|
85
|
+
`reasoningEfforts` is this adapter's spelling of `thinkingLevelMap`: a dict of
|
|
86
|
+
selectable level to the wire value dispatch sends, `false` for a non-reasoning
|
|
87
|
+
model, and only `off` may carry an empty or null value. An empty dict, a level
|
|
88
|
+
mapping to an empty string, or a dict offering nothing beyond `off` is refused.
|
|
89
|
+
|
|
90
|
+
## `dsh-provider-extra`
|
|
91
|
+
|
|
92
|
+
Two config generations exist; a profile normally uses one, and presence of
|
|
93
|
+
`catalog` is what opts a profile into managed ownership:
|
|
94
|
+
|
|
95
|
+
| Config | Use |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| Additive: `apiKeyEnv`, `routeId`, `displayName`, `baseURL`, `headers`, `fallbackSessionId`, `models`, `extraModels`, `codexEnabled`, `codexRouteId`, `codexDisplayName`, `codexModels`, `codexExtraModels`, `codexTransport`, `loginCommandEnabled`, `loginCommandName` | add a gateway or subscription route beside whatever else the profile serves |
|
|
98
|
+
| `catalog` v1 | own the entire directory: routes, ids, aliases, metadata, and the default selection |
|
|
99
|
+
|
|
100
|
+
The generations differ where a declaration cannot resolve. An additive
|
|
101
|
+
`extraModels` entry with a bad `template` lands in that route's model
|
|
102
|
+
diagnostics while the route keeps serving; an additive `models`/`codexModels`
|
|
103
|
+
whitelist naming an id nothing provides refuses the whole route. A **catalog**
|
|
104
|
+
rejects the whole candidate instead: there is no per-model diagnostic path.
|
|
105
|
+
|
|
106
|
+
Catalog route fields (an unknown field rejects the candidate):
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
catalog:
|
|
110
|
+
version: 1 # exactly 1
|
|
111
|
+
default: # null only when nothing is served
|
|
112
|
+
{ provider: opencode-go-session, model: deepseek-flash, reasoningEffort: max }
|
|
113
|
+
providers:
|
|
114
|
+
- id: opencode-go-session # the id every selection and alias check uses
|
|
115
|
+
name: OpenCode Go (session)
|
|
116
|
+
source: opencode-go # installed pi-ai provider id; its data is the base
|
|
117
|
+
api: openai-completions # own endpoint only: required without source, refused beside it
|
|
118
|
+
baseURL: https://api.x.ai/v1 # required without source; http(s), no user/pass/fragment
|
|
119
|
+
headers: { X-Route: tenant-a } # static headers; secrets stay in a credential reference
|
|
120
|
+
transport: sse # openai-codex only: sse|websocket|websocket-cached|auto
|
|
121
|
+
fallbackSessionId: dsh-provider-extra # opencode-go only: session header for sessionless calls
|
|
122
|
+
auth: { apiKeyRef: OPENCODE_GO_API_KEY } # or { credentialProvider: openai-codex }; absent = keyless route
|
|
123
|
+
models:
|
|
124
|
+
- id: deepseek-flash
|
|
125
|
+
name: DeepSeek V4.1 Flash
|
|
126
|
+
aliases: [deepseek-v4.1-flash] # extra selector inputs, unique in the route, never extra rows
|
|
127
|
+
template: deepseek-v4-flash # installed sibling of the SAME source to clone
|
|
128
|
+
defaultMaxTokens: 384000 # request default; must be <= resolved maxTokens
|
|
129
|
+
metadata:
|
|
130
|
+
api: openai-completions
|
|
131
|
+
reasoning: true
|
|
132
|
+
input: [text, image]
|
|
133
|
+
cost: { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0 }
|
|
134
|
+
# a threshold-priced vendor adds its tier rows beside those four rates:
|
|
135
|
+
# cost: { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0,
|
|
136
|
+
# tiers: [{ inputTokensAbove: 272000, input: 0.44, output: 1.32,
|
|
137
|
+
# cacheRead: 0.014, cacheWrite: 0 }] }
|
|
138
|
+
contextWindow: 1000000
|
|
139
|
+
maxTokens: 384000 # capacity, never a request cap
|
|
140
|
+
thinkingLevelMap: { off: null, minimal: null, low: low, medium: null, high: high, xhigh: null, max: max }
|
|
141
|
+
headers: { X-Model: flash }
|
|
142
|
+
compat: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, supportsStrictMode: true, thinkingFormat: qwen }
|
|
143
|
+
- id: zai-catalog
|
|
144
|
+
name: Z.AI Coding Plan
|
|
145
|
+
source: zai
|
|
146
|
+
auth: { apiKeyRef: ZAI_API_KEY }
|
|
147
|
+
filter: { include: ['glm-*'], exclude: ['glm-4*'] } # instead of models; '*' is the only wildcard
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`filter` needs an installed `source`: an absent `include` starts from every
|
|
151
|
+
installed model, matches keep installed order and every installed fact (id, name,
|
|
152
|
+
cost, capacity, protocol, endpoint), and the route serves them minus `exclude`.
|
|
153
|
+
A matched model that publishes no endpoint needs a route `baseURL`.
|
|
154
|
+
|
|
155
|
+
A model with no resolvable template must declare all of `api` (unless the route
|
|
156
|
+
declares `api`), `reasoning`, `input`, `cost`, `contextWindow`, and
|
|
157
|
+
`maxTokens`. `metadata.api` cannot change a template-backed protocol, must equal
|
|
158
|
+
a route's declared `api`, and must be an api the `source` describes.
|
|
159
|
+
|
|
160
|
+
`compat` accepts only the flags the model's protocol honors: completions takes the
|
|
161
|
+
`supports*`/`requires*` booleans plus `maxTokensField`, `thinkingFormat`,
|
|
162
|
+
`thinkingTokenBudgetField`, `cacheControlFormat`, `deferredToolsMode`, and
|
|
163
|
+
`sessionAffinityFormat`; responses takes its own flag set plus
|
|
164
|
+
`sessionAffinityFormat`; anthropic-messages takes its flag set and no enums.
|
|
165
|
+
Template-language and gateway-routing keys (`chatTemplateKwargs`,
|
|
166
|
+
`chatTemplateArgs`, `openRouterRouting`, `vercelGatewayRouting`,
|
|
167
|
+
`vllmPriority`, `allowedFallbackModels`) are refused. The plugin's own
|
|
168
|
+
`src/catalog-routes.ts` lists the accepted names per protocol.
|
|
169
|
+
|
|
170
|
+
Behaviour:
|
|
171
|
+
|
|
172
|
+
| Case | Result |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `template` names a sibling the source ships | clone its wire behavior and metadata |
|
|
175
|
+
| `template` names nothing, or the route has no source | candidate rejected: `unknown template <id>` |
|
|
176
|
+
| unknown id, no template, incomplete metadata | candidate rejected: `unknown model requires complete metadata: missing <field>` |
|
|
177
|
+
| both `models` and `filter`, or neither | candidate rejected: `declare exactly one of models or filter` |
|
|
178
|
+
| duplicate provider id, model id, or alias | candidate rejected |
|
|
179
|
+
| `defaultMaxTokens` above the resolved `maxTokens` | candidate rejected: `exceeds model maxTokens` |
|
|
180
|
+
| `default` names a model outside the served selection | candidate rejected; an alias is canonicalized to its wire id |
|
|
181
|
+
| `reasoningEffort` unsupported by the model | candidate rejected |
|
|
182
|
+
| nothing served (empty providers, or `models: []`) | `default` must be `null`; `currentSelection()` throws `NO_DEFAULT_MODEL` |
|
|
183
|
+
|
|
184
|
+
### Default persistence
|
|
185
|
+
|
|
186
|
+
`catalog.default` reaches a new session as provider and model only: the pinned
|
|
187
|
+
host ignores its `reasoningEffort`, so effort is chosen in the surface.
|
|
188
|
+
`saveSelection(next)` validates first, then writes through the host's
|
|
189
|
+
`configEditor.edit(entry, updater)` so only `catalog.default` changes and
|
|
190
|
+
membership is re-checked inside the editor's lock. A host with no addressable
|
|
191
|
+
entry or no such editor rejects with `CONFIG_PERSISTENCE_UNAVAILABLE` - the
|
|
192
|
+
released `0.1.5-rc.2` host does - and nothing falls back to legacy
|
|
193
|
+
`agent-default-model` settings. Edit the profile patch there.
|
|
194
|
+
|
|
195
|
+
`compileCatalog(config)` and `buildCatalogProfile(provider)` are public: the
|
|
196
|
+
first returns a detached frozen snapshot or `undefined` for absence, the second
|
|
197
|
+
validates one route declaration on its own. Both are what the dump script and a
|
|
198
|
+
candidate check should read against.
|
|
199
|
+
|
|
200
|
+
## Where the base data comes from
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
# the harness install owns the pi-ai catalog the profile resolves against
|
|
204
|
+
DSH=$(readlink -f "$(command -v dsh)")
|
|
205
|
+
PI_AI="${DSH%/lib/bin.js}/node_modules/@earendil-works/pi-ai"
|
|
206
|
+
ls "$PI_AI/dist/providers/data" # one JSON per provider source, keyed by api
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`source` must be an id the installed pi-ai build ships (`builtinProviders()`);
|
|
210
|
+
`ls` the data directory for the list this harness answers. A JSON entry is the
|
|
211
|
+
whole truth for a `source`: `id`, `name`, `cost`, `contextWindow`,
|
|
212
|
+
`maxTokens`, `input`, `reasoning`, `thinkingLevelMap`, `compat`, `baseUrl`.
|
|
213
|
+
A model declared without `template` and without `metadata` is served exactly as
|
|
214
|
+
this file describes it, so an id the vendor renamed or repriced shows stale
|
|
215
|
+
numbers until the catalog declares them. An entry that carries its own complete
|
|
216
|
+
`metadata` needs no installed entry at all, which is how a route the harness does
|
|
217
|
+
not describe is wired.
|
|
218
|
+
|
|
219
|
+
## Credentials
|
|
220
|
+
|
|
221
|
+
| Field | Meaning |
|
|
222
|
+
| --- | --- |
|
|
223
|
+
| `apiKeyEnv` | additive config and `llm-pi-ai`: read the key from this environment variable |
|
|
224
|
+
| `apiKeyRef` | catalog: a host credential reference; without a credential service the route reads that environment variable. Refused on an `openai-codex` source, which needs OAuth |
|
|
225
|
+
| `credentialProvider` | catalog: use a login flow's stored grant. Needs an installed `source` and must equal that source id |
|
|
226
|
+
| no `auth` | catalog: the route carries no key of its own, as a local endpoint does |
|
|
227
|
+
|
|
228
|
+
Keys never belong in a patch, a settings file, or a repository. Put launch-time
|
|
229
|
+
values in `$DSH_HOME/.env` or the environment. A provider row whose credential is
|
|
230
|
+
missing fails its route only; other routes still serve.
|
|
231
|
+
|
|
232
|
+
## Verify
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
dsh --profile tui list-models # provider/model<TAB>display name, one per reachable route
|
|
236
|
+
~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs \
|
|
237
|
+
[--home "$DSH_HOME"] [--profile tui] [--data <pi-ai data dir>] [--json]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The dump reads the catalog row written as a single JSON flow map
|
|
241
|
+
(`- {"id":"dsh-provider-extra","config":{"catalog":…}}`), which is the shape
|
|
242
|
+
`pnpm catalog:setup` appends and the shape a hand-written patch keeps when one
|
|
243
|
+
line must stay machine-readable. A catalog spread over block YAML is invisible to
|
|
244
|
+
it; `list-models` still reads the composed result either way. `--data` points the
|
|
245
|
+
dump at a provider-data directory instead of the installed harness, which is how
|
|
246
|
+
a template clone or a `filter` route is read on a machine with no `dsh`. The dump
|
|
247
|
+
also reports what the resolver would refuse, and exits 1, because a catalog that
|
|
248
|
+
cannot compose serves nothing: incomplete metadata for an unknown model, a
|
|
249
|
+
`defaultMaxTokens` above capacity, both or neither of `models`/`filter`, a
|
|
250
|
+
default the served selection does not contain, an effort the selected model does
|
|
251
|
+
not offer, and a default that is null while models are served.
|
|
252
|
+
|
|
253
|
+
To validate a candidate on a disposable home, provider-extra's clone-only setup
|
|
254
|
+
composes it and disables the competing rows itself:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
cd <dsh-provider-extra checkout> && pnpm catalog:setup \
|
|
258
|
+
--helper <plugin checkout>/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh \
|
|
259
|
+
--source-home ~/.dsh --home /tmp/dsh-catalog-check --profile tui \
|
|
260
|
+
--catalog /tmp/catalog-config.json [--allow-row ROW_ID] [--dsh /abs/path/to/dsh/lib/bin.js]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`--catalog` is a private JSON file holding only `{"catalog": {...}}`. It prints
|
|
264
|
+
`composition-verified` on success, which is composition only: not runtime
|
|
265
|
+
ownership, not authentication. `--allow-row` asserts a provider-looking row
|
|
266
|
+
registers no provider and no default owner, because setup cannot see row
|
|
267
|
+
provenance and refuses rather than guesses.
|
|
268
|
+
|
|
269
|
+
`list-models` exits zero only after printing a line, so a deployment that
|
|
270
|
+
advertises nothing cannot pass for one that was enumerated.
|
|
271
|
+
|
|
272
|
+
For every model you touched, confirm against the vendor's own page:
|
|
273
|
+
|
|
274
|
+
1. price per 1M tokens for input, output, cached input, cache write;
|
|
275
|
+
2. context window and maximum output tokens (a "272K" beside "above 272K input
|
|
276
|
+
is priced 2x" is a billing threshold, not the window);
|
|
277
|
+
3. the accepted reasoning effort values, and how the vendor spells "no reasoning"
|
|
278
|
+
(OpenAI: `none`; codex: omit the parameter);
|
|
279
|
+
4. whether the route bills per token at all - a metered API wants its published
|
|
280
|
+
rates, and a credit or token plan writes `0`, which reads as unknown pricing
|
|
281
|
+
rather than free inference;
|
|
282
|
+
5. that an `xhigh` or `max` mapping exists when the vendor documents one;
|
|
283
|
+
6. that the same model id served on two routes carries the same facts: each route
|
|
284
|
+
inherits its own `source` entry, so one declared `contextWindow` and one
|
|
285
|
+
inherited is two different ctx gauges for one model.
|
|
286
|
+
|
|
287
|
+
A model with no vendor page (stealth releases, previews) is documented only by the
|
|
288
|
+
routing catalog: its pi.dev model page and `models.dev/api.json`.
|
|
289
|
+
|
|
290
|
+
Then run the profile against a cloned home before the real one - see the
|
|
291
|
+
`dsh-tui-dogfood` skill - and `pnpm run build` any linked bundle first: a linked
|
|
292
|
+
profile loads `lib/`, not `src/`.
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print the model directory a dsh profile resolves, with the provenance of
|
|
4
|
+
* every field.
|
|
5
|
+
*
|
|
6
|
+
* The surface reads prices, context windows, and reasoning levels from the
|
|
7
|
+
* composition, not from the vendor's page, so an inherited or stale value is
|
|
8
|
+
* invisible in the UI. This reads the same two inputs the resolver does - the
|
|
9
|
+
* owned catalog in the profile patch, and the installed pi-ai provider data -
|
|
10
|
+
* and names where each number came from: "installed" for the source's own
|
|
11
|
+
* entry, "template:<id>" for a clone, "route" for the protocol a sourceless
|
|
12
|
+
* route declares once for every model it serves, "metadata" for what the
|
|
13
|
+
* catalog declares.
|
|
14
|
+
*
|
|
15
|
+
* An entry the catalog spells out completely needs no installed entry at all,
|
|
16
|
+
* so a missing harness install must not hide it. A `filter` route is the
|
|
17
|
+
* exception: its membership is the installed catalog's, so it cannot be read
|
|
18
|
+
* without that install. A declaration the plugin's resolver would refuse is
|
|
19
|
+
* reported as a refusal here too, because a catalog that cannot compose serves
|
|
20
|
+
* nothing.
|
|
21
|
+
*
|
|
22
|
+
* Usage: dump-model-catalog.mjs [--home <dsh home>] [--profile <name>]
|
|
23
|
+
* [--data <pi-ai data dir>] [--json]
|
|
24
|
+
*
|
|
25
|
+
* `--data` names the installed provider-data directory to inherit from, which
|
|
26
|
+
* is how a reading is checked where no harness is installed - a `filter` route
|
|
27
|
+
* and every template clone need one.
|
|
28
|
+
*/
|
|
29
|
+
import { execFileSync } from 'node:child_process'
|
|
30
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
31
|
+
import { homedir } from 'node:os'
|
|
32
|
+
import { join } from 'node:path'
|
|
33
|
+
import { fileURLToPath } from 'node:url'
|
|
34
|
+
|
|
35
|
+
const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
36
|
+
/** Fields a catalog entry may inherit, and the layer each value came from. */
|
|
37
|
+
const INHERITED_FIELDS = ['contextWindow', 'maxTokens', 'cost', 'input', 'reasoning', 'thinkingLevelMap', 'api']
|
|
38
|
+
/** What a model with no installed sibling and no template must state itself. */
|
|
39
|
+
const REQUIRED_METADATA = ['api', 'reasoning', 'input', 'cost', 'contextWindow', 'maxTokens']
|
|
40
|
+
const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
41
|
+
const INSTALLED = 'installed'
|
|
42
|
+
const METADATA = 'metadata'
|
|
43
|
+
const ROUTE = 'route'
|
|
44
|
+
const ABSENT = 'none'
|
|
45
|
+
/** The only wildcard a `filter` pattern carries; every other character is itself. */
|
|
46
|
+
const WILDCARD = '*'
|
|
47
|
+
|
|
48
|
+
/** The levels pi-ai offers: a null mapping removes one, and xhigh/max need one. */
|
|
49
|
+
export function supportedLevels(levelMap) {
|
|
50
|
+
return LEVELS.filter((level) => {
|
|
51
|
+
const mapped = levelMap?.[level]
|
|
52
|
+
if (mapped === null) return false
|
|
53
|
+
if (level === 'xhigh' || level === 'max') return mapped !== undefined
|
|
54
|
+
return true
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const options = { home: process.env.DSH_HOME ?? join(homedir(), '.dsh'), profile: 'tui', data: undefined, json: false }
|
|
60
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
61
|
+
const flag = argv[index]
|
|
62
|
+
if (flag === '--json') options.json = true
|
|
63
|
+
else if (flag === '--home') options.home = argv[++index]
|
|
64
|
+
else if (flag === '--profile') options.profile = argv[++index]
|
|
65
|
+
else if (flag === '--data') options.data = argv[++index]
|
|
66
|
+
else throw new Error('unknown argument ' + flag)
|
|
67
|
+
}
|
|
68
|
+
return options
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The pi-ai data directory of the harness install the launcher belongs to. */
|
|
72
|
+
function dataDirectory() {
|
|
73
|
+
try {
|
|
74
|
+
const bin = execFileSync('sh', ['-c', 'command -v dsh'], { encoding: 'utf8' }).trim()
|
|
75
|
+
const root = join(realpathSync(bin), '..', '..')
|
|
76
|
+
return join(root, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'data')
|
|
77
|
+
} catch {
|
|
78
|
+
// No harness install is a supported reading: declared entries still resolve.
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every JSON patch entry that configures the owned catalog, last one winning. */
|
|
84
|
+
export function catalogConfig(patch) {
|
|
85
|
+
const lines = readFileSync(patch, 'utf8').split('\n')
|
|
86
|
+
let catalog
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (!line.startsWith('- {') || !line.includes('"catalog"')) continue
|
|
89
|
+
const entry = JSON.parse(line.slice(2))
|
|
90
|
+
if (entry.config?.catalog !== undefined) catalog = entry.config.catalog
|
|
91
|
+
}
|
|
92
|
+
return catalog
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function providerData(directory, source) {
|
|
96
|
+
if (directory === undefined || source === undefined) return undefined
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(readFileSync(join(directory, source + '.json'), 'utf8'))
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Installed models in the order the source publishes them, one api map at a time. */
|
|
105
|
+
function installedModels(data) {
|
|
106
|
+
if (data === undefined) return []
|
|
107
|
+
return Object.entries(data).flatMap(([api, models]) => Object.entries(models)
|
|
108
|
+
.map(([id, model]) => ({ ...model, id, api: model.api ?? api })))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Mirror the resolver's pattern rule so a filter reads the same here and there. */
|
|
112
|
+
function patternToRegExp(pattern) {
|
|
113
|
+
const escaped = pattern.split(WILDCARD).map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
114
|
+
return new RegExp('^' + escaped.join('.*') + '$')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A filter keeps installed facts and order, then drops what exclude names. */
|
|
118
|
+
export function expandFilter(data, filter) {
|
|
119
|
+
const include = filter?.include === undefined ? undefined : filter.include.map(patternToRegExp)
|
|
120
|
+
const exclude = filter?.exclude === undefined ? [] : filter.exclude.map(patternToRegExp)
|
|
121
|
+
return installedModels(data)
|
|
122
|
+
.filter(model => (include === undefined || include.some(pattern => pattern.test(model.id)))
|
|
123
|
+
&& !exclude.some(pattern => pattern.test(model.id)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Look one id up across a provider's api maps; an api in the spec disambiguates. */
|
|
127
|
+
function lookup(data, id, api) {
|
|
128
|
+
if (data === undefined) return undefined
|
|
129
|
+
for (const [key, models] of Object.entries(data)) {
|
|
130
|
+
if ((api === undefined || key === api) && models[id] !== undefined) return { ...models[id], api: models[id].api ?? key }
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function costOf(cost) {
|
|
136
|
+
if (cost === undefined) return undefined
|
|
137
|
+
return '$' + COST_FIELDS.map(field => cost[field]).join('/') + ' per 1M'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Resolve one catalog entry against the installed data it may inherit from. */
|
|
141
|
+
export function resolve(spec, data, routeApi) {
|
|
142
|
+
const metadata = spec.metadata ?? {}
|
|
143
|
+
const declared = Object.keys(metadata).length > 0
|
|
144
|
+
const cloneOf = spec.template ?? spec.id
|
|
145
|
+
const base = lookup(data, cloneOf, metadata.api)
|
|
146
|
+
if (base === undefined && !declared) {
|
|
147
|
+
return { error: 'no source entry for ' + cloneOf + '; declare a template or metadata' }
|
|
148
|
+
}
|
|
149
|
+
const merged = { ...base, ...metadata, id: spec.id, name: spec.name ?? base?.name ?? spec.id }
|
|
150
|
+
// A route that names no source states its protocol once, for every model.
|
|
151
|
+
if (merged.api === undefined && routeApi !== undefined) merged.api = routeApi
|
|
152
|
+
// Capacity and the request default are different numbers, and the resolver
|
|
153
|
+
// caps one by the other: folding them would hide the mistake it refuses.
|
|
154
|
+
const capacity = metadata.maxTokens ?? base?.maxTokens
|
|
155
|
+
const missing = base === undefined
|
|
156
|
+
? REQUIRED_METADATA.filter(field => field === 'api' ? merged.api === undefined : metadata[field] === undefined)
|
|
157
|
+
: []
|
|
158
|
+
if (missing.length > 0) return { error: 'unknown model requires complete metadata: missing ' + missing.join(', ') }
|
|
159
|
+
if (spec.defaultMaxTokens !== undefined && capacity !== undefined && spec.defaultMaxTokens > capacity) {
|
|
160
|
+
return { error: 'defaultMaxTokens ' + spec.defaultMaxTokens + ' exceeds model maxTokens ' + capacity }
|
|
161
|
+
}
|
|
162
|
+
const origins = {}
|
|
163
|
+
for (const field of INHERITED_FIELDS) {
|
|
164
|
+
if (metadata[field] !== undefined) origins[field] = METADATA
|
|
165
|
+
else if (field === 'api' && base === undefined && routeApi !== undefined) origins[field] = ROUTE
|
|
166
|
+
else if (base === undefined) origins[field] = ABSENT
|
|
167
|
+
else origins[field] = spec.template === undefined ? INSTALLED : 'template:' + spec.template
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
model: { ...merged, maxTokens: capacity },
|
|
171
|
+
origins,
|
|
172
|
+
levels: supportedLevels(merged.thinkingLevelMap),
|
|
173
|
+
...(spec.defaultMaxTokens === undefined ? {} : { requestMax: spec.defaultMaxTokens }),
|
|
174
|
+
...(spec.aliases === undefined ? {} : { aliases: spec.aliases.join(',') }),
|
|
175
|
+
...(metadata.cost?.tiers === undefined ? {} : {
|
|
176
|
+
tiers: metadata.cost.tiers.map(tier => 'above ' + tier.inputTokensAbove + ': ' + costOf(tier)).join('; '),
|
|
177
|
+
}),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function rowsOf(catalog, directory) {
|
|
182
|
+
const rows = []
|
|
183
|
+
for (const provider of catalog.providers ?? []) {
|
|
184
|
+
const data = providerData(directory, provider.source)
|
|
185
|
+
const hasModels = 'models' in provider
|
|
186
|
+
const hasFilter = 'filter' in provider
|
|
187
|
+
// The resolver refuses both and neither; saying so here beats serving half.
|
|
188
|
+
if (hasModels === hasFilter) {
|
|
189
|
+
rows.push({ route: provider.id, error: 'declare exactly one of models or filter' })
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (hasFilter) {
|
|
193
|
+
if (data === undefined) {
|
|
194
|
+
rows.push({
|
|
195
|
+
route: provider.id,
|
|
196
|
+
error: 'filter expands the installed catalog of source "' + provider.source + '"; no harness install found',
|
|
197
|
+
})
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
for (const model of expandFilter(data, provider.filter)) {
|
|
201
|
+
rows.push({
|
|
202
|
+
route: provider.id + '/' + model.id,
|
|
203
|
+
name: model.name ?? model.id,
|
|
204
|
+
api: model.api,
|
|
205
|
+
contextWindow: model.contextWindow,
|
|
206
|
+
maxTokens: model.maxTokens,
|
|
207
|
+
cost: costOf(model.cost),
|
|
208
|
+
levels: supportedLevels(model.thinkingLevelMap).join(','),
|
|
209
|
+
origins: 'filter:' + (provider.filter.include ?? [WILDCARD]).join(',') + ' all installed',
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
for (const spec of provider.models ?? []) {
|
|
215
|
+
const result = resolve(spec, data, provider.api)
|
|
216
|
+
const route = provider.id + '/' + spec.id
|
|
217
|
+
if (result.error !== undefined) {
|
|
218
|
+
rows.push({ route, error: result.error })
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
rows.push({
|
|
222
|
+
route,
|
|
223
|
+
name: result.model.name,
|
|
224
|
+
api: result.model.api,
|
|
225
|
+
contextWindow: result.model.contextWindow,
|
|
226
|
+
maxTokens: result.model.maxTokens,
|
|
227
|
+
cost: costOf(result.model.cost),
|
|
228
|
+
levels: result.levels.join(','),
|
|
229
|
+
...(result.requestMax === undefined ? {} : { requestMax: result.requestMax }),
|
|
230
|
+
...(result.aliases === undefined ? {} : { aliases: result.aliases }),
|
|
231
|
+
...(result.tiers === undefined ? {} : { tiers: result.tiers }),
|
|
232
|
+
origins: INHERITED_FIELDS
|
|
233
|
+
.map(field => [field, result.origins[field]])
|
|
234
|
+
.filter(([, origin]) => origin !== INSTALLED)
|
|
235
|
+
.map(([field, origin]) => field + '=' + origin)
|
|
236
|
+
.join(' ') || 'all installed',
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return rows
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The default is part of the catalog, not a comment beside it: the resolver
|
|
245
|
+
* refuses a selection nothing serves, so a reading that skipped it would call a
|
|
246
|
+
* broken catalog readable.
|
|
247
|
+
*/
|
|
248
|
+
export function checkDefault(catalog, rows) {
|
|
249
|
+
const served = rows.filter(row => row.error === undefined)
|
|
250
|
+
const selection = catalog.default ?? null
|
|
251
|
+
if (served.length === 0) {
|
|
252
|
+
return selection === null ? [] : [{ route: 'default', error: 'an empty catalog requires default null' }]
|
|
253
|
+
}
|
|
254
|
+
if (selection === null || typeof selection !== 'object') {
|
|
255
|
+
return [{ route: 'default', error: 'a served catalog requires a default selection' }]
|
|
256
|
+
}
|
|
257
|
+
const alias = new Map()
|
|
258
|
+
for (const provider of catalog.providers ?? []) {
|
|
259
|
+
for (const model of provider.models ?? []) {
|
|
260
|
+
for (const name of [model.id, ...(model.aliases ?? [])]) alias.set(provider.id + '/' + name, model.id)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const route = selection.provider + '/' + selection.model
|
|
264
|
+
const canonical = alias.get(route)
|
|
265
|
+
const row = served.find(candidate => candidate.route === selection.provider + '/' + (canonical ?? selection.model))
|
|
266
|
+
if (row === undefined) {
|
|
267
|
+
return [{ route: 'default', error: 'provider/model is outside the managed selection: ' + route }]
|
|
268
|
+
}
|
|
269
|
+
const effort = selection.reasoningEffort
|
|
270
|
+
if (effort !== undefined && !String(row.levels).split(',').includes(effort)) {
|
|
271
|
+
return [{ route: 'default', error: 'effort is not supported by the selected model: ' + effort }]
|
|
272
|
+
}
|
|
273
|
+
return []
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function main() {
|
|
277
|
+
const options = parseArgs(process.argv.slice(2))
|
|
278
|
+
const patch = join(options.home, 'profiles', options.profile, 'cordis.patch.yml')
|
|
279
|
+
const catalog = catalogConfig(patch)
|
|
280
|
+
if (catalog === undefined) {
|
|
281
|
+
process.stderr.write('no owned catalog in ' + patch + '\n'
|
|
282
|
+
+ 'this profile wires models another way; see references/model-wiring.md\n')
|
|
283
|
+
process.exit(1)
|
|
284
|
+
}
|
|
285
|
+
const directory = options.data ?? dataDirectory()
|
|
286
|
+
if (directory === undefined) {
|
|
287
|
+
process.stderr.write('no harness install found; entries that inherit from it cannot be checked\n')
|
|
288
|
+
}
|
|
289
|
+
const served = rowsOf(catalog, directory)
|
|
290
|
+
const rows = [...served, ...checkDefault(catalog, served)]
|
|
291
|
+
const unresolved = rows.filter(row => row.error !== undefined).length
|
|
292
|
+
if (options.json) {
|
|
293
|
+
process.stdout.write(JSON.stringify({ default: catalog.default ?? null, rows }, null, 2) + '\n')
|
|
294
|
+
} else {
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
if (row.error !== undefined) {
|
|
297
|
+
process.stdout.write(row.route + '\n UNRESOLVED: ' + row.error + '\n')
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
process.stdout.write(row.route + '\n ' + row.name + ' | ' + row.api + ' | ctx ' + row.contextWindow
|
|
301
|
+
+ ' | max ' + row.maxTokens
|
|
302
|
+
+ (row.requestMax === undefined ? '' : ' | req ' + row.requestMax)
|
|
303
|
+
+ ' | ' + row.cost
|
|
304
|
+
+ (row.tiers === undefined ? '' : ' | tiers ' + row.tiers)
|
|
305
|
+
+ ' | efforts ' + row.levels
|
|
306
|
+
+ (row.aliases === undefined ? '' : ' | aliases ' + row.aliases) + '\n inherited: ' + row.origins + '\n')
|
|
307
|
+
}
|
|
308
|
+
process.stdout.write('default: ' + JSON.stringify(catalog.default ?? null) + '\n')
|
|
309
|
+
}
|
|
310
|
+
process.exit(unresolved === 0 ? 0 : 1)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Importing the resolver must not run the CLI: the spec reads it that way.
|
|
314
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main()
|