@sagmans/dsh-tui 0.7.0 → 0.9.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.
Files changed (242) hide show
  1. package/.agents/skills/dsh-tui-dogfood/SKILL.md +5 -1
  2. package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +26 -2
  3. package/.agents/skills/dsh-tui-update-models/SKILL.md +95 -17
  4. package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +158 -28
  5. package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +152 -16
  6. package/README.md +117 -53
  7. package/lib/agent/history.d.ts +0 -2
  8. package/lib/agent/history.d.ts.map +1 -1
  9. package/lib/agent/history.js +1 -1
  10. package/lib/agent/history.js.map +1 -1
  11. package/lib/agent/host.d.ts +0 -25
  12. package/lib/agent/host.d.ts.map +1 -1
  13. package/lib/agent/host.js +2 -2
  14. package/lib/agent/host.js.map +1 -1
  15. package/lib/agent/model.d.ts +38 -6
  16. package/lib/agent/model.d.ts.map +1 -1
  17. package/lib/agent/model.js +38 -14
  18. package/lib/agent/model.js.map +1 -1
  19. package/lib/agent/prompt-history.d.ts +7 -12
  20. package/lib/agent/prompt-history.d.ts.map +1 -1
  21. package/lib/agent/prompt-history.js +10 -31
  22. package/lib/agent/prompt-history.js.map +1 -1
  23. package/lib/cards/composition.d.ts +1 -1
  24. package/lib/cards/composition.d.ts.map +1 -1
  25. package/lib/cards/composition.js +7 -0
  26. package/lib/cards/composition.js.map +1 -1
  27. package/lib/cards/presenter.d.ts.map +1 -1
  28. package/lib/cards/presenter.js +32 -2
  29. package/lib/cards/presenter.js.map +1 -1
  30. package/lib/cards.d.ts +27 -2
  31. package/lib/cards.d.ts.map +1 -1
  32. package/lib/cards.js +17 -3
  33. package/lib/cards.js.map +1 -1
  34. package/lib/compat/probe.js +1 -1
  35. package/lib/compat/probe.js.map +1 -1
  36. package/lib/config.d.ts +10 -0
  37. package/lib/config.d.ts.map +1 -1
  38. package/lib/config.js +18 -0
  39. package/lib/config.js.map +1 -1
  40. package/lib/contracts.d.ts +2 -0
  41. package/lib/contracts.d.ts.map +1 -1
  42. package/lib/export.d.ts +12 -0
  43. package/lib/export.d.ts.map +1 -1
  44. package/lib/export.js +34 -3
  45. package/lib/export.js.map +1 -1
  46. package/lib/herdr/client.d.ts +8 -9
  47. package/lib/herdr/client.d.ts.map +1 -1
  48. package/lib/herdr/client.js +14 -2
  49. package/lib/herdr/client.js.map +1 -1
  50. package/lib/herdr/constants.d.ts +18 -0
  51. package/lib/herdr/constants.d.ts.map +1 -1
  52. package/lib/herdr/constants.js +18 -0
  53. package/lib/herdr/constants.js.map +1 -1
  54. package/lib/herdr/reporter.d.ts +20 -4
  55. package/lib/herdr/reporter.d.ts.map +1 -1
  56. package/lib/herdr/reporter.js +70 -20
  57. package/lib/herdr/reporter.js.map +1 -1
  58. package/lib/herdr/state.d.ts +10 -10
  59. package/lib/herdr/state.d.ts.map +1 -1
  60. package/lib/herdr/state.js +22 -5
  61. package/lib/herdr/state.js.map +1 -1
  62. package/lib/identity.d.ts +0 -2
  63. package/lib/identity.d.ts.map +1 -1
  64. package/lib/identity.js +0 -1
  65. package/lib/identity.js.map +1 -1
  66. package/lib/index.d.ts +1 -4
  67. package/lib/index.d.ts.map +1 -1
  68. package/lib/index.js +15 -5
  69. package/lib/index.js.map +1 -1
  70. package/lib/input/keymap.d.ts +1 -7
  71. package/lib/input/keymap.d.ts.map +1 -1
  72. package/lib/input/keymap.js +3 -11
  73. package/lib/input/keymap.js.map +1 -1
  74. package/lib/input/submission.js +2 -2
  75. package/lib/input/submission.js.map +1 -1
  76. package/lib/install-skills.d.ts +20 -1
  77. package/lib/install-skills.d.ts.map +1 -1
  78. package/lib/install-skills.js +84 -9
  79. package/lib/install-skills.js.map +1 -1
  80. package/lib/model-list.d.ts.map +1 -1
  81. package/lib/model-list.js +17 -5
  82. package/lib/model-list.js.map +1 -1
  83. package/lib/queue.d.ts +0 -2
  84. package/lib/queue.d.ts.map +1 -1
  85. package/lib/queue.js +1 -1
  86. package/lib/queue.js.map +1 -1
  87. package/lib/settings-notice.d.ts +10 -1
  88. package/lib/settings-notice.d.ts.map +1 -1
  89. package/lib/settings-notice.js +17 -7
  90. package/lib/settings-notice.js.map +1 -1
  91. package/lib/spacing.d.ts +40 -0
  92. package/lib/spacing.d.ts.map +1 -0
  93. package/lib/spacing.js +54 -0
  94. package/lib/spacing.js.map +1 -0
  95. package/lib/stash/paths.d.ts +19 -10
  96. package/lib/stash/paths.d.ts.map +1 -1
  97. package/lib/stash/paths.js +52 -23
  98. package/lib/stash/paths.js.map +1 -1
  99. package/lib/stash/schema.d.ts +13 -12
  100. package/lib/stash/schema.d.ts.map +1 -1
  101. package/lib/stash/schema.js +11 -9
  102. package/lib/stash/schema.js.map +1 -1
  103. package/lib/stash/store.d.ts.map +1 -1
  104. package/lib/stash/store.js +9 -19
  105. package/lib/stash/store.js.map +1 -1
  106. package/lib/stash.d.ts +17 -10
  107. package/lib/stash.d.ts.map +1 -1
  108. package/lib/stash.js +31 -20
  109. package/lib/stash.js.map +1 -1
  110. package/lib/subagents.d.ts +0 -4
  111. package/lib/subagents.d.ts.map +1 -1
  112. package/lib/subagents.js +2 -2
  113. package/lib/subagents.js.map +1 -1
  114. package/lib/surface/appearance.d.ts +3 -0
  115. package/lib/surface/appearance.d.ts.map +1 -1
  116. package/lib/surface/appearance.js +5 -0
  117. package/lib/surface/appearance.js.map +1 -1
  118. package/lib/surface/background-work.d.ts +2 -0
  119. package/lib/surface/background-work.d.ts.map +1 -1
  120. package/lib/surface/background-work.js +45 -3
  121. package/lib/surface/background-work.js.map +1 -1
  122. package/lib/surface/commands.d.ts +2 -0
  123. package/lib/surface/commands.d.ts.map +1 -1
  124. package/lib/surface/commands.js +32 -6
  125. package/lib/surface/commands.js.map +1 -1
  126. package/lib/surface/modal-input.d.ts +3 -2
  127. package/lib/surface/modal-input.d.ts.map +1 -1
  128. package/lib/surface/modal-input.js +25 -8
  129. package/lib/surface/modal-input.js.map +1 -1
  130. package/lib/surface/model-choice.d.ts.map +1 -1
  131. package/lib/surface/model-choice.js +18 -18
  132. package/lib/surface/model-choice.js.map +1 -1
  133. package/lib/surface/preset-choice.d.ts.map +1 -1
  134. package/lib/surface/preset-choice.js +18 -10
  135. package/lib/surface/preset-choice.js.map +1 -1
  136. package/lib/surface/prompt-memory.d.ts +3 -1
  137. package/lib/surface/prompt-memory.d.ts.map +1 -1
  138. package/lib/surface/prompt-memory.js +10 -11
  139. package/lib/surface/prompt-memory.js.map +1 -1
  140. package/lib/surface/session-lifecycle.d.ts +6 -1
  141. package/lib/surface/session-lifecycle.d.ts.map +1 -1
  142. package/lib/surface/session-lifecycle.js +30 -8
  143. package/lib/surface/session-lifecycle.js.map +1 -1
  144. package/lib/surface/session-view.d.ts.map +1 -1
  145. package/lib/surface/session-view.js +1 -3
  146. package/lib/surface/session-view.js.map +1 -1
  147. package/lib/surface/staged-turns.d.ts +0 -2
  148. package/lib/surface/staged-turns.d.ts.map +1 -1
  149. package/lib/surface/staged-turns.js +37 -6
  150. package/lib/surface/staged-turns.js.map +1 -1
  151. package/lib/terminal/external-editor.d.ts +16 -0
  152. package/lib/terminal/external-editor.d.ts.map +1 -1
  153. package/lib/terminal/external-editor.js +38 -6
  154. package/lib/terminal/external-editor.js.map +1 -1
  155. package/lib/terminal/host-writes.d.ts +15 -2
  156. package/lib/terminal/host-writes.d.ts.map +1 -1
  157. package/lib/terminal/host-writes.js +10 -1
  158. package/lib/terminal/host-writes.js.map +1 -1
  159. package/lib/terminal/restore.d.ts +1 -2
  160. package/lib/terminal/restore.d.ts.map +1 -1
  161. package/lib/terminal/restore.js +1 -9
  162. package/lib/terminal/restore.js.map +1 -1
  163. package/lib/terminal/signals.d.ts +0 -1
  164. package/lib/terminal/signals.d.ts.map +1 -1
  165. package/lib/terminal/signals.js +1 -1
  166. package/lib/terminal/signals.js.map +1 -1
  167. package/lib/terminal/title.d.ts.map +1 -1
  168. package/lib/terminal/title.js +14 -3
  169. package/lib/terminal/title.js.map +1 -1
  170. package/lib/terminal/warning-screen.d.ts.map +1 -1
  171. package/lib/terminal/warning-screen.js +12 -1
  172. package/lib/terminal/warning-screen.js.map +1 -1
  173. package/lib/theme-capability.d.ts +0 -2
  174. package/lib/theme-capability.d.ts.map +1 -1
  175. package/lib/theme-capability.js +1 -1
  176. package/lib/theme-capability.js.map +1 -1
  177. package/lib/theme-defaults.d.ts.map +1 -1
  178. package/lib/theme-defaults.js +7 -0
  179. package/lib/theme-defaults.js.map +1 -1
  180. package/lib/theme-files.d.ts +1 -1
  181. package/lib/theme-files.d.ts.map +1 -1
  182. package/lib/theme-files.js +1 -1
  183. package/lib/theme-files.js.map +1 -1
  184. package/lib/theme-schema.d.ts +6 -6
  185. package/lib/theme-settings.d.ts +13 -8
  186. package/lib/theme-settings.d.ts.map +1 -1
  187. package/lib/theme-settings.js +17 -3
  188. package/lib/theme-settings.js.map +1 -1
  189. package/lib/theme-tokens.d.ts +1 -1
  190. package/lib/theme-tokens.d.ts.map +1 -1
  191. package/lib/theme-tokens.js +7 -2
  192. package/lib/theme-tokens.js.map +1 -1
  193. package/lib/transcript.d.ts +2 -2
  194. package/lib/transcript.d.ts.map +1 -1
  195. package/lib/transcript.js +9 -4
  196. package/lib/transcript.js.map +1 -1
  197. package/lib/ui/dock.d.ts +2 -0
  198. package/lib/ui/dock.d.ts.map +1 -1
  199. package/lib/ui/dock.js +18 -3
  200. package/lib/ui/dock.js.map +1 -1
  201. package/lib/ui/gap.d.ts +28 -0
  202. package/lib/ui/gap.d.ts.map +1 -0
  203. package/lib/ui/gap.js +47 -0
  204. package/lib/ui/gap.js.map +1 -0
  205. package/lib/ui/gutter.d.ts +25 -0
  206. package/lib/ui/gutter.d.ts.map +1 -0
  207. package/lib/ui/gutter.js +31 -0
  208. package/lib/ui/gutter.js.map +1 -0
  209. package/lib/ui/layout.d.ts +13 -1
  210. package/lib/ui/layout.d.ts.map +1 -1
  211. package/lib/ui/layout.js +32 -6
  212. package/lib/ui/layout.js.map +1 -1
  213. package/lib/ui/queue.d.ts +0 -4
  214. package/lib/ui/queue.d.ts.map +1 -1
  215. package/lib/ui/queue.js +2 -2
  216. package/lib/ui/queue.js.map +1 -1
  217. package/lib/ui/rows.d.ts +0 -8
  218. package/lib/ui/rows.d.ts.map +1 -1
  219. package/lib/ui/rows.js +1 -9
  220. package/lib/ui/rows.js.map +1 -1
  221. package/lib/ui/stash-picker.d.ts +2 -2
  222. package/lib/ui/stash-picker.d.ts.map +1 -1
  223. package/lib/ui/stash-picker.js +3 -3
  224. package/lib/ui/stash-picker.js.map +1 -1
  225. package/lib/ui/view/gate-card.d.ts.map +1 -1
  226. package/lib/ui/view/gate-card.js +4 -1
  227. package/lib/ui/view/gate-card.js.map +1 -1
  228. package/lib/ui/view/tool-card.d.ts +17 -2
  229. package/lib/ui/view/tool-card.d.ts.map +1 -1
  230. package/lib/ui/view/tool-card.js +40 -8
  231. package/lib/ui/view/tool-card.js.map +1 -1
  232. package/lib/ui/view/transcript-message.d.ts +3 -0
  233. package/lib/ui/view/transcript-message.d.ts.map +1 -1
  234. package/lib/ui/view/transcript-message.js +8 -1
  235. package/lib/ui/view/transcript-message.js.map +1 -1
  236. package/lib/ui/view.d.ts +36 -9
  237. package/lib/ui/view.d.ts.map +1 -1
  238. package/lib/ui/view.js +90 -32
  239. package/lib/ui/view.js.map +1 -1
  240. package/package.json +3 -1
  241. package/themes/deepseek-blue.yaml +3 -0
  242. package/themes/violet-orbit.yaml +16 -9
