one-code-extension 0.4.1 → 0.5.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/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tier algorithm's catalog side: every vendor model the public catalogs
|
|
3
|
+
* know (`model-catalog-data.ts`), each placed in a tier by its size, its price
|
|
4
|
+
* and its age measured against the SAME vendor's current models, and matched
|
|
5
|
+
* to the rows pi lists. `model-tier.ts` applies the Claude Code parity gate
|
|
6
|
+
* (frontier), the user's overrides and the no-catalog fallback on top. Pure:
|
|
7
|
+
* pi types only. working-docs/decisions/model-tiers.md.
|
|
8
|
+
*
|
|
9
|
+
* The rules, per vendor (the canonical id's prefix, `alibaba/qwen3.8-27b`):
|
|
10
|
+
*
|
|
11
|
+
* 1. Tiny means small: under `TINY_PARAMS` total parameters (Hugging Face, else
|
|
12
|
+
* a size tag in the id), or, for a model with no published size, a
|
|
13
|
+
* nano/micro/lite/tiny name. A tag followed by an expert count
|
|
14
|
+
* (`llama-4-scout-17b-16e`) counts active parameters and is no size. A
|
|
15
|
+
* model shares its size with its twins, the same vendor's rows of the same
|
|
16
|
+
* model: same stem (the id without `-latest`, a snapshot date or a size
|
|
17
|
+
* tag) and the same release date, or one a dated snapshot of the other. An
|
|
18
|
+
* unsized alias (`voxtral-small-latest`) takes its twins' size when they
|
|
19
|
+
* agree on tiny, and their non-text output; a size tag yields to a larger
|
|
20
|
+
* measured size of an untagged twin (Bedrock's `llama-4-maverick-17b-instruct`
|
|
21
|
+
* is OpenRouter's 401B `llama-4-maverick`), unless its twins carry other
|
|
22
|
+
* tags (a size ladder released together) or it names its active size too
|
|
23
|
+
* (`qwen3.6-35b-a3b`).
|
|
24
|
+
* 2. The comparison set is the vendor's current models: released within
|
|
25
|
+
* `CURRENT_WINDOW_DAYS` of its newest tool-calling, non-deprecated,
|
|
26
|
+
* text-output model, tiny excluded.
|
|
27
|
+
* 3. Workhorse or cheap: when every model in the set has a known size, a model
|
|
28
|
+
* with at least `SIZE_RATIO` of the largest one's parameters is workhorse;
|
|
29
|
+
* otherwise a model whose blended price is at least `PRICE_RATIO` of the
|
|
30
|
+
* set's median price is (the median, so premium SKUs, `-pro` and `-fast`,
|
|
31
|
+
* do not set the bar). Prices are the vendor's own, from models.dev, not a
|
|
32
|
+
* gateway's promotion. A vendor with fewer than `MIN_PRICE_PEERS` current
|
|
33
|
+
* priced models has nothing to compare against: its models are cheap.
|
|
34
|
+
* 4. Age: a workhorse model more than `AGE_DEMOTE_DAYS` older than the
|
|
35
|
+
* vendor's newest workhorse-class model drops to cheap. Age never makes a
|
|
36
|
+
* model tiny.
|
|
37
|
+
* 5. Automatic selection skips a model that is superseded (the vendor has a
|
|
38
|
+
* newer model in the same or a higher tier within `SUPERSEDE_PRICE_BAND`
|
|
39
|
+
* of its price, and the session's provider serves it: a newer Opus
|
|
40
|
+
* supersedes the last Opus, but a newer Sonnet at half the price does not;
|
|
41
|
+
* a moving alias or the model's own dated snapshot never supersedes it),
|
|
42
|
+
* legacy (more than `LEGACY_DAYS` behind the vendor's newest model),
|
|
43
|
+
* deprecated, unable to call tools, or not a text model.
|
|
44
|
+
*
|
|
45
|
+
* Every constant is a ratio or a physical size, never a vendor or a dollar
|
|
46
|
+
* figure, so a new vendor or a price cut needs no code change.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
50
|
+
import { type CatalogSources, loadCatalogSources, MODELS_DEV_PROVIDER, type ModelsDevModel, type OpenRouterModel } from "./model-catalog-data.ts";
|
|
51
|
+
import { BUILTIN_PROVIDER_POLICIES, baseModelId, DAY_MS, isExperimentalBuild, isMovingAlias, modelIdentity, stripSnapshotDate, vendorProfile } from "./model-policy.ts";
|
|
52
|
+
import { oneCodeStateDir } from "./paths.ts";
|
|
53
|
+
|
|
54
|
+
export const TINY_PARAMS = 40e9;
|
|
55
|
+
export const SIZE_RATIO = 0.5;
|
|
56
|
+
export const PRICE_RATIO = 0.3;
|
|
57
|
+
export const CURRENT_WINDOW_DAYS = 182;
|
|
58
|
+
export const AGE_DEMOTE_DAYS = 365;
|
|
59
|
+
export const LEGACY_DAYS = 730;
|
|
60
|
+
/** A successor's price band against the model it supersedes: the same product class, not a cheaper sibling. */
|
|
61
|
+
export const SUPERSEDE_PRICE_BAND = [0.6, 1.5] as const;
|
|
62
|
+
/** Fewer current priced models than this and a vendor has no price to compare against. */
|
|
63
|
+
export const MIN_PRICE_PEERS = 3;
|
|
64
|
+
|
|
65
|
+
export type CatalogTier = "workhorse" | "cheap" | "tiny";
|
|
66
|
+
|
|
67
|
+
export interface CatalogModel {
|
|
68
|
+
/** The vendor's own `vendor/id`, or a gateway's `vendor/id` for a model no vendor row names. */
|
|
69
|
+
id: string;
|
|
70
|
+
vendor: string;
|
|
71
|
+
/** A gateway row no vendor row names: placed against its vendor's models, never part of the comparison. */
|
|
72
|
+
derived: boolean;
|
|
73
|
+
released?: number;
|
|
74
|
+
deprecated: boolean;
|
|
75
|
+
/** False only when a catalog says the model cannot call tools. */
|
|
76
|
+
tools: boolean;
|
|
77
|
+
/** Outputs images, audio or the like: never compared or picked. */
|
|
78
|
+
nonTextOutput: boolean;
|
|
79
|
+
/** Blended USD per million tokens, 3 input to 1 output. */
|
|
80
|
+
price?: number;
|
|
81
|
+
params?: number;
|
|
82
|
+
tier: CatalogTier;
|
|
83
|
+
/** Why the tier, in words, for the catalog snapshot and /doctor. */
|
|
84
|
+
reason: string;
|
|
85
|
+
/**
|
|
86
|
+
* Newer models of the same vendor, same or higher tier, in the same price
|
|
87
|
+
* band, newest first. Automatic selection skips this model where one of
|
|
88
|
+
* them is served (`autoSelectSkipReason`).
|
|
89
|
+
*/
|
|
90
|
+
successors: string[];
|
|
91
|
+
legacy: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const TIER_RANK: Record<CatalogTier, number> = { workhorse: 0, cheap: 1, tiny: 2 };
|
|
95
|
+
const SMALL_WORD = /(?:^|[-/.])(?:nano|micro|lite|tiny)(?:[-/.:]|$)/i;
|
|
96
|
+
/** Any size tag: `27b`, `1.5b`, Gemma's effective-size `e2b`, and an MoE's active `17b`. */
|
|
97
|
+
const ANY_SIZE_TAG = /(?:^|[-/.])e?(\d+(?:\.\d+)?)b(?=[-/.:]|$)/i;
|
|
98
|
+
/** A size tag that names a total: not one followed by an expert count (`17b-16e`, Llama 4's active size). */
|
|
99
|
+
const SIZE_TAG = /(?:^|[-/.])e?(\d+(?:\.\d+)?)b(?=[-/.:]|$)(?![-/.]\d+e(?:[-/.:]|$))/i;
|
|
100
|
+
/** A total size tag that names the active size after it (`35b-a3b`): certainly the total. */
|
|
101
|
+
const TOTAL_AND_ACTIVE_TAG = /(?:^|[-/.])e?\d+(?:\.\d+)?b-a\d+(?:\.\d+)?b(?:[-/.:]|$)/i;
|
|
102
|
+
|
|
103
|
+
/** Parameters a size tag in the id names (`qwen3.8-27b` → 27e9), if any. */
|
|
104
|
+
export function sizeFromName(id: string): number | undefined {
|
|
105
|
+
const match = id.match(SIZE_TAG);
|
|
106
|
+
return match ? Number(match[1]) * 1e9 : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The part of an id its aliases, snapshots and sized variants share:
|
|
111
|
+
* `voxtral-small` for `voxtral-small-latest` and `voxtral-small-24b-2507`.
|
|
112
|
+
*/
|
|
113
|
+
function modelStem(rest: string): string {
|
|
114
|
+
const id = stripSnapshotDate(rest.toLowerCase().replace(/-latest$/, ""));
|
|
115
|
+
const tag = ANY_SIZE_TAG.exec(id);
|
|
116
|
+
return tag && tag.index > 0 ? id.slice(0, tag.index) : id;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Whether a model with no published size is small by name. */
|
|
120
|
+
export function smallByName(id: string): boolean {
|
|
121
|
+
return SMALL_WORD.test(id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const blended = (input: number | undefined, output: number | undefined): number | undefined =>
|
|
125
|
+
input !== undefined && output !== undefined && input >= 0 && output >= 0 && input + output > 0 ? (3 * input + output) / 4 : undefined;
|
|
126
|
+
|
|
127
|
+
function formatParams(params: number): string {
|
|
128
|
+
return params >= 1e12 ? `${(params / 1e12).toFixed(1)}T` : `${Math.round(params / 1e9)}B`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dateOf = (value: string | undefined): number | undefined => {
|
|
132
|
+
const time = value ? Date.parse(value) : Number.NaN;
|
|
133
|
+
return Number.isNaN(time) ? undefined : time;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
interface Draft extends Omit<CatalogModel, "tier" | "reason" | "legacy"> {
|
|
137
|
+
/** The vendor's identity profile: `zhipuai` and `z-ai` are one vendor. */
|
|
138
|
+
group: string;
|
|
139
|
+
/** The id without its vendor, lowercase. */
|
|
140
|
+
rest: string;
|
|
141
|
+
/** `params` is a Hugging Face count, not a size tag. */
|
|
142
|
+
measured: boolean;
|
|
143
|
+
/** The twin whose size this model took (rule 1). */
|
|
144
|
+
sizeFrom?: string;
|
|
145
|
+
tier?: CatalogTier;
|
|
146
|
+
reason?: string;
|
|
147
|
+
legacy?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface CatalogIndex {
|
|
151
|
+
/** Canonical id → model. */
|
|
152
|
+
models: Map<string, CatalogModel>;
|
|
153
|
+
/** `vendorProfile|id` (lowercase) → canonical id, for rows models.dev lists under no provider pi uses. */
|
|
154
|
+
byVendorId: Map<string, string>;
|
|
155
|
+
/** `provider/id` on models.dev → canonical id. */
|
|
156
|
+
byProviderRow: Map<string, string>;
|
|
157
|
+
/** A bare model id (`glm-5.3`) → the one canonical id that ends in it; null when two vendors share it. */
|
|
158
|
+
byBareId: Map<string, string | null>;
|
|
159
|
+
fetchedAt: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build the index from the three payloads (memoized per sources object). */
|
|
163
|
+
export function buildCatalogIndex(sources: CatalogSources): CatalogIndex {
|
|
164
|
+
const cached = indexMemo.get(sources);
|
|
165
|
+
if (cached) return cached;
|
|
166
|
+
const modelsDev = sources.modelsDev.payload;
|
|
167
|
+
const counts = sources.huggingFace.payload;
|
|
168
|
+
|
|
169
|
+
// A vendor is any provider a canonical id names; only its own rows are vendor rows.
|
|
170
|
+
const vendors = new Set<string>();
|
|
171
|
+
for (const provider of Object.values(modelsDev)) {
|
|
172
|
+
for (const row of Object.values(provider.models)) {
|
|
173
|
+
const vendor = row.canonical_model_id?.split("/")[0];
|
|
174
|
+
if (vendor) vendors.add(vendor);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// A nested canonical id (`nvidia/moonshotai/kimi-k2.6`) is a host's copy of
|
|
178
|
+
// another vendor's model: such rows fall through to the vendor's own id.
|
|
179
|
+
// A gateway row no vendor row names (`openrouter` `z-ai/glm-5.3-prime`) keys
|
|
180
|
+
// on its own `vendor/id`, placed against that vendor's models.
|
|
181
|
+
const gateways = new Set(
|
|
182
|
+
Object.entries(BUILTIN_PROVIDER_POLICIES)
|
|
183
|
+
.filter(([, policy]) => policy.kind === "gateway")
|
|
184
|
+
.map(([provider]) => MODELS_DEV_PROVIDER[provider] ?? provider),
|
|
185
|
+
);
|
|
186
|
+
const canonicalOf = (provider: string, id: string, row: ModelsDevModel): string | undefined => {
|
|
187
|
+
const canonical =
|
|
188
|
+
row.canonical_model_id ?? (vendors.has(provider) ? `${provider}/${id}` : gateways.has(provider) && id.split("/").length === 2 ? id : undefined);
|
|
189
|
+
return canonical && canonical.indexOf("/") === canonical.lastIndexOf("/") ? canonical : undefined;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const byProviderRow = new Map<string, string>();
|
|
193
|
+
const refs = new Map<string, ModelsDevModel[]>();
|
|
194
|
+
for (const [provider, { models }] of Object.entries(modelsDev)) {
|
|
195
|
+
for (const [id, row] of Object.entries(models)) {
|
|
196
|
+
const canonical = canonicalOf(provider, id, row);
|
|
197
|
+
if (!canonical) continue;
|
|
198
|
+
byProviderRow.set(`${provider}/${id}`, canonical);
|
|
199
|
+
const list = refs.get(canonical);
|
|
200
|
+
if (list) list.push(row);
|
|
201
|
+
else refs.set(canonical, [row]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const openRouterByCanonical = new Map<string, OpenRouterModel[]>();
|
|
205
|
+
for (const row of sources.openRouter.payload.data) {
|
|
206
|
+
const canonical = byProviderRow.get(`openrouter/${row.id}`);
|
|
207
|
+
if (!canonical) continue;
|
|
208
|
+
const list = openRouterByCanonical.get(canonical);
|
|
209
|
+
if (list) list.push(row);
|
|
210
|
+
else openRouterByCanonical.set(canonical, [row]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const drafts: Draft[] = [];
|
|
214
|
+
for (const [canonical, rows] of refs) {
|
|
215
|
+
const slash = canonical.indexOf("/");
|
|
216
|
+
if (slash <= 0) continue;
|
|
217
|
+
const vendor = canonical.slice(0, slash);
|
|
218
|
+
const rest = canonical.slice(slash + 1);
|
|
219
|
+
// The vendor's own row, unless it is a renamed alias pointing elsewhere
|
|
220
|
+
// (DeepSeek's `deepseek-v4-flash` serves V4.1; OpenRouter's row of that
|
|
221
|
+
// name is the April build and keeps its own date).
|
|
222
|
+
const ownRow = modelsDev[vendor]?.models[rest];
|
|
223
|
+
const own = ownRow && (ownRow.canonical_model_id ?? canonical) === canonical ? ownRow : undefined;
|
|
224
|
+
const openRouter = openRouterByCanonical.get(canonical) ?? [];
|
|
225
|
+
const any = own ?? rows[0];
|
|
226
|
+
const hfCounts = openRouter.map((row) => (row.hugging_face_id ? counts[row.hugging_face_id] : undefined)).filter((n): n is number => typeof n === "number" && n > 0);
|
|
227
|
+
const orPrice = openRouter
|
|
228
|
+
.map((row) => blended(Number(row.pricing?.prompt) * 1e6, Number(row.pricing?.completion) * 1e6))
|
|
229
|
+
.find((price) => price !== undefined && Number.isFinite(price));
|
|
230
|
+
const released =
|
|
231
|
+
dateOf(own?.release_date) ??
|
|
232
|
+
rows.map((row) => dateOf(row.release_date)).find((time) => time !== undefined) ??
|
|
233
|
+
(openRouter[0]?.created ? openRouter[0].created * 1000 : undefined);
|
|
234
|
+
const outputs = [...(any.modalities?.output ?? []), ...openRouter.flatMap((row) => row.architecture?.output_modalities ?? [])];
|
|
235
|
+
const toolFlag = own?.tool_call ?? rows.find((row) => row.tool_call !== undefined)?.tool_call;
|
|
236
|
+
drafts.push({
|
|
237
|
+
id: canonical,
|
|
238
|
+
vendor,
|
|
239
|
+
group: vendorProfile(vendor),
|
|
240
|
+
rest: rest.toLowerCase(),
|
|
241
|
+
measured: hfCounts.length > 0,
|
|
242
|
+
successors: [],
|
|
243
|
+
derived: !own && !rows.some((row) => row.canonical_model_id === canonical),
|
|
244
|
+
released,
|
|
245
|
+
deprecated: own ? own.status === "deprecated" : rows.every((row) => row.status === "deprecated"),
|
|
246
|
+
tools: toolFlag ?? (openRouter.length === 0 || openRouter.some((row) => row.supported_parameters?.includes("tools"))),
|
|
247
|
+
nonTextOutput: outputs.some((modality) => modality !== "text"),
|
|
248
|
+
price: blended(own?.cost?.input, own?.cost?.output) ?? orPrice ?? rows.map((row) => blended(row.cost?.input, row.cost?.output)).find((p) => p !== undefined),
|
|
249
|
+
params: hfCounts.length > 0 ? Math.max(...hfCounts) : sizeFromName(rest),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const byVendor = new Map<string, Draft[]>();
|
|
254
|
+
for (const draft of drafts) {
|
|
255
|
+
const list = byVendor.get(draft.group);
|
|
256
|
+
if (list) list.push(draft);
|
|
257
|
+
else byVendor.set(draft.group, [draft]);
|
|
258
|
+
}
|
|
259
|
+
for (const list of byVendor.values()) {
|
|
260
|
+
shareTwinSizes(list);
|
|
261
|
+
placeVendor(list);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const models = new Map<string, CatalogModel>();
|
|
265
|
+
const byVendorId = new Map<string, string>();
|
|
266
|
+
const byBareId = new Map<string, string | null>();
|
|
267
|
+
for (const draft of drafts) {
|
|
268
|
+
const model = draft as CatalogModel;
|
|
269
|
+
models.set(model.id, model);
|
|
270
|
+
const rest = model.id.slice(model.vendor.length + 1).toLowerCase();
|
|
271
|
+
for (const key of new Set([rest, stripSnapshotDate(rest)])) {
|
|
272
|
+
const lookup = `${vendorProfile(model.vendor)}|${key}`;
|
|
273
|
+
if (!byVendorId.has(lookup)) byVendorId.set(lookup, model.id);
|
|
274
|
+
}
|
|
275
|
+
const bare = bareId(rest);
|
|
276
|
+
const prior = byBareId.get(bare);
|
|
277
|
+
byBareId.set(bare, prior === undefined || prior === model.id ? model.id : null);
|
|
278
|
+
}
|
|
279
|
+
const index: CatalogIndex = { models, byVendorId, byProviderRow, byBareId, fetchedAt: sources.modelsDev.fetchedAt };
|
|
280
|
+
indexMemo.set(sources, index);
|
|
281
|
+
return index;
|
|
282
|
+
}
|
|
283
|
+
const indexMemo = new WeakMap<CatalogSources, CatalogIndex>();
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A model id reduced to the part every host spells alike: the last path
|
|
287
|
+
* segment (`zai-org/GLM-5.3`, `workers-ai/@cf/zai-org/glm-5.3`), lowercase,
|
|
288
|
+
* Fireworks' `5p3` read as `5.3`.
|
|
289
|
+
*/
|
|
290
|
+
export function bareId(id: string): string {
|
|
291
|
+
return id.slice(id.lastIndexOf("/") + 1).toLowerCase().replace(/(\d)p(\d)/g, "$1.$2");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const isTiny = (draft: Draft): boolean =>
|
|
295
|
+
draft.params !== undefined ? draft.params < TINY_PARAMS : smallByName(draft.id.slice(draft.vendor.length + 1));
|
|
296
|
+
const isLive = (draft: Draft): boolean => draft.tools && !draft.deprecated && !draft.nonTextOutput && draft.released !== undefined;
|
|
297
|
+
/** Live and a vendor's own model: what sets the comparison and can supersede. */
|
|
298
|
+
const isReference = (draft: Draft): boolean => isLive(draft) && !draft.derived;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Rule 1's twins, in place for one vendor: a size tag yields to a larger
|
|
302
|
+
* measured size of an untagged twin (or one with the same tag), and an
|
|
303
|
+
* unsized model takes its twins' size when they agree on tiny, and their
|
|
304
|
+
* non-text output.
|
|
305
|
+
*/
|
|
306
|
+
function shareTwinSizes(list: Draft[]): void {
|
|
307
|
+
const byStem = new Map<string, Draft[]>();
|
|
308
|
+
for (const draft of list) {
|
|
309
|
+
const stem = modelStem(draft.rest);
|
|
310
|
+
const group = byStem.get(stem);
|
|
311
|
+
if (group) group.push(draft);
|
|
312
|
+
else byStem.set(stem, [draft]);
|
|
313
|
+
}
|
|
314
|
+
const twinsOf = (draft: Draft): Draft[] =>
|
|
315
|
+
(byStem.get(modelStem(draft.rest)) ?? []).filter(
|
|
316
|
+
(other) =>
|
|
317
|
+
other !== draft &&
|
|
318
|
+
((draft.released !== undefined && other.released === draft.released) ||
|
|
319
|
+
stripSnapshotDate(other.rest) === draft.rest ||
|
|
320
|
+
stripSnapshotDate(draft.rest) === other.rest),
|
|
321
|
+
);
|
|
322
|
+
const adopt = (draft: Draft, from: Draft[]): void => {
|
|
323
|
+
const source = from.reduce((a, b) => (b.params! > a.params! ? b : a));
|
|
324
|
+
draft.params = source.params;
|
|
325
|
+
draft.sizeFrom = source.id;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
for (const draft of list) {
|
|
329
|
+
const tag = sizeFromName(draft.rest);
|
|
330
|
+
if (draft.measured || tag === undefined || TOTAL_AND_ACTIVE_TAG.test(draft.rest)) continue;
|
|
331
|
+
const twins = twinsOf(draft);
|
|
332
|
+
// A size ladder released together: each tag is its own model's total.
|
|
333
|
+
if (twins.some((twin) => (sizeFromName(twin.rest) ?? tag) !== tag)) continue;
|
|
334
|
+
const measured = twins.filter((twin) => twin.measured && twin.params! > tag);
|
|
335
|
+
if (measured.length > 0) adopt(draft, measured);
|
|
336
|
+
}
|
|
337
|
+
for (const draft of list) {
|
|
338
|
+
if (draft.params !== undefined) continue;
|
|
339
|
+
const twins = twinsOf(draft);
|
|
340
|
+
if (twins.some((twin) => twin.nonTextOutput)) draft.nonTextOutput = true;
|
|
341
|
+
const sized = twins.filter((twin) => twin.params !== undefined);
|
|
342
|
+
if (sized.length > 0 && new Set(sized.map((twin) => twin.params! < TINY_PARAMS)).size === 1) adopt(draft, sized);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function median(values: number[]): number | undefined {
|
|
347
|
+
if (values.length === 0) return undefined;
|
|
348
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
349
|
+
return sorted[Math.floor((sorted.length - 1) / 2)];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Rules 1–5 for one vendor's models, in place. */
|
|
353
|
+
function placeVendor(list: Draft[]): void {
|
|
354
|
+
const live = list.filter(isReference);
|
|
355
|
+
const newest = Math.max(...live.map((draft) => draft.released!));
|
|
356
|
+
const current = live.filter((draft) => newest - draft.released! <= CURRENT_WINDOW_DAYS * DAY_MS && !isTiny(draft));
|
|
357
|
+
const sizeBasis = current.length > 0 && current.every((draft) => draft.params !== undefined);
|
|
358
|
+
const maxParams = sizeBasis ? Math.max(...current.map((draft) => draft.params!)) : undefined;
|
|
359
|
+
const prices = current.map((draft) => draft.price).filter((price): price is number => price !== undefined && price > 0);
|
|
360
|
+
const referencePrice = prices.length >= MIN_PRICE_PEERS ? median(prices) : undefined;
|
|
361
|
+
const vendor = (list.find((draft) => !draft.derived) ?? list[0])?.vendor ?? "";
|
|
362
|
+
|
|
363
|
+
const size = (draft: Draft): string => `${formatParams(draft.params!)} parameters${draft.sizeFrom ? ` (as ${draft.sizeFrom})` : ""}`;
|
|
364
|
+
for (const draft of list) {
|
|
365
|
+
if (isTiny(draft)) {
|
|
366
|
+
draft.tier = "tiny";
|
|
367
|
+
draft.reason = draft.params !== undefined ? `${size(draft)}, under ${formatParams(TINY_PARAMS)}` : "a small-model name, size unpublished";
|
|
368
|
+
} else if (maxParams !== undefined && draft.params !== undefined) {
|
|
369
|
+
const ratio = draft.params / maxParams;
|
|
370
|
+
draft.tier = ratio >= SIZE_RATIO ? "workhorse" : "cheap";
|
|
371
|
+
draft.reason = `${size(draft)}, ${ratio.toFixed(2)} of ${vendor}'s largest current model (${formatParams(maxParams)})`;
|
|
372
|
+
} else if (referencePrice !== undefined && draft.price !== undefined) {
|
|
373
|
+
const ratio = draft.price / referencePrice;
|
|
374
|
+
draft.tier = ratio >= PRICE_RATIO ? "workhorse" : "cheap";
|
|
375
|
+
draft.reason = `$${draft.price.toFixed(2)}/M blended, ${ratio.toFixed(2)} of ${vendor}'s median current price ($${referencePrice.toFixed(2)})`;
|
|
376
|
+
} else {
|
|
377
|
+
draft.tier = "cheap";
|
|
378
|
+
draft.reason = prices.length < MIN_PRICE_PEERS ? `${vendor} has too few current priced models to compare against` : "no size or price to compare";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Age: within the vendor's workhorse class only (rule 4).
|
|
383
|
+
const workhorseNewest = Math.max(...live.filter((draft) => draft.tier === "workhorse").map((draft) => draft.released!));
|
|
384
|
+
for (const draft of list) {
|
|
385
|
+
if (draft.tier === "workhorse" && draft.released !== undefined && workhorseNewest - draft.released > AGE_DEMOTE_DAYS * DAY_MS) {
|
|
386
|
+
draft.tier = "cheap";
|
|
387
|
+
draft.reason += `; over a year older than ${vendor}'s newest model of its size class`;
|
|
388
|
+
}
|
|
389
|
+
draft.legacy = draft.released !== undefined && Number.isFinite(newest) && newest - draft.released > LEGACY_DAYS * DAY_MS;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Supersession (rule 5): every qualifying successor, newest first; selection
|
|
393
|
+
// skips the model only where one is served. A moving alias names some other
|
|
394
|
+
// model, and a dated snapshot is the model it dates.
|
|
395
|
+
const successors = live.filter((draft) => !draft.legacy && !isMovingAlias(draft.rest)).sort((a, b) => b.released! - a.released!);
|
|
396
|
+
for (const draft of list) {
|
|
397
|
+
if (draft.released === undefined || draft.price === undefined) continue;
|
|
398
|
+
draft.successors = successors
|
|
399
|
+
.filter(
|
|
400
|
+
(other) =>
|
|
401
|
+
other !== draft &&
|
|
402
|
+
other.released! > draft.released! &&
|
|
403
|
+
stripSnapshotDate(other.rest) !== draft.rest &&
|
|
404
|
+
TIER_RANK[other.tier!] <= TIER_RANK[draft.tier!] &&
|
|
405
|
+
other.price !== undefined &&
|
|
406
|
+
other.price >= draft.price! * SUPERSEDE_PRICE_BAND[0] &&
|
|
407
|
+
other.price <= draft.price! * SUPERSEDE_PRICE_BAND[1],
|
|
408
|
+
)
|
|
409
|
+
.map((other) => other.id);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Whether automatic selection may pick this model at all (rule 5). `served`
|
|
415
|
+
* holds the catalog ids the session's contained models serve
|
|
416
|
+
* (`model-tier.ts servedCatalogIds`): only a served successor supersedes.
|
|
417
|
+
*/
|
|
418
|
+
export function autoSelectable(model: CatalogModel, served: ReadonlySet<string>): boolean {
|
|
419
|
+
return autoSelectSkipReason(model, served) === undefined;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Why automatic selection skips the model, in words; undefined when it does not. */
|
|
423
|
+
export function autoSelectSkipReason(model: CatalogModel, served: ReadonlySet<string>): string | undefined {
|
|
424
|
+
const successor = model.successors.find((id) => served.has(id));
|
|
425
|
+
if (successor) return `superseded by ${successor}, which this provider serves`;
|
|
426
|
+
if (model.legacy) return "over two years older than its vendor's newest model";
|
|
427
|
+
if (model.deprecated) return "deprecated";
|
|
428
|
+
if (!model.tools) return "cannot call tools";
|
|
429
|
+
if (model.nonTextOutput) return "not a text model";
|
|
430
|
+
if (isExperimentalBuild(model.id)) return "an experimental build";
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The index over the sources in use (bundled or refreshed, whichever is newer). */
|
|
435
|
+
export function currentCatalog(): CatalogIndex {
|
|
436
|
+
return buildCatalogIndex(loadCatalogSources(oneCodeStateDir()));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The catalog model a pi row serves, or undefined. A built-in provider's row
|
|
441
|
+
* is looked up on models.dev under its provider and followed to its canonical
|
|
442
|
+
* id; a row models.dev does not list (OpenAI Codex, Azure) is matched by
|
|
443
|
+
* vendor and id through its identity; a host that serves open models under
|
|
444
|
+
* their own names (Together, Fireworks, the Qwen plans) by the bare id, when
|
|
445
|
+
* exactly one vendor has it. A custom provider is never matched: its ids can
|
|
446
|
+
* name anything.
|
|
447
|
+
*/
|
|
448
|
+
export function catalogModelFor(model: { provider: string; id: string }, index: CatalogIndex = currentCatalog()): CatalogModel | undefined {
|
|
449
|
+
if (!(model.provider in BUILTIN_PROVIDER_POLICIES)) return undefined;
|
|
450
|
+
const provider = MODELS_DEV_PROVIDER[model.provider] ?? model.provider;
|
|
451
|
+
const listed = index.byProviderRow.get(`${provider}/${model.id}`) ?? index.byProviderRow.get(`${provider}/${baseModelId(model.id)}`);
|
|
452
|
+
if (listed) return index.models.get(listed);
|
|
453
|
+
const identity = modelIdentity(model as Model<Api>);
|
|
454
|
+
if (identity.confidence !== "opaque" && identity.profile) {
|
|
455
|
+
const id = baseModelId(identity.normalizedId).toLowerCase();
|
|
456
|
+
const canonical = index.byVendorId.get(`${identity.profile}|${id}`) ?? index.byVendorId.get(`${identity.profile}|${stripSnapshotDate(id)}`);
|
|
457
|
+
if (canonical) return index.models.get(canonical);
|
|
458
|
+
}
|
|
459
|
+
const bare = bareId(baseModelId(model.id));
|
|
460
|
+
const canonical = index.byBareId.get(bare) ?? index.byBareId.get(stripSnapshotDate(bare));
|
|
461
|
+
return canonical ? index.models.get(canonical) : undefined;
|
|
462
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warnings for a model the user chose by hand for a secondary role: the
|
|
3
|
+
* subagent default (`/subagent`) or the auto-mode classifier
|
|
4
|
+
* (`/auto-mode model`). Pure: no pi imports. Automatic selection never needs
|
|
5
|
+
* them (it skips superseded models and never goes below the session's tier);
|
|
6
|
+
* a hand choice is honoured, so these say what it costs instead of refusing it.
|
|
7
|
+
* The session model's own newer-model notice lives in `extensions/newer-model`.
|
|
8
|
+
*
|
|
9
|
+
* - **Newer model:** the same model line has a newer release at about the same
|
|
10
|
+
* price (`newerModelSuggestion`).
|
|
11
|
+
* - **Far weaker than the session:** two or more tiers below the session model
|
|
12
|
+
* (a frontier session with a cheap or tiny model, a workhorse one with tiny).
|
|
13
|
+
* - **Classifier only, smaller context window:** the classifier receives the
|
|
14
|
+
* whole transcript, so once the session outgrows the chosen model's window
|
|
15
|
+
* each call goes unjudged and asks for approval until `/compact`.
|
|
16
|
+
* - **Classifier only, costlier than the session:** the classifier runs on
|
|
17
|
+
* nearly every gated call, so a model that costs more per call than the
|
|
18
|
+
* session model can cost more than the session itself. Automatic selection
|
|
19
|
+
* never picks a costlier one.
|
|
20
|
+
* - **Classifier only, unstable model:** a moving `-latest` alias or an
|
|
21
|
+
* experimental build can change what answers without notice, and with it the
|
|
22
|
+
* permission boundary. Automatic selection never picks one as the classifier.
|
|
23
|
+
*
|
|
24
|
+
* working-docs/decisions/model-policy.md ("Warnings on a chosen model").
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
28
|
+
import { classifierCostRatio, hasCatalogContextWindow, isExperimentalBuild, isMovingAlias, modelSpec as spec } from "./model-policy.ts";
|
|
29
|
+
import { intrinsicTier, tiersBelow } from "./model-tier.ts";
|
|
30
|
+
import { newerModelSuggestion } from "./newer-model.ts";
|
|
31
|
+
|
|
32
|
+
export type ChosenModelRole = "subagent" | "classifier";
|
|
33
|
+
|
|
34
|
+
export interface ChosenModelWarning {
|
|
35
|
+
kind: "newer" | "weaker" | "window" | "unstable" | "costlier";
|
|
36
|
+
text: string;
|
|
37
|
+
/** What to run instead, in the role's own command. */
|
|
38
|
+
fix: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Tiers below the session at which a chosen model is "far weaker" (the text says "two or more"). */
|
|
42
|
+
const WEAKER_TIER_GAP = 2;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How many times another price `ratio` is, for a warning: "1.4 times", "21 times".
|
|
46
|
+
* One decimal under ten, whole numbers above.
|
|
47
|
+
*/
|
|
48
|
+
export function costRatioText(ratio: number): string {
|
|
49
|
+
return `${ratio < 10 ? Math.round(ratio * 10) / 10 : Math.round(ratio)} times`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A cost ratio worth a warning: one that `costRatioText` shows above "1 times". */
|
|
53
|
+
export function isNotablyCostlier(ratio: number | undefined): ratio is number {
|
|
54
|
+
return ratio !== undefined && ratio >= 1.05;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const COMMAND: Record<ChosenModelRole, string> = { subagent: "/subagent", classifier: "/auto-mode model" };
|
|
58
|
+
const LABEL: Record<ChosenModelRole, string> = { subagent: "subagent default", classifier: "auto-mode classifier" };
|
|
59
|
+
|
|
60
|
+
/** Every warning that applies to `chosen` in `role` for this session, most important first. */
|
|
61
|
+
export function chosenModelWarnings({
|
|
62
|
+
available,
|
|
63
|
+
sessionModel,
|
|
64
|
+
chosen,
|
|
65
|
+
role,
|
|
66
|
+
suggestNewer = true,
|
|
67
|
+
}: {
|
|
68
|
+
available: readonly Model<Api>[];
|
|
69
|
+
sessionModel: Model<Api> | undefined;
|
|
70
|
+
chosen: Model<Api>;
|
|
71
|
+
role: ChosenModelRole;
|
|
72
|
+
/** `suggestNewerModels` in ~/.onecode/settings.json; false drops the newer-model warning. */
|
|
73
|
+
suggestNewer?: boolean;
|
|
74
|
+
}): ChosenModelWarning[] {
|
|
75
|
+
const warnings: ChosenModelWarning[] = [];
|
|
76
|
+
const command = COMMAND[role];
|
|
77
|
+
const label = LABEL[role];
|
|
78
|
+
const orAutomatic = `or ${command} clear for the automatic choice.`;
|
|
79
|
+
const isSession = !!sessionModel && sessionModel.provider === chosen.provider && sessionModel.id === chosen.id;
|
|
80
|
+
|
|
81
|
+
if (role === "classifier" && !isSession && (isMovingAlias(chosen.id) || isExperimentalBuild(chosen.id))) {
|
|
82
|
+
const what = isMovingAlias(chosen.id) ? "a moving alias: the model behind it can change without notice" : "an experimental build, which can change or be withdrawn without notice";
|
|
83
|
+
warnings.push({
|
|
84
|
+
kind: "unstable",
|
|
85
|
+
text: `The ${label} ${spec(chosen)} is ${what}, and the permission boundary changes with it.`,
|
|
86
|
+
fix: `Pick a fixed release with ${command}, ${orAutomatic}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (sessionModel && !isSession) {
|
|
91
|
+
const chosenTier = intrinsicTier(chosen);
|
|
92
|
+
const sessionTier = intrinsicTier(sessionModel);
|
|
93
|
+
if (tiersBelow(chosenTier, sessionTier) >= WEAKER_TIER_GAP) {
|
|
94
|
+
const why =
|
|
95
|
+
role === "classifier"
|
|
96
|
+
? "A weak classifier is a weak permission boundary: it may let through calls a stronger one would stop."
|
|
97
|
+
: "A much weaker subagent tends to spend the saving on retries and mistakes.";
|
|
98
|
+
warnings.push({
|
|
99
|
+
kind: "weaker",
|
|
100
|
+
text: `The ${label} ${spec(chosen)} is a ${chosenTier}-tier model, two or more tiers below this session's ${spec(sessionModel)} (${sessionTier}). ${why}`,
|
|
101
|
+
fix: `Pick a stronger one with ${command}, ${orAutomatic}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (role === "classifier" && hasCatalogContextWindow(chosen) && hasCatalogContextWindow(sessionModel) && sessionModel.contextWindow > chosen.contextWindow) {
|
|
105
|
+
warnings.push({
|
|
106
|
+
kind: "window",
|
|
107
|
+
text:
|
|
108
|
+
`The ${label} ${spec(chosen)} has a ${chosen.contextWindow.toLocaleString("en-US")}-token context window, smaller than this session's ` +
|
|
109
|
+
`${sessionModel.contextWindow.toLocaleString("en-US")}. The classifier reads the whole transcript: once the session outgrows it, ` +
|
|
110
|
+
"calls are not judged and each asks for your approval (a headless run stops) until you /compact.",
|
|
111
|
+
fix: `Pick a model with a larger window with ${command}, ${orAutomatic}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (role === "classifier") {
|
|
115
|
+
const ratio = classifierCostRatio(chosen, sessionModel);
|
|
116
|
+
if (isNotablyCostlier(ratio)) {
|
|
117
|
+
warnings.push({
|
|
118
|
+
kind: "costlier",
|
|
119
|
+
text:
|
|
120
|
+
`The ${label} ${spec(chosen)} costs about ${costRatioText(ratio)} as much per call as this session's ${spec(sessionModel)}. ` +
|
|
121
|
+
"It screens nearly every gated call, so it can cost more than the session itself.",
|
|
122
|
+
fix: `Pick a cheaper one with ${command}, ${orAutomatic}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const newer = suggestNewer ? newerModelSuggestion(available, chosen) : undefined;
|
|
129
|
+
if (newer) {
|
|
130
|
+
warnings.push({
|
|
131
|
+
kind: "newer",
|
|
132
|
+
text: `The ${label} ${spec(chosen)} has a newer model in its line: ${newer.text}`,
|
|
133
|
+
fix: `Switch with ${command} ${spec(newer.model)}.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return warnings;
|
|
137
|
+
}
|