one-code-extension 0.1.4 → 0.3.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/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts about models that pi's catalog strips: release date and tool-calling
|
|
3
|
+
* support, from models.dev, bundled as `model-facts.generated.json` and refreshed
|
|
4
|
+
* with each pi bump (`scripts/gen-model-facts.mjs`). Pure: reads one
|
|
5
|
+
* JSON file lazily, no pi imports.
|
|
6
|
+
*
|
|
7
|
+
* Why release dates: the tier price floors are absolute dollars calibrated on US
|
|
8
|
+
* vendor prices, and nothing else in the classifier knew a model's generation,
|
|
9
|
+
* so on a ~10x-cheaper vendor a boundary-priced 2025 model (deepseek-r1-0528)
|
|
10
|
+
* beat a 2026 model priced below the floor (V4 Flash). A model's *generation* is
|
|
11
|
+
* measured against its vendor family's newest release, never against today, so
|
|
12
|
+
* a family that ships rarely is not aged by the calendar.
|
|
13
|
+
* docs/features/tiering/plan.md (Phase 1), docs/decisions/model-tiers.md.
|
|
14
|
+
*
|
|
15
|
+
* Lookups resolve OpenRouter's `~vendor/id` redirect aliases and `:variant`
|
|
16
|
+
* endpoints through their base id, and — since 2026-09-11 — BORROW across
|
|
17
|
+
* providers: models.dev has no `openai-codex` (or `azure-*`) provider entries,
|
|
18
|
+
* so `openai-codex/gpt-5.6-terra` had no facts while `openai/gpt-5.6-terra` did,
|
|
19
|
+
* which left every Codex row unscored by the measured floor and judged by name
|
|
20
|
+
* anchors alone. A row without its own facts takes the release date of the same
|
|
21
|
+
* model (same canonical vendor, same normalized id) on any other provider,
|
|
22
|
+
* provided every such row agrees on the date — a disagreement (OpenRouter's
|
|
23
|
+
* April `deepseek-v4-flash` vs DeepSeek's July one) borrows nothing. Only the
|
|
24
|
+
* date is borrowed, never the tool-call flag. A row models.dev does not know
|
|
25
|
+
* anywhere has no facts and gets the price-and-name behaviour that preceded
|
|
26
|
+
* this module. docs/decisions/model-tiers.md (2026-09-11).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
31
|
+
import { baseModelId, DAY_MS, isAliasOrVariantId, modelIdentity } from "./model-policy.ts";
|
|
32
|
+
|
|
33
|
+
export interface ModelFactsRow {
|
|
34
|
+
/** ISO date (YYYY-MM-DD) the model was released, per models.dev. */
|
|
35
|
+
releaseDate: string;
|
|
36
|
+
/** Present only when models.dev marks the model as NOT supporting tool calls. */
|
|
37
|
+
toolCall?: false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface FactsFile {
|
|
41
|
+
facts: Record<string, ModelFactsRow>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A model released this many days before its family's newest is one generation behind. */
|
|
45
|
+
export const PRIOR_GENERATION_DAYS = 365;
|
|
46
|
+
/** …and this many days behind is two generations: maximum scaffolding, never auto-selected. */
|
|
47
|
+
export const ANCIENT_GENERATION_DAYS = 730;
|
|
48
|
+
|
|
49
|
+
let loaded:
|
|
50
|
+
| {
|
|
51
|
+
facts: Record<string, ModelFactsRow>;
|
|
52
|
+
familyNewest: Map<string, number>;
|
|
53
|
+
/** `vendor|normalizedId` → the agreed release date, or null when providers disagree. */
|
|
54
|
+
borrowed: Map<string, ModelFactsRow | null>;
|
|
55
|
+
}
|
|
56
|
+
| undefined;
|
|
57
|
+
|
|
58
|
+
/** Test seam: replace the bundled table (pass `undefined` to restore it). */
|
|
59
|
+
export function setModelFactsForTest(facts: Record<string, ModelFactsRow> | undefined): void {
|
|
60
|
+
loaded = facts ? index(facts) : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function load(): NonNullable<typeof loaded> {
|
|
64
|
+
if (loaded) return loaded;
|
|
65
|
+
let facts: Record<string, ModelFactsRow> = {};
|
|
66
|
+
try {
|
|
67
|
+
const file = JSON.parse(readFileSync(new URL("./model-facts.generated.json", import.meta.url), "utf8")) as FactsFile;
|
|
68
|
+
if (file && typeof file.facts === "object") facts = file.facts;
|
|
69
|
+
} catch {
|
|
70
|
+
// A missing or unreadable table means "no facts": every row degrades to the
|
|
71
|
+
// pre-facts price-and-name behaviour rather than throwing inside selection.
|
|
72
|
+
}
|
|
73
|
+
loaded = index(facts);
|
|
74
|
+
return loaded;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The vendor family a row's generation is judged within: the canonical vendor
|
|
79
|
+
* when the identity policy knows it (so `openrouter/deepseek/…` and the direct
|
|
80
|
+
* `deepseek` provider share one family), else the containment key.
|
|
81
|
+
*/
|
|
82
|
+
export function modelFamily(model: { provider: string; id: string }): string {
|
|
83
|
+
const identity = modelIdentity(model as Model<Api>);
|
|
84
|
+
return identity.profile ?? identity.containment;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function index(facts: Record<string, ModelFactsRow>) {
|
|
88
|
+
const familyNewest = new Map<string, number>();
|
|
89
|
+
const borrowed = new Map<string, ModelFactsRow | null>();
|
|
90
|
+
for (const [key, row] of Object.entries(facts)) {
|
|
91
|
+
const slash = key.indexOf("/");
|
|
92
|
+
if (slash <= 0) continue;
|
|
93
|
+
const model = { provider: key.slice(0, slash), id: key.slice(slash + 1) };
|
|
94
|
+
// Variants and redirect aliases carry no generation of their own.
|
|
95
|
+
if (isAliasOrVariantId(model.id)) continue;
|
|
96
|
+
const t = Date.parse(row.releaseDate);
|
|
97
|
+
if (Number.isNaN(t)) continue;
|
|
98
|
+
const family = modelFamily(model);
|
|
99
|
+
if (t > (familyNewest.get(family) ?? -Infinity)) familyNewest.set(family, t);
|
|
100
|
+
const lend = borrowKey(model);
|
|
101
|
+
if (!lend) continue;
|
|
102
|
+
const prior = borrowed.get(lend);
|
|
103
|
+
if (prior === undefined) borrowed.set(lend, { releaseDate: row.releaseDate });
|
|
104
|
+
else if (prior !== null && prior.releaseDate !== row.releaseDate) borrowed.set(lend, null);
|
|
105
|
+
}
|
|
106
|
+
return { facts, familyNewest, borrowed };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The cross-provider identity a row may lend its release date under, or undefined for an unverifiable one. */
|
|
110
|
+
function borrowKey(model: { provider: string; id: string }): string | undefined {
|
|
111
|
+
const identity = modelIdentity(model as Model<Api>);
|
|
112
|
+
if (identity.confidence === "opaque" || !identity.profile) return undefined;
|
|
113
|
+
return `${identity.profile}|${baseModelId(identity.normalizedId)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The models.dev row for a model, resolved through alias and variant spellings,
|
|
118
|
+
* else borrowed from the same model on another provider (module doc).
|
|
119
|
+
*/
|
|
120
|
+
export function modelFacts(model: { provider: string; id: string }): ModelFactsRow | undefined {
|
|
121
|
+
const { facts, borrowed } = load();
|
|
122
|
+
const own = facts[`${model.provider}/${model.id}`] ?? facts[`${model.provider}/${baseModelId(model.id)}`];
|
|
123
|
+
if (own) return own;
|
|
124
|
+
const key = borrowKey(model);
|
|
125
|
+
return (key && borrowed.get(key)) || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Whether models.dev says the model cannot call tools (undefined = unknown or yes). */
|
|
129
|
+
export function lacksToolCalls(model: { provider: string; id: string }): boolean {
|
|
130
|
+
return modelFacts(model)?.toolCall === false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* How many days behind its family's newest release the model is, or undefined
|
|
135
|
+
* when either date is unknown. 0 for the newest model itself.
|
|
136
|
+
*/
|
|
137
|
+
export function generationLagDays(model: { provider: string; id: string }): number | undefined {
|
|
138
|
+
const row = modelFacts(model);
|
|
139
|
+
if (!row) return undefined;
|
|
140
|
+
const released = Date.parse(row.releaseDate);
|
|
141
|
+
if (Number.isNaN(released)) return undefined;
|
|
142
|
+
const newest = load().familyNewest.get(modelFamily(model));
|
|
143
|
+
if (newest === undefined) return undefined;
|
|
144
|
+
return Math.max(0, Math.round((newest - released) / DAY_MS));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type Generation = "current" | "prior" | "ancient";
|
|
148
|
+
|
|
149
|
+
/** The model's generation relative to its family, or undefined without facts. */
|
|
150
|
+
export function modelGeneration(model: { provider: string; id: string }): Generation | undefined {
|
|
151
|
+
const lag = generationLagDays(model);
|
|
152
|
+
if (lag === undefined) return undefined;
|
|
153
|
+
if (lag > ANCIENT_GENERATION_DAYS) return "ancient";
|
|
154
|
+
if (lag > PRIOR_GENERATION_DAYS) return "prior";
|
|
155
|
+
return "current";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Prior or ancient: excluded from every automatic pick (subagent, classifier, reader, presets). */
|
|
159
|
+
export function isPriorGeneration(model: { provider: string; id: string }): boolean {
|
|
160
|
+
const generation = modelGeneration(model);
|
|
161
|
+
return generation === "prior" || generation === "ancient";
|
|
162
|
+
}
|
|
@@ -1,112 +1,15 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
/** The two automatic model-selection jobs have different capability floors. */
|
|
4
|
-
export type ModelRole = "classifier" | "subagent";
|
|
5
|
-
|
|
6
|
-
export interface RoleProfile {
|
|
7
|
-
/** Vetted, small-but-capable models. Price alone never adds to this list. */
|
|
8
|
-
classifier: readonly string[];
|
|
9
|
-
/** Economical models suitable for delegated coding/reasoning work. */
|
|
10
|
-
subagent: readonly string[];
|
|
11
|
-
}
|
|
1
|
+
import { type Api, clampThinkingLevel, type Model, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
12
2
|
|
|
13
3
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
4
|
+
* Whether a session runs on a Claude-family model. Claude Code's own subagent
|
|
5
|
+
* conventions (`CLAUDE_CODE_SUBAGENT_MODEL`, `.claude/agents` model fields) are
|
|
6
|
+
* honored across providers only on such sessions; elsewhere subagents stay on
|
|
7
|
+
* the session's provider. Only provider/id are consulted, so a minimal model
|
|
8
|
+
* shape suffices.
|
|
17
9
|
*/
|
|
18
|
-
export
|
|
19
|
-
anthropic
|
|
20
|
-
|
|
21
|
-
subagent: ["claude-sonnet-5", "claude-haiku-4-5", "claude-sonnet-4-6"],
|
|
22
|
-
},
|
|
23
|
-
openai: {
|
|
24
|
-
classifier: ["gpt-5.6-luna", "gpt-5.4-mini", "gpt-5-mini", "gpt-4.1-mini", "gpt-4o-mini"],
|
|
25
|
-
subagent: ["gpt-5.6-luna", "gpt-5.4-mini", "gpt-5.3-codex-spark", "gpt-5.6-terra"],
|
|
26
|
-
},
|
|
27
|
-
google: {
|
|
28
|
-
classifier: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3-flash-preview", "gemini-2.5-flash"],
|
|
29
|
-
subagent: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3-flash-preview", "gemini-2.5-flash"],
|
|
30
|
-
},
|
|
31
|
-
xai: {
|
|
32
|
-
classifier: ["grok-4.3", "grok-4.5"],
|
|
33
|
-
subagent: ["grok-build-0.1", "grok-4.3", "grok-4.5"],
|
|
34
|
-
},
|
|
35
|
-
mistral: {
|
|
36
|
-
classifier: ["mistral-small-2603", "mistral-small-latest", "mistral-medium-3.5"],
|
|
37
|
-
subagent: ["mistral-small-2603", "mistral-medium-3.5", "codestral-latest"],
|
|
38
|
-
},
|
|
39
|
-
deepseek: {
|
|
40
|
-
classifier: ["deepseek-v4-flash", "deepseek-v4-pro"],
|
|
41
|
-
subagent: ["deepseek-v4-flash", "deepseek-v4-pro"],
|
|
42
|
-
},
|
|
43
|
-
zai: {
|
|
44
|
-
classifier: ["glm-5-turbo", "glm-4.7", "glm-5.2", "glm-4.5-air", "glm-4-flash"],
|
|
45
|
-
subagent: ["glm-5-turbo", "glm-4.7", "glm-5.2", "glm-4.5-air"],
|
|
46
|
-
},
|
|
47
|
-
moonshot: {
|
|
48
|
-
classifier: ["kimi-k2.6", "kimi-k2.5"],
|
|
49
|
-
subagent: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k3"],
|
|
50
|
-
},
|
|
51
|
-
minimax: {
|
|
52
|
-
classifier: ["MiniMax-M2.7", "MiniMax-M3"],
|
|
53
|
-
subagent: ["MiniMax-M2.7", "MiniMax-M3"],
|
|
54
|
-
},
|
|
55
|
-
xiaomi: {
|
|
56
|
-
classifier: ["mimo-v2.5", "mimo-v2-pro"],
|
|
57
|
-
subagent: ["mimo-v2.5", "mimo-v2.5-pro", "mimo-v2-pro"],
|
|
58
|
-
},
|
|
59
|
-
"ant-ling": {
|
|
60
|
-
// The current pi catalog predates Ling 3.0. Flash remains opt-in: it has
|
|
61
|
-
// not been calibrated as the approval boundary.
|
|
62
|
-
classifier: ["Ling-2.6-1T", "Ring-2.6-1T"],
|
|
63
|
-
subagent: ["Ling-2.6-1T", "Ring-2.6-1T", "Ling-2.6-flash"],
|
|
64
|
-
},
|
|
65
|
-
meta: {
|
|
66
|
-
classifier: ["llama-3.3-70b", "llama-4-scout"],
|
|
67
|
-
subagent: ["llama-4-scout", "llama-3.3-70b"],
|
|
68
|
-
},
|
|
69
|
-
amazon: {
|
|
70
|
-
classifier: ["nova-2-lite", "nova-lite"],
|
|
71
|
-
subagent: ["nova-2-lite", "nova-lite", "nova-pro"],
|
|
72
|
-
},
|
|
73
|
-
"host:nvidia": {
|
|
74
|
-
classifier: ["nvidia/nvidia-nemotron-nano-9b-v2", "openai/gpt-oss-20b", "meta/llama-3.3-70b-instruct"],
|
|
75
|
-
subagent: ["poolside/laguna-xs-2.1", "nvidia/nemotron-3-super-120b-a12b", "openai/gpt-oss-20b"],
|
|
76
|
-
},
|
|
77
|
-
"host:groq": {
|
|
78
|
-
classifier: ["openai/gpt-oss-20b", "llama-3.3-70b-versatile"],
|
|
79
|
-
subagent: ["openai/gpt-oss-20b", "openai/gpt-oss-120b", "llama-3.3-70b-versatile"],
|
|
80
|
-
},
|
|
81
|
-
"host:cerebras": {
|
|
82
|
-
classifier: ["gpt-oss-120b", "zai-glm-4.7"],
|
|
83
|
-
subagent: ["gpt-oss-120b", "zai-glm-4.7"],
|
|
84
|
-
},
|
|
85
|
-
"host:fireworks": {
|
|
86
|
-
classifier: ["accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/gpt-oss-20b"],
|
|
87
|
-
subagent: ["accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/gpt-oss-20b"],
|
|
88
|
-
},
|
|
89
|
-
"host:together": {
|
|
90
|
-
classifier: ["Qwen/Qwen3.5-9B", "openai/gpt-oss-20b"],
|
|
91
|
-
subagent: ["openai/gpt-oss-20b", "moonshotai/Kimi-K2.7-Code", "Qwen/Qwen3.5-9B"],
|
|
92
|
-
},
|
|
93
|
-
"host:baseten": {
|
|
94
|
-
classifier: ["openai/gpt-oss-120b", "deepseek-ai/DeepSeek-V4-Flash"],
|
|
95
|
-
subagent: ["openai/gpt-oss-120b", "zai-org/GLM-5.2", "moonshotai/Kimi-K2.7-Code"],
|
|
96
|
-
},
|
|
97
|
-
"host:cloudflare-workers": {
|
|
98
|
-
classifier: ["@cf/openai/gpt-oss-20b", "@cf/zai-org/glm-4.7-flash"],
|
|
99
|
-
subagent: ["@cf/moonshotai/kimi-k2.7-code", "@cf/openai/gpt-oss-20b"],
|
|
100
|
-
},
|
|
101
|
-
"host:qwen-plan": {
|
|
102
|
-
classifier: ["qwen3.6-flash", "deepseek-v4-flash", "qwen3.6-plus"],
|
|
103
|
-
subagent: ["kimi-k2.7-code", "deepseek-v4-flash", "qwen3.7-plus", "qwen3.6-plus"],
|
|
104
|
-
},
|
|
105
|
-
"host:kimi-coding": {
|
|
106
|
-
classifier: ["kimi-for-coding", "k3"],
|
|
107
|
-
subagent: ["kimi-for-coding", "k3"],
|
|
108
|
-
},
|
|
109
|
-
};
|
|
10
|
+
export function isClaudeFamilyModel(model: { provider: string; id: string }): boolean {
|
|
11
|
+
return model.provider === "anthropic" || /claude/i.test(model.id);
|
|
12
|
+
}
|
|
110
13
|
|
|
111
14
|
export type ProviderPolicyKind = "direct" | "hosted" | "gateway" | "opaque";
|
|
112
15
|
|
|
@@ -114,14 +17,12 @@ export interface ProviderPolicy {
|
|
|
114
17
|
kind: ProviderPolicyKind;
|
|
115
18
|
/** Fixed profile for direct vendors and stable hosted catalogs. */
|
|
116
19
|
profile?: string;
|
|
117
|
-
/** Whether a price-ranked subagent may follow the vetted profile. */
|
|
118
|
-
dynamicSubagents: boolean;
|
|
119
20
|
}
|
|
120
21
|
|
|
121
|
-
const direct = (profile: string): ProviderPolicy => ({ kind: "direct", profile
|
|
122
|
-
const hosted = (profile?: string): ProviderPolicy => ({ kind: "hosted", profile
|
|
123
|
-
const gateway = (): ProviderPolicy => ({ kind: "gateway"
|
|
124
|
-
const opaque = (): ProviderPolicy => ({ kind: "opaque"
|
|
22
|
+
const direct = (profile: string): ProviderPolicy => ({ kind: "direct", profile });
|
|
23
|
+
const hosted = (profile?: string): ProviderPolicy => ({ kind: "hosted", profile });
|
|
24
|
+
const gateway = (): ProviderPolicy => ({ kind: "gateway" });
|
|
25
|
+
const opaque = (): ProviderPolicy => ({ kind: "opaque" });
|
|
125
26
|
|
|
126
27
|
/**
|
|
127
28
|
* Every built-in language-model provider in pi 0.84.0. Unknown/custom providers
|
|
@@ -301,38 +202,96 @@ export function modelIdentity(model: Model<Api>): ModelIdentity {
|
|
|
301
202
|
};
|
|
302
203
|
}
|
|
303
204
|
|
|
205
|
+
const UNSUITABLE_VARIANT = /:(batch|free|online|thinking)$/i;
|
|
206
|
+
|
|
304
207
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
208
|
+
* The id without OpenRouter's `~` redirect-alias marker and without a
|
|
209
|
+
* `:batch`-style endpoint variant — the spelling the name rules, the facts
|
|
210
|
+
* table and the capability index all key on (`~deepseek/deepseek-v4-flash-latest`
|
|
211
|
+
* → `deepseek/deepseek-v4-flash-latest`, `gpt-5-mini:batch` → `gpt-5-mini`).
|
|
309
212
|
*/
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
/** Index into NAME_HINTS; NAME_HINTS.length means "no known small-model name". */
|
|
313
|
-
export function hintRank(model: Model<Api>): number {
|
|
314
|
-
const index = NAME_HINTS.findIndex((pattern) => pattern.test(model.id));
|
|
315
|
-
return index === -1 ? NAME_HINTS.length : index;
|
|
213
|
+
export function baseModelId(id: string): string {
|
|
214
|
+
return (id.startsWith("~") ? id.slice(1) : id).replace(/:[a-z]+$/i, "");
|
|
316
215
|
}
|
|
317
216
|
|
|
318
|
-
/** Whether the
|
|
319
|
-
export function
|
|
320
|
-
return
|
|
217
|
+
/** Whether the id carries a redirect marker or endpoint variant (`baseModelId` would change it). */
|
|
218
|
+
export function isAliasOrVariantId(id: string): boolean {
|
|
219
|
+
return baseModelId(id) !== id;
|
|
321
220
|
}
|
|
322
221
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Automatic selection excludes endpoint variants with the wrong execution shape,
|
|
224
|
+
* and OpenRouter's `~vendor/family-latest` redirect aliases ("always redirects to
|
|
225
|
+
* the latest model in the family"): the model behind one moves without notice,
|
|
226
|
+
* so it has no stable identity to tier, date or price — and the classifier is a
|
|
227
|
+
* permission boundary. The alias stays available by hand (/model, an explicit
|
|
228
|
+
* `subagentModel` / `classifierModel` setting).
|
|
229
|
+
*/
|
|
326
230
|
export function isSelectableVariant(model: Model<Api>): boolean {
|
|
327
|
-
return !UNSUITABLE_VARIANT.test(model.id);
|
|
231
|
+
return !UNSUITABLE_VARIANT.test(model.id) && !model.id.startsWith("~");
|
|
328
232
|
}
|
|
329
233
|
|
|
234
|
+
/** One day in milliseconds, for the release-date arithmetic in model-facts.ts and capability-index.ts. */
|
|
235
|
+
export const DAY_MS = 86_400_000;
|
|
236
|
+
|
|
330
237
|
/** Non-positive values are catalog sentinels/unpriced, not evidence of being free. */
|
|
331
238
|
export function pricedInput(model: Model<Api>): number | undefined {
|
|
332
239
|
const input = model.cost?.input;
|
|
333
240
|
return typeof input === "number" && input > 0 ? input : undefined;
|
|
334
241
|
}
|
|
335
242
|
|
|
243
|
+
/** A dated snapshot id (`claude-haiku-4-5-20251001`): the trailing `-YYYYMMDD` marks a pinned alias of an undated model. */
|
|
244
|
+
export function isDatedModelId(id: string): boolean {
|
|
245
|
+
return /-20\d{6}$/.test(id);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The id with a trailing snapshot date removed: `-YYYYMMDD` (Anthropic, OpenAI)
|
|
250
|
+
* or `-MMDD` (DeepSeek's `deepseek-v4-flash-0731`, `deepseek-v4-pro-0813`; the
|
|
251
|
+
* four digits must be a real month and day, so a `-2411`-style version tag is
|
|
252
|
+
* left alone). The id itself when it carries no such suffix.
|
|
253
|
+
*/
|
|
254
|
+
export function stripSnapshotDate(id: string): string {
|
|
255
|
+
if (isDatedModelId(id)) return id.slice(0, -9);
|
|
256
|
+
const short = id.match(/-(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/);
|
|
257
|
+
return short ? id.slice(0, -5) : id;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Whether the id ends in a snapshot date of either shape (`stripSnapshotDate` would change it). */
|
|
261
|
+
export function isSnapshotDatedId(id: string): boolean {
|
|
262
|
+
return stripSnapshotDate(id) !== id;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether `model` is a dated snapshot whose undated alias is also present in
|
|
267
|
+
* `pool` — the row a listing collapses so the same model is not offered twice.
|
|
268
|
+
* Exact: the alias must be the id minus its date, so `deepseek-v4-flash-vision-exp`
|
|
269
|
+
* is never read as a snapshot of `deepseek-v4-flash`.
|
|
270
|
+
*/
|
|
271
|
+
export function isDatedDuplicate(model: { id: string }, pool: ReadonlyArray<{ id: string }>): boolean {
|
|
272
|
+
const alias = stripSnapshotDate(model.id);
|
|
273
|
+
return alias !== model.id && pool.some((other) => other.id === alias);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The canonical `provider/id` spec — one source of truth for the string form. */
|
|
277
|
+
export function modelSpec(model: { provider: string; id: string }): string {
|
|
278
|
+
return `${model.provider}/${model.id}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Whether a stamped-default setting was set for a provider this session has since left. */
|
|
282
|
+
export function isStaleContainmentStamp(
|
|
283
|
+
setForContainment: string | undefined,
|
|
284
|
+
sessionModel: { provider: string; id: string } | undefined,
|
|
285
|
+
): boolean {
|
|
286
|
+
return setForContainment !== (sessionModel ? modelIdentity(sessionModel as Model<Api>).containment : undefined);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Whether an explicit model leaves the session's provider/route/family boundary. */
|
|
290
|
+
export function crossesProvider(model: Model<Api>, sessionModel: Model<Api>): boolean {
|
|
291
|
+
if (model.provider !== sessionModel.provider) return true;
|
|
292
|
+
return modelIdentity(model).containment !== modelIdentity(sessionModel).containment;
|
|
293
|
+
}
|
|
294
|
+
|
|
336
295
|
/** Models that automatic selection may consider without changing containment. */
|
|
337
296
|
export function modelsContainedToSession(available: Model<Api>[], sessionModel: Model<Api>): Model<Api>[] {
|
|
338
297
|
const sessionIdentity = modelIdentity(sessionModel);
|
|
@@ -344,74 +303,71 @@ export function modelsContainedToSession(available: Model<Api>[], sessionModel:
|
|
|
344
303
|
);
|
|
345
304
|
}
|
|
346
305
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
306
|
+
/**
|
|
307
|
+
* The thinking level to send on a one-shot `completeSimple` call that would
|
|
308
|
+
* rather run with thinking OFF (classifier, setup drafting, recap, readers).
|
|
309
|
+
*
|
|
310
|
+
* `undefined` = the model supports off — omit `reasoning`, pi disables thinking
|
|
311
|
+
* explicitly, the request stays exactly as before. A level = the model cannot
|
|
312
|
+
* disable thinking (catalog `thinkingLevelMap.off: null`, e.g. Gemini 3.6+
|
|
313
|
+
* Flash); omitting `reasoning` makes the provider 400 ("Reasoning is mandatory
|
|
314
|
+
* for this endpoint and cannot be disabled"), so send the lowest level the
|
|
315
|
+
* model supports instead.
|
|
316
|
+
*/
|
|
317
|
+
export function forcedReasoningLevel(model: Model<Api>): ThinkingLevel | undefined {
|
|
318
|
+
const clamped = clampThinkingLevel(model, "off");
|
|
319
|
+
return clamped === "off" ? undefined : (clamped as ThinkingLevel);
|
|
355
320
|
}
|
|
356
321
|
|
|
357
|
-
/**
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
precomputedContained?: Model<Api>[],
|
|
366
|
-
): Model<Api> | undefined {
|
|
367
|
-
const identity = modelIdentity(sessionModel);
|
|
368
|
-
if (!identity.profile) return undefined;
|
|
369
|
-
const prefixes = ROLE_PROFILES[identity.profile]?.[role] ?? [];
|
|
370
|
-
const contained = precomputedContained ?? modelsContainedToSession(available, sessionModel);
|
|
371
|
-
for (const prefix of prefixes) {
|
|
372
|
-
const wanted = comparableId(prefix);
|
|
373
|
-
const exact = contained.find(
|
|
374
|
-
(model) => comparableId(modelIdentity(model).normalizedId) === wanted && accept(model),
|
|
375
|
-
);
|
|
376
|
-
if (exact) return exact;
|
|
377
|
-
const prefixed = contained.find((model) => matchesPrefix(model, prefix) && accept(model));
|
|
378
|
-
if (prefixed) return prefixed;
|
|
379
|
-
}
|
|
380
|
-
return undefined;
|
|
322
|
+
/**
|
|
323
|
+
* Provider error saying thinking cannot be disabled — the reactive net for a
|
|
324
|
+
* model whose catalog entry LACKS the `thinkingLevelMap.off: null` marker
|
|
325
|
+
* (OpenRouter entries, hand-added models newer than the bundled catalog).
|
|
326
|
+
* A false positive only costs one retry with minimal thinking.
|
|
327
|
+
*/
|
|
328
|
+
export function isReasoningMandatoryError(message: string): boolean {
|
|
329
|
+
return /(reasoning|thinking)[^.]*\b(mandatory|cannot be disabled|can't be disabled|must be enabled|is required)\b/i.test(message);
|
|
381
330
|
}
|
|
382
331
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
332
|
+
/** The level to retry with after isReasoningMandatoryError: the lowest real level the model supports. */
|
|
333
|
+
export function reasoningRetryLevel(model: Model<Api>): ThinkingLevel {
|
|
334
|
+
const clamped = clampThinkingLevel(model, "minimal");
|
|
335
|
+
return clamped === "off" ? "minimal" : (clamped as ThinkingLevel);
|
|
386
336
|
}
|
|
387
337
|
|
|
388
338
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
339
|
+
* Run a one-shot `completeSimple`-style call that wants thinking OFF, handling
|
|
340
|
+
* the models that can't. The proactive path ({@link forcedReasoningLevel}) sends
|
|
341
|
+
* a level up front for catalog-marked models; the reactive path catches a
|
|
342
|
+
* provider's mandatory-thinking 400 ({@link isReasoningMandatoryError}) for the
|
|
343
|
+
* metadata gaps and retries ONCE at the model's floor. `call` receives the level
|
|
344
|
+
* to send (undefined = omit `reasoning`) and is invoked at most twice.
|
|
345
|
+
*
|
|
346
|
+
* `learned` optionally memoizes the reactive result per `provider/id` across
|
|
347
|
+
* calls, so a repeatedly-called site (the web-fetch reader) pays the failed
|
|
348
|
+
* round-trip once, not every call. Omit it for one-off callers.
|
|
349
|
+
*
|
|
350
|
+
* `onUsage` receives the final reply's `usage` once per invocation — this is the
|
|
351
|
+
* single choke point for the reader-style one-shots (web-fetch, recap, setup),
|
|
352
|
+
* so instrumenting it here reports their cost to the footer without each call
|
|
353
|
+
* site having to remember to.
|
|
394
354
|
*/
|
|
395
|
-
export function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
export function allowsDynamicSubagentSelection(sessionModel: Model<Api>): boolean {
|
|
412
|
-
const policy = providerPolicy(sessionModel.provider);
|
|
413
|
-
if (!policy.dynamicSubagents) return false;
|
|
414
|
-
return modelIdentity(sessionModel).confidence !== "opaque";
|
|
355
|
+
export async function withReasoningFallback<R extends { stopReason: string; errorMessage?: string; usage?: unknown }>(
|
|
356
|
+
model: Model<Api>,
|
|
357
|
+
call: (reasoning: ThinkingLevel | undefined) => Promise<R>,
|
|
358
|
+
learned?: Map<string, ThinkingLevel>,
|
|
359
|
+
onUsage?: (usage: unknown) => void,
|
|
360
|
+
): Promise<R> {
|
|
361
|
+
const key = `${model.provider}/${model.id}`;
|
|
362
|
+
const reasoning = learned?.get(key) ?? forcedReasoningLevel(model);
|
|
363
|
+
let result = await call(reasoning);
|
|
364
|
+
if (result.stopReason === "error" && !reasoning && isReasoningMandatoryError(result.errorMessage ?? "")) {
|
|
365
|
+
const level = reasoningRetryLevel(model);
|
|
366
|
+
learned?.set(key, level);
|
|
367
|
+
result = await call(level);
|
|
368
|
+
}
|
|
369
|
+
onUsage?.(result.usage);
|
|
370
|
+
return result;
|
|
415
371
|
}
|
|
416
372
|
|
|
417
373
|
/** Resolve an explicit provider/id, bare id, or prefix. Explicit choices are not contained. */
|