@@ -22,7 +22,11 @@ directory or branch tail. A path selects that checkout directly. The helper read
22
22
  runs `pnpm run build` only when the checkout declares a build script. It adds
23
23
  the bundle once if absent from the cloned profile, updates only its dependency,
24
24
  restores local links from `link:` specifications, and launches
25
- `dsh --profile tui` by default. The legacy
25
+ `dsh --profile tui` by default. For this TUI, the packaged helper checks
26
+ the launcher against compatible releases in the checkout’s `package.json` before
27
+ cloning. This checkout uses installed `dsh 0.1.5-rc.3` for PTY tests. Never
28
+ substitute `pnpm dsh` or a `--dsh` path from a Harness source checkout: that
29
+ host may migrate the cloned `settings.yaml`. The legacy
26
30
  `scripts/dogfood/run-tui-from-worktree.sh` remains available in dsh-tui and
27
31
  defaults to that script's checkout even when called elsewhere.
28
32
 
@@ -3,6 +3,7 @@
3
3
  set -euo pipefail
4
4
 
5
5
  name="$(basename "${BASH_SOURCE[0]}")"
6
+ TUI_PACKAGE_NAME='@sagmans/dsh-tui'
6
7
  # The link policy is shared with the validator, so both read one rule set.
7
8
  helper_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
