omegon 0.6.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/.gitattributes +3 -0
- package/AGENTS.md +16 -0
- package/LICENSE +15 -0
- package/README.md +289 -0
- package/bin/pi.mjs +30 -0
- package/extensions/00-secrets/index.ts +1126 -0
- package/extensions/01-auth/auth.ts +401 -0
- package/extensions/01-auth/index.ts +289 -0
- package/extensions/auto-compact.ts +42 -0
- package/extensions/bootstrap/deps.ts +291 -0
- package/extensions/bootstrap/index.ts +811 -0
- package/extensions/chronos/chronos.sh +487 -0
- package/extensions/chronos/index.ts +148 -0
- package/extensions/cleave/assessment.ts +754 -0
- package/extensions/cleave/bridge.ts +31 -0
- package/extensions/cleave/conflicts.ts +250 -0
- package/extensions/cleave/dispatcher.ts +808 -0
- package/extensions/cleave/guardrails.ts +426 -0
- package/extensions/cleave/index.ts +3121 -0
- package/extensions/cleave/lifecycle-emitter.ts +20 -0
- package/extensions/cleave/openspec.ts +811 -0
- package/extensions/cleave/planner.ts +260 -0
- package/extensions/cleave/review.ts +579 -0
- package/extensions/cleave/skills.ts +355 -0
- package/extensions/cleave/types.ts +261 -0
- package/extensions/cleave/workspace.ts +861 -0
- package/extensions/cleave/worktree.ts +243 -0
- package/extensions/core-renderers.ts +253 -0
- package/extensions/dashboard/context-gauge.ts +58 -0
- package/extensions/dashboard/file-watch.ts +14 -0
- package/extensions/dashboard/footer.ts +1145 -0
- package/extensions/dashboard/git.ts +185 -0
- package/extensions/dashboard/index.ts +478 -0
- package/extensions/dashboard/memory-audit.ts +34 -0
- package/extensions/dashboard/overlay-data.ts +705 -0
- package/extensions/dashboard/overlay.ts +365 -0
- package/extensions/dashboard/render-utils.ts +54 -0
- package/extensions/dashboard/types.ts +191 -0
- package/extensions/dashboard/uri-helper.ts +45 -0
- package/extensions/debug.ts +69 -0
- package/extensions/defaults.ts +282 -0
- package/extensions/design-tree/dashboard-state.ts +161 -0
- package/extensions/design-tree/design-card.ts +362 -0
- package/extensions/design-tree/index.ts +2130 -0
- package/extensions/design-tree/lifecycle-emitter.ts +41 -0
- package/extensions/design-tree/tree.ts +1607 -0
- package/extensions/design-tree/types.ts +163 -0
- package/extensions/distill.ts +127 -0
- package/extensions/effort/index.ts +395 -0
- package/extensions/effort/tiers.ts +146 -0
- package/extensions/effort/types.ts +105 -0
- package/extensions/lib/git-state.ts +227 -0
- package/extensions/lib/local-models.ts +157 -0
- package/extensions/lib/model-preferences.ts +51 -0
- package/extensions/lib/model-routing.ts +720 -0
- package/extensions/lib/operator-fallback.ts +205 -0
- package/extensions/lib/operator-profile.ts +360 -0
- package/extensions/lib/slash-command-bridge.ts +253 -0
- package/extensions/lib/typebox-helpers.ts +16 -0
- package/extensions/local-inference/index.ts +727 -0
- package/extensions/mcp-bridge/README.md +220 -0
- package/extensions/mcp-bridge/index.ts +951 -0
- package/extensions/mcp-bridge/lib.ts +365 -0
- package/extensions/mcp-bridge/mcp.json +3 -0
- package/extensions/mcp-bridge/package.json +11 -0
- package/extensions/model-budget.ts +752 -0
- package/extensions/offline-driver.ts +403 -0
- package/extensions/openspec/archive-gate.ts +164 -0
- package/extensions/openspec/branch-cleanup.ts +64 -0
- package/extensions/openspec/dashboard-state.ts +50 -0
- package/extensions/openspec/index.ts +1917 -0
- package/extensions/openspec/lifecycle-emitter.ts +65 -0
- package/extensions/openspec/lifecycle-files.ts +70 -0
- package/extensions/openspec/lifecycle.ts +50 -0
- package/extensions/openspec/reconcile.ts +187 -0
- package/extensions/openspec/spec.ts +1385 -0
- package/extensions/openspec/types.ts +98 -0
- package/extensions/project-memory/DESIGN-global-mind.md +198 -0
- package/extensions/project-memory/README.md +202 -0
- package/extensions/project-memory/api-types.ts +382 -0
- package/extensions/project-memory/compaction-policy.ts +29 -0
- package/extensions/project-memory/core.ts +164 -0
- package/extensions/project-memory/embeddings.ts +230 -0
- package/extensions/project-memory/extraction-v2.ts +861 -0
- package/extensions/project-memory/factstore.ts +2177 -0
- package/extensions/project-memory/index.ts +3459 -0
- package/extensions/project-memory/injection-metrics.ts +91 -0
- package/extensions/project-memory/jsonl-io.ts +12 -0
- package/extensions/project-memory/lifecycle.ts +331 -0
- package/extensions/project-memory/migration.ts +293 -0
- package/extensions/project-memory/package.json +9 -0
- package/extensions/project-memory/sci-renderers.ts +7 -0
- package/extensions/project-memory/template.ts +103 -0
- package/extensions/project-memory/triggers.ts +52 -0
- package/extensions/project-memory/types.ts +102 -0
- package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
- package/extensions/render/composition/package-lock.json +534 -0
- package/extensions/render/composition/package.json +22 -0
- package/extensions/render/composition/render.mjs +246 -0
- package/extensions/render/composition/test-comp.tsx +87 -0
- package/extensions/render/composition/types.ts +24 -0
- package/extensions/render/excalidraw/UPSTREAM.md +81 -0
- package/extensions/render/excalidraw/elements.ts +764 -0
- package/extensions/render/excalidraw/index.ts +66 -0
- package/extensions/render/excalidraw/types.ts +223 -0
- package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
- package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
- package/extensions/render/excalidraw-renderer/render_template.html +59 -0
- package/extensions/render/index.ts +830 -0
- package/extensions/render/native-diagrams/index.ts +57 -0
- package/extensions/render/native-diagrams/motifs.ts +542 -0
- package/extensions/render/native-diagrams/raster.ts +8 -0
- package/extensions/render/native-diagrams/scene.ts +75 -0
- package/extensions/render/native-diagrams/spec.ts +204 -0
- package/extensions/render/native-diagrams/svg.ts +116 -0
- package/extensions/sci-ui.ts +304 -0
- package/extensions/session-log.ts +174 -0
- package/extensions/shared-state.ts +146 -0
- package/extensions/spinner-verbs.ts +91 -0
- package/extensions/style.ts +281 -0
- package/extensions/terminal-title.ts +191 -0
- package/extensions/tool-profile/index.ts +291 -0
- package/extensions/tool-profile/profiles.ts +290 -0
- package/extensions/types.d.ts +9 -0
- package/extensions/vault/index.ts +185 -0
- package/extensions/version-check.ts +90 -0
- package/extensions/view/index.ts +859 -0
- package/extensions/view/uri-resolver.ts +148 -0
- package/extensions/web-search/index.ts +182 -0
- package/extensions/web-search/providers.ts +121 -0
- package/extensions/web-ui/index.ts +110 -0
- package/extensions/web-ui/server.ts +265 -0
- package/extensions/web-ui/state.ts +462 -0
- package/extensions/web-ui/static/index.html +145 -0
- package/extensions/web-ui/types.ts +284 -0
- package/package.json +76 -0
- package/prompts/init.md +75 -0
- package/prompts/new-repo.md +54 -0
- package/prompts/oci-login.md +56 -0
- package/prompts/status.md +50 -0
- package/settings.json +4 -0
- package/skills/cleave/SKILL.md +218 -0
- package/skills/git/SKILL.md +209 -0
- package/skills/git/_reference/ci-validation.md +204 -0
- package/skills/oci/SKILL.md +338 -0
- package/skills/openspec/SKILL.md +346 -0
- package/skills/pi-extensions/SKILL.md +191 -0
- package/skills/pi-tui/SKILL.md +517 -0
- package/skills/python/SKILL.md +189 -0
- package/skills/rust/SKILL.md +268 -0
- package/skills/security/SKILL.md +206 -0
- package/skills/style/SKILL.md +264 -0
- package/skills/typescript/SKILL.md +225 -0
- package/skills/vault/SKILL.md +102 -0
- package/themes/alpharius-legacy.json +85 -0
- package/themes/alpharius.conf +59 -0
- package/themes/alpharius.json +88 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared provider-aware model resolver for Omegon.
|
|
3
|
+
*
|
|
4
|
+
* Keeps canonical compatibility tiers (local|retribution|victory|gloriana) stable while
|
|
5
|
+
* also supporting public operator capability roles
|
|
6
|
+
* (archmagos|magos|adept|servitor|servoskull).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PREFERRED_ORDER } from "./local-models.ts";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Types
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export type ModelTier = "local" | "retribution" | "victory" | "gloriana";
|
|
16
|
+
export type ProviderName = "openai" | "anthropic" | "local";
|
|
17
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
|
|
18
|
+
export type CapabilityRole = "archmagos" | "magos" | "adept" | "servitor" | "servoskull";
|
|
19
|
+
export type CandidateSource = "upstream" | "local";
|
|
20
|
+
export type CandidateWeight = "light" | "normal" | "heavy" | "unknown";
|
|
21
|
+
export type FallbackDisposition = "allow" | "ask" | "deny";
|
|
22
|
+
export type UpstreamFailureClass =
|
|
23
|
+
| "retryable-flake"
|
|
24
|
+
| "rate-limit"
|
|
25
|
+
| "backoff"
|
|
26
|
+
| "auth"
|
|
27
|
+
| "quota"
|
|
28
|
+
| "tool-output"
|
|
29
|
+
| "context-overflow"
|
|
30
|
+
| "invalid-request"
|
|
31
|
+
| "user-abort"
|
|
32
|
+
| "non-retryable";
|
|
33
|
+
export type UpstreamRecoveryAction = "retry-same-model" | "failover" | "surface" | "handled-elsewhere";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Operator-driven session routing policy.
|
|
37
|
+
* Reflects current provider posture rather than hard quota tracking.
|
|
38
|
+
*/
|
|
39
|
+
export interface ProviderRoutingPolicy {
|
|
40
|
+
/** Providers to try in preference order. */
|
|
41
|
+
providerOrder: ProviderName[];
|
|
42
|
+
/** Providers to skip unless no acceptable alternative exists. */
|
|
43
|
+
avoidProviders: ProviderName[];
|
|
44
|
+
/** When true, prefer inexpensive cloud over local for background tasks. */
|
|
45
|
+
cheapCloudPreferredOverLocal: boolean;
|
|
46
|
+
/** When true, ask operator for current provider posture before large Cleave runs. */
|
|
47
|
+
requirePreflightForLargeRuns: boolean;
|
|
48
|
+
/** Optional free-text note (e.g. "Anthropic budget is low today"). */
|
|
49
|
+
notes?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CapabilityCandidate {
|
|
53
|
+
id: string;
|
|
54
|
+
provider: ProviderName;
|
|
55
|
+
source: CandidateSource;
|
|
56
|
+
weight: CandidateWeight;
|
|
57
|
+
maxThinking: ThinkingLevel;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RoleProfile {
|
|
61
|
+
candidates: CapabilityCandidate[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CapabilityProfilePolicy {
|
|
65
|
+
sameRoleCrossProvider: FallbackDisposition;
|
|
66
|
+
crossSource: FallbackDisposition;
|
|
67
|
+
heavyLocal: FallbackDisposition;
|
|
68
|
+
unknownLocalPerformance: FallbackDisposition;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CapabilityProfile {
|
|
72
|
+
roles: Record<CapabilityRole, RoleProfile>;
|
|
73
|
+
internalAliases: Record<string, CapabilityRole>;
|
|
74
|
+
policy: CapabilityProfilePolicy;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface CooldownEntry {
|
|
78
|
+
until: number;
|
|
79
|
+
reason?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CapabilityRuntimeState {
|
|
83
|
+
candidateCooldowns?: Record<string, CooldownEntry>;
|
|
84
|
+
providerCooldowns?: Partial<Record<ProviderName, CooldownEntry>>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface UpstreamFailureClassification {
|
|
88
|
+
class: UpstreamFailureClass;
|
|
89
|
+
recoveryAction: UpstreamRecoveryAction;
|
|
90
|
+
summary: string;
|
|
91
|
+
reason: string;
|
|
92
|
+
retryable: boolean;
|
|
93
|
+
cooldownProvider: boolean;
|
|
94
|
+
cooldownCandidate: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolved concrete model for a requested tier.
|
|
99
|
+
*/
|
|
100
|
+
export interface ResolvedTierModel {
|
|
101
|
+
tier: ModelTier;
|
|
102
|
+
provider: ProviderName;
|
|
103
|
+
modelId: string;
|
|
104
|
+
maxThinking?: ThinkingLevel;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ResolvedCapabilityCandidate {
|
|
108
|
+
role: CapabilityRole;
|
|
109
|
+
candidate: CapabilityCandidate;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface RoleResolution {
|
|
113
|
+
ok: boolean;
|
|
114
|
+
role: CapabilityRole;
|
|
115
|
+
selected?: ResolvedCapabilityCandidate;
|
|
116
|
+
blockedBy?: "cross-source" | "heavy-local" | "unknown-local-performance" | "denied";
|
|
117
|
+
requiresConfirmation?: boolean;
|
|
118
|
+
reason?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Minimal model shape expected from the pi model registry.
|
|
123
|
+
*/
|
|
124
|
+
export interface RegistryModel {
|
|
125
|
+
id: string;
|
|
126
|
+
provider: string;
|
|
127
|
+
[key: string]: unknown;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Constants
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
export const TRANSIENT_PROVIDER_COOLDOWN_MS = 5 * 60 * 1000;
|
|
135
|
+
|
|
136
|
+
const THINKING_ORDER: Record<ThinkingLevel, number> = {
|
|
137
|
+
off: 0,
|
|
138
|
+
minimal: 1,
|
|
139
|
+
low: 2,
|
|
140
|
+
medium: 3,
|
|
141
|
+
high: 4,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const ROLE_COMPATIBILITY_MAP: Record<ModelTier, CapabilityRole> = {
|
|
145
|
+
local: "servitor",
|
|
146
|
+
retribution: "adept",
|
|
147
|
+
victory: "magos",
|
|
148
|
+
gloriana: "archmagos",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const TIER_DISPLAY_LABELS: Record<ModelTier, string> = {
|
|
152
|
+
local: "Servitor [Local]",
|
|
153
|
+
retribution: "Adept [Retribution Class]",
|
|
154
|
+
victory: "Magos [Victory Class]",
|
|
155
|
+
gloriana: "Archmagos [Gloriana Class]",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const ROLE_DISPLAY_LABELS: Record<CapabilityRole, string> = {
|
|
159
|
+
archmagos: "Archmagos",
|
|
160
|
+
magos: "Magos",
|
|
161
|
+
adept: "Adept",
|
|
162
|
+
servitor: "Servitor",
|
|
163
|
+
servoskull: "Servoskull",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Anthropic/OpenAI defaults
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const ANTHROPIC_TIER_PREFIXES: Record<Exclude<ModelTier, "local">, string[]> = {
|
|
171
|
+
retribution: ["claude-haiku"],
|
|
172
|
+
victory: ["claude-sonnet"],
|
|
173
|
+
gloriana: ["claude-opus"],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const OPENAI_TIER_MODELS: Record<Exclude<ModelTier, "local">, string[]> = {
|
|
177
|
+
retribution: ["gpt-5.1-codex", "gpt-4o-mini", "gpt-4.1-mini"],
|
|
178
|
+
victory: ["gpt-5.3-codex-spark", "gpt-4.1", "gpt-4o"],
|
|
179
|
+
gloriana: ["gpt-5.4", "gpt-4.5", "o3"],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Helpers
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
function parseModelVersion(id: string): number[] {
|
|
187
|
+
const parts = id.split("-");
|
|
188
|
+
const versions: number[] = [];
|
|
189
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
190
|
+
const n = parseInt(parts[i], 10);
|
|
191
|
+
if (!isNaN(n)) versions.unshift(n);
|
|
192
|
+
else break;
|
|
193
|
+
}
|
|
194
|
+
return versions;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function compareModelVersionsDesc(a: string, b: string): number {
|
|
198
|
+
const va = parseModelVersion(a);
|
|
199
|
+
const vb = parseModelVersion(b);
|
|
200
|
+
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
|
|
201
|
+
const diff = (vb[i] ?? 0) - (va[i] ?? 0);
|
|
202
|
+
if (diff !== 0) return diff;
|
|
203
|
+
}
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function matchAnthropicTier(models: RegistryModel[], tier: Exclude<ModelTier, "local">): RegistryModel | undefined {
|
|
208
|
+
const prefixes = ANTHROPIC_TIER_PREFIXES[tier];
|
|
209
|
+
for (const prefix of prefixes) {
|
|
210
|
+
const candidates = models
|
|
211
|
+
.filter((m) => m.provider === "anthropic" && m.id.startsWith(prefix))
|
|
212
|
+
.sort((a, b) => compareModelVersionsDesc(a.id, b.id));
|
|
213
|
+
if (candidates.length > 0) return candidates[0];
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function matchOpenAITier(models: RegistryModel[], tier: Exclude<ModelTier, "local">): RegistryModel | undefined {
|
|
219
|
+
const exactIds = OPENAI_TIER_MODELS[tier];
|
|
220
|
+
for (const modelId of exactIds) {
|
|
221
|
+
const match = models.find((m) => m.provider === "openai" && m.id === modelId);
|
|
222
|
+
if (match) return match;
|
|
223
|
+
}
|
|
224
|
+
const exactIdSet = new Set(exactIds);
|
|
225
|
+
const prefixFallbacks: Record<string, string[]> = {
|
|
226
|
+
retribution: ["gpt-4o-mini-", "gpt-4.1-mini-"],
|
|
227
|
+
victory: ["gpt-4o-", "gpt-4.1-"],
|
|
228
|
+
gloriana: ["gpt-4.5-", "o3-", "gpt-5."],
|
|
229
|
+
};
|
|
230
|
+
for (const prefix of prefixFallbacks[tier] ?? []) {
|
|
231
|
+
const found = models.find(
|
|
232
|
+
(m) => m.provider === "openai" && m.id.startsWith(prefix) && !exactIdSet.has(m.id),
|
|
233
|
+
);
|
|
234
|
+
if (found) return found;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function matchLocalTier(models: RegistryModel[]): RegistryModel | undefined {
|
|
240
|
+
const locals = models.filter((m) => m.provider === "local");
|
|
241
|
+
if (locals.length === 0) return undefined;
|
|
242
|
+
for (const preferred of PREFERRED_ORDER) {
|
|
243
|
+
const match = locals.find((m) => m.id === preferred);
|
|
244
|
+
if (match) return match;
|
|
245
|
+
}
|
|
246
|
+
return locals[0];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function dedupeProviderOrder(order: ProviderName[], avoided: ProviderName[]): ProviderName[] {
|
|
250
|
+
const seen = new Set<ProviderName>();
|
|
251
|
+
const result: ProviderName[] = [];
|
|
252
|
+
for (const p of order) {
|
|
253
|
+
if (!seen.has(p)) {
|
|
254
|
+
seen.add(p);
|
|
255
|
+
result.push(p);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const p of avoided) {
|
|
259
|
+
if (!seen.has(p)) {
|
|
260
|
+
seen.add(p);
|
|
261
|
+
result.push(p);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getCandidateKey(candidate: CapabilityCandidate): string {
|
|
268
|
+
return `${candidate.provider}/${candidate.id}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isCandidateCooledDown(
|
|
272
|
+
candidate: CapabilityCandidate,
|
|
273
|
+
runtimeState: CapabilityRuntimeState | undefined,
|
|
274
|
+
now: number,
|
|
275
|
+
): boolean {
|
|
276
|
+
const candidateCooldown = runtimeState?.candidateCooldowns?.[getCandidateKey(candidate)];
|
|
277
|
+
if (candidateCooldown && candidateCooldown.until > now) return true;
|
|
278
|
+
const providerCooldown = runtimeState?.providerCooldowns?.[candidate.provider];
|
|
279
|
+
return Boolean(providerCooldown && providerCooldown.until > now);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function registryHasCandidate(candidate: CapabilityCandidate, models: RegistryModel[]): boolean {
|
|
283
|
+
return models.some((m) => m.provider === candidate.provider && m.id === candidate.id);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function applyProviderPolicyOrder(
|
|
287
|
+
candidates: CapabilityCandidate[],
|
|
288
|
+
policy: ProviderRoutingPolicy,
|
|
289
|
+
): CapabilityCandidate[] {
|
|
290
|
+
const providerOrder = dedupeProviderOrder(policy.providerOrder, policy.avoidProviders);
|
|
291
|
+
const providerRank = new Map(providerOrder.map((provider, index) => [provider, index]));
|
|
292
|
+
return [...candidates].sort((a, b) => {
|
|
293
|
+
const aRank = providerRank.get(a.provider) ?? Number.MAX_SAFE_INTEGER;
|
|
294
|
+
const bRank = providerRank.get(b.provider) ?? Number.MAX_SAFE_INTEGER;
|
|
295
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
296
|
+
return candidates.indexOf(a) - candidates.indexOf(b);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function fallbackDispositionForCandidate(
|
|
301
|
+
firstCandidate: CapabilityCandidate | undefined,
|
|
302
|
+
candidate: CapabilityCandidate,
|
|
303
|
+
profilePolicy: CapabilityProfilePolicy,
|
|
304
|
+
): { blockedBy?: RoleResolution["blockedBy"]; disposition: FallbackDisposition } {
|
|
305
|
+
if (!firstCandidate) return { disposition: "allow" };
|
|
306
|
+
if (candidate.source !== firstCandidate.source) {
|
|
307
|
+
return { blockedBy: "cross-source", disposition: profilePolicy.crossSource };
|
|
308
|
+
}
|
|
309
|
+
if (candidate.provider !== firstCandidate.provider) {
|
|
310
|
+
return { disposition: profilePolicy.sameRoleCrossProvider };
|
|
311
|
+
}
|
|
312
|
+
if (candidate.source === "local" && candidate.weight === "heavy") {
|
|
313
|
+
return { blockedBy: "heavy-local", disposition: profilePolicy.heavyLocal };
|
|
314
|
+
}
|
|
315
|
+
if (candidate.source === "local" && candidate.weight === "unknown") {
|
|
316
|
+
return { blockedBy: "unknown-local-performance", disposition: profilePolicy.unknownLocalPerformance };
|
|
317
|
+
}
|
|
318
|
+
return { disposition: "allow" };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function explainBlockedResolution(
|
|
322
|
+
role: CapabilityRole,
|
|
323
|
+
candidate: CapabilityCandidate,
|
|
324
|
+
blockedBy: NonNullable<RoleResolution["blockedBy"]>,
|
|
325
|
+
disposition: FallbackDisposition,
|
|
326
|
+
): string {
|
|
327
|
+
const roleLabel = getRoleDisplayLabel(role);
|
|
328
|
+
const target = `${candidate.provider}/${candidate.id}`;
|
|
329
|
+
const reason = blockedBy === "cross-source"
|
|
330
|
+
? `cross-source fallback to ${candidate.source}`
|
|
331
|
+
: blockedBy === "heavy-local"
|
|
332
|
+
? "heavy local fallback"
|
|
333
|
+
: blockedBy === "unknown-local-performance"
|
|
334
|
+
? "unknown local performance"
|
|
335
|
+
: "policy";
|
|
336
|
+
if (disposition === "ask") {
|
|
337
|
+
return `${roleLabel} resolution requires operator confirmation before ${reason} via ${target}.`;
|
|
338
|
+
}
|
|
339
|
+
return `${roleLabel} resolution blocked by policy: ${reason} via ${target} is not permitted.`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function inferWeightFromModel(model: RegistryModel): CandidateWeight {
|
|
343
|
+
const id = model.id.toLowerCase();
|
|
344
|
+
if (id.includes("70b") || id.includes("72b") || id.includes("30b") || id.includes("32b") || id.includes("24b")) {
|
|
345
|
+
return "heavy";
|
|
346
|
+
}
|
|
347
|
+
if (id.includes("14b") || id.includes("8b")) return "normal";
|
|
348
|
+
return "light";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function classifyFailureMessage(message: string): UpstreamFailureClassification {
|
|
352
|
+
const normalized = message.toLowerCase();
|
|
353
|
+
|
|
354
|
+
const patterns: Array<{
|
|
355
|
+
match: boolean;
|
|
356
|
+
classification: UpstreamFailureClassification;
|
|
357
|
+
}> = [
|
|
358
|
+
{
|
|
359
|
+
// User-initiated cancellation: Esc in pi, SIGINT, AbortController.abort(), etc.
|
|
360
|
+
// These are NOT upstream API failures and must never surface as recovery events.
|
|
361
|
+
match: ["operation aborted", "command aborted", "user aborted", "abortederror", "request aborted", "abort was called"].some((needle) => normalized.includes(needle))
|
|
362
|
+
|| normalized === "aborted",
|
|
363
|
+
classification: {
|
|
364
|
+
class: "user-abort",
|
|
365
|
+
recoveryAction: "handled-elsewhere",
|
|
366
|
+
summary: "user-initiated abort",
|
|
367
|
+
reason: "The operation was cancelled by the user (Esc / SIGINT / AbortSignal). No upstream failure occurred.",
|
|
368
|
+
retryable: false,
|
|
369
|
+
cooldownProvider: false,
|
|
370
|
+
cooldownCandidate: false,
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
match: ["context window", "context length", "too many tokens", "maximum context", "prompt is too long"].some((needle) => normalized.includes(needle)),
|
|
375
|
+
classification: {
|
|
376
|
+
class: "context-overflow",
|
|
377
|
+
recoveryAction: "handled-elsewhere",
|
|
378
|
+
summary: "context overflow",
|
|
379
|
+
reason: "Context overflow should be handled by explicit compaction/context management logic.",
|
|
380
|
+
retryable: false,
|
|
381
|
+
cooldownProvider: false,
|
|
382
|
+
cooldownCandidate: false,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
match: ["invalid api key", "authentication", "unauthorized", "forbidden", "permission denied", "auth failed", "401", "403"].some((needle) => normalized.includes(needle)),
|
|
387
|
+
classification: {
|
|
388
|
+
class: "auth",
|
|
389
|
+
recoveryAction: "surface",
|
|
390
|
+
summary: "authentication failure",
|
|
391
|
+
reason: "Authentication and authorization failures are not generic transient retries.",
|
|
392
|
+
retryable: false,
|
|
393
|
+
cooldownProvider: false,
|
|
394
|
+
cooldownCandidate: false,
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
match: ["quota exceeded", "insufficient_quota", "hard quota", "billing", "credits", "usage limit exceeded"].some((needle) => normalized.includes(needle)),
|
|
399
|
+
classification: {
|
|
400
|
+
class: "quota",
|
|
401
|
+
recoveryAction: "surface",
|
|
402
|
+
summary: "quota exhaustion",
|
|
403
|
+
reason: "Hard quota exhaustion requires explicit operator or provider action.",
|
|
404
|
+
retryable: false,
|
|
405
|
+
cooldownProvider: false,
|
|
406
|
+
cooldownCandidate: false,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
match: ["malformed tool output", "invalid tool output", "tool result schema", "tool output parse", "tool call parse", "schema validation", "malformed json", "invalid json", "structured output"].some((needle) => normalized.includes(needle)),
|
|
411
|
+
classification: {
|
|
412
|
+
class: "tool-output",
|
|
413
|
+
recoveryAction: "surface",
|
|
414
|
+
summary: "malformed tool output",
|
|
415
|
+
reason: "Malformed tool output should be surfaced explicitly rather than retried as an upstream flake.",
|
|
416
|
+
retryable: false,
|
|
417
|
+
cooldownProvider: false,
|
|
418
|
+
cooldownCandidate: false,
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
match: ["429", "rate limit", "rate-limit", "too many requests", "session limit"].some((needle) => normalized.includes(needle)),
|
|
423
|
+
classification: {
|
|
424
|
+
class: "rate-limit",
|
|
425
|
+
recoveryAction: "failover",
|
|
426
|
+
summary: "rate limited",
|
|
427
|
+
reason: "Rate limits and session limits should cool down the failing route and prefer failover.",
|
|
428
|
+
retryable: false,
|
|
429
|
+
cooldownProvider: true,
|
|
430
|
+
cooldownCandidate: true,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
match: ["try again later", "backoff", "retry-after", "retry after", "temporarily unavailable", "temporarily blocked"].some((needle) => normalized.includes(needle)),
|
|
435
|
+
classification: {
|
|
436
|
+
class: "backoff",
|
|
437
|
+
recoveryAction: "failover",
|
|
438
|
+
summary: "explicit backoff",
|
|
439
|
+
reason: "Explicit backoff guidance should avoid an immediate retry on the same provider/model.",
|
|
440
|
+
retryable: false,
|
|
441
|
+
cooldownProvider: true,
|
|
442
|
+
cooldownCandidate: true,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
match: ["image dimensions exceed", "image.source.base64.data", "image too large", "image size exceeds", "max allowed size: 8000"].some((needle) => normalized.includes(needle)),
|
|
447
|
+
classification: {
|
|
448
|
+
class: "invalid-request",
|
|
449
|
+
recoveryAction: "surface",
|
|
450
|
+
summary: "image too large for API (max 8000px per dimension)",
|
|
451
|
+
reason: "An image in the conversation exceeds the API's 8000px dimension limit. Resize or remove the image before retrying.",
|
|
452
|
+
retryable: false,
|
|
453
|
+
cooldownProvider: false,
|
|
454
|
+
cooldownCandidate: false,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
match: ["invalid_request_error", "invalid request", "malformed request", "bad request"].some((needle) => normalized.includes(needle)) && !["rate limit", "429", "quota", "authentication", "unauthorized"].some((needle) => normalized.includes(needle)),
|
|
459
|
+
classification: {
|
|
460
|
+
class: "invalid-request",
|
|
461
|
+
recoveryAction: "surface",
|
|
462
|
+
summary: "invalid API request",
|
|
463
|
+
reason: "The request was rejected by the API as malformed or invalid. Check the error details and fix the request payload.",
|
|
464
|
+
retryable: false,
|
|
465
|
+
cooldownProvider: false,
|
|
466
|
+
cooldownCandidate: false,
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
match: ["server_error", "internal server error", "bad gateway", "gateway timeout", "timed out", "timeout", "econnreset", "socket hang up", "overloaded", "5xx", "502", "503", "504"].some((needle) => normalized.includes(needle)),
|
|
471
|
+
classification: {
|
|
472
|
+
class: "retryable-flake",
|
|
473
|
+
recoveryAction: "retry-same-model",
|
|
474
|
+
summary: "transient upstream flake",
|
|
475
|
+
reason: "Obvious upstream flakiness is eligible for one bounded retry on the same model.",
|
|
476
|
+
retryable: true,
|
|
477
|
+
cooldownProvider: false,
|
|
478
|
+
cooldownCandidate: false,
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
for (const entry of patterns) {
|
|
484
|
+
if (entry.match) return entry.classification;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
class: "non-retryable",
|
|
489
|
+
recoveryAction: "surface",
|
|
490
|
+
summary: "non-retryable upstream failure",
|
|
491
|
+
reason: "The failure does not match a known transient, failover, or separately handled recovery class.",
|
|
492
|
+
retryable: false,
|
|
493
|
+
cooldownProvider: false,
|
|
494
|
+
cooldownCandidate: false,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function clampThinkingLevel(requested: ThinkingLevel, maxThinking: ThinkingLevel): ThinkingLevel {
|
|
499
|
+
return THINKING_ORDER[requested] <= THINKING_ORDER[maxThinking] ? requested : maxThinking;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function classifyUpstreamFailure(error: unknown): UpstreamFailureClassification {
|
|
503
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
504
|
+
return classifyFailureMessage(message);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function classifyTransientFailure(error: unknown): boolean {
|
|
508
|
+
return classifyUpstreamFailure(error).retryable || classifyUpstreamFailure(error).recoveryAction === "failover";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function withProviderCooldown(
|
|
512
|
+
runtimeState: CapabilityRuntimeState | undefined,
|
|
513
|
+
provider: ProviderName,
|
|
514
|
+
reason: string,
|
|
515
|
+
now: number = Date.now(),
|
|
516
|
+
cooldownMs: number = TRANSIENT_PROVIDER_COOLDOWN_MS,
|
|
517
|
+
): CapabilityRuntimeState {
|
|
518
|
+
return {
|
|
519
|
+
candidateCooldowns: { ...(runtimeState?.candidateCooldowns ?? {}) },
|
|
520
|
+
providerCooldowns: {
|
|
521
|
+
...(runtimeState?.providerCooldowns ?? {}),
|
|
522
|
+
[provider]: { until: now + cooldownMs, reason },
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function withCandidateCooldown(
|
|
528
|
+
runtimeState: CapabilityRuntimeState | undefined,
|
|
529
|
+
candidate: CapabilityCandidate,
|
|
530
|
+
reason: string,
|
|
531
|
+
now: number = Date.now(),
|
|
532
|
+
cooldownMs: number = TRANSIENT_PROVIDER_COOLDOWN_MS,
|
|
533
|
+
): CapabilityRuntimeState {
|
|
534
|
+
return {
|
|
535
|
+
candidateCooldowns: {
|
|
536
|
+
...(runtimeState?.candidateCooldowns ?? {}),
|
|
537
|
+
[getCandidateKey(candidate)]: { until: now + cooldownMs, reason },
|
|
538
|
+
},
|
|
539
|
+
providerCooldowns: { ...(runtimeState?.providerCooldowns ?? {}) },
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function getDefaultCapabilityProfile(models: RegistryModel[] = []): CapabilityProfile {
|
|
544
|
+
const archmagosCandidates: CapabilityCandidate[] = [];
|
|
545
|
+
const magosCandidates: CapabilityCandidate[] = [];
|
|
546
|
+
const adeptCandidates: CapabilityCandidate[] = [];
|
|
547
|
+
const servitorCandidates: CapabilityCandidate[] = [];
|
|
548
|
+
const servoskullCandidates: CapabilityCandidate[] = [];
|
|
549
|
+
|
|
550
|
+
const anthropicOpus = matchAnthropicTier(models, "gloriana");
|
|
551
|
+
const openaiOpus = matchOpenAITier(models, "gloriana");
|
|
552
|
+
const anthropicSonnet = matchAnthropicTier(models, "victory");
|
|
553
|
+
const openaiSonnet = matchOpenAITier(models, "victory");
|
|
554
|
+
const anthropicHaiku = matchAnthropicTier(models, "retribution");
|
|
555
|
+
const openaiHaiku = matchOpenAITier(models, "retribution");
|
|
556
|
+
const local = matchLocalTier(models);
|
|
557
|
+
|
|
558
|
+
if (anthropicOpus) archmagosCandidates.push({ id: anthropicOpus.id, provider: "anthropic", source: "upstream", weight: "heavy", maxThinking: "high" });
|
|
559
|
+
if (openaiOpus) archmagosCandidates.push({ id: openaiOpus.id, provider: "openai", source: "upstream", weight: "heavy", maxThinking: "high" });
|
|
560
|
+
|
|
561
|
+
if (anthropicSonnet) magosCandidates.push({ id: anthropicSonnet.id, provider: "anthropic", source: "upstream", weight: "normal", maxThinking: "high" });
|
|
562
|
+
if (openaiSonnet) magosCandidates.push({ id: openaiSonnet.id, provider: "openai", source: "upstream", weight: "normal", maxThinking: "medium" });
|
|
563
|
+
|
|
564
|
+
if (openaiHaiku) adeptCandidates.push({ id: openaiHaiku.id, provider: "openai", source: "upstream", weight: "light", maxThinking: "low" });
|
|
565
|
+
if (anthropicHaiku) adeptCandidates.push({ id: anthropicHaiku.id, provider: "anthropic", source: "upstream", weight: "light", maxThinking: "low" });
|
|
566
|
+
|
|
567
|
+
if (anthropicHaiku) servitorCandidates.push({ id: anthropicHaiku.id, provider: "anthropic", source: "upstream", weight: "light", maxThinking: "low" });
|
|
568
|
+
if (openaiHaiku) servitorCandidates.push({ id: openaiHaiku.id, provider: "openai", source: "upstream", weight: "light", maxThinking: "low" });
|
|
569
|
+
if (local) servitorCandidates.push({ id: local.id, provider: "local", source: "local", weight: inferWeightFromModel(local), maxThinking: "medium" });
|
|
570
|
+
|
|
571
|
+
if (local) servoskullCandidates.push({ id: local.id, provider: "local", source: "local", weight: inferWeightFromModel(local), maxThinking: "off" });
|
|
572
|
+
if (openaiHaiku) servoskullCandidates.push({ id: openaiHaiku.id, provider: "openai", source: "upstream", weight: "light", maxThinking: "off" });
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
roles: {
|
|
576
|
+
archmagos: { candidates: archmagosCandidates },
|
|
577
|
+
magos: { candidates: magosCandidates },
|
|
578
|
+
adept: { candidates: adeptCandidates },
|
|
579
|
+
servitor: { candidates: servitorCandidates },
|
|
580
|
+
servoskull: { candidates: servoskullCandidates },
|
|
581
|
+
},
|
|
582
|
+
internalAliases: {
|
|
583
|
+
gloriana: "archmagos",
|
|
584
|
+
victory: "magos",
|
|
585
|
+
retribution: "adept",
|
|
586
|
+
local: "servitor",
|
|
587
|
+
review: "archmagos",
|
|
588
|
+
planning: "archmagos",
|
|
589
|
+
compaction: "servitor",
|
|
590
|
+
extraction: "servitor",
|
|
591
|
+
"cleave.leaf": "adept",
|
|
592
|
+
summary: "servoskull",
|
|
593
|
+
background: "servoskull",
|
|
594
|
+
},
|
|
595
|
+
policy: {
|
|
596
|
+
sameRoleCrossProvider: "allow",
|
|
597
|
+
crossSource: "ask",
|
|
598
|
+
heavyLocal: "ask",
|
|
599
|
+
unknownLocalPerformance: "ask",
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function resolveCapabilityRole(
|
|
605
|
+
requestedRole: CapabilityRole | string,
|
|
606
|
+
models: RegistryModel[],
|
|
607
|
+
policy: ProviderRoutingPolicy,
|
|
608
|
+
profile: CapabilityProfile = getDefaultCapabilityProfile(models),
|
|
609
|
+
runtimeState?: CapabilityRuntimeState,
|
|
610
|
+
now: number = Date.now(),
|
|
611
|
+
): RoleResolution {
|
|
612
|
+
const role = (profile.internalAliases[requestedRole] ?? requestedRole) as CapabilityRole;
|
|
613
|
+
const roleProfile = profile.roles[role];
|
|
614
|
+
if (!roleProfile) {
|
|
615
|
+
return {
|
|
616
|
+
ok: false,
|
|
617
|
+
role: "servitor",
|
|
618
|
+
blockedBy: "denied",
|
|
619
|
+
reason: `Unknown capability role: ${requestedRole}`,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const orderedCandidates = applyProviderPolicyOrder(roleProfile.candidates, policy);
|
|
624
|
+
const firstCandidate = orderedCandidates[0];
|
|
625
|
+
|
|
626
|
+
for (const candidate of orderedCandidates) {
|
|
627
|
+
if (!registryHasCandidate(candidate, models)) continue;
|
|
628
|
+
if (isCandidateCooledDown(candidate, runtimeState, now)) continue;
|
|
629
|
+
|
|
630
|
+
const fallback = fallbackDispositionForCandidate(firstCandidate, candidate, profile.policy);
|
|
631
|
+
if (fallback.disposition === "deny") {
|
|
632
|
+
return {
|
|
633
|
+
ok: false,
|
|
634
|
+
role,
|
|
635
|
+
blockedBy: fallback.blockedBy ?? "denied",
|
|
636
|
+
reason: explainBlockedResolution(role, candidate, fallback.blockedBy ?? "denied", fallback.disposition),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (fallback.disposition === "ask") {
|
|
640
|
+
return {
|
|
641
|
+
ok: false,
|
|
642
|
+
role,
|
|
643
|
+
blockedBy: fallback.blockedBy ?? "denied",
|
|
644
|
+
requiresConfirmation: true,
|
|
645
|
+
reason: explainBlockedResolution(role, candidate, fallback.blockedBy ?? "denied", fallback.disposition),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
role,
|
|
652
|
+
selected: { role, candidate },
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
ok: false,
|
|
658
|
+
role,
|
|
659
|
+
blockedBy: "denied",
|
|
660
|
+
reason: `No viable candidate available for ${getRoleDisplayLabel(role)}.`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// Core compatibility resolver
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Resolve an abstract tier to a concrete {provider, modelId} using the
|
|
670
|
+
* session routing policy and the available model registry.
|
|
671
|
+
*/
|
|
672
|
+
export function resolveTier(
|
|
673
|
+
tier: ModelTier,
|
|
674
|
+
models: RegistryModel[],
|
|
675
|
+
policy: ProviderRoutingPolicy,
|
|
676
|
+
runtimeState?: CapabilityRuntimeState,
|
|
677
|
+
profile?: CapabilityProfile,
|
|
678
|
+
): ResolvedTierModel | undefined {
|
|
679
|
+
if (tier === "local") {
|
|
680
|
+
const local = matchLocalTier(models);
|
|
681
|
+
if (!local) return undefined;
|
|
682
|
+
return { tier, provider: "local", modelId: local.id, maxThinking: "high" };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const resolution = resolveCapabilityRole(
|
|
686
|
+
ROLE_COMPATIBILITY_MAP[tier],
|
|
687
|
+
models,
|
|
688
|
+
policy,
|
|
689
|
+
profile ?? getDefaultCapabilityProfile(models),
|
|
690
|
+
runtimeState,
|
|
691
|
+
);
|
|
692
|
+
if (!resolution.ok || !resolution.selected) return undefined;
|
|
693
|
+
return {
|
|
694
|
+
tier,
|
|
695
|
+
provider: resolution.selected.candidate.provider,
|
|
696
|
+
modelId: resolution.selected.candidate.id,
|
|
697
|
+
maxThinking: resolution.selected.candidate.maxThinking,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Display labels + defaults
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
export function getTierDisplayLabel(tier: ModelTier): string {
|
|
706
|
+
return TIER_DISPLAY_LABELS[tier];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function getRoleDisplayLabel(role: CapabilityRole): string {
|
|
710
|
+
return ROLE_DISPLAY_LABELS[role];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function getDefaultPolicy(): ProviderRoutingPolicy {
|
|
714
|
+
return {
|
|
715
|
+
providerOrder: ["anthropic", "openai", "local"],
|
|
716
|
+
avoidProviders: [],
|
|
717
|
+
cheapCloudPreferredOverLocal: false,
|
|
718
|
+
requirePreflightForLargeRuns: true,
|
|
719
|
+
};
|
|
720
|
+
}
|