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,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public model catalogs the tier algorithm reads (`model-catalog.ts`):
|
|
3
|
+
* models.dev (release dates, vendor prices, canonical ids, tool support),
|
|
4
|
+
* OpenRouter's model list (Hugging Face repo ids, output modalities) and
|
|
5
|
+
* Hugging Face's safetensors metadata (total parameter counts). None needs a
|
|
6
|
+
* key. Pure: no pi imports.
|
|
7
|
+
*
|
|
8
|
+
* Each source is kept as its own API payload, trimmed to the fields the
|
|
9
|
+
* algorithm reads but in the API's own shape, so the copy bundled with a
|
|
10
|
+
* release (`model-catalog/*.json`, written by `scripts/gen-model-catalog.mjs`)
|
|
11
|
+
* and the copy an interactive session refreshes into the One Code state dir go
|
|
12
|
+
* through the same code. The newer copy of each source wins; parameter counts
|
|
13
|
+
* merge, since a refresh only looks up repos it has not seen.
|
|
14
|
+
*
|
|
15
|
+
* A refresh runs at most once a day, only from a session that outlives its
|
|
16
|
+
* turn, never awaited in `session_start` (the caller's job), and a failure
|
|
17
|
+
* keeps the copy already on disk and waits a day before the next try (a
|
|
18
|
+
* stamp in the cache dir). Each fetch is bounded, body included.
|
|
19
|
+
* `refreshModelCatalog: false` in
|
|
20
|
+
* `~/.onecode/settings.json` or pi's `PI_OFFLINE` turns it off; the copy on
|
|
21
|
+
* disk (or the bundled one) then decides.
|
|
22
|
+
* working-docs/decisions/model-tiers.md.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { closeSync, openSync, readSync, rmSync, statSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { readJsonFile, writeJsonAtomic } from "./atomic-write.ts";
|
|
29
|
+
import { fetchWithTimeout } from "./fetch-timeout.ts";
|
|
30
|
+
|
|
31
|
+
export const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
32
|
+
export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
33
|
+
export const HUGGING_FACE_MODELS_URL = "https://huggingface.co/api/models/";
|
|
34
|
+
|
|
35
|
+
export const CATALOG_REFRESH_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
36
|
+
export const CATALOG_FETCH_TIMEOUT_MS = 30_000;
|
|
37
|
+
/** Hugging Face lookups per refresh: only repos new since the last one, so normally a handful. */
|
|
38
|
+
export const HUGGING_FACE_LOOKUPS_PER_REFRESH = 25;
|
|
39
|
+
|
|
40
|
+
/** pi providers models.dev lists under another name. */
|
|
41
|
+
export const MODELS_DEV_PROVIDER: Readonly<Record<string, string>> = { "vercel-ai-gateway": "vercel" };
|
|
42
|
+
|
|
43
|
+
/** A models.dev model row, trimmed to what the tier algorithm reads. */
|
|
44
|
+
export interface ModelsDevModel {
|
|
45
|
+
release_date?: string;
|
|
46
|
+
/** models.dev's lifecycle marker; only `deprecated` is acted on. */
|
|
47
|
+
status?: string;
|
|
48
|
+
tool_call?: boolean;
|
|
49
|
+
/** USD per million tokens. */
|
|
50
|
+
cost?: { input?: number; output?: number };
|
|
51
|
+
/** The vendor's own `provider/id` for the model a hosted or gateway row serves. */
|
|
52
|
+
canonical_model_id?: string;
|
|
53
|
+
/** Kept only when a model outputs something besides text (an image model). */
|
|
54
|
+
modalities?: { output?: string[] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ModelsDevPayload = Record<string, { models: Record<string, ModelsDevModel> }>;
|
|
58
|
+
|
|
59
|
+
/** An OpenRouter model row, trimmed. */
|
|
60
|
+
export interface OpenRouterModel {
|
|
61
|
+
id: string;
|
|
62
|
+
/** Unix seconds the row was listed: a release date when models.dev has none. */
|
|
63
|
+
created?: number;
|
|
64
|
+
hugging_face_id?: string;
|
|
65
|
+
/** Kept only as `["tools"]` when present. */
|
|
66
|
+
supported_parameters?: string[];
|
|
67
|
+
architecture?: { output_modalities?: string[] };
|
|
68
|
+
/** USD per token, as strings, OpenRouter's own spelling. */
|
|
69
|
+
pricing?: { prompt?: string; completion?: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface OpenRouterPayload {
|
|
73
|
+
data: OpenRouterModel[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Hugging Face repo id → total parameters from its safetensors metadata; null when the repo has none (gated, removed, no safetensors). */
|
|
77
|
+
export type ParameterCounts = Record<string, number | null>;
|
|
78
|
+
|
|
79
|
+
export interface CatalogFile<T> {
|
|
80
|
+
fetchedAt: string;
|
|
81
|
+
source: string;
|
|
82
|
+
payload: T;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface CatalogSources {
|
|
86
|
+
/** Whether the models.dev copy in use came from a runtime refresh rather than the release. */
|
|
87
|
+
refreshed?: boolean;
|
|
88
|
+
modelsDev: CatalogFile<ModelsDevPayload>;
|
|
89
|
+
openRouter: CatalogFile<OpenRouterPayload>;
|
|
90
|
+
huggingFace: CatalogFile<ParameterCounts>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const FILES = { modelsDev: "models-dev.json", openRouter: "openrouter.json", huggingFace: "huggingface.json" } as const;
|
|
94
|
+
/** The last failed refresh, which holds the next one off for a day (`recentFailure`). */
|
|
95
|
+
const FAILURE_FILE = "refresh-failed.json";
|
|
96
|
+
type SourceName = keyof typeof FILES;
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Trimming (shared by the build script and the runtime refresh)
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
const record = (value: unknown): Record<string, unknown> | undefined =>
|
|
103
|
+
value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
104
|
+
const finite = (value: unknown): number | undefined => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
|
|
105
|
+
const strings = (value: unknown): string[] => (Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : []);
|
|
106
|
+
|
|
107
|
+
function trimModelsDevRow(raw: Record<string, unknown>): ModelsDevModel {
|
|
108
|
+
const row: ModelsDevModel = {};
|
|
109
|
+
if (typeof raw.release_date === "string") row.release_date = raw.release_date;
|
|
110
|
+
if (typeof raw.status === "string") row.status = raw.status;
|
|
111
|
+
if (typeof raw.tool_call === "boolean") row.tool_call = raw.tool_call;
|
|
112
|
+
const cost = record(raw.cost);
|
|
113
|
+
if (cost && (finite(cost.input) !== undefined || finite(cost.output) !== undefined)) {
|
|
114
|
+
row.cost = {};
|
|
115
|
+
if (finite(cost.input) !== undefined) row.cost.input = finite(cost.input);
|
|
116
|
+
if (finite(cost.output) !== undefined) row.cost.output = finite(cost.output);
|
|
117
|
+
}
|
|
118
|
+
if (typeof raw.canonical_model_id === "string") row.canonical_model_id = raw.canonical_model_id;
|
|
119
|
+
const output = strings(record(raw.modalities)?.output);
|
|
120
|
+
if (output.some((modality) => modality !== "text")) row.modalities = { output };
|
|
121
|
+
return row;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* models.dev's `api.json` reduced to the providers One Code can run on (pi's
|
|
126
|
+
* built-in providers, under models.dev's names) plus OpenRouter and every
|
|
127
|
+
* vendor those rows name as canonical, whole: a vendor's comparison set needs
|
|
128
|
+
* its other models too.
|
|
129
|
+
*/
|
|
130
|
+
export function trimModelsDev(body: unknown, piProviders: Iterable<string>): ModelsDevPayload {
|
|
131
|
+
const all = record(body) ?? {};
|
|
132
|
+
const keep = new Set<string>(["openrouter"]);
|
|
133
|
+
for (const provider of piProviders) keep.add(MODELS_DEV_PROVIDER[provider] ?? provider);
|
|
134
|
+
const out: ModelsDevPayload = {};
|
|
135
|
+
const pending = [...keep];
|
|
136
|
+
while (pending.length > 0) {
|
|
137
|
+
const provider = pending.pop()!;
|
|
138
|
+
const models = record(record(all[provider])?.models);
|
|
139
|
+
if (!models || out[provider]) continue;
|
|
140
|
+
const trimmed: Record<string, ModelsDevModel> = {};
|
|
141
|
+
for (const [id, raw] of Object.entries(models)) {
|
|
142
|
+
const row = record(raw);
|
|
143
|
+
if (!row) continue;
|
|
144
|
+
trimmed[id] = trimModelsDevRow(row);
|
|
145
|
+
const vendor = trimmed[id].canonical_model_id?.split("/")[0];
|
|
146
|
+
if (vendor && !keep.has(vendor)) {
|
|
147
|
+
keep.add(vendor);
|
|
148
|
+
pending.push(vendor);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
out[provider] = { models: trimmed };
|
|
152
|
+
}
|
|
153
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** OpenRouter's `/api/v1/models` reduced to the fields the algorithm reads. */
|
|
157
|
+
export function trimOpenRouter(body: unknown): OpenRouterPayload {
|
|
158
|
+
const data: OpenRouterModel[] = [];
|
|
159
|
+
for (const raw of Array.isArray(record(body)?.data) ? (record(body)!.data as unknown[]) : []) {
|
|
160
|
+
const row = record(raw);
|
|
161
|
+
if (!row || typeof row.id !== "string") continue;
|
|
162
|
+
const model: OpenRouterModel = { id: row.id };
|
|
163
|
+
if (finite(row.created) !== undefined) model.created = finite(row.created);
|
|
164
|
+
if (typeof row.hugging_face_id === "string" && row.hugging_face_id) model.hugging_face_id = row.hugging_face_id;
|
|
165
|
+
if (strings(row.supported_parameters).includes("tools")) model.supported_parameters = ["tools"];
|
|
166
|
+
const output = strings(record(row.architecture)?.output_modalities);
|
|
167
|
+
if (output.some((modality) => modality !== "text")) model.architecture = { output_modalities: output };
|
|
168
|
+
const pricing = record(row.pricing);
|
|
169
|
+
if (pricing && typeof pricing.prompt === "string" && typeof pricing.completion === "string") {
|
|
170
|
+
model.pricing = { prompt: pricing.prompt, completion: pricing.completion };
|
|
171
|
+
}
|
|
172
|
+
data.push(model);
|
|
173
|
+
}
|
|
174
|
+
data.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
175
|
+
return { data };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The total parameter count in a Hugging Face model response, or null. */
|
|
179
|
+
export function parameterCountFromResponse(body: unknown): number | null {
|
|
180
|
+
const total = finite(record(record(body)?.safetensors)?.total);
|
|
181
|
+
return total !== undefined && total > 0 ? total : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The repo ids an OpenRouter payload names that `known` has no entry for. */
|
|
185
|
+
export function unknownRepos(openRouter: OpenRouterPayload, known: ParameterCounts): string[] {
|
|
186
|
+
const repos = new Set<string>();
|
|
187
|
+
for (const row of openRouter.data) if (row.hugging_face_id && !(row.hugging_face_id in known)) repos.add(row.hugging_face_id);
|
|
188
|
+
return [...repos].sort();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One repo's parameter count; a failed lookup is undefined (retried next refresh), a repo without one is null. */
|
|
192
|
+
export async function fetchParameterCount(repo: string, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<number | null | undefined> {
|
|
193
|
+
const url = `${HUGGING_FACE_MODELS_URL}${repo.split("/").map(encodeURIComponent).join("/")}?expand%5B%5D=safetensors`;
|
|
194
|
+
try {
|
|
195
|
+
return await fetchWithTimeout(
|
|
196
|
+
url,
|
|
197
|
+
CATALOG_FETCH_TIMEOUT_MS,
|
|
198
|
+
async (response) => {
|
|
199
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) return null;
|
|
200
|
+
if (!response.ok) return undefined;
|
|
201
|
+
return parameterCountFromResponse(await response.json());
|
|
202
|
+
},
|
|
203
|
+
{ fetchImpl, signal },
|
|
204
|
+
);
|
|
205
|
+
} catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Loading: the bundled copy, the refreshed copy, the newer of the two
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
export function bundledCatalogDir(): string {
|
|
215
|
+
return fileURLToPath(new URL("./model-catalog/", import.meta.url));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function catalogCacheDir(stateDir: string): string {
|
|
219
|
+
return join(stateDir, "cache", "model-catalog");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The refreshed catalog files and the failure stamp that holds a refresh off.
|
|
224
|
+
* They decide a model's tier, and the tier decides which auto-mode fast paths
|
|
225
|
+
* skip the classifier, so the safety floor guards them like the settings
|
|
226
|
+
* files (auto-mode/safety-floor.ts).
|
|
227
|
+
*/
|
|
228
|
+
export function catalogCacheFiles(stateDir: string): string[] {
|
|
229
|
+
return [...Object.values(FILES), FAILURE_FILE].map((file) => join(catalogCacheDir(stateDir), file));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A refreshed catalog file under any literal `.onecode` directory, as a
|
|
234
|
+
* forward-slashed path tail: the floor's counterpart of `catalogCacheFiles`,
|
|
235
|
+
* for one in another home or spelled through a symlink the resolver missed.
|
|
236
|
+
*/
|
|
237
|
+
export const CATALOG_CACHE_TAIL = new RegExp(`/\\.onecode/cache/model-catalog/(${[...Object.values(FILES), FAILURE_FILE].map((file) => file.replace(/[.]/g, "\\.")).join("|")})$`);
|
|
238
|
+
|
|
239
|
+
function isCatalogFile(value: unknown): value is CatalogFile<unknown> {
|
|
240
|
+
const file = record(value);
|
|
241
|
+
return !!file && typeof file.fetchedAt === "string" && !Number.isNaN(Date.parse(file.fetchedAt)) && record(file.payload) !== undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readCatalogFile<T>(path: string): CatalogFile<T> | undefined {
|
|
245
|
+
// Missing or corrupt: the other copy decides.
|
|
246
|
+
const parsed = readJsonFile<unknown>(path);
|
|
247
|
+
return isCatalogFile(parsed) ? (parsed as CatalogFile<T>) : undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const EMPTY: CatalogSources = {
|
|
251
|
+
modelsDev: { fetchedAt: new Date(0).toISOString(), source: MODELS_DEV_URL, payload: {} },
|
|
252
|
+
openRouter: { fetchedAt: new Date(0).toISOString(), source: OPENROUTER_MODELS_URL, payload: { data: [] } },
|
|
253
|
+
huggingFace: { fetchedAt: new Date(0).toISOString(), source: HUGGING_FACE_MODELS_URL, payload: {} },
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
let bundled: CatalogSources | undefined;
|
|
257
|
+
/** The copy bundled with this release, ignoring any refresh. */
|
|
258
|
+
export function bundledSources(): CatalogSources {
|
|
259
|
+
if (bundled) return bundled;
|
|
260
|
+
const dir = bundledCatalogDir();
|
|
261
|
+
bundled = {
|
|
262
|
+
modelsDev: readCatalogFile<ModelsDevPayload>(join(dir, FILES.modelsDev)) ?? EMPTY.modelsDev,
|
|
263
|
+
openRouter: readCatalogFile<OpenRouterPayload>(join(dir, FILES.openRouter)) ?? EMPTY.openRouter,
|
|
264
|
+
huggingFace: readCatalogFile<ParameterCounts>(join(dir, FILES.huggingFace)) ?? EMPTY.huggingFace,
|
|
265
|
+
};
|
|
266
|
+
return bundled;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const newer = <T>(a: CatalogFile<T>, b: CatalogFile<T> | undefined): CatalogFile<T> =>
|
|
270
|
+
b && Date.parse(b.fetchedAt) > Date.parse(a.fetchedAt) ? b : a;
|
|
271
|
+
|
|
272
|
+
function combine(base: CatalogSources, cached: Partial<CatalogSources>): CatalogSources {
|
|
273
|
+
const huggingFace = newer(base.huggingFace, cached.huggingFace);
|
|
274
|
+
const modelsDev = newer(base.modelsDev, cached.modelsDev);
|
|
275
|
+
return {
|
|
276
|
+
refreshed: modelsDev !== base.modelsDev,
|
|
277
|
+
modelsDev,
|
|
278
|
+
openRouter: newer(base.openRouter, cached.openRouter),
|
|
279
|
+
// Counts never go stale, and each copy may hold repos the other lacks.
|
|
280
|
+
huggingFace: { ...huggingFace, payload: { ...base.huggingFace.payload, ...cached.huggingFace?.payload } },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** How long a stat result is trusted before the cache files are stat'ed again. */
|
|
285
|
+
const RESTAT_AFTER_MS = 1000;
|
|
286
|
+
let memo: { dir: string; stamp: string; sources: CatalogSources; checkedAt: number } | undefined;
|
|
287
|
+
let pinned: { value: CatalogSources } | undefined;
|
|
288
|
+
|
|
289
|
+
/** Test seam: pin the sources every lookup sees; `undefined` reads the real files again. */
|
|
290
|
+
export function setCatalogSourcesForTest(sources: CatalogSources | undefined): void {
|
|
291
|
+
pinned = sources ? { value: sources } : undefined;
|
|
292
|
+
memo = undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** An empty catalog: every model falls back to the no-catalog rules. */
|
|
296
|
+
export function emptyCatalogSources(): CatalogSources {
|
|
297
|
+
return structuredClone(EMPTY);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Forget the disk memo so a lifecycle boundary sees another extension instance's refresh. */
|
|
301
|
+
export function invalidateCatalogCache(): void {
|
|
302
|
+
// Re-stat on the next load; unchanged files keep the same sources object, so
|
|
303
|
+
// the index memo (keyed by it) still hits.
|
|
304
|
+
if (memo) memo.checkedAt = 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function stampOf(dir: string): string {
|
|
308
|
+
return (Object.values(FILES) as string[])
|
|
309
|
+
.map((file) => {
|
|
310
|
+
try {
|
|
311
|
+
return String(statSync(join(dir, file)).mtimeMs);
|
|
312
|
+
} catch {
|
|
313
|
+
return "-";
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
.join("|");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The sources lookups read: for each, the newer of the bundled and the
|
|
321
|
+
* refreshed copy. The same object is returned until a cache file changes, so
|
|
322
|
+
* callers may memoize what they derive from it.
|
|
323
|
+
*/
|
|
324
|
+
export function loadCatalogSources(stateDir: string): CatalogSources {
|
|
325
|
+
if (pinned) return pinned.value;
|
|
326
|
+
const dir = catalogCacheDir(stateDir);
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
if (memo && memo.dir === dir && now - memo.checkedAt < RESTAT_AFTER_MS) return memo.sources;
|
|
329
|
+
const stamp = stampOf(dir);
|
|
330
|
+
if (memo && memo.dir === dir && memo.stamp === stamp) {
|
|
331
|
+
memo.checkedAt = now;
|
|
332
|
+
return memo.sources;
|
|
333
|
+
}
|
|
334
|
+
const cached: Partial<CatalogSources> = {
|
|
335
|
+
modelsDev: readCatalogFile<ModelsDevPayload>(join(dir, FILES.modelsDev)),
|
|
336
|
+
openRouter: readCatalogFile<OpenRouterPayload>(join(dir, FILES.openRouter)),
|
|
337
|
+
huggingFace: readCatalogFile<ParameterCounts>(join(dir, FILES.huggingFace)),
|
|
338
|
+
};
|
|
339
|
+
const sources = combine(bundledSources(), cached);
|
|
340
|
+
memo = { dir, stamp, sources, checkedAt: now };
|
|
341
|
+
return sources;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const olderThanRefresh = (stamp: string, now: Date): boolean => now.getTime() - Date.parse(stamp) > CATALOG_REFRESH_AFTER_MS;
|
|
345
|
+
|
|
346
|
+
/** The `fetchedAt` stamp at the head of a catalog file, without parsing its payload. */
|
|
347
|
+
function fetchedAtOf(path: string): string | undefined {
|
|
348
|
+
let fd: number | undefined;
|
|
349
|
+
try {
|
|
350
|
+
fd = openSync(path, "r");
|
|
351
|
+
const head = Buffer.alloc(256);
|
|
352
|
+
const read = readSync(fd, head, 0, head.length, 0);
|
|
353
|
+
const stamp = head.subarray(0, read).toString("utf8").match(/^\{\s*"fetchedAt"\s*:\s*"([^"]+)"/)?.[1];
|
|
354
|
+
if (stamp && !Number.isNaN(Date.parse(stamp))) return stamp;
|
|
355
|
+
} catch {
|
|
356
|
+
return undefined;
|
|
357
|
+
} finally {
|
|
358
|
+
if (fd !== undefined) closeSync(fd);
|
|
359
|
+
}
|
|
360
|
+
// Written some other way: the whole file decides.
|
|
361
|
+
return readCatalogFile<unknown>(path)?.fetchedAt;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* When the models.dev copy in use was fetched (the newer of the bundled and
|
|
366
|
+
* the refreshed one, as `loadCatalogSources` picks), read from the files'
|
|
367
|
+
* heads: a session start checks this without parsing the catalogs.
|
|
368
|
+
*/
|
|
369
|
+
export function catalogFetchedAt(stateDir: string): string {
|
|
370
|
+
if (pinned) return pinned.value.modelsDev.fetchedAt;
|
|
371
|
+
const stamps = [fetchedAtOf(join(bundledCatalogDir(), FILES.modelsDev)), fetchedAtOf(join(catalogCacheDir(stateDir), FILES.modelsDev))];
|
|
372
|
+
return stamps.reduce<string>((a, b) => (b && Date.parse(b) > Date.parse(a) ? b : a), EMPTY.modelsDev.fetchedAt);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The last refresh that failed, kept so the next one waits a day. */
|
|
376
|
+
interface RefreshFailure {
|
|
377
|
+
failedAt: string;
|
|
378
|
+
error: string;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function recentFailure(stateDir: string, now: Date): RefreshFailure | undefined {
|
|
382
|
+
const failure = readJsonFile<RefreshFailure>(join(catalogCacheDir(stateDir), FAILURE_FILE));
|
|
383
|
+
const failedAt = typeof failure?.failedAt === "string" ? Date.parse(failure.failedAt) : Number.NaN;
|
|
384
|
+
// A stamp later than now was not written by a refresh; it must not hold refreshes off.
|
|
385
|
+
return !Number.isNaN(failedAt) && failedAt <= now.getTime() && !olderThanRefresh(failure!.failedAt, now) ? failure : undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Whether a refresh should run: the copy in use is over a day old, and no
|
|
390
|
+
* refresh failed within the last day (a failure writes nothing, so without
|
|
391
|
+
* the wait every start would fetch again). Cheap: no catalog is parsed.
|
|
392
|
+
*/
|
|
393
|
+
export function catalogRefreshDue(stateDir: string, now: Date = new Date()): boolean {
|
|
394
|
+
return !pinned && olderThanRefresh(catalogFetchedAt(stateDir), now) && !recentFailure(stateDir, now);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Refresh
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
export type CatalogRefreshOutcome =
|
|
402
|
+
| { status: "fresh" }
|
|
403
|
+
| { status: "refreshed"; models: number; repos: number }
|
|
404
|
+
| { status: "failed"; error: string }
|
|
405
|
+
/** A refresh failed within the last day; the next try waits for the day to pass. */
|
|
406
|
+
| { status: "backing-off"; error: string };
|
|
407
|
+
|
|
408
|
+
const inflight = new Map<string, Promise<CatalogRefreshOutcome>>();
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Fetch both catalogs and any parameter counts not yet known, into the cache,
|
|
412
|
+
* when the copy in use is older than a day and no refresh failed within the
|
|
413
|
+
* last day (`force` skips both checks). Never throws; a failure leaves the
|
|
414
|
+
* cache as it was and is remembered for a day, unless `signal` cancelled it.
|
|
415
|
+
* Every fetch, bodies included, is bounded by `CATALOG_FETCH_TIMEOUT_MS`. One
|
|
416
|
+
* fetch per state dir at a time.
|
|
417
|
+
*/
|
|
418
|
+
export async function refreshModelCatalog(options: {
|
|
419
|
+
stateDir: string;
|
|
420
|
+
piProviders: Iterable<string>;
|
|
421
|
+
fetchImpl?: typeof fetch;
|
|
422
|
+
now?: Date;
|
|
423
|
+
force?: boolean;
|
|
424
|
+
signal?: AbortSignal;
|
|
425
|
+
}): Promise<CatalogRefreshOutcome> {
|
|
426
|
+
const { stateDir, now = new Date(), force = false } = options;
|
|
427
|
+
// A pinned catalog (tests) is never replaced from the network.
|
|
428
|
+
if (pinned) return { status: "fresh" };
|
|
429
|
+
if (!force) {
|
|
430
|
+
if (!olderThanRefresh(catalogFetchedAt(stateDir), now)) return { status: "fresh" };
|
|
431
|
+
const failure = recentFailure(stateDir, now);
|
|
432
|
+
if (failure) return { status: "backing-off", error: failure.error };
|
|
433
|
+
}
|
|
434
|
+
const running = inflight.get(stateDir);
|
|
435
|
+
if (running) return running;
|
|
436
|
+
const attempt = fetchCatalog(options, now).finally(() => inflight.delete(stateDir));
|
|
437
|
+
inflight.set(stateDir, attempt);
|
|
438
|
+
return attempt;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function fetchJson(url: string, fetchImpl: typeof fetch | undefined, signal: AbortSignal | undefined): Promise<unknown> {
|
|
442
|
+
// models.dev answers a bare client with 403; a browser User-Agent is accepted.
|
|
443
|
+
const init: RequestInit = { headers: { accept: "application/json", "user-agent": "Mozilla/5.0 (compatible; one-code-model-catalog)" } };
|
|
444
|
+
return fetchWithTimeout(
|
|
445
|
+
url,
|
|
446
|
+
CATALOG_FETCH_TIMEOUT_MS,
|
|
447
|
+
async (response) => {
|
|
448
|
+
if (!response.ok) throw new Error(`${new URL(url).host} responded ${response.status}`);
|
|
449
|
+
return (await response.json()) as unknown;
|
|
450
|
+
},
|
|
451
|
+
{ init, fetchImpl, signal },
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function fetchCatalog(
|
|
456
|
+
options: { stateDir: string; piProviders: Iterable<string>; fetchImpl?: typeof fetch; signal?: AbortSignal },
|
|
457
|
+
now: Date,
|
|
458
|
+
): Promise<CatalogRefreshOutcome> {
|
|
459
|
+
const { stateDir, piProviders, fetchImpl, signal } = options;
|
|
460
|
+
const dir = catalogCacheDir(stateDir);
|
|
461
|
+
const fail = (error: string): CatalogRefreshOutcome => {
|
|
462
|
+
// Cancelled by the caller (a session ending) is not the network failing.
|
|
463
|
+
if (!signal?.aborted) {
|
|
464
|
+
try {
|
|
465
|
+
writeJsonAtomic(join(dir, FAILURE_FILE), { failedAt: now.toISOString(), error } satisfies RefreshFailure);
|
|
466
|
+
} catch {
|
|
467
|
+
// Unwritable: the next start tries again, as before.
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return { status: "failed", error };
|
|
471
|
+
};
|
|
472
|
+
try {
|
|
473
|
+
const [modelsDevBody, openRouterBody] = await Promise.all([fetchJson(MODELS_DEV_URL, fetchImpl, signal), fetchJson(OPENROUTER_MODELS_URL, fetchImpl, signal)]);
|
|
474
|
+
const modelsDev = trimModelsDev(modelsDevBody, piProviders);
|
|
475
|
+
const openRouter = trimOpenRouter(openRouterBody);
|
|
476
|
+
const rows = Object.values(modelsDev).reduce((sum, provider) => sum + Object.keys(provider.models).length, 0);
|
|
477
|
+
if (rows === 0 || openRouter.data.length === 0) return fail("a model catalog came back empty");
|
|
478
|
+
const fetchedAt = now.toISOString();
|
|
479
|
+
const known = loadCatalogSources(stateDir).huggingFace.payload;
|
|
480
|
+
const counts: ParameterCounts = { ...readCatalogFile<ParameterCounts>(join(dir, FILES.huggingFace))?.payload };
|
|
481
|
+
const lookups = unknownRepos(openRouter, known).slice(0, HUGGING_FACE_LOOKUPS_PER_REFRESH);
|
|
482
|
+
const found = await Promise.all(lookups.map((repo) => fetchParameterCount(repo, fetchImpl, signal)));
|
|
483
|
+
if (signal?.aborted) throw signal.reason;
|
|
484
|
+
lookups.forEach((repo, index) => {
|
|
485
|
+
if (found[index] !== undefined) counts[repo] = found[index]!;
|
|
486
|
+
});
|
|
487
|
+
// models.dev last: its stamp is what marks the copy fresh, so a write
|
|
488
|
+
// that fails before it leaves the copy stale and retried, never a
|
|
489
|
+
// fresh-looking mix of old and new.
|
|
490
|
+
writeJsonAtomic(join(dir, FILES.openRouter), { fetchedAt, source: OPENROUTER_MODELS_URL, payload: openRouter });
|
|
491
|
+
writeJsonAtomic(join(dir, FILES.huggingFace), { fetchedAt, source: HUGGING_FACE_MODELS_URL, payload: counts });
|
|
492
|
+
writeJsonAtomic(join(dir, FILES.modelsDev), { fetchedAt, source: MODELS_DEV_URL, payload: modelsDev });
|
|
493
|
+
rmSync(join(dir, FAILURE_FILE), { force: true });
|
|
494
|
+
memo = undefined;
|
|
495
|
+
return { status: "refreshed", models: rows, repos: lookups.length };
|
|
496
|
+
} catch (error) {
|
|
497
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Whether the daily refresh may run: not with pi's `PI_OFFLINE` set, and not
|
|
503
|
+
* when the user turned it off in `~/.onecode/settings.json`.
|
|
504
|
+
*/
|
|
505
|
+
export function catalogRefreshEnabled(settings: unknown, env: Record<string, string | undefined> = process.env): boolean {
|
|
506
|
+
if (/^(1|true|yes)$/i.test(String(env.PI_OFFLINE ?? "").trim())) return false;
|
|
507
|
+
return record(settings)?.refreshModelCatalog !== false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** The settings file reader the refresh switch uses, for callers without one. */
|
|
511
|
+
export function readCatalogRefreshEnabled(settingsPath: string, env: Record<string, string | undefined> = process.env): boolean {
|
|
512
|
+
return catalogRefreshEnabled(readJsonFile(settingsPath), env);
|
|
513
|
+
}
|