8
9
  default_repo="${DSH_DOGFOOD_DEFAULT_REPO:-$PWD}"
@@ -268,11 +269,26 @@ if [[ "$dry_run" == 1 ]]; then
268
269
  fi
269
270
 
270
271
  [[ -n "$dsh_bin" ]] || die "no dsh launcher found; put dsh on PATH or pass --dsh"
272
+ # This plugin's daily host must not be replaced by a source checkout during setup or launch.
273
+ if [[ "$package_name" == "$TUI_PACKAGE_NAME" ]]; then
274
+ # Keep launcher startup writes out of both the live home and the clone.
275
+ version_probe_home="$(mktemp -d /tmp/dsh-dogfood-version.XXXXXXXX)" || die "cannot create isolated version probe home"
276
+ trap 'rm -rf -- "$version_probe_home"' EXIT
277
+ trap 'exit 1' INT TERM
278
+ reported_version="$(HOME="$version_probe_home" DSH_HOME="$version_probe_home" "$dsh_bin" --version 2>/dev/null)" || die "TUI dogfood requires compatible installed dsh"
279
+ rm -rf -- "$version_probe_home"
280
+ trap - EXIT INT TERM
281
+ node -e "const p=require(process.argv[1]);process.exit(p.dsh?.compatibility?.dshReleases?.[process.argv[2]] === 'compatible' ? 0 : 1)" "$target_path/package.json" "$reported_version" || die "TUI dogfood requires compatible installed dsh (got ${reported_version:-<empty>})"
282
+ fi
271
283
 
272
284
  # --- seed the scratch home ---------------------------------------------------
273
285
 
274
286
  if [[ -d "$home" && "$action" != "clean" && "$dry_run" != 1 ]]; then
275
287
  validate_marker
288
+ # A marker that still reads 0 belongs to a run the kernel killed before it could
289
+ # clean up: the identity matches, so --reseed takes it, but nothing may reuse a
290
+ # home whose copy stopped halfway. A marker without the line predates this.
291
+ [[ "$reseed" == 1 ]] || [[ "$(marker_value ready)" != 0 ]] || die "$home is a half-seeded clone; pass --reseed to clone it again"
276
292
  check_credentials
277
293
  chmod 700 "$home"
278
294
  if [[ -f "$home/.credentials.yaml" ]]; then chmod 600 "$home/.credentials.yaml"; fi
@@ -293,6 +309,11 @@ if [[ ! -d "$home" ]]; then
293
309
  step "seeding $home from $source_home"
294
310
  mkdir -p "$home"
295
311
  chmod 700 "$home"
312
+ # Written before the copy, so whatever the copy does this directory is one that
313
+ # --clean and --reseed already own: an interrupted clone is recoverable rather
314
+ # than a husk neither action will touch. The copy marks it ready when it lands.
315
+ printf 'home=%s\nsource=%s\ntarget=%s\nprofile=%s\nready=0\n' "$home" "$source_home" "$target_path" "$profile" > "$marker"
316
+ chmod 600 "$marker"
296
317
  if [[ "$fresh" == 1 ]]; then
297
318
  for seed in .credentials.yaml settings.yaml; do
298
319
  if [[ -f "$source_home/$seed" ]]; then cp -p "$source_home/$seed" "$home/$seed"; fi
@@ -316,8 +337,6 @@ if [[ ! -d "$home" ]]; then
316
337
  fi
317
338
  fi
318
339
 
319
- printf 'home=%s\nsource=%s\ntarget=%s\nprofile=%s\n' "$home" "$source_home" "$target_path" "$profile" > "$marker"
320
- chmod 600 "$marker"
321
340
  materialize_cloned_links
322
341
  check_clone_links
323
342
 
@@ -398,6 +417,11 @@ if ! node -e "process.exit(require(process.argv[1]).dsh.profile.bundles.includes
398
417
  die "profile '$profile' does not list $package_name after relink (bundles: $bundles)"
399
418
  fi
400
419
 
420
+ # The home is finished: it points at the target and its bundles answer, so it is
421
+ # ready to be reused rather than re-seeded.
422
+ printf 'home=%s\nsource=%s\ntarget=%s\nprofile=%s\nready=1\n' "$home" "$source_home" "$target_path" "$profile" > "$marker"
423
+ chmod 600 "$marker"
424
+
401
425
  printf '\n' >&2
402
426
  printf 'home: %s\n' "$home" >&2
403
427
  printf 'target: %s\n' "$target_path" >&2
@@ -13,10 +13,11 @@ the ctx gauge. Find the owner first; then edit exactly one place.
13
13
  | Where the wiring is | What it owns |
14
14
  | --- | --- |
15
15
  | `~/.dsh/profiles/<profile>/cordis.patch.yml` | The patch layer: applied after every bundle layer, last entry wins. Holds the catalog or `llm-pi-ai` config a developer actually edits. |
16
- | `~/.dsh/settings.yaml` | Host settings. Plugin sections keyed by plugin id; used when a host writes config through the settings service instead of a patch. |
16
+ | `~/.dsh/settings.yaml` | Host settings, hot-reloaded. A `llm-pi-ai:` or `llm-deepseek:` section overrides that adapter's patch entry without a restart - it is what the web Models page writes, and what an additive sign-in declares a route in. The legacy `dsh-provider-extra:` section carries `extraModels` and `codexExtraModels` only. **A catalog is never settings-owned**: managed mode installs no section, reads no overlay, and turns off settings projection for its own Config, so the profile patch is the only place a catalog exists. |
17
17
  | `~/.dsh/profiles/<profile>/package.json` | Bundles and their dependency specs (`link:` for a checkout, a range for a release). |
18
18
  | `node_modules/<bundle>/cordis.patch.yml` | The bundle's own defaults: disabled base rows, inserted rows, default config. |
19
- | `node_modules/@earendil-works/pi-ai/dist/providers/data/<source>.json` | Installed per-provider catalog: ids, and the cost/context/effort data a template clone inherits. |
19
+ | `node_modules/@earendil-works/pi-ai/dist/providers/data/<source>.json` | Installed per-provider catalog: ids, and the cost/context/effort data a template clone or a `filter` match inherits. |
20
+ | `node_modules/@sagmans/dsh-provider-extra/docs/catalog.md` | The catalog's own field list and rejection rules, matching the plugin build the profile resolves. Read it before inventing a field. |
20
21
 
21
22
  Entry shapes in the patch layer:
22
23
 
@@ -39,8 +40,13 @@ A config in a patch **replaces** the base config; it never merges field by field
39
40
  | One new id that behaves like a catalog sibling | `extraModels: [{ id, template, name? }]` (clone the sibling's wire behavior) |
40
41
  | Serve only some of a route's catalog | a `models:` whitelist of ids, in the order the picker should show |
41
42
  | Own the whole model directory (gateways, subscriptions, logins) | `dsh-provider-extra` `catalog` v1, and disable the base rows |
43
+ | Serve most of an installed source, minus a few ids | catalog route `filter: { include?, exclude? }` - keeps every installed fact |
44
+ | Point a route at your own endpoint | catalog route with `api` + `baseURL` and no `source` |
42
45
  | Retire a model or a provider | drop it from the list, or disable the row that offers it |
43
46
 
47
+ A catalog route declares **exactly one** of `models` and `filter`; `models: []`
48
+ is the explicit empty selection, and `filter` needs an installed `source`.
49
+
44
50
  `dsh-provider-extra` catalog skeleton, with the two override layers a correct
45
51
  entry needs as soon as a vendor number differs from the inherited one:
46
52
 
@@ -54,20 +60,61 @@ entry needs as soon as a vendor number differs from the inherited one:
54
60
  - id: openai
55
61
  name: OpenAI API
56
62
  source: openai # pi-ai provider id; its data is the base
57
- auth: { apiKeyRef: OPENAI_API_KEY }
63
+ auth: { apiKeyRef: OPENAI_API_KEY } # omit auth for a keyless local route
58
64
  models:
59
65
  - id: gpt-6-sol
60
66
  name: GPT-6 Sol
67
+ aliases: [gpt-6-sol-preview] # extra selector inputs, never extra rows
61
68
  template: gpt-5.6-sol # inherits wire behavior AND stale metadata
69
+ defaultMaxTokens: 128000 # request cap, only when the request gives none
62
70
  metadata: # override what the vendor documented
63
71
  contextWindow: 1050000
64
72
  cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }
73
+ - id: local-llm
74
+ name: Local llama.cpp
75
+ api: openai-completions # own endpoint: api is required, source is absent
76
+ baseURL: http://127.0.0.1:8080/v1
77
+ models:
78
+ - id: qwen3-coder
79
+ name: Qwen3 Coder
80
+ metadata: # no installed sibling: every fact is declared
81
+ reasoning: true
82
+ input: [text]
83
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
84
+ contextWindow: 262144
85
+ maxTokens: 65536
86
+ - id: zai-all
87
+ name: Z.AI Coding Plan
88
+ source: zai
89
+ auth: { apiKeyRef: ZAI_API_KEY }
90
+ filter: { include: ['glm-*'], exclude: ['glm-4*'] } # '*' is the only wildcard
65
91
  ```
66
92
 
67
- Rules the resolver enforces: `version` is exactly `1`; duplicate provider ids
68
- are refused; an empty catalog requires `default: null`; `default` must resolve to
69
- a declared provider/model; a `reasoningEffort` the model does not support fails
70
- composition.
93
+ Rules the resolver enforces, each rejecting the **whole** candidate: `version` is
94
+ exactly `1`; an unknown field anywhere; duplicate provider ids, model ids, or
95
+ aliases; a `source` no installed pi-ai provider ships; `api` beside `source`, or
96
+ absent when `source` is absent; `baseURL` absent, non-HTTP(S), or carrying
97
+ credentials; a `transport` on a route whose source is not `openai-codex`; a
98
+ `fallbackSessionId` on a route whose source is not `opencode-go`; `auth` naming
99
+ both modes, or `apiKeyRef` on `openai-codex`, or a `credentialProvider` that is
100
+ not the `source`; both or neither of `models`/`filter`; an unknown `template`; a
101
+ model with no resolvable template and incomplete `metadata`; a
102
+ `defaultMaxTokens` above the model's `maxTokens`; a `metadata.api` that changes a
103
+ template-backed protocol; an empty catalog whose `default` is not `null`; a
104
+ `default` outside the served selection; a `reasoningEffort` the model does not
105
+ support. Omitting `catalog` is not `providers: []`: the first keeps the plugin's
106
+ legacy additive routes, the second serves nothing.
107
+
108
+ Ownership is exclusive and checked at boot. Disable **every** competing row in
109
+ the whole profile - on `dsh-base` those are `llm-pi-ai`, `llm-deepseek`, and
110
+ `agent-default-model` - and mount provider-extra after them. A surviving adapter,
111
+ provider directory, or default owner logs `CATALOG_OWNER_COLLISION` and fails
112
+ listing, selection, dispatch, and sign-in closed until it is gone.
113
+
114
+ The pinned host reads only `provider` and `model` from `catalog.default`: it
115
+ ignores `reasoningEffort`, so effort is chosen in the session surface. Saving a
116
+ default needs the host's `configEditor`; a host without one rejects with
117
+ `CONFIG_PERSISTENCE_UNAVAILABLE`, and the profile patch stays the place to edit.
71
118
 
72
119
  ## 3. Reasoning efforts
73
120
 
@@ -86,21 +133,47 @@ concept differently - map it, never rename the vendor value.
86
133
 
87
134
  ## 4. Costs
88
135
 
89
- Per 1M tokens: `{ input, output, cacheRead, cacheWrite }`. `0` is right for a plan
90
- that bills credits, wrong for a per-token route. A vendor that charges more above
91
- an input threshold needs the tier row, otherwise long prompts are understated.
136
+ Per 1M tokens: `{ input, output, cacheRead, cacheWrite }`, plus optional
137
+ `tiers: [{ inputTokensAbove, input, output, cacheRead, cacheWrite }]`. `0` means
138
+ unknown pricing on a credit plan, not free inference. A vendor that charges more
139
+ above an input threshold needs its tier row, otherwise long prompts are
140
+ understated. `metadata.maxTokens` is model capacity and never a request cap;
141
+ `defaultMaxTokens` is the request default and must not exceed that capacity.
92
142
 
93
143
  ## 5. Verify before and after the edit
94
144
 
95
145
  ```sh
96
- dsh --profile tui list-models # every reachable route, id<TAB>name
97
- ~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs # resolved cost/ctx/efforts
146
+ dsh --profile tui list-models # every reachable route, id<TAB>name
147
+ # resolved cost/ctx/efforts, naming the layer each number came from:
148
+ ~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs \
149
+ [--home "$DSH_HOME"] [--profile tui] [--data <pi-ai data dir>] [--json]
150
+ ```
151
+
152
+ The dump marks each value `installed`, `template:<id>`, `route`, or `metadata`, so
153
+ an inherited number is visible instead of trusted, expands a `filter` route
154
+ against the installed data the same way the resolver does, and exits 1 on the
155
+ declarations the resolver refuses - incomplete metadata, a request default above
156
+ capacity, both membership styles, a default the served selection does not
157
+ contain.
158
+
159
+ Then check the vendor's own page for price, context window, max output, and
160
+ accepted effort values - the installed catalog lags vendors, and a template
161
+ clone carries a sibling's numbers.
162
+
163
+ To prove a catalog on a disposable home instead of the live one, provider-extra
164
+ ships a clone-only setup that composes the candidate and disables the competing
165
+ rows for you:
166
+
167
+ ```sh
168
+ cd <dsh-provider-extra checkout> && pnpm catalog:setup \
169
+ --helper ~/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh \
170
+ --source-home ~/.dsh --home /tmp/dsh-catalog-check --profile tui \
171
+ --catalog /tmp/catalog-config.json [--allow-row ROW_ID] [--dsh /abs/path/to/dsh/lib/bin.js]
98
172
  ```
99
173
 
100
- The dump marks each value `installed`, `template:<id>`, or `metadata`, so an
101
- inherited number is visible instead of trusted. Then check the vendor's own page
102
- for price, context window, max output, and accepted effort values - the installed
103
- catalog lags vendors, and a template clone carries a sibling's numbers.
174
+ `--catalog` is a private JSON file holding only `{"catalog": {...}}`. Success
175
+ means `composition-verified`, not authenticated runtime; launch only the clone
176
+ command it prints.
104
177
 
105
178
  ## 6. Pitfalls
106
179
 
@@ -110,8 +183,13 @@ catalog lags vendors, and a template clone carries a sibling's numbers.
110
183
  | A window that is really a pricing threshold | vendors publish e.g. "above 272K input priced 2x" beside the real window |
111
184
  | `off` missing on a reasoning model | `thinkingLevelMap.off` is `null` or absent where the vendor takes `none` |
112
185
  | A stealth model with no vendor page | its numbers exist only in the routing catalog (pi.dev model page, models.dev) |
113
- | Config ignored after an edit | a linked bundle loads `lib/`; run `pnpm run build` in that checkout |
186
+ | Config ignored after an edit | a linked bundle loads `lib/` or `dist/`; run the build in that checkout |
114
187
  | ctx gauge and auto-compact trigger early/late | the declared `contextWindow` is wrong, not the gauge |
188
+ | Same model id, two context windows | each route inherits its own source's numbers; declare `metadata` on both |
189
+ | Everything fails closed, logs `CATALOG_OWNER_COLLISION` | a competing adapter, provider directory, or default owner row survived |
190
+ | A new field looks accepted but does nothing | the resolver rejects unknown fields; a stale plugin build silently predates it |
191
+ | `/model` saves a default that reverts | the host has no `configEditor`: `CONFIG_PERSISTENCE_UNAVAILABLE`; edit the patch |
192
+ | Route serves nothing after a `filter` edit | patterns match installed ids only, and `exclude` wins over `include` |
115
193
 
116
194
  Never put a key in a profile or a patch: name it (`apiKeyEnv`, `apiKeyRef`,
117
195
  `credentialProvider`) and keep the value in the environment or `$DSH_HOME/.env`.
@@ -12,6 +12,12 @@ Companion to [SKILL.md](../SKILL.md): exact fields, files, and checks.
12
12
  4. The model directory a profile sees is owned by whichever row is enabled:
13
13
  `llm-pi-ai` (additive provider config), `dsh-provider-extra` (owned catalog),
14
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.
15
21
 
16
22
  Exclusive ownership is a set of disable rows plus the catalog itself:
17
23
 
@@ -23,6 +29,19 @@ Exclusive ownership is a set of disable rows plus the catalog itself:
23
29
  config: { catalog: { version: 1, ... } }
24
30
  ```
25
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
+
26
45
  ## `llm-pi-ai` provider config
27
46
 
28
47
  ```yaml
@@ -52,57 +71,131 @@ Exclusive ownership is a set of disable rows plus the catalog itself:
52
71
  An id that the installed catalog already describes needs no restatement. An id it
53
72
  does not describe needs the metadata (or a `template` sibling to clone).
54
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
+
55
90
  ## `dsh-provider-extra`
56
91
 
57
- Two config generations exist; a profile normally uses one:
92
+ Two config generations exist; a profile normally uses one, and presence of
93
+ `catalog` is what opts a profile into managed ownership:
58
94
 
59
95
  | Config | Use |
60
96
  | --- | --- |
61
- | Additive: `routeId`, `displayName`, `apiKeyEnv`, `fallbackSessionId`, `extraModels`, `models`, `codexExtraModels`, `codexModels`, `codexTransport` | add a gateway or subscription route beside whatever else the profile serves |
62
- | `catalog` v1 | own the entire directory: ids, aliases, metadata, and the default selection |
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 |
63
99
 
64
- Catalog fields:
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):
65
107
 
66
108
  ```yaml
67
109
  catalog:
68
110
  version: 1 # exactly 1
69
- default: # null only when the catalog is empty
111
+ default: # null only when nothing is served
70
112
  { provider: opencode-go-session, model: deepseek-flash, reasoningEffort: max }
71
113
  providers:
72
114
  - id: opencode-go-session # the id every selection and alias check uses
73
115
  name: OpenCode Go (session)
74
- source: opencode-go # pi-ai provider id: its installed data is the base
75
- baseURL: https://api.x.ai/v1 # optional
76
- transport: sse # optional
77
- fallbackSessionId: dsh-provider-extra # optional
78
- auth: { apiKeyRef: OPENCODE_GO_API_KEY } # or { credentialProvider: openai-codex }
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
79
123
  models:
80
124
  - id: deepseek-flash
81
125
  name: DeepSeek V4.1 Flash
82
- aliases: [deepseek-v4.1-flash] # extra ids that resolve to this model
83
- template: deepseek-v4-flash # clone wire behavior when the source lacks the id
84
- defaultMaxTokens: 384000 # request ceiling; metadata.maxTokens overrides
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
85
129
  metadata:
86
130
  api: openai-completions
87
131
  reasoning: true
88
132
  input: [text, image]
89
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 }] }
90
138
  contextWindow: 1000000
91
- maxTokens: 384000
139
+ maxTokens: 384000 # capacity, never a request cap
92
140
  thinkingLevelMap: { off: null, minimal: null, low: low, medium: null, high: high, xhigh: null, max: max }
141
+ headers: { X-Model: flash }
93
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
94
148
  ```
95
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
+
96
170
  Behaviour:
97
171
 
98
172
  | Case | Result |
99
173
  | --- | --- |
100
174
  | `template` names a sibling the source ships | clone its wire behavior and metadata |
101
- | `template` names nothing | that model lands in the route's diagnostics; the rest of the route still serves |
102
- | a `models` whitelist names an id nothing provides | the whole route is refused - a typo there is a broken deployment |
103
- | two providers share an id | composition error |
104
- | `default` names a model outside the catalog | composition error |
105
- | `reasoningEffort` unsupported by the model | composition error |
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.
106
199
 
107
200
  ## Where the base data comes from
108
201
 
@@ -113,8 +206,10 @@ PI_AI="${DSH%/lib/bin.js}/node_modules/@earendil-works/pi-ai"
113
206
  ls "$PI_AI/dist/providers/data" # one JSON per provider source, keyed by api
114
207
  ```
115
208
 
116
- A JSON entry is the whole truth for a `source`: `id`, `name`, `cost`,
117
- `contextWindow`, `maxTokens`, `input`, `reasoning`, `thinkingLevelMap`, `compat`.
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`.
118
213
  A model declared without `template` and without `metadata` is served exactly as
119
214
  this file describes it, so an id the vendor renamed or repriced shows stale
120
215
  numbers until the catalog declares them. An entry that carries its own complete
@@ -125,9 +220,10 @@ not describe is wired.
125
220
 
126
221
  | Field | Meaning |
127
222
  | --- | --- |
128
- | `apiKeyEnv` | read the key from this environment variable |
129
- | `apiKeyRef` | read it from the profiles' stored credentials under this reference |
130
- | `credentialProvider` | use a login flow's stored credentials (codex subscriptions) |
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 |
131
227
 
132
228
  Keys never belong in a patch, a settings file, or a repository. Put launch-time
133
229
  values in `$DSH_HOME/.env` or the environment. A provider row whose credential is
@@ -137,9 +233,39 @@ missing fails its route only; other routes still serve.
137
233
 
138
234
  ```sh
139
235
  dsh --profile tui list-models # provider/model<TAB>display name, one per reachable route
140
- ~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs --home "$DSH_HOME"
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]
141
261
  ```
142
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
+
143
269
  `list-models` exits zero only after printing a line, so a deployment that
144
270
  advertises nothing cannot pass for one that was enumerated.
145
271
 
@@ -150,9 +276,13 @@ For every model you touched, confirm against the vendor's own page:
150
276
  is priced 2x" is a billing threshold, not the window);
151
277
  3. the accepted reasoning effort values, and how the vendor spells "no reasoning"
152
278
  (OpenAI: `none`; codex: omit the parameter);
153
- 4. whether the route bills per token at all - a credit or token plan wants cost
154
- `0`, a metered API wants its published rates;
155
- 5. that an `xhigh` or `max` mapping exists when the vendor documents one.
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.
156
286
 
157
287
  A model with no vendor page (stealth releases, previews) is documented only by the
158
288
  routing catalog: its pi.dev model page and `models.dev/api.json`.