oh-my-opencode-slim 0.7.0 → 0.8.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 +9 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/orchestrator.d.ts +1 -1
- package/dist/background/background-manager.d.ts +42 -0
- package/dist/background/tmux-session-manager.d.ts +5 -0
- package/dist/cli/config-manager.d.ts +3 -0
- package/dist/cli/dynamic-model-selection.d.ts +14 -2
- package/dist/cli/external-rankings.d.ts +8 -0
- package/dist/cli/index.js +1823 -200
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/opencode-models.d.ts +4 -0
- package/dist/cli/paths.d.ts +2 -0
- package/dist/cli/precedence-resolver.d.ts +16 -0
- package/dist/cli/scoring-v2/engine.d.ts +4 -0
- package/dist/cli/scoring-v2/features.d.ts +3 -0
- package/dist/cli/scoring-v2/index.d.ts +4 -0
- package/dist/cli/scoring-v2/types.d.ts +17 -0
- package/dist/cli/scoring-v2/weights.d.ts +2 -0
- package/dist/cli/system.d.ts +2 -0
- package/dist/cli/types.d.ts +45 -1
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/schema.d.ts +94 -2
- package/dist/index.js +144 -7
- package/dist/tools/quota/api.d.ts +5 -0
- package/dist/tools/quota/command.d.ts +1 -0
- package/dist/tools/quota/index.d.ts +21 -0
- package/dist/tools/quota/types.d.ts +41 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +8 -8
package/dist/cli/index.js
CHANGED
|
@@ -13666,6 +13666,34 @@ function date4(params) {
|
|
|
13666
13666
|
// node_modules/zod/v4/classic/external.js
|
|
13667
13667
|
config(en_default());
|
|
13668
13668
|
// src/config/schema.ts
|
|
13669
|
+
var ProviderModelIdSchema = exports_external.string().regex(/^[^/\s]+\/[^\s]+$/, "Expected provider/model format (provider/.../model)");
|
|
13670
|
+
var ManualAgentPlanSchema = exports_external.object({
|
|
13671
|
+
primary: ProviderModelIdSchema,
|
|
13672
|
+
fallback1: ProviderModelIdSchema,
|
|
13673
|
+
fallback2: ProviderModelIdSchema,
|
|
13674
|
+
fallback3: ProviderModelIdSchema
|
|
13675
|
+
}).superRefine((value, ctx) => {
|
|
13676
|
+
const unique = new Set([
|
|
13677
|
+
value.primary,
|
|
13678
|
+
value.fallback1,
|
|
13679
|
+
value.fallback2,
|
|
13680
|
+
value.fallback3
|
|
13681
|
+
]);
|
|
13682
|
+
if (unique.size !== 4) {
|
|
13683
|
+
ctx.addIssue({
|
|
13684
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13685
|
+
message: "primary and fallbacks must be unique per agent"
|
|
13686
|
+
});
|
|
13687
|
+
}
|
|
13688
|
+
});
|
|
13689
|
+
var ManualPlanSchema = exports_external.object({
|
|
13690
|
+
orchestrator: ManualAgentPlanSchema,
|
|
13691
|
+
oracle: ManualAgentPlanSchema,
|
|
13692
|
+
designer: ManualAgentPlanSchema,
|
|
13693
|
+
explorer: ManualAgentPlanSchema,
|
|
13694
|
+
librarian: ManualAgentPlanSchema,
|
|
13695
|
+
fixer: ManualAgentPlanSchema
|
|
13696
|
+
}).strict();
|
|
13669
13697
|
var AgentModelChainSchema = exports_external.array(exports_external.string()).min(1);
|
|
13670
13698
|
var FallbackChainsSchema = exports_external.object({
|
|
13671
13699
|
orchestrator: AgentModelChainSchema.optional(),
|
|
@@ -13674,7 +13702,7 @@ var FallbackChainsSchema = exports_external.object({
|
|
|
13674
13702
|
explorer: AgentModelChainSchema.optional(),
|
|
13675
13703
|
librarian: AgentModelChainSchema.optional(),
|
|
13676
13704
|
fixer: AgentModelChainSchema.optional()
|
|
13677
|
-
}).
|
|
13705
|
+
}).catchall(AgentModelChainSchema);
|
|
13678
13706
|
var AgentOverrideConfigSchema = exports_external.object({
|
|
13679
13707
|
model: exports_external.string().optional(),
|
|
13680
13708
|
temperature: exports_external.number().min(0).max(2).optional(),
|
|
@@ -13706,6 +13734,9 @@ var FailoverConfigSchema = exports_external.object({
|
|
|
13706
13734
|
});
|
|
13707
13735
|
var PluginConfigSchema = exports_external.object({
|
|
13708
13736
|
preset: exports_external.string().optional(),
|
|
13737
|
+
scoringEngineVersion: exports_external.enum(["v1", "v2-shadow", "v2"]).optional(),
|
|
13738
|
+
balanceProviderUsage: exports_external.boolean().optional(),
|
|
13739
|
+
manualPlan: ManualPlanSchema.optional(),
|
|
13709
13740
|
presets: exports_external.record(exports_external.string(), PresetSchema).optional(),
|
|
13710
13741
|
agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
|
|
13711
13742
|
disabled_mcps: exports_external.array(exports_external.string()).optional(),
|
|
@@ -13942,8 +13973,45 @@ function generateAntigravityMixedPreset(config2, existingPreset) {
|
|
|
13942
13973
|
function generateLiteConfig(installConfig) {
|
|
13943
13974
|
const config2 = {
|
|
13944
13975
|
preset: "zen-free",
|
|
13945
|
-
presets: {}
|
|
13976
|
+
presets: {},
|
|
13977
|
+
balanceProviderUsage: installConfig.balanceProviderUsage ?? false
|
|
13946
13978
|
};
|
|
13979
|
+
if (installConfig.setupMode === "manual" && installConfig.manualAgentConfigs) {
|
|
13980
|
+
config2.preset = "manual";
|
|
13981
|
+
const manualPreset = {};
|
|
13982
|
+
const chains = {};
|
|
13983
|
+
for (const agentName of AGENT_NAMES) {
|
|
13984
|
+
const manualConfig = installConfig.manualAgentConfigs[agentName];
|
|
13985
|
+
if (manualConfig) {
|
|
13986
|
+
manualPreset[agentName] = {
|
|
13987
|
+
model: manualConfig.primary,
|
|
13988
|
+
skills: agentName === "orchestrator" ? ["*"] : RECOMMENDED_SKILLS.filter((s) => s.allowedAgents.includes("*") || s.allowedAgents.includes(agentName)).map((s) => s.skillName),
|
|
13989
|
+
mcps: DEFAULT_AGENT_MCPS[agentName] ?? []
|
|
13990
|
+
};
|
|
13991
|
+
const fallbackChain = [
|
|
13992
|
+
manualConfig.primary,
|
|
13993
|
+
manualConfig.fallback1,
|
|
13994
|
+
manualConfig.fallback2,
|
|
13995
|
+
manualConfig.fallback3
|
|
13996
|
+
].filter((m, i, arr) => m && arr.indexOf(m) === i);
|
|
13997
|
+
chains[agentName] = fallbackChain;
|
|
13998
|
+
}
|
|
13999
|
+
}
|
|
14000
|
+
config2.presets.manual = manualPreset;
|
|
14001
|
+
config2.fallback = {
|
|
14002
|
+
enabled: true,
|
|
14003
|
+
timeoutMs: 15000,
|
|
14004
|
+
chains
|
|
14005
|
+
};
|
|
14006
|
+
if (installConfig.hasTmux) {
|
|
14007
|
+
config2.tmux = {
|
|
14008
|
+
enabled: true,
|
|
14009
|
+
layout: "main-vertical",
|
|
14010
|
+
main_pane_size: 60
|
|
14011
|
+
};
|
|
14012
|
+
}
|
|
14013
|
+
return config2;
|
|
14014
|
+
}
|
|
13947
14015
|
let activePreset = "zen-free";
|
|
13948
14016
|
if (installConfig.hasAntigravity && installConfig.hasKimi && installConfig.hasOpenAI) {
|
|
13949
14017
|
activePreset = "antigravity-mixed-both";
|
|
@@ -14023,7 +14091,7 @@ function generateLiteConfig(installConfig) {
|
|
|
14023
14091
|
const applyChutesAssignments = (presetAgents) => {
|
|
14024
14092
|
if (!installConfig.hasChutes)
|
|
14025
14093
|
return;
|
|
14026
|
-
const hasExternalProviders = installConfig.hasKimi || installConfig.hasOpenAI || installConfig.hasAntigravity;
|
|
14094
|
+
const hasExternalProviders = installConfig.hasKimi || installConfig.hasOpenAI || installConfig.hasAnthropic || installConfig.hasCopilot || installConfig.hasZaiPlan || installConfig.hasAntigravity;
|
|
14027
14095
|
if (hasExternalProviders && activePreset !== "chutes")
|
|
14028
14096
|
return;
|
|
14029
14097
|
const primaryModel = installConfig.selectedChutesPrimaryModel;
|
|
@@ -14379,7 +14447,7 @@ function addGoogleProvider() {
|
|
|
14379
14447
|
function addChutesProvider() {
|
|
14380
14448
|
const configPath = getExistingConfigPath();
|
|
14381
14449
|
try {
|
|
14382
|
-
const {
|
|
14450
|
+
const { error: error48 } = parseConfig(configPath);
|
|
14383
14451
|
if (error48) {
|
|
14384
14452
|
return {
|
|
14385
14453
|
success: false,
|
|
@@ -14387,24 +14455,12 @@ function addChutesProvider() {
|
|
|
14387
14455
|
error: `Failed to parse config: ${error48}`
|
|
14388
14456
|
};
|
|
14389
14457
|
}
|
|
14390
|
-
const config2 = parsedConfig ?? {};
|
|
14391
|
-
const providers = config2.provider ?? {};
|
|
14392
|
-
providers.chutes = {
|
|
14393
|
-
npm: "@ai-sdk/openai-compatible",
|
|
14394
|
-
name: "Chutes",
|
|
14395
|
-
options: {
|
|
14396
|
-
baseURL: "https://llm.chutes.ai/v1",
|
|
14397
|
-
apiKey: "{env:CHUTES_API_KEY}"
|
|
14398
|
-
}
|
|
14399
|
-
};
|
|
14400
|
-
config2.provider = providers;
|
|
14401
|
-
writeConfig(configPath, config2);
|
|
14402
14458
|
return { success: true, configPath };
|
|
14403
14459
|
} catch (err) {
|
|
14404
14460
|
return {
|
|
14405
14461
|
success: false,
|
|
14406
14462
|
configPath,
|
|
14407
|
-
error: `Failed to
|
|
14463
|
+
error: `Failed to validate chutes provider config: ${err}`
|
|
14408
14464
|
};
|
|
14409
14465
|
}
|
|
14410
14466
|
}
|
|
@@ -14462,6 +14518,317 @@ function detectCurrentConfig() {
|
|
|
14462
14518
|
}
|
|
14463
14519
|
return result;
|
|
14464
14520
|
}
|
|
14521
|
+
// src/cli/model-key-normalization.ts
|
|
14522
|
+
function cleanupAlias(input, preserveSlash) {
|
|
14523
|
+
let value = input.toLowerCase().trim();
|
|
14524
|
+
value = value.replace(/\bfp[a-z0-9.-]*\b/g, " ");
|
|
14525
|
+
value = value.replace(/\btee\b/g, " ");
|
|
14526
|
+
if (preserveSlash) {
|
|
14527
|
+
value = value.replace(/[_\s]+/g, "-");
|
|
14528
|
+
value = value.replace(/-+/g, "-");
|
|
14529
|
+
value = value.replace(/\/+/g, "/");
|
|
14530
|
+
value = value.replace(/\/-+/g, "/");
|
|
14531
|
+
value = value.replace(/-+\//g, "/");
|
|
14532
|
+
value = value.replace(/^\/+|\/+$/g, "");
|
|
14533
|
+
value = value.replace(/^-+|-+$/g, "");
|
|
14534
|
+
return value;
|
|
14535
|
+
}
|
|
14536
|
+
value = value.replace(/[/_\s]+/g, "-");
|
|
14537
|
+
value = value.replace(/-+/g, "-");
|
|
14538
|
+
value = value.replace(/^-+|-+$/g, "");
|
|
14539
|
+
return value;
|
|
14540
|
+
}
|
|
14541
|
+
function addDerivedAliases(seed, aliases) {
|
|
14542
|
+
const slashAlias = cleanupAlias(seed, true);
|
|
14543
|
+
const flatAlias = cleanupAlias(seed, false);
|
|
14544
|
+
if (slashAlias)
|
|
14545
|
+
aliases.add(slashAlias);
|
|
14546
|
+
if (flatAlias)
|
|
14547
|
+
aliases.add(flatAlias);
|
|
14548
|
+
if (slashAlias) {
|
|
14549
|
+
aliases.add(slashAlias.replace(/-(free|flash)$/i, ""));
|
|
14550
|
+
}
|
|
14551
|
+
if (flatAlias) {
|
|
14552
|
+
aliases.add(flatAlias.replace(/-(free|flash)$/i, ""));
|
|
14553
|
+
}
|
|
14554
|
+
if (slashAlias.includes("/")) {
|
|
14555
|
+
aliases.add(cleanupAlias(slashAlias.replace(/\//g, " "), false));
|
|
14556
|
+
aliases.add(cleanupAlias(slashAlias.replace(/\//g, "-"), false));
|
|
14557
|
+
const lastPart = slashAlias.split("/").at(-1);
|
|
14558
|
+
if (lastPart) {
|
|
14559
|
+
addDerivedAliases(lastPart, aliases);
|
|
14560
|
+
}
|
|
14561
|
+
}
|
|
14562
|
+
}
|
|
14563
|
+
function buildModelKeyAliases(input) {
|
|
14564
|
+
const normalized = input.trim().toLowerCase();
|
|
14565
|
+
if (!normalized)
|
|
14566
|
+
return [];
|
|
14567
|
+
const aliases = new Set;
|
|
14568
|
+
const slashIndex = normalized.indexOf("/");
|
|
14569
|
+
const afterProvider = slashIndex >= 0 ? normalized.slice(slashIndex + 1) : normalized;
|
|
14570
|
+
addDerivedAliases(normalized, aliases);
|
|
14571
|
+
addDerivedAliases(afterProvider, aliases);
|
|
14572
|
+
return [...aliases].filter((alias) => alias.length > 0);
|
|
14573
|
+
}
|
|
14574
|
+
|
|
14575
|
+
// src/cli/precedence-resolver.ts
|
|
14576
|
+
function dedupe(models) {
|
|
14577
|
+
const seen = new Set;
|
|
14578
|
+
const result = [];
|
|
14579
|
+
for (const model of models) {
|
|
14580
|
+
if (!model || seen.has(model))
|
|
14581
|
+
continue;
|
|
14582
|
+
seen.add(model);
|
|
14583
|
+
result.push(model);
|
|
14584
|
+
}
|
|
14585
|
+
return result;
|
|
14586
|
+
}
|
|
14587
|
+
function buildLayerOrder(input) {
|
|
14588
|
+
return [
|
|
14589
|
+
{
|
|
14590
|
+
layer: "opencode-direct-override",
|
|
14591
|
+
models: input.openCodeDirectOverride ? [input.openCodeDirectOverride] : []
|
|
14592
|
+
},
|
|
14593
|
+
{
|
|
14594
|
+
layer: "manual-user-plan",
|
|
14595
|
+
models: input.manualUserPlan ?? []
|
|
14596
|
+
},
|
|
14597
|
+
{
|
|
14598
|
+
layer: "pinned-model",
|
|
14599
|
+
models: input.pinnedModel ? [input.pinnedModel] : []
|
|
14600
|
+
},
|
|
14601
|
+
{
|
|
14602
|
+
layer: "dynamic-recommendation",
|
|
14603
|
+
models: input.dynamicRecommendation ?? []
|
|
14604
|
+
},
|
|
14605
|
+
{
|
|
14606
|
+
layer: "provider-fallback-policy",
|
|
14607
|
+
models: input.providerFallbackPolicy ?? []
|
|
14608
|
+
},
|
|
14609
|
+
{
|
|
14610
|
+
layer: "system-default",
|
|
14611
|
+
models: input.systemDefault
|
|
14612
|
+
}
|
|
14613
|
+
];
|
|
14614
|
+
}
|
|
14615
|
+
function resolveAgentWithPrecedence(input) {
|
|
14616
|
+
const ordered = buildLayerOrder(input);
|
|
14617
|
+
const firstWinningIndex = ordered.findIndex((layer) => layer.models.length > 0);
|
|
14618
|
+
const winnerIndex = firstWinningIndex >= 0 ? firstWinningIndex : ordered.length - 1;
|
|
14619
|
+
const winnerLayer = ordered[winnerIndex];
|
|
14620
|
+
const chain = dedupe(ordered.slice(winnerIndex).flatMap((layer) => layer.models).concat(input.systemDefault));
|
|
14621
|
+
const model = chain[0] ?? input.systemDefault[0] ?? "opencode/big-pickle";
|
|
14622
|
+
return {
|
|
14623
|
+
model,
|
|
14624
|
+
chain,
|
|
14625
|
+
provenance: {
|
|
14626
|
+
winnerLayer: winnerLayer?.layer ?? "system-default",
|
|
14627
|
+
winnerModel: model
|
|
14628
|
+
}
|
|
14629
|
+
};
|
|
14630
|
+
}
|
|
14631
|
+
|
|
14632
|
+
// src/cli/scoring-v2/features.ts
|
|
14633
|
+
function modelLookupKeys(model) {
|
|
14634
|
+
return buildModelKeyAliases(model.model);
|
|
14635
|
+
}
|
|
14636
|
+
function findSignal(model, externalSignals) {
|
|
14637
|
+
if (!externalSignals)
|
|
14638
|
+
return;
|
|
14639
|
+
return modelLookupKeys(model).map((key) => externalSignals[key]).find((item) => item !== undefined);
|
|
14640
|
+
}
|
|
14641
|
+
function statusValue(status) {
|
|
14642
|
+
if (status === "active")
|
|
14643
|
+
return 1;
|
|
14644
|
+
if (status === "beta")
|
|
14645
|
+
return 0.4;
|
|
14646
|
+
if (status === "alpha")
|
|
14647
|
+
return -0.25;
|
|
14648
|
+
return -1;
|
|
14649
|
+
}
|
|
14650
|
+
function capability(value) {
|
|
14651
|
+
return value ? 1 : 0;
|
|
14652
|
+
}
|
|
14653
|
+
function blendedPrice(signal) {
|
|
14654
|
+
if (!signal)
|
|
14655
|
+
return 0;
|
|
14656
|
+
if (signal.inputPricePer1M !== undefined && signal.outputPricePer1M !== undefined) {
|
|
14657
|
+
return signal.inputPricePer1M * 0.75 + signal.outputPricePer1M * 0.25;
|
|
14658
|
+
}
|
|
14659
|
+
return signal.inputPricePer1M ?? signal.outputPricePer1M ?? 0;
|
|
14660
|
+
}
|
|
14661
|
+
function kimiVersionBonus(agent, model) {
|
|
14662
|
+
const lowered = `${model.model} ${model.name}`.toLowerCase();
|
|
14663
|
+
const isChutes = model.providerID === "chutes";
|
|
14664
|
+
const isQwen3 = isChutes && /qwen3/.test(lowered);
|
|
14665
|
+
const isKimiK25 = /kimi-k2\.5|k2\.5/.test(lowered);
|
|
14666
|
+
const isMinimaxM21 = isChutes && /minimax[-_ ]?m2\.1/.test(lowered);
|
|
14667
|
+
const qwenPenalty = {
|
|
14668
|
+
orchestrator: -6,
|
|
14669
|
+
oracle: -6,
|
|
14670
|
+
designer: -8,
|
|
14671
|
+
explorer: -6,
|
|
14672
|
+
librarian: -12,
|
|
14673
|
+
fixer: -12
|
|
14674
|
+
};
|
|
14675
|
+
const kimiBonus = {
|
|
14676
|
+
orchestrator: 1,
|
|
14677
|
+
oracle: 1,
|
|
14678
|
+
designer: 3,
|
|
14679
|
+
explorer: 2,
|
|
14680
|
+
librarian: 2,
|
|
14681
|
+
fixer: 3
|
|
14682
|
+
};
|
|
14683
|
+
const minimaxBonus = {
|
|
14684
|
+
orchestrator: 1,
|
|
14685
|
+
oracle: 1,
|
|
14686
|
+
designer: 2,
|
|
14687
|
+
explorer: 4,
|
|
14688
|
+
librarian: 4,
|
|
14689
|
+
fixer: 4
|
|
14690
|
+
};
|
|
14691
|
+
if (isQwen3)
|
|
14692
|
+
return qwenPenalty[agent];
|
|
14693
|
+
if (isKimiK25)
|
|
14694
|
+
return kimiBonus[agent];
|
|
14695
|
+
if (isMinimaxM21)
|
|
14696
|
+
return minimaxBonus[agent];
|
|
14697
|
+
return 0;
|
|
14698
|
+
}
|
|
14699
|
+
function extractFeatureVector(model, agent, externalSignals) {
|
|
14700
|
+
const signal = findSignal(model, externalSignals);
|
|
14701
|
+
const latency = signal?.latencySeconds ?? 0;
|
|
14702
|
+
const normalizedContext = Math.min(model.contextLimit, 1e6) / 1e5;
|
|
14703
|
+
const normalizedOutput = Math.min(model.outputLimit, 300000) / 30000;
|
|
14704
|
+
const designerOutputScore = model.outputLimit < 64000 ? -1 : 0;
|
|
14705
|
+
const versionBonus = kimiVersionBonus(agent, model);
|
|
14706
|
+
const quality = (signal?.qualityScore ?? 0) / 100;
|
|
14707
|
+
const coding = (signal?.codingScore ?? 0) / 100;
|
|
14708
|
+
const pricePenalty = Math.min(blendedPrice(signal), 50) / 10;
|
|
14709
|
+
const explorerLatencyMultiplier = agent === "explorer" ? 1.4 : 1;
|
|
14710
|
+
return {
|
|
14711
|
+
status: statusValue(model.status),
|
|
14712
|
+
context: normalizedContext,
|
|
14713
|
+
output: agent === "designer" ? designerOutputScore : normalizedOutput,
|
|
14714
|
+
versionBonus,
|
|
14715
|
+
reasoning: capability(model.reasoning),
|
|
14716
|
+
toolcall: capability(model.toolcall),
|
|
14717
|
+
attachment: capability(model.attachment),
|
|
14718
|
+
quality,
|
|
14719
|
+
coding,
|
|
14720
|
+
latencyPenalty: Math.min(latency, 20) * explorerLatencyMultiplier,
|
|
14721
|
+
pricePenalty
|
|
14722
|
+
};
|
|
14723
|
+
}
|
|
14724
|
+
|
|
14725
|
+
// src/cli/scoring-v2/weights.ts
|
|
14726
|
+
var BASE_WEIGHTS = {
|
|
14727
|
+
status: 22,
|
|
14728
|
+
context: 6,
|
|
14729
|
+
output: 6,
|
|
14730
|
+
versionBonus: 8,
|
|
14731
|
+
reasoning: 10,
|
|
14732
|
+
toolcall: 16,
|
|
14733
|
+
attachment: 2,
|
|
14734
|
+
quality: 14,
|
|
14735
|
+
coding: 18,
|
|
14736
|
+
latencyPenalty: -3,
|
|
14737
|
+
pricePenalty: -2
|
|
14738
|
+
};
|
|
14739
|
+
var AGENT_WEIGHT_OVERRIDES = {
|
|
14740
|
+
orchestrator: {
|
|
14741
|
+
reasoning: 22,
|
|
14742
|
+
toolcall: 22,
|
|
14743
|
+
quality: 16,
|
|
14744
|
+
coding: 16,
|
|
14745
|
+
latencyPenalty: -2
|
|
14746
|
+
},
|
|
14747
|
+
oracle: {
|
|
14748
|
+
reasoning: 26,
|
|
14749
|
+
quality: 20,
|
|
14750
|
+
coding: 18,
|
|
14751
|
+
latencyPenalty: -2,
|
|
14752
|
+
output: 7
|
|
14753
|
+
},
|
|
14754
|
+
designer: {
|
|
14755
|
+
attachment: 12,
|
|
14756
|
+
output: 10,
|
|
14757
|
+
quality: 16,
|
|
14758
|
+
coding: 10
|
|
14759
|
+
},
|
|
14760
|
+
explorer: {
|
|
14761
|
+
latencyPenalty: -8,
|
|
14762
|
+
toolcall: 24,
|
|
14763
|
+
reasoning: 2,
|
|
14764
|
+
context: 4,
|
|
14765
|
+
output: 4
|
|
14766
|
+
},
|
|
14767
|
+
librarian: {
|
|
14768
|
+
context: 14,
|
|
14769
|
+
output: 10,
|
|
14770
|
+
quality: 18,
|
|
14771
|
+
coding: 14
|
|
14772
|
+
},
|
|
14773
|
+
fixer: {
|
|
14774
|
+
coding: 28,
|
|
14775
|
+
toolcall: 22,
|
|
14776
|
+
reasoning: 12,
|
|
14777
|
+
output: 10
|
|
14778
|
+
}
|
|
14779
|
+
};
|
|
14780
|
+
function getFeatureWeights(agent) {
|
|
14781
|
+
return {
|
|
14782
|
+
...BASE_WEIGHTS,
|
|
14783
|
+
...AGENT_WEIGHT_OVERRIDES[agent]
|
|
14784
|
+
};
|
|
14785
|
+
}
|
|
14786
|
+
|
|
14787
|
+
// src/cli/scoring-v2/engine.ts
|
|
14788
|
+
function weightedFeatures(features, weights) {
|
|
14789
|
+
return {
|
|
14790
|
+
status: features.status * weights.status,
|
|
14791
|
+
context: features.context * weights.context,
|
|
14792
|
+
output: features.output * weights.output,
|
|
14793
|
+
versionBonus: features.versionBonus * weights.versionBonus,
|
|
14794
|
+
reasoning: features.reasoning * weights.reasoning,
|
|
14795
|
+
toolcall: features.toolcall * weights.toolcall,
|
|
14796
|
+
attachment: features.attachment * weights.attachment,
|
|
14797
|
+
quality: features.quality * weights.quality,
|
|
14798
|
+
coding: features.coding * weights.coding,
|
|
14799
|
+
latencyPenalty: features.latencyPenalty * weights.latencyPenalty,
|
|
14800
|
+
pricePenalty: features.pricePenalty * weights.pricePenalty
|
|
14801
|
+
};
|
|
14802
|
+
}
|
|
14803
|
+
function sumFeatures(features) {
|
|
14804
|
+
return features.status + features.context + features.output + features.versionBonus + features.reasoning + features.toolcall + features.attachment + features.quality + features.coding + features.latencyPenalty + features.pricePenalty;
|
|
14805
|
+
}
|
|
14806
|
+
function withStableTieBreak(left, right) {
|
|
14807
|
+
if (left.totalScore !== right.totalScore) {
|
|
14808
|
+
return right.totalScore - left.totalScore;
|
|
14809
|
+
}
|
|
14810
|
+
const providerDelta = left.model.providerID.localeCompare(right.model.providerID);
|
|
14811
|
+
if (providerDelta !== 0) {
|
|
14812
|
+
return providerDelta;
|
|
14813
|
+
}
|
|
14814
|
+
return left.model.model.localeCompare(right.model.model);
|
|
14815
|
+
}
|
|
14816
|
+
function scoreCandidateV2(model, agent, externalSignals) {
|
|
14817
|
+
const features = extractFeatureVector(model, agent, externalSignals);
|
|
14818
|
+
const weights = getFeatureWeights(agent);
|
|
14819
|
+
const weighted = weightedFeatures(features, weights);
|
|
14820
|
+
return {
|
|
14821
|
+
model,
|
|
14822
|
+
totalScore: Math.round(sumFeatures(weighted) * 1000) / 1000,
|
|
14823
|
+
scoreBreakdown: {
|
|
14824
|
+
features,
|
|
14825
|
+
weighted
|
|
14826
|
+
}
|
|
14827
|
+
};
|
|
14828
|
+
}
|
|
14829
|
+
function rankModelsV2(models, agent, externalSignals) {
|
|
14830
|
+
return models.map((model) => scoreCandidateV2(model, agent, externalSignals)).sort(withStableTieBreak);
|
|
14831
|
+
}
|
|
14465
14832
|
// src/cli/dynamic-model-selection.ts
|
|
14466
14833
|
var AGENTS = [
|
|
14467
14834
|
"orchestrator",
|
|
@@ -14471,6 +14838,15 @@ var AGENTS = [
|
|
|
14471
14838
|
"librarian",
|
|
14472
14839
|
"fixer"
|
|
14473
14840
|
];
|
|
14841
|
+
var FREE_BIASED_PROVIDERS = new Set(["opencode"]);
|
|
14842
|
+
var PRIMARY_ASSIGNMENT_ORDER = [
|
|
14843
|
+
"oracle",
|
|
14844
|
+
"orchestrator",
|
|
14845
|
+
"fixer",
|
|
14846
|
+
"designer",
|
|
14847
|
+
"librarian",
|
|
14848
|
+
"explorer"
|
|
14849
|
+
];
|
|
14474
14850
|
var ROLE_VARIANT = {
|
|
14475
14851
|
orchestrator: undefined,
|
|
14476
14852
|
oracle: "high",
|
|
@@ -14511,17 +14887,156 @@ function statusScore(status) {
|
|
|
14511
14887
|
return -5;
|
|
14512
14888
|
return -40;
|
|
14513
14889
|
}
|
|
14514
|
-
function
|
|
14890
|
+
function toVersionTuple(major, minor, patch) {
|
|
14891
|
+
return [
|
|
14892
|
+
Number.parseInt(major, 10) || 0,
|
|
14893
|
+
Number.parseInt(minor ?? "0", 10) || 0,
|
|
14894
|
+
Number.parseInt(patch ?? "0", 10) || 0
|
|
14895
|
+
];
|
|
14896
|
+
}
|
|
14897
|
+
function compareVersionTuple(a, b) {
|
|
14898
|
+
if (a[0] !== b[0])
|
|
14899
|
+
return a[0] - b[0];
|
|
14900
|
+
if (a[1] !== b[1])
|
|
14901
|
+
return a[1] - b[1];
|
|
14902
|
+
return a[2] - b[2];
|
|
14903
|
+
}
|
|
14904
|
+
function extractVersionFamily(model) {
|
|
14905
|
+
const text = `${model.model} ${model.name}`.toLowerCase();
|
|
14906
|
+
const gpt = text.match(/\bgpt[-_ ]?(\d+)(?:[.-](\d+))?(?:[.-](\d+))?\b/);
|
|
14907
|
+
if (gpt) {
|
|
14908
|
+
return {
|
|
14909
|
+
family: "gpt",
|
|
14910
|
+
version: toVersionTuple(gpt[1] ?? "0", gpt[2], gpt[3]),
|
|
14911
|
+
confidence: 1,
|
|
14912
|
+
prereleasePenalty: /preview|experimental|exp|\brc\b/.test(text) ? -2 : 0
|
|
14913
|
+
};
|
|
14914
|
+
}
|
|
14915
|
+
const gemini = text.match(/\bgemini[-_ ]?(\d+)(?:[.-](\d+))?(?:[.-](\d+))?\b/);
|
|
14916
|
+
if (gemini) {
|
|
14917
|
+
return {
|
|
14918
|
+
family: "gemini",
|
|
14919
|
+
version: toVersionTuple(gemini[1] ?? "0", gemini[2], gemini[3]),
|
|
14920
|
+
confidence: 1,
|
|
14921
|
+
prereleasePenalty: /preview|experimental|exp|\brc\b/.test(text) ? -2 : 0
|
|
14922
|
+
};
|
|
14923
|
+
}
|
|
14924
|
+
const kimi = text.match(/\bkimi[-_ ]?k(\d+)(?:[.-]?(\d+))?(?:[.-](\d+))?\b/);
|
|
14925
|
+
if (kimi) {
|
|
14926
|
+
return {
|
|
14927
|
+
family: "kimi-k",
|
|
14928
|
+
version: toVersionTuple(kimi[1] ?? "0", kimi[2], kimi[3]),
|
|
14929
|
+
confidence: 1,
|
|
14930
|
+
prereleasePenalty: /preview|experimental|exp|\brc\b/.test(text) ? -2 : 0
|
|
14931
|
+
};
|
|
14932
|
+
}
|
|
14933
|
+
const generic = text.match(/\b([a-z][a-z0-9-]{1,20})[-_ ](\d+)(?:[.-](\d+))?(?:[.-](\d+))?\b/);
|
|
14934
|
+
if (generic) {
|
|
14935
|
+
return {
|
|
14936
|
+
family: generic[1] ?? "generic",
|
|
14937
|
+
version: toVersionTuple(generic[2] ?? "0", generic[3], generic[4]),
|
|
14938
|
+
confidence: 0.7,
|
|
14939
|
+
prereleasePenalty: /preview|experimental|exp|\brc\b/.test(text) ? -2 : 0
|
|
14940
|
+
};
|
|
14941
|
+
}
|
|
14942
|
+
return null;
|
|
14943
|
+
}
|
|
14944
|
+
function getVersionRecencyMap(models) {
|
|
14945
|
+
const familyVersions = new Map;
|
|
14946
|
+
const modelInfo = new Map;
|
|
14947
|
+
for (const model of models) {
|
|
14948
|
+
const info = extractVersionFamily(model);
|
|
14949
|
+
if (!info)
|
|
14950
|
+
continue;
|
|
14951
|
+
modelInfo.set(model.model, info);
|
|
14952
|
+
const current = familyVersions.get(info.family) ?? [];
|
|
14953
|
+
current.push(info.version);
|
|
14954
|
+
familyVersions.set(info.family, current);
|
|
14955
|
+
}
|
|
14956
|
+
const recencyMap = {};
|
|
14957
|
+
for (const model of models) {
|
|
14958
|
+
const info = modelInfo.get(model.model);
|
|
14959
|
+
if (!info) {
|
|
14960
|
+
recencyMap[model.model] = 0;
|
|
14961
|
+
continue;
|
|
14962
|
+
}
|
|
14963
|
+
const versions2 = familyVersions.get(info.family) ?? [];
|
|
14964
|
+
const unique = versions2.map((tuple2) => `${tuple2[0]}.${tuple2[1]}.${tuple2[2]}`).filter((value, index2, arr) => arr.indexOf(value) === index2).map((value) => {
|
|
14965
|
+
const [major, minor, patch] = value.split(".").map((v) => Number.parseInt(v, 10) || 0);
|
|
14966
|
+
return [major, minor, patch];
|
|
14967
|
+
}).sort(compareVersionTuple);
|
|
14968
|
+
if (unique.length === 0) {
|
|
14969
|
+
recencyMap[model.model] = 0;
|
|
14970
|
+
continue;
|
|
14971
|
+
}
|
|
14972
|
+
const index = unique.findIndex((tuple2) => compareVersionTuple(tuple2, info.version) === 0);
|
|
14973
|
+
const percentile = unique.length === 1 ? 0.5 : index / (unique.length - 1);
|
|
14974
|
+
const raw = -3 + percentile * (12 - -3);
|
|
14975
|
+
const final = Math.max(-3, Math.min(12, raw * info.confidence + info.prereleasePenalty));
|
|
14976
|
+
recencyMap[model.model] = final;
|
|
14977
|
+
}
|
|
14978
|
+
return recencyMap;
|
|
14979
|
+
}
|
|
14980
|
+
function baseScore(model, versionRecencyBoost = 0) {
|
|
14515
14981
|
const lowered = `${model.model} ${model.name}`.toLowerCase();
|
|
14516
14982
|
const context = Math.min(model.contextLimit, 1e6) / 50000;
|
|
14517
14983
|
const output = Math.min(model.outputLimit, 300000) / 30000;
|
|
14518
14984
|
const deep = tokenScore(lowered, /(opus|pro|thinking|reason|r1|gpt-5|k2\.5)/i, 12);
|
|
14519
|
-
const fast = tokenScore(lowered, /(nano|flash|mini|lite|fast|turbo|haiku|small)/i,
|
|
14985
|
+
const fast = tokenScore(lowered, /(nano|flash|mini|lite|fast|turbo|haiku|small)/i, 4);
|
|
14520
14986
|
const code = tokenScore(lowered, /(codex|coder|code|dev|program)/i, 12);
|
|
14521
|
-
|
|
14522
|
-
|
|
14987
|
+
return statusScore(model.status) + context + output + deep + fast + code + versionRecencyBoost + (model.toolcall ? 25 : 0);
|
|
14988
|
+
}
|
|
14989
|
+
function hasFlashToken(model) {
|
|
14990
|
+
return /flash/i.test(`${model.model} ${model.name}`);
|
|
14523
14991
|
}
|
|
14524
|
-
function
|
|
14992
|
+
function isZai47Model(model) {
|
|
14993
|
+
return model.providerID === "zai-coding-plan" && /glm-4\.7/i.test(`${model.model} ${model.name}`);
|
|
14994
|
+
}
|
|
14995
|
+
function isKimiK25Model(model) {
|
|
14996
|
+
return /kimi-k2\.?5|k2\.?5/i.test(`${model.model} ${model.name}`);
|
|
14997
|
+
}
|
|
14998
|
+
function geminiPreferenceAdjustment(_agent, model) {
|
|
14999
|
+
const lowered = `${model.model} ${model.name}`.toLowerCase();
|
|
15000
|
+
const isGemini25Pro = /gemini-2\.5-pro/.test(lowered);
|
|
15001
|
+
return isGemini25Pro ? -14 : 0;
|
|
15002
|
+
}
|
|
15003
|
+
function chutesPreferenceAdjustment(agent, model) {
|
|
15004
|
+
if (model.providerID !== "chutes")
|
|
15005
|
+
return 0;
|
|
15006
|
+
const lowered = `${model.model} ${model.name}`.toLowerCase();
|
|
15007
|
+
const isQwen3 = /qwen3/.test(lowered);
|
|
15008
|
+
const isKimiK25 = /kimi-k2\.5|k2\.5/.test(lowered);
|
|
15009
|
+
const isMinimaxM21 = /minimax[-_ ]?m2\.1/.test(lowered);
|
|
15010
|
+
const qwenPenalty = {
|
|
15011
|
+
oracle: -12,
|
|
15012
|
+
orchestrator: -10,
|
|
15013
|
+
fixer: -22,
|
|
15014
|
+
designer: -14,
|
|
15015
|
+
librarian: -18,
|
|
15016
|
+
explorer: -10
|
|
15017
|
+
};
|
|
15018
|
+
const kimiBonus = {
|
|
15019
|
+
oracle: 0,
|
|
15020
|
+
orchestrator: 0,
|
|
15021
|
+
fixer: 8,
|
|
15022
|
+
designer: 6,
|
|
15023
|
+
librarian: 5,
|
|
15024
|
+
explorer: 4
|
|
15025
|
+
};
|
|
15026
|
+
const minimaxBonus = {
|
|
15027
|
+
oracle: 0,
|
|
15028
|
+
orchestrator: 0,
|
|
15029
|
+
fixer: 10,
|
|
15030
|
+
designer: 3,
|
|
15031
|
+
librarian: 9,
|
|
15032
|
+
explorer: 12
|
|
15033
|
+
};
|
|
15034
|
+
return (isQwen3 ? qwenPenalty[agent] : 0) + (isKimiK25 ? kimiBonus[agent] : 0) + (isMinimaxM21 ? minimaxBonus[agent] : 0);
|
|
15035
|
+
}
|
|
15036
|
+
function modelLookupKeys2(model) {
|
|
15037
|
+
return buildModelKeyAliases(model.model);
|
|
15038
|
+
}
|
|
15039
|
+
function roleScore(agent, model, versionRecencyBoost = 0) {
|
|
14525
15040
|
const lowered = `${model.model} ${model.name}`.toLowerCase();
|
|
14526
15041
|
const reasoning = model.reasoning ? 1 : 0;
|
|
14527
15042
|
const toolcall = model.toolcall ? 1 : 0;
|
|
@@ -14537,28 +15052,295 @@ function roleScore(agent, model) {
|
|
|
14537
15052
|
if (model.status === "deprecated") {
|
|
14538
15053
|
return -5000;
|
|
14539
15054
|
}
|
|
14540
|
-
const score = baseScore(model);
|
|
15055
|
+
const score = baseScore(model, versionRecencyBoost);
|
|
15056
|
+
const flash = hasFlashToken(model);
|
|
15057
|
+
const isZai47 = isZai47Model(model);
|
|
15058
|
+
const zai47Flash = isZai47 && flash;
|
|
15059
|
+
const zai47NonFlash = isZai47 && !flash;
|
|
15060
|
+
const providerBias = model.providerID === "openai" ? 3 : model.providerID === "anthropic" ? 3 : model.providerID === "kimi-for-coding" ? 2 : model.providerID === "google" ? 2 : model.providerID === "github-copilot" ? 1 : model.providerID === "zai-coding-plan" ? 0 : model.providerID === "chutes" ? 2 : model.providerID === "opencode" ? -2 : 0;
|
|
15061
|
+
const geminiAdjustment = geminiPreferenceAdjustment(agent, model);
|
|
15062
|
+
const chutesAdjustment = chutesPreferenceAdjustment(agent, model);
|
|
14541
15063
|
if (agent === "orchestrator") {
|
|
14542
|
-
|
|
15064
|
+
const flashAdjustment2 = flash ? -22 : 0;
|
|
15065
|
+
const zaiAdjustment2 = zai47NonFlash ? 16 : zai47Flash ? -18 : 0;
|
|
15066
|
+
const nonReasoningFlashPenalty2 = flash && !model.reasoning ? -16 : 0;
|
|
15067
|
+
return score + reasoning * 40 + toolcall * 25 + deep * 10 + code * 8 + context + flashAdjustment2 + zaiAdjustment2 + nonReasoningFlashPenalty2 + geminiAdjustment + chutesAdjustment + providerBias;
|
|
14543
15068
|
}
|
|
14544
15069
|
if (agent === "oracle") {
|
|
14545
|
-
|
|
15070
|
+
const flashAdjustment2 = flash ? -34 : 0;
|
|
15071
|
+
const zaiAdjustment2 = zai47NonFlash ? 16 : zai47Flash ? -18 : 0;
|
|
15072
|
+
const nonReasoningFlashPenalty2 = flash && !model.reasoning ? -16 : 0;
|
|
15073
|
+
return score + reasoning * 55 + deep * 18 + context * 1.2 + toolcall * 10 + flashAdjustment2 + zaiAdjustment2 + nonReasoningFlashPenalty2 + geminiAdjustment + chutesAdjustment + providerBias;
|
|
14546
15074
|
}
|
|
14547
15075
|
if (agent === "designer") {
|
|
14548
|
-
|
|
15076
|
+
const flashAdjustment2 = flash ? -8 : 0;
|
|
15077
|
+
const zaiAdjustment2 = zai47NonFlash ? 10 : zai47Flash ? -8 : 0;
|
|
15078
|
+
return score + attachment * 25 + reasoning * 18 + toolcall * 15 + context * 0.8 + output + flashAdjustment2 + zaiAdjustment2 + geminiAdjustment + chutesAdjustment + providerBias;
|
|
14549
15079
|
}
|
|
14550
15080
|
if (agent === "explorer") {
|
|
14551
|
-
|
|
15081
|
+
const flashAdjustment2 = flash ? 26 : -10;
|
|
15082
|
+
const zaiAdjustment2 = zai47NonFlash ? 2 : zai47Flash ? 6 : 0;
|
|
15083
|
+
const deepPenalty = deep * -18;
|
|
15084
|
+
return score + fast * 68 + toolcall * 28 + reasoning * 2 + context * 0.2 + flashAdjustment2 + zaiAdjustment2 + deepPenalty + geminiAdjustment + chutesAdjustment + providerBias;
|
|
14552
15085
|
}
|
|
14553
15086
|
if (agent === "librarian") {
|
|
14554
|
-
|
|
15087
|
+
const flashAdjustment2 = flash ? -12 : 0;
|
|
15088
|
+
const zaiAdjustment2 = zai47NonFlash ? 16 : zai47Flash ? -18 : 0;
|
|
15089
|
+
return score + context * 30 + toolcall * 22 + reasoning * 15 + output * 10 + flashAdjustment2 + zaiAdjustment2 + geminiAdjustment + chutesAdjustment + providerBias;
|
|
15090
|
+
}
|
|
15091
|
+
const flashAdjustment = flash ? -18 : 0;
|
|
15092
|
+
const zaiAdjustment = zai47NonFlash ? 16 : zai47Flash ? -18 : 0;
|
|
15093
|
+
const nonReasoningFlashPenalty = flash && !model.reasoning ? -16 : 0;
|
|
15094
|
+
return score + code * 28 + toolcall * 24 + fast * 18 + reasoning * 14 + output * 8 + flashAdjustment + zaiAdjustment + nonReasoningFlashPenalty + geminiAdjustment + chutesAdjustment + providerBias;
|
|
15095
|
+
}
|
|
15096
|
+
function getExternalSignalBoost(agent, model, externalSignals) {
|
|
15097
|
+
if (!externalSignals)
|
|
15098
|
+
return 0;
|
|
15099
|
+
const signal = modelLookupKeys2(model).map((key) => externalSignals[key]).find((item) => item !== undefined);
|
|
15100
|
+
if (!signal)
|
|
15101
|
+
return 0;
|
|
15102
|
+
const qualityScore = signal.qualityScore ?? 0;
|
|
15103
|
+
const codingScore = signal.codingScore ?? 0;
|
|
15104
|
+
const latencySeconds = signal.latencySeconds;
|
|
15105
|
+
const blendedPrice2 = signal.inputPricePer1M !== undefined && signal.outputPricePer1M !== undefined ? signal.inputPricePer1M * 0.75 + signal.outputPricePer1M * 0.25 : signal.inputPricePer1M ?? signal.outputPricePer1M ?? 0;
|
|
15106
|
+
if (agent === "explorer") {
|
|
15107
|
+
const qualityBoost2 = qualityScore * 0.05;
|
|
15108
|
+
const codingBoost2 = codingScore * 0.08;
|
|
15109
|
+
const latencyPenalty2 = typeof latencySeconds === "number" && Number.isFinite(latencySeconds) ? Math.min(latencySeconds, 12) * 3.2 + (latencySeconds > 7 ? 16 : latencySeconds > 4 ? 10 : 0) : 0;
|
|
15110
|
+
const pricePenalty2 = Math.min(blendedPrice2, 30) * 0.03;
|
|
15111
|
+
const qualityFloorPenalty = qualityScore > 0 && qualityScore < 35 ? (35 - qualityScore) * 0.8 : 0;
|
|
15112
|
+
const boost2 = qualityBoost2 + codingBoost2 - latencyPenalty2 - pricePenalty2 - qualityFloorPenalty;
|
|
15113
|
+
return Math.max(-90, Math.min(25, boost2));
|
|
15114
|
+
}
|
|
15115
|
+
const qualityBoost = qualityScore * 0.16;
|
|
15116
|
+
const codingBoost = codingScore * 0.24;
|
|
15117
|
+
const latencyPenalty = typeof latencySeconds === "number" && Number.isFinite(latencySeconds) ? Math.min(latencySeconds, 25) * 0.22 : 0;
|
|
15118
|
+
const pricePenalty = Math.min(blendedPrice2, 30) * 0.08;
|
|
15119
|
+
const boost = qualityBoost + codingBoost - latencyPenalty - pricePenalty;
|
|
15120
|
+
return Math.max(-30, Math.min(45, boost));
|
|
15121
|
+
}
|
|
15122
|
+
function rankModels2(models, agent, externalSignals) {
|
|
15123
|
+
const versionRecencyMap = getVersionRecencyMap(models);
|
|
15124
|
+
return [...models].sort((a, b) => {
|
|
15125
|
+
const scoreA = roleScore(agent, a, versionRecencyMap[a.model] ?? 0) + getExternalSignalBoost(agent, a, externalSignals);
|
|
15126
|
+
const scoreB = roleScore(agent, b, versionRecencyMap[b.model] ?? 0) + getExternalSignalBoost(agent, b, externalSignals);
|
|
15127
|
+
const scoreDelta = scoreB - scoreA;
|
|
15128
|
+
if (scoreDelta !== 0)
|
|
15129
|
+
return scoreDelta;
|
|
15130
|
+
const providerTieBreak = a.providerID.localeCompare(b.providerID);
|
|
15131
|
+
if (providerTieBreak !== 0)
|
|
15132
|
+
return providerTieBreak;
|
|
15133
|
+
return a.model.localeCompare(b.model);
|
|
15134
|
+
});
|
|
15135
|
+
}
|
|
15136
|
+
function combinedScore(agent, model, externalSignals, versionRecencyMap) {
|
|
15137
|
+
return roleScore(agent, model, versionRecencyMap?.[model.model] ?? 0) + getExternalSignalBoost(agent, model, externalSignals);
|
|
15138
|
+
}
|
|
15139
|
+
function effectiveEngine(engineVersion) {
|
|
15140
|
+
return engineVersion === "v2" ? "v2" : "v1";
|
|
15141
|
+
}
|
|
15142
|
+
function scoreForEngine(engineVersion, agent, model, externalSignals, versionRecencyMap) {
|
|
15143
|
+
if (effectiveEngine(engineVersion) === "v2") {
|
|
15144
|
+
return scoreCandidateV2(model, agent, externalSignals).totalScore;
|
|
15145
|
+
}
|
|
15146
|
+
return combinedScore(agent, model, externalSignals, versionRecencyMap);
|
|
15147
|
+
}
|
|
15148
|
+
function selectTopModelsPerProvider(models, engineVersion, externalSignals, versionRecencyMap) {
|
|
15149
|
+
const byProvider = new Map;
|
|
15150
|
+
for (const model of models) {
|
|
15151
|
+
const current = byProvider.get(model.providerID) ?? [];
|
|
15152
|
+
current.push(model);
|
|
15153
|
+
byProvider.set(model.providerID, current);
|
|
15154
|
+
}
|
|
15155
|
+
const selected = [];
|
|
15156
|
+
for (const providerModels of byProvider.values()) {
|
|
15157
|
+
if (providerModels.length <= 2) {
|
|
15158
|
+
selected.push(...providerModels);
|
|
15159
|
+
continue;
|
|
15160
|
+
}
|
|
15161
|
+
const ranked = [...providerModels].map((model) => {
|
|
15162
|
+
const total = AGENTS.reduce((sum, agent) => {
|
|
15163
|
+
return sum + scoreForEngine(engineVersion, agent, model, externalSignals, versionRecencyMap);
|
|
15164
|
+
}, 0);
|
|
15165
|
+
return {
|
|
15166
|
+
model,
|
|
15167
|
+
score: total / AGENTS.length
|
|
15168
|
+
};
|
|
15169
|
+
}).sort((a, b) => {
|
|
15170
|
+
if (a.score !== b.score)
|
|
15171
|
+
return b.score - a.score;
|
|
15172
|
+
return a.model.model.localeCompare(b.model.model);
|
|
15173
|
+
}).slice(0, 2).map((entry) => entry.model);
|
|
15174
|
+
selected.push(...ranked);
|
|
15175
|
+
}
|
|
15176
|
+
return selected;
|
|
15177
|
+
}
|
|
15178
|
+
function countProviderUsage(agents) {
|
|
15179
|
+
const counts = new Map;
|
|
15180
|
+
for (const assignment of Object.values(agents)) {
|
|
15181
|
+
const provider = assignment.model.split("/")[0];
|
|
15182
|
+
if (!provider)
|
|
15183
|
+
continue;
|
|
15184
|
+
counts.set(provider, (counts.get(provider) ?? 0) + 1);
|
|
14555
15185
|
}
|
|
14556
|
-
return
|
|
15186
|
+
return counts;
|
|
14557
15187
|
}
|
|
14558
|
-
function
|
|
14559
|
-
|
|
15188
|
+
function rebalanceForSubscriptionMode(agents, chains, provenance, paidProviders, getRankedModels, getPinnedModelForProvider, targetByProvider, externalSignals, versionRecencyMap, engineVersion) {
|
|
15189
|
+
if (paidProviders.length <= 1)
|
|
15190
|
+
return;
|
|
15191
|
+
const MAX_ALLOWED_SCORE_LOSS = 20;
|
|
15192
|
+
while (true) {
|
|
15193
|
+
const providerUsage = countProviderUsage(agents);
|
|
15194
|
+
const underProviders = paidProviders.filter((providerID) => (providerUsage.get(providerID) ?? 0) < (targetByProvider[providerID] ?? 0));
|
|
15195
|
+
const overProviders = paidProviders.filter((providerID) => (providerUsage.get(providerID) ?? 0) > (targetByProvider[providerID] ?? 0));
|
|
15196
|
+
if (underProviders.length === 0 || overProviders.length === 0)
|
|
15197
|
+
break;
|
|
15198
|
+
let bestSwap;
|
|
15199
|
+
for (const agent of PRIMARY_ASSIGNMENT_ORDER) {
|
|
15200
|
+
const currentModelID = agents[agent]?.model;
|
|
15201
|
+
if (!currentModelID)
|
|
15202
|
+
continue;
|
|
15203
|
+
const currentProvider = currentModelID.split("/")[0];
|
|
15204
|
+
if (!currentProvider || !overProviders.includes(currentProvider))
|
|
15205
|
+
continue;
|
|
15206
|
+
const ranked = getRankedModels(agent);
|
|
15207
|
+
const currentModel = ranked.find((model) => model.model === currentModelID) ?? ranked.find((model) => model.providerID === currentProvider);
|
|
15208
|
+
if (!currentModel)
|
|
15209
|
+
continue;
|
|
15210
|
+
const currentScore = scoreForEngine(engineVersion, agent, currentModel, externalSignals, versionRecencyMap);
|
|
15211
|
+
for (const underProvider of underProviders) {
|
|
15212
|
+
const pinned = getPinnedModelForProvider(agent, underProvider);
|
|
15213
|
+
const candidate = ranked.find((model) => model.model === pinned) ?? ranked.find((model) => model.providerID === underProvider);
|
|
15214
|
+
if (!candidate)
|
|
15215
|
+
continue;
|
|
15216
|
+
const candidateScore = scoreForEngine(engineVersion, agent, candidate, externalSignals, versionRecencyMap);
|
|
15217
|
+
const loss = currentScore - candidateScore;
|
|
15218
|
+
if (loss > MAX_ALLOWED_SCORE_LOSS)
|
|
15219
|
+
continue;
|
|
15220
|
+
if (!bestSwap || loss < bestSwap.loss) {
|
|
15221
|
+
bestSwap = { agent, candidate, loss };
|
|
15222
|
+
}
|
|
15223
|
+
}
|
|
15224
|
+
}
|
|
15225
|
+
if (!bestSwap)
|
|
15226
|
+
break;
|
|
15227
|
+
agents[bestSwap.agent].model = bestSwap.candidate.model;
|
|
15228
|
+
chains[bestSwap.agent] = dedupe2([
|
|
15229
|
+
bestSwap.candidate.model,
|
|
15230
|
+
...chains[bestSwap.agent] ?? []
|
|
15231
|
+
]).slice(0, 10);
|
|
15232
|
+
provenance[bestSwap.agent] = {
|
|
15233
|
+
winnerLayer: "provider-fallback-policy",
|
|
15234
|
+
winnerModel: bestSwap.candidate.model
|
|
15235
|
+
};
|
|
15236
|
+
}
|
|
14560
15237
|
}
|
|
14561
|
-
function
|
|
15238
|
+
function chooseProviderRepresentative(providerModels, agent, externalSignals, versionRecencyMap) {
|
|
15239
|
+
if (providerModels.length === 0)
|
|
15240
|
+
return null;
|
|
15241
|
+
const flashBest = providerModels.find((model) => hasFlashToken(model));
|
|
15242
|
+
const nonFlashBest = providerModels.find((model) => !hasFlashToken(model));
|
|
15243
|
+
if (!nonFlashBest)
|
|
15244
|
+
return providerModels[0] ?? null;
|
|
15245
|
+
if (!flashBest)
|
|
15246
|
+
return nonFlashBest;
|
|
15247
|
+
const flashScore = combinedScore(agent, flashBest, externalSignals, versionRecencyMap);
|
|
15248
|
+
const nonFlashScore = combinedScore(agent, nonFlashBest, externalSignals, versionRecencyMap);
|
|
15249
|
+
const threshold = agent === "explorer" ? -6 : 12;
|
|
15250
|
+
return flashScore >= nonFlashScore + threshold ? flashBest : nonFlashBest;
|
|
15251
|
+
}
|
|
15252
|
+
function getQualityWindow(agent) {
|
|
15253
|
+
if (agent === "oracle" || agent === "orchestrator")
|
|
15254
|
+
return 12;
|
|
15255
|
+
if (agent === "fixer")
|
|
15256
|
+
return 15;
|
|
15257
|
+
if (agent === "designer")
|
|
15258
|
+
return 16;
|
|
15259
|
+
if (agent === "librarian")
|
|
15260
|
+
return 18;
|
|
15261
|
+
return 22;
|
|
15262
|
+
}
|
|
15263
|
+
function getProviderBundle(providerModels, agent, externalSignals, versionRecencyMap) {
|
|
15264
|
+
if (providerModels.length === 0)
|
|
15265
|
+
return [];
|
|
15266
|
+
const representative = chooseProviderRepresentative(providerModels, agent, externalSignals, versionRecencyMap);
|
|
15267
|
+
if (!representative)
|
|
15268
|
+
return [];
|
|
15269
|
+
const second = providerModels.find((m) => m.model !== representative.model);
|
|
15270
|
+
if (!second)
|
|
15271
|
+
return [representative.model];
|
|
15272
|
+
const score1 = combinedScore(agent, representative, externalSignals, versionRecencyMap);
|
|
15273
|
+
const score2 = combinedScore(agent, second, externalSignals, versionRecencyMap);
|
|
15274
|
+
const gap = Math.abs(score1 - score2);
|
|
15275
|
+
const includeSecond = representative.providerID === "chutes" || gap <= (agent === "oracle" || agent === "orchestrator" ? 8 : agent === "designer" || agent === "librarian" ? 12 : agent === "fixer" ? 15 : 18);
|
|
15276
|
+
return includeSecond ? [representative.model, second.model] : [representative.model];
|
|
15277
|
+
}
|
|
15278
|
+
function selectPrimaryWithDiversity(candidates, agent, providerUsage, targetByProvider, remainingSlots, externalSignals, versionRecencyMap) {
|
|
15279
|
+
if (candidates.length === 0)
|
|
15280
|
+
return null;
|
|
15281
|
+
const candidateScores = candidates.map((model) => {
|
|
15282
|
+
const usage = providerUsage.get(model.providerID) ?? 0;
|
|
15283
|
+
const target = targetByProvider[model.providerID] ?? 1;
|
|
15284
|
+
const softCap = target;
|
|
15285
|
+
const hardCap = Math.min(target + 1, 4);
|
|
15286
|
+
const deficit = Math.max(0, target - usage);
|
|
15287
|
+
const softOverflow = Math.max(0, usage + 1 - softCap);
|
|
15288
|
+
const hardOverflow = Math.max(0, usage + 1 - hardCap);
|
|
15289
|
+
const rawScore = combinedScore(agent, model, externalSignals, versionRecencyMap);
|
|
15290
|
+
const adjustedScore = rawScore + deficit * 14 - softOverflow * 18 - hardOverflow * 100;
|
|
15291
|
+
return {
|
|
15292
|
+
model,
|
|
15293
|
+
usage,
|
|
15294
|
+
target,
|
|
15295
|
+
rawScore,
|
|
15296
|
+
adjustedScore: Math.round(adjustedScore * 1000) / 1000
|
|
15297
|
+
};
|
|
15298
|
+
});
|
|
15299
|
+
const bestRaw = Math.max(...candidateScores.map((item) => item.rawScore));
|
|
15300
|
+
const window = getQualityWindow(agent);
|
|
15301
|
+
let eligible = candidateScores.filter((item) => item.rawScore >= bestRaw - window);
|
|
15302
|
+
const mustFillProviders = Object.entries(targetByProvider).filter(([providerID, target]) => {
|
|
15303
|
+
const usage = providerUsage.get(providerID) ?? 0;
|
|
15304
|
+
return Math.max(0, target - usage) >= remainingSlots;
|
|
15305
|
+
}).map(([providerID]) => providerID);
|
|
15306
|
+
if (mustFillProviders.length > 0) {
|
|
15307
|
+
const forced = eligible.filter((item) => mustFillProviders.includes(item.model.providerID));
|
|
15308
|
+
if (forced.length > 0)
|
|
15309
|
+
eligible = forced;
|
|
15310
|
+
}
|
|
15311
|
+
eligible.sort((a, b) => {
|
|
15312
|
+
const delta = b.adjustedScore - a.adjustedScore;
|
|
15313
|
+
if (delta !== 0)
|
|
15314
|
+
return delta;
|
|
15315
|
+
const ratioA = a.target > 0 ? a.usage / a.target : a.usage;
|
|
15316
|
+
const ratioB = b.target > 0 ? b.usage / b.target : b.usage;
|
|
15317
|
+
if (ratioA !== ratioB)
|
|
15318
|
+
return ratioA - ratioB;
|
|
15319
|
+
if (a.rawScore !== b.rawScore)
|
|
15320
|
+
return b.rawScore - a.rawScore;
|
|
15321
|
+
const providerTie = a.model.providerID.localeCompare(b.model.providerID);
|
|
15322
|
+
if (providerTie !== 0)
|
|
15323
|
+
return providerTie;
|
|
15324
|
+
return a.model.model.localeCompare(b.model.model);
|
|
15325
|
+
});
|
|
15326
|
+
let chosen = eligible[0] ?? candidateScores[0];
|
|
15327
|
+
if (!chosen)
|
|
15328
|
+
return null;
|
|
15329
|
+
if (chosen.usage >= 2) {
|
|
15330
|
+
const bestUnused = candidateScores.find((item) => item.usage === 0);
|
|
15331
|
+
if (bestUnused && bestUnused.adjustedScore >= chosen.adjustedScore - 9) {
|
|
15332
|
+
chosen = bestUnused;
|
|
15333
|
+
}
|
|
15334
|
+
}
|
|
15335
|
+
if (agent !== "explorer" && isZai47Model(chosen.model) && hasFlashToken(chosen.model)) {
|
|
15336
|
+
const kimiCandidate = candidateScores.find((item) => isKimiK25Model(item.model));
|
|
15337
|
+
if (kimiCandidate && kimiCandidate.rawScore >= chosen.rawScore - 2) {
|
|
15338
|
+
chosen = kimiCandidate;
|
|
15339
|
+
}
|
|
15340
|
+
}
|
|
15341
|
+
return chosen.model;
|
|
15342
|
+
}
|
|
15343
|
+
function dedupe2(models) {
|
|
14562
15344
|
const seen = new Set;
|
|
14563
15345
|
const result = [];
|
|
14564
15346
|
for (const model of models) {
|
|
@@ -14569,41 +15351,494 @@ function dedupe(models) {
|
|
|
14569
15351
|
}
|
|
14570
15352
|
return result;
|
|
14571
15353
|
}
|
|
14572
|
-
function
|
|
15354
|
+
function finalizeChainWithTail(prefix, preferredTail) {
|
|
15355
|
+
if (!preferredTail) {
|
|
15356
|
+
return dedupe2([...prefix, "opencode/big-pickle"]).slice(0, 10);
|
|
15357
|
+
}
|
|
15358
|
+
const withoutTail = prefix.filter((model) => model !== preferredTail).slice(0, 9);
|
|
15359
|
+
return [...withoutTail, preferredTail];
|
|
15360
|
+
}
|
|
15361
|
+
function ensureSyntheticModel(models, fullModelID) {
|
|
15362
|
+
if (!fullModelID)
|
|
15363
|
+
return models;
|
|
15364
|
+
if (models.some((model) => model.model === fullModelID))
|
|
15365
|
+
return models;
|
|
15366
|
+
const [providerID, modelID] = fullModelID.split("/");
|
|
15367
|
+
if (!providerID || !modelID)
|
|
15368
|
+
return models;
|
|
15369
|
+
return [
|
|
15370
|
+
...models,
|
|
15371
|
+
{
|
|
15372
|
+
providerID,
|
|
15373
|
+
model: fullModelID,
|
|
15374
|
+
name: modelID,
|
|
15375
|
+
status: "active",
|
|
15376
|
+
contextLimit: 200000,
|
|
15377
|
+
outputLimit: 32000,
|
|
15378
|
+
reasoning: true,
|
|
15379
|
+
toolcall: true,
|
|
15380
|
+
attachment: false
|
|
15381
|
+
}
|
|
15382
|
+
];
|
|
15383
|
+
}
|
|
15384
|
+
function buildDynamicModelPlan(catalog, config2, externalSignals, options) {
|
|
15385
|
+
const catalogWithSelectedModels = [
|
|
15386
|
+
config2.selectedChutesPrimaryModel,
|
|
15387
|
+
config2.selectedChutesSecondaryModel,
|
|
15388
|
+
config2.selectedOpenCodePrimaryModel,
|
|
15389
|
+
config2.selectedOpenCodeSecondaryModel
|
|
15390
|
+
].reduce((acc, modelID) => ensureSyntheticModel(acc, modelID), catalog);
|
|
14573
15391
|
const enabledProviders = new Set(getEnabledProviders(config2));
|
|
14574
|
-
const
|
|
15392
|
+
const providerUniverse = catalogWithSelectedModels.filter((m) => {
|
|
15393
|
+
if (!enabledProviders.has(m.providerID))
|
|
15394
|
+
return false;
|
|
15395
|
+
if (m.providerID === "chutes" && /qwen/i.test(m.model)) {
|
|
15396
|
+
return false;
|
|
15397
|
+
}
|
|
15398
|
+
return true;
|
|
15399
|
+
});
|
|
15400
|
+
const engineVersion = options?.scoringEngineVersion ?? config2.scoringEngineVersion ?? "v1";
|
|
15401
|
+
const versionRecencyMap = getVersionRecencyMap(providerUniverse);
|
|
15402
|
+
const providerCandidates = selectTopModelsPerProvider(providerUniverse, engineVersion, externalSignals, versionRecencyMap);
|
|
14575
15403
|
if (providerCandidates.length === 0) {
|
|
14576
15404
|
return null;
|
|
14577
15405
|
}
|
|
15406
|
+
const hasPaidProviderEnabled = config2.hasOpenAI || config2.hasAnthropic || config2.hasCopilot || config2.hasZaiPlan || config2.hasKimi || config2.hasAntigravity;
|
|
15407
|
+
const paidProviders = dedupe2(providerCandidates.map((model) => model.providerID).filter((providerID) => providerID !== "opencode")).sort((a, b) => a.localeCompare(b));
|
|
15408
|
+
const targetByProvider = {};
|
|
15409
|
+
if (paidProviders.length > 0) {
|
|
15410
|
+
const baseTarget = Math.floor(AGENTS.length / paidProviders.length);
|
|
15411
|
+
const extra = AGENTS.length % paidProviders.length;
|
|
15412
|
+
for (const [index, providerID] of paidProviders.entries()) {
|
|
15413
|
+
targetByProvider[providerID] = baseTarget + (index < extra ? 1 : 0);
|
|
15414
|
+
}
|
|
15415
|
+
}
|
|
15416
|
+
const providerUsage = new Map;
|
|
15417
|
+
const rankCache = new Map;
|
|
15418
|
+
const shadowDiffs = {};
|
|
14578
15419
|
const agents = {};
|
|
14579
15420
|
const chains = {};
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
15421
|
+
const provenance = {};
|
|
15422
|
+
const getSelectedChutesForAgent = (agent) => {
|
|
15423
|
+
if (!config2.hasChutes)
|
|
15424
|
+
return;
|
|
15425
|
+
return agent === "explorer" || agent === "librarian" || agent === "fixer" ? config2.selectedChutesSecondaryModel ?? config2.selectedChutesPrimaryModel : config2.selectedChutesPrimaryModel;
|
|
15426
|
+
};
|
|
15427
|
+
const getSelectedOpenCodeForAgent = (agent) => {
|
|
15428
|
+
if (!config2.useOpenCodeFreeModels)
|
|
15429
|
+
return;
|
|
15430
|
+
return agent === "explorer" || agent === "librarian" || agent === "fixer" ? config2.selectedOpenCodeSecondaryModel ?? config2.selectedOpenCodePrimaryModel : config2.selectedOpenCodePrimaryModel;
|
|
15431
|
+
};
|
|
15432
|
+
const getPinnedModelForProvider = (agent, providerID) => {
|
|
15433
|
+
if (providerID === "chutes")
|
|
15434
|
+
return getSelectedChutesForAgent(agent);
|
|
15435
|
+
if (providerID === "opencode")
|
|
15436
|
+
return getSelectedOpenCodeForAgent(agent);
|
|
15437
|
+
return;
|
|
15438
|
+
};
|
|
15439
|
+
const getRankedModels = (agent) => {
|
|
15440
|
+
const cached2 = rankCache.get(agent);
|
|
15441
|
+
if (cached2)
|
|
15442
|
+
return cached2;
|
|
15443
|
+
const rankedV1 = rankModels2(providerCandidates, agent, externalSignals);
|
|
15444
|
+
if (engineVersion === "v1") {
|
|
15445
|
+
rankCache.set(agent, rankedV1);
|
|
15446
|
+
return rankedV1;
|
|
15447
|
+
}
|
|
15448
|
+
const rankedV2 = rankModelsV2(providerCandidates, agent, externalSignals).map((candidate) => candidate.model);
|
|
15449
|
+
if (engineVersion === "v2-shadow") {
|
|
15450
|
+
shadowDiffs[agent] = {
|
|
15451
|
+
v1TopModel: rankedV1[0]?.model,
|
|
15452
|
+
v2TopModel: rankedV2[0]?.model
|
|
15453
|
+
};
|
|
15454
|
+
rankCache.set(agent, rankedV1);
|
|
15455
|
+
return rankedV1;
|
|
15456
|
+
}
|
|
15457
|
+
rankCache.set(agent, rankedV2);
|
|
15458
|
+
return rankedV2;
|
|
15459
|
+
};
|
|
15460
|
+
for (const [agentIndex, agent] of PRIMARY_ASSIGNMENT_ORDER.entries()) {
|
|
15461
|
+
const ranked = getRankedModels(agent);
|
|
15462
|
+
const primaryPool = hasPaidProviderEnabled ? ranked.filter((model) => !FREE_BIASED_PROVIDERS.has(model.providerID)) : ranked;
|
|
15463
|
+
const remainingSlots = PRIMARY_ASSIGNMENT_ORDER.length - agentIndex;
|
|
15464
|
+
const primary = selectPrimaryWithDiversity(primaryPool.length > 0 ? primaryPool : ranked, agent, providerUsage, targetByProvider, remainingSlots, externalSignals, versionRecencyMap) ?? ranked[0];
|
|
14583
15465
|
if (!primary)
|
|
14584
15466
|
continue;
|
|
14585
|
-
|
|
14586
|
-
const
|
|
14587
|
-
const
|
|
14588
|
-
|
|
14589
|
-
|
|
15467
|
+
providerUsage.set(primary.providerID, (providerUsage.get(primary.providerID) ?? 0) + 1);
|
|
15468
|
+
const providerOrder = dedupe2(ranked.map((m) => m.providerID));
|
|
15469
|
+
const perProviderBest = providerOrder.flatMap((providerID) => {
|
|
15470
|
+
const providerModels = ranked.filter((m) => m.providerID === providerID);
|
|
15471
|
+
const pinned = getPinnedModelForProvider(agent, providerID);
|
|
15472
|
+
if (pinned && providerModels.some((m) => m.model === pinned)) {
|
|
15473
|
+
return [pinned];
|
|
15474
|
+
}
|
|
15475
|
+
return getProviderBundle(providerModels, agent, externalSignals, versionRecencyMap);
|
|
15476
|
+
});
|
|
15477
|
+
const nonFreePerProviderBest = perProviderBest.filter((model) => !model.startsWith("opencode/"));
|
|
15478
|
+
const freePerProviderBest = perProviderBest.filter((model) => model.startsWith("opencode/"));
|
|
15479
|
+
const selectedOpencode = getSelectedOpenCodeForAgent(agent);
|
|
15480
|
+
const selectedChutes = getSelectedChutesForAgent(agent);
|
|
15481
|
+
const chain = dedupe2([
|
|
14590
15482
|
primary.model,
|
|
14591
|
-
...
|
|
15483
|
+
...nonFreePerProviderBest,
|
|
14592
15484
|
selectedChutes,
|
|
14593
15485
|
selectedOpencode,
|
|
14594
|
-
|
|
14595
|
-
])
|
|
15486
|
+
...freePerProviderBest
|
|
15487
|
+
]);
|
|
15488
|
+
const deterministicFreeTail = selectedOpencode ?? freePerProviderBest[0] ?? ranked.find((model) => model.model.startsWith("opencode/"))?.model;
|
|
15489
|
+
const finalizedChain = finalizeChainWithTail(chain, deterministicFreeTail);
|
|
15490
|
+
const providerPolicyChain = dedupe2([selectedChutes, selectedOpencode]);
|
|
15491
|
+
const systemDefaultModel = selectedOpencode ?? "opencode/big-pickle";
|
|
15492
|
+
const resolved = resolveAgentWithPrecedence({
|
|
15493
|
+
agentName: agent,
|
|
15494
|
+
dynamicRecommendation: finalizedChain,
|
|
15495
|
+
providerFallbackPolicy: providerPolicyChain,
|
|
15496
|
+
systemDefault: [systemDefaultModel]
|
|
15497
|
+
});
|
|
15498
|
+
let finalModel = resolved.model;
|
|
15499
|
+
let finalChain = resolved.chain;
|
|
15500
|
+
const selectedChutesForAgent = getSelectedChutesForAgent(agent);
|
|
15501
|
+
const selectedOpenCodeForAgent = getSelectedOpenCodeForAgent(agent);
|
|
15502
|
+
const forceChutes = finalModel.startsWith("chutes/") && Boolean(selectedChutesForAgent);
|
|
15503
|
+
const forceOpenCode = finalModel.startsWith("opencode/") && Boolean(selectedOpenCodeForAgent);
|
|
15504
|
+
if (forceOpenCode && selectedOpenCodeForAgent) {
|
|
15505
|
+
finalModel = selectedOpenCodeForAgent;
|
|
15506
|
+
finalChain = dedupe2([selectedOpenCodeForAgent, ...finalChain]);
|
|
15507
|
+
}
|
|
15508
|
+
if (forceChutes && selectedChutesForAgent) {
|
|
15509
|
+
finalModel = selectedChutesForAgent;
|
|
15510
|
+
finalChain = dedupe2([selectedChutesForAgent, ...finalChain]);
|
|
15511
|
+
}
|
|
15512
|
+
const wasForced = forceChutes || forceOpenCode;
|
|
14596
15513
|
agents[agent] = {
|
|
14597
|
-
model:
|
|
15514
|
+
model: finalModel,
|
|
14598
15515
|
variant: ROLE_VARIANT[agent]
|
|
14599
15516
|
};
|
|
14600
|
-
chains[agent] =
|
|
15517
|
+
chains[agent] = finalChain;
|
|
15518
|
+
provenance[agent] = {
|
|
15519
|
+
winnerLayer: wasForced ? "manual-user-plan" : resolved.provenance.winnerLayer,
|
|
15520
|
+
winnerModel: finalModel
|
|
15521
|
+
};
|
|
15522
|
+
}
|
|
15523
|
+
if (hasPaidProviderEnabled) {
|
|
15524
|
+
for (const providerID of paidProviders) {
|
|
15525
|
+
if ((providerUsage.get(providerID) ?? 0) > 0)
|
|
15526
|
+
continue;
|
|
15527
|
+
let bestSwap;
|
|
15528
|
+
for (const agent of PRIMARY_ASSIGNMENT_ORDER) {
|
|
15529
|
+
const currentModel = agents[agent]?.model;
|
|
15530
|
+
if (!currentModel)
|
|
15531
|
+
continue;
|
|
15532
|
+
const ranked = getRankedModels(agent);
|
|
15533
|
+
const pinned = getPinnedModelForProvider(agent, providerID);
|
|
15534
|
+
const candidate = ranked.find((model) => model.model === pinned) ?? ranked.find((model) => model.providerID === providerID);
|
|
15535
|
+
const current = ranked.find((model) => model.model === currentModel);
|
|
15536
|
+
if (!candidate || !current)
|
|
15537
|
+
continue;
|
|
15538
|
+
const currentScore = combinedScore(agent, current, externalSignals, versionRecencyMap);
|
|
15539
|
+
const candidateScore = combinedScore(agent, candidate, externalSignals, versionRecencyMap);
|
|
15540
|
+
const loss = currentScore - candidateScore;
|
|
15541
|
+
if (!bestSwap || loss < bestSwap.loss) {
|
|
15542
|
+
bestSwap = {
|
|
15543
|
+
agent,
|
|
15544
|
+
candidateModel: candidate.model,
|
|
15545
|
+
loss
|
|
15546
|
+
};
|
|
15547
|
+
}
|
|
15548
|
+
}
|
|
15549
|
+
if (!bestSwap)
|
|
15550
|
+
continue;
|
|
15551
|
+
const existingProvider = agents[bestSwap.agent]?.model.split("/")[0] ?? providerID;
|
|
15552
|
+
agents[bestSwap.agent].model = bestSwap.candidateModel;
|
|
15553
|
+
chains[bestSwap.agent] = dedupe2([
|
|
15554
|
+
bestSwap.candidateModel,
|
|
15555
|
+
...chains[bestSwap.agent] ?? []
|
|
15556
|
+
]).slice(0, 10);
|
|
15557
|
+
provenance[bestSwap.agent] = {
|
|
15558
|
+
winnerLayer: "provider-fallback-policy",
|
|
15559
|
+
winnerModel: bestSwap.candidateModel
|
|
15560
|
+
};
|
|
15561
|
+
providerUsage.set(providerID, (providerUsage.get(providerID) ?? 0) + 1);
|
|
15562
|
+
providerUsage.set(existingProvider, Math.max(0, (providerUsage.get(existingProvider) ?? 1) - 1));
|
|
15563
|
+
}
|
|
15564
|
+
}
|
|
15565
|
+
if (config2.balanceProviderUsage && hasPaidProviderEnabled) {
|
|
15566
|
+
rebalanceForSubscriptionMode(agents, chains, provenance, paidProviders, getRankedModels, getPinnedModelForProvider, targetByProvider, externalSignals, versionRecencyMap, engineVersion);
|
|
14601
15567
|
}
|
|
14602
15568
|
if (Object.keys(agents).length === 0) {
|
|
14603
15569
|
return null;
|
|
14604
15570
|
}
|
|
14605
|
-
return {
|
|
15571
|
+
return {
|
|
15572
|
+
agents,
|
|
15573
|
+
chains,
|
|
15574
|
+
provenance,
|
|
15575
|
+
scoring: {
|
|
15576
|
+
engineVersionApplied: engineVersion === "v2" ? "v2" : "v1",
|
|
15577
|
+
shadowCompared: engineVersion === "v2-shadow",
|
|
15578
|
+
diffs: engineVersion === "v2-shadow" ? shadowDiffs : undefined
|
|
15579
|
+
}
|
|
15580
|
+
};
|
|
15581
|
+
}
|
|
15582
|
+
// src/utils/logger.ts
|
|
15583
|
+
import * as os from "os";
|
|
15584
|
+
import * as path from "path";
|
|
15585
|
+
var logFile = path.join(os.tmpdir(), "oh-my-opencode-slim.log");
|
|
15586
|
+
// src/utils/env.ts
|
|
15587
|
+
function getEnv(name) {
|
|
15588
|
+
const bunValue = globalThis.Bun?.env?.[name];
|
|
15589
|
+
if (typeof bunValue === "string" && bunValue.length > 0)
|
|
15590
|
+
return bunValue;
|
|
15591
|
+
const processValue = globalThis.process?.env?.[name];
|
|
15592
|
+
return typeof processValue === "string" && processValue.length > 0 ? processValue : undefined;
|
|
15593
|
+
}
|
|
15594
|
+
// src/cli/external-rankings.ts
|
|
15595
|
+
function normalizeKey(input) {
|
|
15596
|
+
return input.trim().toLowerCase();
|
|
15597
|
+
}
|
|
15598
|
+
function baseAliases(key) {
|
|
15599
|
+
return buildModelKeyAliases(normalizeKey(key));
|
|
15600
|
+
}
|
|
15601
|
+
function providerScopedAlias(alias, providerPrefix) {
|
|
15602
|
+
if (!providerPrefix || alias.includes("/"))
|
|
15603
|
+
return alias;
|
|
15604
|
+
return `${providerPrefix}/${alias}`;
|
|
15605
|
+
}
|
|
15606
|
+
function mergeSignal(existing, incoming) {
|
|
15607
|
+
if (!existing)
|
|
15608
|
+
return incoming;
|
|
15609
|
+
return {
|
|
15610
|
+
qualityScore: incoming.qualityScore ?? existing.qualityScore,
|
|
15611
|
+
codingScore: incoming.codingScore ?? existing.codingScore,
|
|
15612
|
+
latencySeconds: incoming.latencySeconds ?? existing.latencySeconds,
|
|
15613
|
+
inputPricePer1M: incoming.inputPricePer1M ?? existing.inputPricePer1M,
|
|
15614
|
+
outputPricePer1M: incoming.outputPricePer1M ?? existing.outputPricePer1M,
|
|
15615
|
+
source: "merged"
|
|
15616
|
+
};
|
|
15617
|
+
}
|
|
15618
|
+
function providerPrefixFromCreator(creatorSlug) {
|
|
15619
|
+
if (!creatorSlug)
|
|
15620
|
+
return;
|
|
15621
|
+
const slug = creatorSlug.toLowerCase();
|
|
15622
|
+
if (slug.includes("openai"))
|
|
15623
|
+
return "openai";
|
|
15624
|
+
if (slug.includes("anthropic"))
|
|
15625
|
+
return "anthropic";
|
|
15626
|
+
if (slug.includes("google"))
|
|
15627
|
+
return "google";
|
|
15628
|
+
if (slug.includes("chutes"))
|
|
15629
|
+
return "chutes";
|
|
15630
|
+
if (slug.includes("copilot") || slug.includes("github"))
|
|
15631
|
+
return "github-copilot";
|
|
15632
|
+
if (slug.includes("zai") || slug.includes("z-ai"))
|
|
15633
|
+
return "zai-coding-plan";
|
|
15634
|
+
if (slug.includes("kimi"))
|
|
15635
|
+
return "kimi-for-coding";
|
|
15636
|
+
if (slug.includes("opencode"))
|
|
15637
|
+
return "opencode";
|
|
15638
|
+
return;
|
|
15639
|
+
}
|
|
15640
|
+
function parseOpenRouterPrice(value) {
|
|
15641
|
+
if (!value)
|
|
15642
|
+
return;
|
|
15643
|
+
const parsed = Number.parseFloat(value);
|
|
15644
|
+
if (!Number.isFinite(parsed))
|
|
15645
|
+
return;
|
|
15646
|
+
return parsed * 1e6;
|
|
15647
|
+
}
|
|
15648
|
+
async function fetchJsonWithTimeout(url2, init, timeoutMs) {
|
|
15649
|
+
const controller = new AbortController;
|
|
15650
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
15651
|
+
try {
|
|
15652
|
+
return await fetch(url2, {
|
|
15653
|
+
...init,
|
|
15654
|
+
signal: controller.signal
|
|
15655
|
+
});
|
|
15656
|
+
} finally {
|
|
15657
|
+
clearTimeout(timer);
|
|
15658
|
+
}
|
|
15659
|
+
}
|
|
15660
|
+
async function fetchArtificialAnalysisSignals(apiKey) {
|
|
15661
|
+
const response = await fetchJsonWithTimeout("https://artificialanalysis.ai/api/v2/data/llms/models", {
|
|
15662
|
+
headers: {
|
|
15663
|
+
"x-api-key": apiKey
|
|
15664
|
+
}
|
|
15665
|
+
}, 8000);
|
|
15666
|
+
if (!response.ok) {
|
|
15667
|
+
throw new Error(`Artificial Analysis request failed (${response.status} ${response.statusText})`);
|
|
15668
|
+
}
|
|
15669
|
+
const parsed = await response.json();
|
|
15670
|
+
const map2 = {};
|
|
15671
|
+
for (const model of parsed.data ?? []) {
|
|
15672
|
+
const baseSignal = {
|
|
15673
|
+
qualityScore: model.evaluations?.artificial_analysis_intelligence_index,
|
|
15674
|
+
codingScore: model.evaluations?.artificial_analysis_coding_index ?? model.evaluations?.livecodebench,
|
|
15675
|
+
latencySeconds: model.median_time_to_first_token_seconds,
|
|
15676
|
+
inputPricePer1M: model.pricing?.price_1m_input_tokens ?? model.pricing?.price_1m_blended_3_to_1,
|
|
15677
|
+
outputPricePer1M: model.pricing?.price_1m_output_tokens ?? model.pricing?.price_1m_blended_3_to_1,
|
|
15678
|
+
source: "artificial-analysis"
|
|
15679
|
+
};
|
|
15680
|
+
const id = model.id ? normalizeKey(model.id) : undefined;
|
|
15681
|
+
const slug = model.slug ? normalizeKey(model.slug) : undefined;
|
|
15682
|
+
const name = model.name ? normalizeKey(model.name) : undefined;
|
|
15683
|
+
const providerPrefix = providerPrefixFromCreator(model.model_creator?.slug);
|
|
15684
|
+
for (const key of [id, slug, name]) {
|
|
15685
|
+
if (!key)
|
|
15686
|
+
continue;
|
|
15687
|
+
for (const alias of baseAliases(key)) {
|
|
15688
|
+
if (!providerPrefix || alias.includes("/")) {
|
|
15689
|
+
map2[alias] = mergeSignal(map2[alias], baseSignal);
|
|
15690
|
+
}
|
|
15691
|
+
const scopedAlias = providerScopedAlias(alias, providerPrefix);
|
|
15692
|
+
map2[scopedAlias] = mergeSignal(map2[scopedAlias], baseSignal);
|
|
15693
|
+
}
|
|
15694
|
+
}
|
|
15695
|
+
}
|
|
15696
|
+
return map2;
|
|
15697
|
+
}
|
|
15698
|
+
async function fetchOpenRouterSignals(apiKey) {
|
|
15699
|
+
const response = await fetchJsonWithTimeout("https://openrouter.ai/api/v1/models", {
|
|
15700
|
+
headers: {
|
|
15701
|
+
Authorization: `Bearer ${apiKey}`
|
|
15702
|
+
}
|
|
15703
|
+
}, 8000);
|
|
15704
|
+
if (!response.ok) {
|
|
15705
|
+
throw new Error(`OpenRouter request failed (${response.status} ${response.statusText})`);
|
|
15706
|
+
}
|
|
15707
|
+
const parsed = await response.json();
|
|
15708
|
+
const map2 = {};
|
|
15709
|
+
for (const model of parsed.data ?? []) {
|
|
15710
|
+
if (!model.id)
|
|
15711
|
+
continue;
|
|
15712
|
+
const key = normalizeKey(model.id);
|
|
15713
|
+
const providerPrefix = key.split("/")[0];
|
|
15714
|
+
const signal = {
|
|
15715
|
+
inputPricePer1M: parseOpenRouterPrice(model.pricing?.prompt),
|
|
15716
|
+
outputPricePer1M: parseOpenRouterPrice(model.pricing?.completion),
|
|
15717
|
+
source: "openrouter"
|
|
15718
|
+
};
|
|
15719
|
+
for (const alias of baseAliases(key)) {
|
|
15720
|
+
if (alias.includes("/")) {
|
|
15721
|
+
map2[alias] = mergeSignal(map2[alias], signal);
|
|
15722
|
+
}
|
|
15723
|
+
const scopedAlias = providerScopedAlias(alias, providerPrefix);
|
|
15724
|
+
map2[scopedAlias] = mergeSignal(map2[scopedAlias], signal);
|
|
15725
|
+
}
|
|
15726
|
+
}
|
|
15727
|
+
return map2;
|
|
15728
|
+
}
|
|
15729
|
+
async function fetchExternalModelSignals(options) {
|
|
15730
|
+
const warnings = [];
|
|
15731
|
+
const aggregate = {};
|
|
15732
|
+
const aaKey = options?.artificialAnalysisApiKey ?? getEnv("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
15733
|
+
const orKey = options?.openRouterApiKey ?? getEnv("OPENROUTER_API_KEY");
|
|
15734
|
+
const aaPromise = aaKey ? fetchArtificialAnalysisSignals(aaKey) : Promise.resolve({});
|
|
15735
|
+
const orPromise = orKey ? fetchOpenRouterSignals(orKey) : Promise.resolve({});
|
|
15736
|
+
const [aaResult, orResult] = await Promise.allSettled([aaPromise, orPromise]);
|
|
15737
|
+
if (aaResult.status === "fulfilled") {
|
|
15738
|
+
for (const [key, signal] of Object.entries(aaResult.value)) {
|
|
15739
|
+
aggregate[key] = mergeSignal(aggregate[key], signal);
|
|
15740
|
+
}
|
|
15741
|
+
} else if (aaKey) {
|
|
15742
|
+
warnings.push(`Artificial Analysis unavailable: ${aaResult.reason instanceof Error ? aaResult.reason.message : String(aaResult.reason)}`);
|
|
15743
|
+
}
|
|
15744
|
+
if (orResult.status === "fulfilled") {
|
|
15745
|
+
for (const [key, signal] of Object.entries(orResult.value)) {
|
|
15746
|
+
aggregate[key] = mergeSignal(aggregate[key], signal);
|
|
15747
|
+
}
|
|
15748
|
+
} else if (orKey) {
|
|
15749
|
+
warnings.push(`OpenRouter unavailable: ${orResult.reason instanceof Error ? orResult.reason.message : String(orResult.reason)}`);
|
|
15750
|
+
}
|
|
15751
|
+
return { signals: aggregate, warnings };
|
|
15752
|
+
}
|
|
15753
|
+
// src/cli/system.ts
|
|
15754
|
+
import { statSync as statSync2 } from "fs";
|
|
15755
|
+
var cachedOpenCodePath = null;
|
|
15756
|
+
function getOpenCodePaths() {
|
|
15757
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
15758
|
+
return [
|
|
15759
|
+
"opencode",
|
|
15760
|
+
`${home}/.local/bin/opencode`,
|
|
15761
|
+
`${home}/.opencode/bin/opencode`,
|
|
15762
|
+
`${home}/bin/opencode`,
|
|
15763
|
+
"/usr/local/bin/opencode",
|
|
15764
|
+
"/opt/opencode/bin/opencode",
|
|
15765
|
+
"/usr/bin/opencode",
|
|
15766
|
+
"/bin/opencode",
|
|
15767
|
+
"/Applications/OpenCode.app/Contents/MacOS/opencode",
|
|
15768
|
+
`${home}/Applications/OpenCode.app/Contents/MacOS/opencode`,
|
|
15769
|
+
"/opt/homebrew/bin/opencode",
|
|
15770
|
+
"/home/linuxbrew/.linuxbrew/bin/opencode",
|
|
15771
|
+
`${home}/homebrew/bin/opencode`,
|
|
15772
|
+
`${home}/Library/Application Support/opencode/bin/opencode`,
|
|
15773
|
+
"/snap/bin/opencode",
|
|
15774
|
+
"/var/snap/opencode/current/bin/opencode",
|
|
15775
|
+
"/var/lib/flatpak/exports/bin/ai.opencode.OpenCode",
|
|
15776
|
+
`${home}/.local/share/flatpak/exports/bin/ai.opencode.OpenCode`,
|
|
15777
|
+
"/nix/store/opencode/bin/opencode",
|
|
15778
|
+
`${home}/.nix-profile/bin/opencode`,
|
|
15779
|
+
"/run/current-system/sw/bin/opencode",
|
|
15780
|
+
`${home}/.cargo/bin/opencode`,
|
|
15781
|
+
`${home}/.npm-global/bin/opencode`,
|
|
15782
|
+
"/usr/local/lib/node_modules/opencode/bin/opencode",
|
|
15783
|
+
`${home}/.yarn/bin/opencode`,
|
|
15784
|
+
`${home}/.pnpm-global/bin/opencode`
|
|
15785
|
+
];
|
|
15786
|
+
}
|
|
15787
|
+
function resolveOpenCodePath() {
|
|
15788
|
+
if (cachedOpenCodePath) {
|
|
15789
|
+
return cachedOpenCodePath;
|
|
15790
|
+
}
|
|
15791
|
+
const paths = getOpenCodePaths();
|
|
15792
|
+
for (const opencodePath of paths) {
|
|
15793
|
+
if (opencodePath === "opencode")
|
|
15794
|
+
continue;
|
|
15795
|
+
try {
|
|
15796
|
+
const stat = statSync2(opencodePath);
|
|
15797
|
+
if (stat.isFile()) {
|
|
15798
|
+
cachedOpenCodePath = opencodePath;
|
|
15799
|
+
return opencodePath;
|
|
15800
|
+
}
|
|
15801
|
+
} catch {}
|
|
15802
|
+
}
|
|
15803
|
+
return "opencode";
|
|
15804
|
+
}
|
|
15805
|
+
async function isOpenCodeInstalled() {
|
|
15806
|
+
const paths = getOpenCodePaths();
|
|
15807
|
+
for (const opencodePath of paths) {
|
|
15808
|
+
try {
|
|
15809
|
+
const proc = Bun.spawn([opencodePath, "--version"], {
|
|
15810
|
+
stdout: "pipe",
|
|
15811
|
+
stderr: "pipe"
|
|
15812
|
+
});
|
|
15813
|
+
await proc.exited;
|
|
15814
|
+
if (proc.exitCode === 0) {
|
|
15815
|
+
cachedOpenCodePath = opencodePath;
|
|
15816
|
+
return true;
|
|
15817
|
+
}
|
|
15818
|
+
} catch {}
|
|
15819
|
+
}
|
|
15820
|
+
return false;
|
|
14606
15821
|
}
|
|
15822
|
+
async function getOpenCodeVersion() {
|
|
15823
|
+
const opencodePath = resolveOpenCodePath();
|
|
15824
|
+
try {
|
|
15825
|
+
const proc = Bun.spawn([opencodePath, "--version"], {
|
|
15826
|
+
stdout: "pipe",
|
|
15827
|
+
stderr: "pipe"
|
|
15828
|
+
});
|
|
15829
|
+
const output = await new Response(proc.stdout).text();
|
|
15830
|
+
await proc.exited;
|
|
15831
|
+
if (proc.exitCode === 0) {
|
|
15832
|
+
return output.trim();
|
|
15833
|
+
}
|
|
15834
|
+
} catch {}
|
|
15835
|
+
return null;
|
|
15836
|
+
}
|
|
15837
|
+
function getOpenCodePath() {
|
|
15838
|
+
const path2 = resolveOpenCodePath();
|
|
15839
|
+
return path2 === "opencode" ? null : path2;
|
|
15840
|
+
}
|
|
15841
|
+
|
|
14607
15842
|
// src/cli/opencode-models.ts
|
|
14608
15843
|
function isFreeModel(record2) {
|
|
14609
15844
|
const inputCost = record2.cost?.input ?? 0;
|
|
@@ -14646,12 +15881,12 @@ function normalizeDiscoveredModel(record2, providerFilter) {
|
|
|
14646
15881
|
function parseOpenCodeModelsVerboseOutput(output, providerFilter, freeOnly = true) {
|
|
14647
15882
|
const lines = output.split(/\r?\n/);
|
|
14648
15883
|
const models = [];
|
|
15884
|
+
const modelHeaderPattern = /^[a-z0-9-]+\/.+$/i;
|
|
14649
15885
|
for (let index = 0;index < lines.length; index++) {
|
|
14650
15886
|
const line = lines[index]?.trim();
|
|
14651
15887
|
if (!line || !line.includes("/"))
|
|
14652
15888
|
continue;
|
|
14653
|
-
|
|
14654
|
-
if (!isModelHeader)
|
|
15889
|
+
if (!modelHeaderPattern.test(line))
|
|
14655
15890
|
continue;
|
|
14656
15891
|
let jsonStart = -1;
|
|
14657
15892
|
for (let search = index + 1;search < lines.length; search++) {
|
|
@@ -14659,7 +15894,7 @@ function parseOpenCodeModelsVerboseOutput(output, providerFilter, freeOnly = tru
|
|
|
14659
15894
|
jsonStart = search;
|
|
14660
15895
|
break;
|
|
14661
15896
|
}
|
|
14662
|
-
if (
|
|
15897
|
+
if (modelHeaderPattern.test(lines[search]?.trim() ?? "")) {
|
|
14663
15898
|
break;
|
|
14664
15899
|
}
|
|
14665
15900
|
}
|
|
@@ -14699,9 +15934,10 @@ function parseOpenCodeModelsVerboseOutput(output, providerFilter, freeOnly = tru
|
|
|
14699
15934
|
}
|
|
14700
15935
|
return models;
|
|
14701
15936
|
}
|
|
14702
|
-
async function
|
|
15937
|
+
async function discoverModelsByProvider(providerID, freeOnly = true) {
|
|
14703
15938
|
try {
|
|
14704
|
-
const
|
|
15939
|
+
const opencodePath = resolveOpenCodePath();
|
|
15940
|
+
const proc = Bun.spawn([opencodePath, "models", "--refresh", "--verbose"], {
|
|
14705
15941
|
stdout: "pipe",
|
|
14706
15942
|
stderr: "pipe"
|
|
14707
15943
|
});
|
|
@@ -14715,7 +15951,7 @@ async function discoverFreeModelsByProvider(providerID) {
|
|
|
14715
15951
|
};
|
|
14716
15952
|
}
|
|
14717
15953
|
return {
|
|
14718
|
-
models: parseOpenCodeModelsVerboseOutput(stdout, providerID,
|
|
15954
|
+
models: parseOpenCodeModelsVerboseOutput(stdout, providerID, freeOnly)
|
|
14719
15955
|
};
|
|
14720
15956
|
} catch {
|
|
14721
15957
|
return {
|
|
@@ -14726,7 +15962,8 @@ async function discoverFreeModelsByProvider(providerID) {
|
|
|
14726
15962
|
}
|
|
14727
15963
|
async function discoverModelCatalog() {
|
|
14728
15964
|
try {
|
|
14729
|
-
const
|
|
15965
|
+
const opencodePath = resolveOpenCodePath();
|
|
15966
|
+
const proc = Bun.spawn([opencodePath, "models", "--refresh", "--verbose"], {
|
|
14730
15967
|
stdout: "pipe",
|
|
14731
15968
|
stderr: "pipe"
|
|
14732
15969
|
});
|
|
@@ -14750,10 +15987,11 @@ async function discoverModelCatalog() {
|
|
|
14750
15987
|
}
|
|
14751
15988
|
}
|
|
14752
15989
|
async function discoverOpenCodeFreeModels() {
|
|
14753
|
-
|
|
15990
|
+
const result = await discoverModelsByProvider("opencode", true);
|
|
15991
|
+
return { models: result.models, error: result.error };
|
|
14754
15992
|
}
|
|
14755
|
-
async function
|
|
14756
|
-
return
|
|
15993
|
+
async function discoverProviderModels(providerID) {
|
|
15994
|
+
return discoverModelsByProvider(providerID, false);
|
|
14757
15995
|
}
|
|
14758
15996
|
// src/cli/opencode-selection.ts
|
|
14759
15997
|
var scoreOpenCodePrimaryForCoding = (model) => {
|
|
@@ -14785,42 +16023,16 @@ function pickSupportOpenCodeModel(models, primaryModel) {
|
|
|
14785
16023
|
}, primaryModel);
|
|
14786
16024
|
return support;
|
|
14787
16025
|
}
|
|
14788
|
-
// src/cli/system.ts
|
|
14789
|
-
async function isOpenCodeInstalled() {
|
|
14790
|
-
try {
|
|
14791
|
-
const proc = Bun.spawn(["opencode", "--version"], {
|
|
14792
|
-
stdout: "pipe",
|
|
14793
|
-
stderr: "pipe"
|
|
14794
|
-
});
|
|
14795
|
-
await proc.exited;
|
|
14796
|
-
return proc.exitCode === 0;
|
|
14797
|
-
} catch {
|
|
14798
|
-
return false;
|
|
14799
|
-
}
|
|
14800
|
-
}
|
|
14801
|
-
async function getOpenCodeVersion() {
|
|
14802
|
-
try {
|
|
14803
|
-
const proc = Bun.spawn(["opencode", "--version"], {
|
|
14804
|
-
stdout: "pipe",
|
|
14805
|
-
stderr: "pipe"
|
|
14806
|
-
});
|
|
14807
|
-
const output = await new Response(proc.stdout).text();
|
|
14808
|
-
await proc.exited;
|
|
14809
|
-
return proc.exitCode === 0 ? output.trim() : null;
|
|
14810
|
-
} catch {
|
|
14811
|
-
return null;
|
|
14812
|
-
}
|
|
14813
|
-
}
|
|
14814
16026
|
// src/cli/custom-skills.ts
|
|
14815
16027
|
import {
|
|
14816
16028
|
copyFileSync as copyFileSync2,
|
|
14817
16029
|
existsSync as existsSync3,
|
|
14818
16030
|
mkdirSync as mkdirSync2,
|
|
14819
16031
|
readdirSync,
|
|
14820
|
-
statSync as
|
|
16032
|
+
statSync as statSync3
|
|
14821
16033
|
} from "fs";
|
|
14822
16034
|
import { homedir as homedir2 } from "os";
|
|
14823
|
-
import { dirname, join as
|
|
16035
|
+
import { dirname, join as join3 } from "path";
|
|
14824
16036
|
import { fileURLToPath } from "url";
|
|
14825
16037
|
var CUSTOM_SKILLS = [
|
|
14826
16038
|
{
|
|
@@ -14831,7 +16043,7 @@ var CUSTOM_SKILLS = [
|
|
|
14831
16043
|
}
|
|
14832
16044
|
];
|
|
14833
16045
|
function getCustomSkillsDir() {
|
|
14834
|
-
return
|
|
16046
|
+
return join3(homedir2(), ".config", "opencode", "skills");
|
|
14835
16047
|
}
|
|
14836
16048
|
function copyDirRecursive(src, dest) {
|
|
14837
16049
|
if (!existsSync3(dest)) {
|
|
@@ -14839,9 +16051,9 @@ function copyDirRecursive(src, dest) {
|
|
|
14839
16051
|
}
|
|
14840
16052
|
const entries = readdirSync(src);
|
|
14841
16053
|
for (const entry of entries) {
|
|
14842
|
-
const srcPath =
|
|
14843
|
-
const destPath =
|
|
14844
|
-
const stat =
|
|
16054
|
+
const srcPath = join3(src, entry);
|
|
16055
|
+
const destPath = join3(dest, entry);
|
|
16056
|
+
const stat = statSync3(srcPath);
|
|
14845
16057
|
if (stat.isDirectory()) {
|
|
14846
16058
|
copyDirRecursive(srcPath, destPath);
|
|
14847
16059
|
} else {
|
|
@@ -14856,8 +16068,8 @@ function copyDirRecursive(src, dest) {
|
|
|
14856
16068
|
function installCustomSkill(skill) {
|
|
14857
16069
|
try {
|
|
14858
16070
|
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
14859
|
-
const sourcePath =
|
|
14860
|
-
const targetPath =
|
|
16071
|
+
const sourcePath = join3(packageRoot, skill.sourcePath);
|
|
16072
|
+
const targetPath = join3(getCustomSkillsDir(), skill.name);
|
|
14861
16073
|
if (!existsSync3(sourcePath)) {
|
|
14862
16074
|
console.error(`Custom skill source not found: ${sourcePath}`);
|
|
14863
16075
|
return false;
|
|
@@ -14914,11 +16126,16 @@ async function checkOpenCodeInstalled() {
|
|
|
14914
16126
|
printError("OpenCode is not installed on this system.");
|
|
14915
16127
|
printInfo("Install it with:");
|
|
14916
16128
|
console.log(` ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`);
|
|
16129
|
+
console.log();
|
|
16130
|
+
printInfo("Or if already installed, add it to your PATH:");
|
|
16131
|
+
console.log(` ${BLUE}export PATH="$HOME/.local/bin:$PATH"${RESET}`);
|
|
16132
|
+
console.log(` ${BLUE}export PATH="$HOME/.opencode/bin:$PATH"${RESET}`);
|
|
14917
16133
|
return { ok: false };
|
|
14918
16134
|
}
|
|
14919
16135
|
const version2 = await getOpenCodeVersion();
|
|
14920
|
-
|
|
14921
|
-
|
|
16136
|
+
const path2 = getOpenCodePath();
|
|
16137
|
+
printSuccess(`OpenCode ${version2 ?? ""} detected${path2 ? ` (${DIM}${path2}${RESET})` : ""}`);
|
|
16138
|
+
return { ok: true, version: version2 ?? undefined, path: path2 ?? undefined };
|
|
14922
16139
|
}
|
|
14923
16140
|
function handleStepResult(result, successMsg) {
|
|
14924
16141
|
if (!result.success) {
|
|
@@ -14943,6 +16160,7 @@ function formatConfigSummary(config2) {
|
|
|
14943
16160
|
lines.push(` ${config2.hasAntigravity ? SYMBOLS.check : `${DIM}\u25CB${RESET}`} Antigravity (Google)`);
|
|
14944
16161
|
lines.push(` ${config2.hasChutes ? SYMBOLS.check : `${DIM}\u25CB${RESET}`} Chutes`);
|
|
14945
16162
|
lines.push(` ${SYMBOLS.check} Opencode Zen`);
|
|
16163
|
+
lines.push(` ${config2.balanceProviderUsage ? SYMBOLS.check : `${DIM}\u25CB${RESET}`} Balanced provider spend`);
|
|
14946
16164
|
if (config2.useOpenCodeFreeModels && config2.selectedOpenCodePrimaryModel) {
|
|
14947
16165
|
lines.push(` ${SYMBOLS.check} OpenCode Free Primary: ${BLUE}${config2.selectedOpenCodePrimaryModel}${RESET}`);
|
|
14948
16166
|
}
|
|
@@ -14988,9 +16206,15 @@ function argsToConfig(args) {
|
|
|
14988
16206
|
hasOpencodeZen: true,
|
|
14989
16207
|
useOpenCodeFreeModels: args.opencodeFree === "yes",
|
|
14990
16208
|
preferredOpenCodeModel: args.opencodeFreeModel && args.opencodeFreeModel !== "auto" ? args.opencodeFreeModel : undefined,
|
|
16209
|
+
artificialAnalysisApiKey: args.aaKey,
|
|
16210
|
+
openRouterApiKey: args.openrouterKey,
|
|
16211
|
+
balanceProviderUsage: args.balancedSpend === "yes",
|
|
14991
16212
|
hasTmux: args.tmux === "yes",
|
|
14992
16213
|
installSkills: args.skills === "yes",
|
|
14993
|
-
installCustomSkills: args.skills === "yes"
|
|
16214
|
+
installCustomSkills: args.skills === "yes",
|
|
16215
|
+
setupMode: "quick",
|
|
16216
|
+
dryRun: args.dryRun,
|
|
16217
|
+
modelsOnly: args.modelsOnly
|
|
14994
16218
|
};
|
|
14995
16219
|
}
|
|
14996
16220
|
async function askModelSelection(rl, models, defaultModel, prompt) {
|
|
@@ -15022,20 +16246,310 @@ async function askYesNo(rl, prompt, defaultValue = "no") {
|
|
|
15022
16246
|
return "no";
|
|
15023
16247
|
return defaultValue;
|
|
15024
16248
|
}
|
|
15025
|
-
async function
|
|
16249
|
+
async function askOptionalApiKey(rl, prompt, fromEnv) {
|
|
16250
|
+
const hint = fromEnv ? "[optional, Enter keeps env value]" : "[optional]";
|
|
16251
|
+
const answer = (await rl.question(`${BLUE}${prompt}${RESET} ${DIM}${hint}${RESET}: `)).trim();
|
|
16252
|
+
if (!answer)
|
|
16253
|
+
return fromEnv;
|
|
16254
|
+
return answer;
|
|
16255
|
+
}
|
|
16256
|
+
async function askSetupMode(rl) {
|
|
16257
|
+
console.log(`${BOLD}Choose setup mode:${RESET}`);
|
|
16258
|
+
console.log(` ${DIM}1.${RESET} Quick setup - you choose providers, we auto-pick models`);
|
|
16259
|
+
console.log(` ${DIM}2.${RESET} Manual setup - you choose providers and models per agent`);
|
|
16260
|
+
console.log();
|
|
16261
|
+
const answer = (await rl.question(`${BLUE}Selection${RESET} ${DIM}[default: 1]${RESET}: `)).trim().toLowerCase();
|
|
16262
|
+
if (answer === "2" || answer === "manual")
|
|
16263
|
+
return "manual";
|
|
16264
|
+
return "quick";
|
|
16265
|
+
}
|
|
16266
|
+
async function askModelByNumber(rl, models, prompt, allowEmpty = false) {
|
|
16267
|
+
let showAll = false;
|
|
16268
|
+
while (true) {
|
|
16269
|
+
console.log(`${BOLD}${prompt}${RESET}`);
|
|
16270
|
+
console.log(`${DIM}Available models:${RESET}`);
|
|
16271
|
+
const modelsToShow = showAll ? models : models.slice(0, 5);
|
|
16272
|
+
const remainingCount = models.length - modelsToShow.length;
|
|
16273
|
+
for (const [index, model] of modelsToShow.entries()) {
|
|
16274
|
+
const displayIndex = showAll ? index + 1 : index + 1;
|
|
16275
|
+
const name = model.name ? ` ${DIM}(${model.name})${RESET}` : "";
|
|
16276
|
+
console.log(` ${DIM}${displayIndex}.${RESET} ${BLUE}${model.model}${RESET}${name}`);
|
|
16277
|
+
}
|
|
16278
|
+
if (!showAll && remainingCount > 0) {
|
|
16279
|
+
console.log(`${DIM} ... and ${remainingCount} more${RESET}`);
|
|
16280
|
+
console.log(`${DIM} (type "all" to show the full list)${RESET}`);
|
|
16281
|
+
}
|
|
16282
|
+
console.log(`${DIM} (or type any model ID directly)${RESET}`);
|
|
16283
|
+
console.log();
|
|
16284
|
+
const answer = (await rl.question(`${BLUE}Selection${RESET}: `)).trim().toLowerCase();
|
|
16285
|
+
if (!answer) {
|
|
16286
|
+
if (allowEmpty)
|
|
16287
|
+
return;
|
|
16288
|
+
return models[0]?.model;
|
|
16289
|
+
}
|
|
16290
|
+
if (answer === "all") {
|
|
16291
|
+
showAll = true;
|
|
16292
|
+
console.log();
|
|
16293
|
+
continue;
|
|
16294
|
+
}
|
|
16295
|
+
const asNumber = Number.parseInt(answer, 10);
|
|
16296
|
+
if (Number.isFinite(asNumber) && asNumber >= 1 && asNumber <= models.length) {
|
|
16297
|
+
return models[asNumber - 1]?.model;
|
|
16298
|
+
}
|
|
16299
|
+
const byId = models.find((m) => m.model.toLowerCase() === answer);
|
|
16300
|
+
if (byId)
|
|
16301
|
+
return byId.model;
|
|
16302
|
+
if (answer.includes("/"))
|
|
16303
|
+
return answer;
|
|
16304
|
+
printWarning(`Invalid selection: "${answer}". Using first available model.`);
|
|
16305
|
+
return models[0]?.model;
|
|
16306
|
+
}
|
|
16307
|
+
}
|
|
16308
|
+
async function configureAgentManually(rl, agentName, allModels) {
|
|
16309
|
+
console.log();
|
|
16310
|
+
console.log(`${BOLD}Configure ${agentName}:${RESET}`);
|
|
16311
|
+
console.log();
|
|
16312
|
+
const selectedModels = new Set;
|
|
16313
|
+
const primary = await askModelByNumber(rl, allModels, "Primary model") ?? allModels[0]?.model ?? "opencode/big-pickle";
|
|
16314
|
+
selectedModels.add(primary);
|
|
16315
|
+
const availableForFallback1 = allModels.filter((m) => !selectedModels.has(m.model));
|
|
16316
|
+
const fallback1 = availableForFallback1.length > 0 ? await askModelByNumber(rl, availableForFallback1, "Fallback 1 (optional, press Enter to skip)", true) ?? primary : primary;
|
|
16317
|
+
if (fallback1 !== primary)
|
|
16318
|
+
selectedModels.add(fallback1);
|
|
16319
|
+
const availableForFallback2 = allModels.filter((m) => !selectedModels.has(m.model));
|
|
16320
|
+
const fallback2 = availableForFallback2.length > 0 ? await askModelByNumber(rl, availableForFallback2, "Fallback 2 (optional, press Enter to skip)", true) ?? fallback1 : fallback1;
|
|
16321
|
+
if (fallback2 !== fallback1)
|
|
16322
|
+
selectedModels.add(fallback2);
|
|
16323
|
+
const availableForFallback3 = allModels.filter((m) => !selectedModels.has(m.model));
|
|
16324
|
+
const fallback3 = availableForFallback3.length > 0 ? await askModelByNumber(rl, availableForFallback3, "Fallback 3 (optional, press Enter to skip)", true) ?? fallback2 : fallback2;
|
|
16325
|
+
return {
|
|
16326
|
+
primary,
|
|
16327
|
+
fallback1,
|
|
16328
|
+
fallback2,
|
|
16329
|
+
fallback3
|
|
16330
|
+
};
|
|
16331
|
+
}
|
|
16332
|
+
async function runManualSetupMode(rl, detected, modelsOnly = false) {
|
|
16333
|
+
console.log();
|
|
16334
|
+
console.log(`${BOLD}Manual Setup Mode${RESET}`);
|
|
16335
|
+
console.log("=".repeat(20));
|
|
16336
|
+
console.log();
|
|
16337
|
+
const existingAaKey = getEnv("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
16338
|
+
const existingOpenRouterKey = getEnv("OPENROUTER_API_KEY");
|
|
16339
|
+
const artificialAnalysisApiKey = await askOptionalApiKey(rl, "Artificial Analysis API key for better ranking signals", existingAaKey);
|
|
16340
|
+
if (existingAaKey && !artificialAnalysisApiKey) {
|
|
16341
|
+
printInfo("Using existing ARTIFICIAL_ANALYSIS_API_KEY from environment.");
|
|
16342
|
+
}
|
|
16343
|
+
console.log();
|
|
16344
|
+
const openRouterApiKey = await askOptionalApiKey(rl, "OpenRouter API key for pricing/metadata signals", existingOpenRouterKey);
|
|
16345
|
+
if (existingOpenRouterKey && !openRouterApiKey) {
|
|
16346
|
+
printInfo("Using existing OPENROUTER_API_KEY from environment.");
|
|
16347
|
+
}
|
|
16348
|
+
console.log();
|
|
16349
|
+
const useOpenCodeFree = await askYesNo(rl, "Use Opencode Free models (opencode/*)?", "yes");
|
|
16350
|
+
console.log();
|
|
16351
|
+
let availableOpenCodeFreeModels;
|
|
16352
|
+
let availableChutesModels;
|
|
16353
|
+
if (useOpenCodeFree === "yes") {
|
|
16354
|
+
printInfo("Refreshing models with: opencode models --refresh --verbose");
|
|
16355
|
+
const discovery = await discoverOpenCodeFreeModels();
|
|
16356
|
+
if (discovery.models.length === 0) {
|
|
16357
|
+
printWarning(discovery.error ?? "No OpenCode free models found. Continuing without OpenCode free-model assignment.");
|
|
16358
|
+
} else {
|
|
16359
|
+
availableOpenCodeFreeModels = discovery.models;
|
|
16360
|
+
printSuccess(`Found ${discovery.models.length} OpenCode free models`);
|
|
16361
|
+
}
|
|
16362
|
+
console.log();
|
|
16363
|
+
}
|
|
16364
|
+
const kimi = await askYesNo(rl, "Enable Kimi provider?", detected.hasKimi ? "yes" : "no");
|
|
16365
|
+
console.log();
|
|
16366
|
+
const openai = await askYesNo(rl, "Enable OpenAI provider?", detected.hasOpenAI ? "yes" : "no");
|
|
16367
|
+
console.log();
|
|
16368
|
+
const anthropic = await askYesNo(rl, "Enable Anthropic provider?", detected.hasAnthropic ? "yes" : "no");
|
|
16369
|
+
console.log();
|
|
16370
|
+
const copilot = await askYesNo(rl, "Enable GitHub Copilot provider?", detected.hasCopilot ? "yes" : "no");
|
|
16371
|
+
console.log();
|
|
16372
|
+
const zaiPlan = await askYesNo(rl, "Enable ZAI Coding Plan provider?", detected.hasZaiPlan ? "yes" : "no");
|
|
16373
|
+
console.log();
|
|
16374
|
+
const antigravity = await askYesNo(rl, "Enable Antigravity (Google) provider?", detected.hasAntigravity ? "yes" : "no");
|
|
16375
|
+
console.log();
|
|
16376
|
+
const chutes = await askYesNo(rl, "Enable Chutes provider?", detected.hasChutes ? "yes" : "no");
|
|
16377
|
+
console.log();
|
|
16378
|
+
if (chutes === "yes") {
|
|
16379
|
+
printInfo("Refreshing Chutes model list with: opencode models --refresh --verbose");
|
|
16380
|
+
const discovery = await discoverProviderModels("chutes");
|
|
16381
|
+
if (discovery.models.length === 0) {
|
|
16382
|
+
printWarning(discovery.error ?? "No Chutes models found. Continuing without Chutes dynamic assignment.");
|
|
16383
|
+
} else {
|
|
16384
|
+
availableChutesModels = discovery.models;
|
|
16385
|
+
printSuccess(`Found ${discovery.models.length} Chutes models`);
|
|
16386
|
+
}
|
|
16387
|
+
console.log();
|
|
16388
|
+
}
|
|
16389
|
+
const availableModels = [];
|
|
16390
|
+
if (useOpenCodeFree === "yes" && availableOpenCodeFreeModels) {
|
|
16391
|
+
for (const model of availableOpenCodeFreeModels) {
|
|
16392
|
+
availableModels.push({ model: model.model, name: model.name });
|
|
16393
|
+
}
|
|
16394
|
+
}
|
|
16395
|
+
if (kimi === "yes") {
|
|
16396
|
+
availableModels.push({ model: "kimi-for-coding/k2p5", name: "Kimi K2.5" });
|
|
16397
|
+
}
|
|
16398
|
+
if (openai === "yes") {
|
|
16399
|
+
availableModels.push({
|
|
16400
|
+
model: "openai/gpt-5.3-codex",
|
|
16401
|
+
name: "GPT-5.3 Codex"
|
|
16402
|
+
});
|
|
16403
|
+
availableModels.push({
|
|
16404
|
+
model: "openai/gpt-5.1-codex-mini",
|
|
16405
|
+
name: "GPT-5.1 Codex Mini"
|
|
16406
|
+
});
|
|
16407
|
+
}
|
|
16408
|
+
if (anthropic === "yes") {
|
|
16409
|
+
availableModels.push({
|
|
16410
|
+
model: "anthropic/claude-opus-4-6",
|
|
16411
|
+
name: "Claude Opus 4.6"
|
|
16412
|
+
});
|
|
16413
|
+
availableModels.push({
|
|
16414
|
+
model: "anthropic/claude-sonnet-4-5",
|
|
16415
|
+
name: "Claude Sonnet 4.5"
|
|
16416
|
+
});
|
|
16417
|
+
availableModels.push({
|
|
16418
|
+
model: "anthropic/claude-haiku-4-5",
|
|
16419
|
+
name: "Claude Haiku 4.5"
|
|
16420
|
+
});
|
|
16421
|
+
}
|
|
16422
|
+
if (copilot === "yes") {
|
|
16423
|
+
availableModels.push({
|
|
16424
|
+
model: "github-copilot/grok-code-fast-1",
|
|
16425
|
+
name: "Grok Code Fast"
|
|
16426
|
+
});
|
|
16427
|
+
}
|
|
16428
|
+
if (zaiPlan === "yes") {
|
|
16429
|
+
availableModels.push({ model: "zai-coding-plan/glm-4.7", name: "GLM 4.7" });
|
|
16430
|
+
}
|
|
16431
|
+
if (antigravity === "yes") {
|
|
16432
|
+
availableModels.push({
|
|
16433
|
+
model: "google/antigravity-gemini-3-pro",
|
|
16434
|
+
name: "Gemini 3 Pro"
|
|
16435
|
+
});
|
|
16436
|
+
availableModels.push({
|
|
16437
|
+
model: "google/antigravity-gemini-3-flash",
|
|
16438
|
+
name: "Gemini 3 Flash"
|
|
16439
|
+
});
|
|
16440
|
+
}
|
|
16441
|
+
if (chutes === "yes" && availableChutesModels) {
|
|
16442
|
+
for (const model of availableChutesModels) {
|
|
16443
|
+
availableModels.push({ model: model.model, name: model.name });
|
|
16444
|
+
}
|
|
16445
|
+
}
|
|
16446
|
+
availableModels.push({
|
|
16447
|
+
model: "opencode/big-pickle",
|
|
16448
|
+
name: "OpenCode Big Pickle"
|
|
16449
|
+
});
|
|
16450
|
+
if (availableModels.length === 0) {
|
|
16451
|
+
printWarning("No models available. Please enable at least one provider.");
|
|
16452
|
+
availableModels.push({
|
|
16453
|
+
model: "opencode/big-pickle",
|
|
16454
|
+
name: "OpenCode Big Pickle"
|
|
16455
|
+
});
|
|
16456
|
+
}
|
|
16457
|
+
const manualAgentConfigs = {};
|
|
16458
|
+
const agentNames = [
|
|
16459
|
+
"orchestrator",
|
|
16460
|
+
"oracle",
|
|
16461
|
+
"designer",
|
|
16462
|
+
"explorer",
|
|
16463
|
+
"librarian",
|
|
16464
|
+
"fixer"
|
|
16465
|
+
];
|
|
16466
|
+
for (const agentName of agentNames) {
|
|
16467
|
+
manualAgentConfigs[agentName] = await configureAgentManually(rl, agentName, availableModels);
|
|
16468
|
+
}
|
|
16469
|
+
const balancedSpend = await askYesNo(rl, "Do you have subscriptions or pay per API? If yes, we will distribute assignments evenly across selected providers so your subscriptions last longer.", "no");
|
|
16470
|
+
console.log();
|
|
16471
|
+
let skills = "no";
|
|
16472
|
+
let customSkills = "no";
|
|
16473
|
+
if (!modelsOnly) {
|
|
16474
|
+
console.log(`${BOLD}Recommended Skills:${RESET}`);
|
|
16475
|
+
for (const skill of RECOMMENDED_SKILLS) {
|
|
16476
|
+
console.log(` ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
|
|
16477
|
+
}
|
|
16478
|
+
console.log();
|
|
16479
|
+
skills = await askYesNo(rl, "Install recommended skills?", "yes");
|
|
16480
|
+
console.log();
|
|
16481
|
+
console.log(`${BOLD}Custom Skills:${RESET}`);
|
|
16482
|
+
for (const skill of CUSTOM_SKILLS) {
|
|
16483
|
+
console.log(` ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
|
|
16484
|
+
}
|
|
16485
|
+
console.log();
|
|
16486
|
+
customSkills = await askYesNo(rl, "Install custom skills?", "yes");
|
|
16487
|
+
console.log();
|
|
16488
|
+
} else {
|
|
16489
|
+
printInfo("Models-only mode: skipping plugin/auth setup and skills prompts.");
|
|
16490
|
+
console.log();
|
|
16491
|
+
}
|
|
16492
|
+
return {
|
|
16493
|
+
hasKimi: kimi === "yes",
|
|
16494
|
+
hasOpenAI: openai === "yes",
|
|
16495
|
+
hasAnthropic: anthropic === "yes",
|
|
16496
|
+
hasCopilot: copilot === "yes",
|
|
16497
|
+
hasZaiPlan: zaiPlan === "yes",
|
|
16498
|
+
hasAntigravity: antigravity === "yes",
|
|
16499
|
+
hasChutes: chutes === "yes",
|
|
16500
|
+
hasOpencodeZen: true,
|
|
16501
|
+
useOpenCodeFreeModels: useOpenCodeFree === "yes" && availableOpenCodeFreeModels !== undefined,
|
|
16502
|
+
availableOpenCodeFreeModels,
|
|
16503
|
+
availableChutesModels,
|
|
16504
|
+
artificialAnalysisApiKey,
|
|
16505
|
+
openRouterApiKey,
|
|
16506
|
+
balanceProviderUsage: balancedSpend === "yes",
|
|
16507
|
+
hasTmux: false,
|
|
16508
|
+
installSkills: skills === "yes",
|
|
16509
|
+
installCustomSkills: customSkills === "yes",
|
|
16510
|
+
setupMode: "manual",
|
|
16511
|
+
manualAgentConfigs,
|
|
16512
|
+
modelsOnly
|
|
16513
|
+
};
|
|
16514
|
+
}
|
|
16515
|
+
async function runInteractiveMode(detected, modelsOnly = false) {
|
|
15026
16516
|
const rl = readline.createInterface({
|
|
15027
16517
|
input: process.stdin,
|
|
15028
16518
|
output: process.stdout
|
|
15029
16519
|
});
|
|
15030
|
-
const totalQuestions = 8;
|
|
15031
16520
|
try {
|
|
16521
|
+
console.log();
|
|
16522
|
+
console.log(`${BOLD}oh-my-opencode-slim Setup${RESET}`);
|
|
16523
|
+
console.log("=".repeat(25));
|
|
16524
|
+
console.log();
|
|
16525
|
+
const setupMode = await askSetupMode(rl);
|
|
16526
|
+
if (setupMode === "manual") {
|
|
16527
|
+
const config2 = await runManualSetupMode(rl, detected, modelsOnly);
|
|
16528
|
+
rl.close();
|
|
16529
|
+
return config2;
|
|
16530
|
+
}
|
|
16531
|
+
const totalQuestions = 11;
|
|
16532
|
+
const existingAaKey = getEnv("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
16533
|
+
const existingOpenRouterKey = getEnv("OPENROUTER_API_KEY");
|
|
15032
16534
|
console.log(`${BOLD}Question 1/${totalQuestions}:${RESET}`);
|
|
15033
|
-
const
|
|
16535
|
+
const artificialAnalysisApiKey = await askOptionalApiKey(rl, "Artificial Analysis API key for better ranking signals", existingAaKey);
|
|
16536
|
+
if (existingAaKey && !artificialAnalysisApiKey) {
|
|
16537
|
+
printInfo("Using existing ARTIFICIAL_ANALYSIS_API_KEY from environment.");
|
|
16538
|
+
}
|
|
16539
|
+
console.log();
|
|
16540
|
+
console.log(`${BOLD}Question 2/${totalQuestions}:${RESET}`);
|
|
16541
|
+
const openRouterApiKey = await askOptionalApiKey(rl, "OpenRouter API key for pricing/metadata signals", existingOpenRouterKey);
|
|
16542
|
+
if (existingOpenRouterKey && !openRouterApiKey) {
|
|
16543
|
+
printInfo("Using existing OPENROUTER_API_KEY from environment.");
|
|
16544
|
+
}
|
|
16545
|
+
console.log();
|
|
16546
|
+
console.log(`${BOLD}Question 3/${totalQuestions}:${RESET}`);
|
|
16547
|
+
const useOpenCodeFree = await askYesNo(rl, "Use Opencode Free models (opencode/*)?", "yes");
|
|
15034
16548
|
console.log();
|
|
15035
16549
|
let availableOpenCodeFreeModels;
|
|
15036
16550
|
let selectedOpenCodePrimaryModel;
|
|
15037
16551
|
let selectedOpenCodeSecondaryModel;
|
|
15038
|
-
let
|
|
16552
|
+
let availableChutesModels;
|
|
15039
16553
|
let selectedChutesPrimaryModel;
|
|
15040
16554
|
let selectedChutesSecondaryModel;
|
|
15041
16555
|
if (useOpenCodeFree === "yes") {
|
|
@@ -15047,70 +16561,85 @@ async function runInteractiveMode(detected) {
|
|
|
15047
16561
|
availableOpenCodeFreeModels = discovery.models;
|
|
15048
16562
|
const recommendedPrimary = pickBestCodingOpenCodeModel(discovery.models)?.model ?? discovery.models[0]?.model;
|
|
15049
16563
|
if (recommendedPrimary) {
|
|
16564
|
+
printInfo("This step configures only OpenCode Free primary/support models.");
|
|
15050
16565
|
console.log(`${BOLD}OpenCode Free Models:${RESET}`);
|
|
15051
16566
|
selectedOpenCodePrimaryModel = await askModelSelection(rl, discovery.models, recommendedPrimary, "Choose primary model for orchestrator/oracle");
|
|
15052
16567
|
}
|
|
15053
16568
|
if (selectedOpenCodePrimaryModel) {
|
|
15054
16569
|
const recommendedSecondary = pickSupportOpenCodeModel(discovery.models, selectedOpenCodePrimaryModel)?.model ?? selectedOpenCodePrimaryModel;
|
|
15055
|
-
|
|
16570
|
+
const openCodeSupportList = discovery.models.filter((model) => model.model !== selectedOpenCodePrimaryModel);
|
|
16571
|
+
const openCodeSupportDefault = recommendedSecondary === selectedOpenCodePrimaryModel ? openCodeSupportList[0]?.model ?? recommendedSecondary : recommendedSecondary;
|
|
16572
|
+
selectedOpenCodeSecondaryModel = await askModelSelection(rl, openCodeSupportList, openCodeSupportDefault, "Choose support model for explorer/librarian/fixer");
|
|
15056
16573
|
}
|
|
15057
16574
|
console.log();
|
|
15058
16575
|
}
|
|
15059
16576
|
}
|
|
15060
|
-
console.log(`${BOLD}Question 2/${totalQuestions}:${RESET}`);
|
|
15061
|
-
const kimi = await askYesNo(rl, "Do you want to use Kimi For Coding?", detected.hasKimi ? "yes" : "no");
|
|
15062
|
-
console.log();
|
|
15063
|
-
console.log(`${BOLD}Question 3/${totalQuestions}:${RESET}`);
|
|
15064
|
-
const openai = await askYesNo(rl, "Do you have access to OpenAI API?", detected.hasOpenAI ? "yes" : "no");
|
|
15065
|
-
console.log();
|
|
15066
16577
|
console.log(`${BOLD}Question 4/${totalQuestions}:${RESET}`);
|
|
15067
|
-
const
|
|
16578
|
+
const kimi = await askYesNo(rl, "Enable Kimi provider?", detected.hasKimi ? "yes" : "no");
|
|
15068
16579
|
console.log();
|
|
15069
16580
|
console.log(`${BOLD}Question 5/${totalQuestions}:${RESET}`);
|
|
15070
|
-
const
|
|
16581
|
+
const openai = await askYesNo(rl, "Enable OpenAI provider?", detected.hasOpenAI ? "yes" : "no");
|
|
15071
16582
|
console.log();
|
|
15072
16583
|
console.log(`${BOLD}Question 6/${totalQuestions}:${RESET}`);
|
|
15073
|
-
const
|
|
16584
|
+
const anthropic = await askYesNo(rl, "Enable Anthropic provider?", detected.hasAnthropic ? "yes" : "no");
|
|
15074
16585
|
console.log();
|
|
15075
16586
|
console.log(`${BOLD}Question 7/${totalQuestions}:${RESET}`);
|
|
15076
|
-
const
|
|
16587
|
+
const copilot = await askYesNo(rl, "Enable GitHub Copilot provider?", detected.hasCopilot ? "yes" : "no");
|
|
15077
16588
|
console.log();
|
|
15078
16589
|
console.log(`${BOLD}Question 8/${totalQuestions}:${RESET}`);
|
|
15079
|
-
const
|
|
16590
|
+
const zaiPlan = await askYesNo(rl, "Enable ZAI Coding Plan provider?", detected.hasZaiPlan ? "yes" : "no");
|
|
16591
|
+
console.log();
|
|
16592
|
+
console.log(`${BOLD}Question 9/${totalQuestions}:${RESET}`);
|
|
16593
|
+
const antigravity = await askYesNo(rl, "Enable Antigravity (Google) provider?", detected.hasAntigravity ? "yes" : "no");
|
|
16594
|
+
console.log();
|
|
16595
|
+
console.log(`${BOLD}Question 10/${totalQuestions}:${RESET}`);
|
|
16596
|
+
const chutes = await askYesNo(rl, "Enable Chutes provider?", detected.hasChutes ? "yes" : "no");
|
|
15080
16597
|
console.log();
|
|
15081
16598
|
if (chutes === "yes") {
|
|
15082
16599
|
printInfo("Refreshing Chutes model list with: opencode models --refresh --verbose");
|
|
15083
|
-
const discovery = await
|
|
16600
|
+
const discovery = await discoverProviderModels("chutes");
|
|
15084
16601
|
if (discovery.models.length === 0) {
|
|
15085
|
-
printWarning(discovery.error ?? "No
|
|
16602
|
+
printWarning(discovery.error ?? "No Chutes models found. Continuing without Chutes dynamic assignment.");
|
|
15086
16603
|
} else {
|
|
15087
|
-
|
|
16604
|
+
availableChutesModels = discovery.models;
|
|
15088
16605
|
const recommendedPrimary = pickBestCodingChutesModel(discovery.models)?.model ?? discovery.models[0]?.model;
|
|
15089
16606
|
if (recommendedPrimary) {
|
|
15090
|
-
console.log(`${BOLD}Chutes
|
|
16607
|
+
console.log(`${BOLD}Chutes Models:${RESET}`);
|
|
15091
16608
|
selectedChutesPrimaryModel = await askModelSelection(rl, discovery.models, recommendedPrimary, "Choose Chutes primary model for orchestrator/oracle/designer");
|
|
15092
16609
|
}
|
|
15093
16610
|
if (selectedChutesPrimaryModel) {
|
|
15094
16611
|
const recommendedSecondary = pickSupportChutesModel(discovery.models, selectedChutesPrimaryModel)?.model ?? selectedChutesPrimaryModel;
|
|
15095
|
-
|
|
16612
|
+
const chutesSupportList = discovery.models.filter((model) => model.model !== selectedChutesPrimaryModel);
|
|
16613
|
+
const chutesSupportDefault = recommendedSecondary === selectedChutesPrimaryModel ? chutesSupportList[0]?.model ?? recommendedSecondary : recommendedSecondary;
|
|
16614
|
+
selectedChutesSecondaryModel = await askModelSelection(rl, chutesSupportList, chutesSupportDefault, "Choose Chutes support model for explorer/librarian/fixer");
|
|
15096
16615
|
}
|
|
15097
16616
|
console.log();
|
|
15098
16617
|
}
|
|
15099
16618
|
}
|
|
15100
|
-
console.log(`${BOLD}
|
|
15101
|
-
|
|
15102
|
-
console.log(` ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
|
|
15103
|
-
}
|
|
15104
|
-
console.log();
|
|
15105
|
-
const skills = await askYesNo(rl, "Install recommended skills?", "yes");
|
|
16619
|
+
console.log(`${BOLD}Question 11/${totalQuestions}:${RESET}`);
|
|
16620
|
+
const balancedSpend = await askYesNo(rl, "Do you have subscriptions or pay per API? If yes, we will distribute assignments evenly across selected providers so your subscriptions last longer.", "no");
|
|
15106
16621
|
console.log();
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
16622
|
+
let skills = "no";
|
|
16623
|
+
let customSkills = "no";
|
|
16624
|
+
if (!modelsOnly) {
|
|
16625
|
+
console.log(`${BOLD}Recommended Skills:${RESET}`);
|
|
16626
|
+
for (const skill of RECOMMENDED_SKILLS) {
|
|
16627
|
+
console.log(` ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
|
|
16628
|
+
}
|
|
16629
|
+
console.log();
|
|
16630
|
+
skills = await askYesNo(rl, "Install recommended skills?", "yes");
|
|
16631
|
+
console.log();
|
|
16632
|
+
console.log(`${BOLD}Custom Skills:${RESET}`);
|
|
16633
|
+
for (const skill of CUSTOM_SKILLS) {
|
|
16634
|
+
console.log(` ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
|
|
16635
|
+
}
|
|
16636
|
+
console.log();
|
|
16637
|
+
customSkills = await askYesNo(rl, "Install custom skills?", "yes");
|
|
16638
|
+
console.log();
|
|
16639
|
+
} else {
|
|
16640
|
+
printInfo("Models-only mode: skipping plugin/auth setup and skills prompts.");
|
|
16641
|
+
console.log();
|
|
15110
16642
|
}
|
|
15111
|
-
console.log();
|
|
15112
|
-
const customSkills = await askYesNo(rl, "Install custom skills?", "yes");
|
|
15113
|
-
console.log();
|
|
15114
16643
|
return {
|
|
15115
16644
|
hasKimi: kimi === "yes",
|
|
15116
16645
|
hasOpenAI: openai === "yes",
|
|
@@ -15126,10 +16655,15 @@ async function runInteractiveMode(detected) {
|
|
|
15126
16655
|
availableOpenCodeFreeModels,
|
|
15127
16656
|
selectedChutesPrimaryModel,
|
|
15128
16657
|
selectedChutesSecondaryModel,
|
|
15129
|
-
|
|
16658
|
+
availableChutesModels,
|
|
16659
|
+
artificialAnalysisApiKey,
|
|
16660
|
+
openRouterApiKey,
|
|
16661
|
+
balanceProviderUsage: balancedSpend === "yes",
|
|
15130
16662
|
hasTmux: false,
|
|
15131
16663
|
installSkills: skills === "yes",
|
|
15132
|
-
installCustomSkills: customSkills === "yes"
|
|
16664
|
+
installCustomSkills: customSkills === "yes",
|
|
16665
|
+
setupMode: "quick",
|
|
16666
|
+
modelsOnly
|
|
15133
16667
|
};
|
|
15134
16668
|
} finally {
|
|
15135
16669
|
rl.close();
|
|
@@ -15143,24 +16677,32 @@ async function runInstall(config2) {
|
|
|
15143
16677
|
const isUpdate = detected.isInstalled;
|
|
15144
16678
|
printHeader(isUpdate);
|
|
15145
16679
|
const hasAnyEnabledProvider = resolvedConfig.hasKimi || resolvedConfig.hasOpenAI || resolvedConfig.hasAnthropic || resolvedConfig.hasCopilot || resolvedConfig.hasZaiPlan || resolvedConfig.hasAntigravity || resolvedConfig.hasChutes || resolvedConfig.useOpenCodeFreeModels;
|
|
15146
|
-
|
|
16680
|
+
const modelsOnly = resolvedConfig.modelsOnly === true;
|
|
16681
|
+
let totalSteps = modelsOnly ? 2 : 4;
|
|
15147
16682
|
if (resolvedConfig.useOpenCodeFreeModels)
|
|
15148
16683
|
totalSteps += 1;
|
|
15149
|
-
if (resolvedConfig.hasAntigravity)
|
|
16684
|
+
if (!modelsOnly && resolvedConfig.hasAntigravity)
|
|
15150
16685
|
totalSteps += 2;
|
|
15151
|
-
if (resolvedConfig.hasChutes)
|
|
16686
|
+
if (!modelsOnly && resolvedConfig.hasChutes)
|
|
15152
16687
|
totalSteps += 1;
|
|
15153
16688
|
if (hasAnyEnabledProvider)
|
|
15154
16689
|
totalSteps += 1;
|
|
15155
|
-
if (resolvedConfig.installSkills)
|
|
16690
|
+
if (!modelsOnly && resolvedConfig.installSkills)
|
|
15156
16691
|
totalSteps += 1;
|
|
15157
|
-
if (resolvedConfig.installCustomSkills)
|
|
16692
|
+
if (!modelsOnly && resolvedConfig.installCustomSkills)
|
|
15158
16693
|
totalSteps += 1;
|
|
15159
16694
|
let step = 1;
|
|
16695
|
+
if (modelsOnly) {
|
|
16696
|
+
printInfo("Models-only mode: updating model assignments without reinstalling plugins/skills.");
|
|
16697
|
+
}
|
|
15160
16698
|
printStep(step++, totalSteps, "Checking OpenCode installation...");
|
|
15161
|
-
|
|
15162
|
-
|
|
15163
|
-
|
|
16699
|
+
if (resolvedConfig.dryRun) {
|
|
16700
|
+
printInfo("Dry run mode - skipping OpenCode check");
|
|
16701
|
+
} else {
|
|
16702
|
+
const { ok } = await checkOpenCodeInstalled();
|
|
16703
|
+
if (!ok)
|
|
16704
|
+
return 1;
|
|
16705
|
+
}
|
|
15164
16706
|
if (resolvedConfig.useOpenCodeFreeModels && (resolvedConfig.availableOpenCodeFreeModels?.length ?? 0) === 0) {
|
|
15165
16707
|
printStep(step++, totalSteps, "Refreshing OpenCode free models (opencode/*)...");
|
|
15166
16708
|
const discovery = await discoverOpenCodeFreeModels();
|
|
@@ -15181,43 +16723,61 @@ async function runInstall(config2) {
|
|
|
15181
16723
|
printStep(step++, totalSteps, "Using previously refreshed OpenCode free model list...");
|
|
15182
16724
|
printSuccess(`OpenCode free models ready (${availableModels.length} models found)`);
|
|
15183
16725
|
}
|
|
15184
|
-
if (resolvedConfig.hasChutes && (resolvedConfig.
|
|
15185
|
-
printStep(step++, totalSteps, "Refreshing Chutes
|
|
15186
|
-
const discovery = await
|
|
16726
|
+
if (resolvedConfig.hasChutes && (resolvedConfig.availableChutesModels?.length ?? 0) === 0) {
|
|
16727
|
+
printStep(step++, totalSteps, "Refreshing Chutes models (chutes/*)...");
|
|
16728
|
+
const discovery = await discoverProviderModels("chutes");
|
|
15187
16729
|
if (discovery.models.length === 0) {
|
|
15188
|
-
printWarning(discovery.error ?? "No
|
|
16730
|
+
printWarning(discovery.error ?? "No Chutes models found. Continuing with fallback Chutes mapping.");
|
|
15189
16731
|
} else {
|
|
15190
|
-
resolvedConfig.
|
|
16732
|
+
resolvedConfig.availableChutesModels = discovery.models;
|
|
15191
16733
|
resolvedConfig.selectedChutesPrimaryModel = resolvedConfig.selectedChutesPrimaryModel ?? pickBestCodingChutesModel(discovery.models)?.model ?? discovery.models[0]?.model;
|
|
15192
16734
|
resolvedConfig.selectedChutesSecondaryModel = resolvedConfig.selectedChutesSecondaryModel ?? pickSupportChutesModel(discovery.models, resolvedConfig.selectedChutesPrimaryModel)?.model ?? resolvedConfig.selectedChutesPrimaryModel;
|
|
15193
16735
|
printSuccess(`Chutes models ready (${discovery.models.length} models found)`);
|
|
15194
16736
|
}
|
|
15195
|
-
} else if (resolvedConfig.hasChutes && (resolvedConfig.
|
|
15196
|
-
const availableChutes = resolvedConfig.
|
|
16737
|
+
} else if (resolvedConfig.hasChutes && (resolvedConfig.availableChutesModels?.length ?? 0) > 0) {
|
|
16738
|
+
const availableChutes = resolvedConfig.availableChutesModels ?? [];
|
|
15197
16739
|
resolvedConfig.selectedChutesPrimaryModel = resolvedConfig.selectedChutesPrimaryModel ?? pickBestCodingChutesModel(availableChutes)?.model;
|
|
15198
16740
|
resolvedConfig.selectedChutesSecondaryModel = resolvedConfig.selectedChutesSecondaryModel ?? pickSupportChutesModel(availableChutes, resolvedConfig.selectedChutesPrimaryModel)?.model ?? resolvedConfig.selectedChutesPrimaryModel;
|
|
15199
|
-
printStep(step++, totalSteps, "Using previously refreshed Chutes
|
|
16741
|
+
printStep(step++, totalSteps, "Using previously refreshed Chutes model list...");
|
|
15200
16742
|
printSuccess(`Chutes models ready (${availableChutes.length} models found)`);
|
|
15201
16743
|
}
|
|
15202
|
-
|
|
15203
|
-
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
|
|
16744
|
+
if (!modelsOnly) {
|
|
16745
|
+
printStep(step++, totalSteps, "Adding oh-my-opencode-slim plugin...");
|
|
16746
|
+
if (resolvedConfig.dryRun) {
|
|
16747
|
+
printInfo("Dry run mode - skipping plugin installation");
|
|
16748
|
+
} else {
|
|
16749
|
+
const pluginResult = await addPluginToOpenCodeConfig();
|
|
16750
|
+
if (!handleStepResult(pluginResult, "Plugin added"))
|
|
16751
|
+
return 1;
|
|
16752
|
+
}
|
|
16753
|
+
}
|
|
16754
|
+
if (!modelsOnly && resolvedConfig.hasAntigravity) {
|
|
15207
16755
|
printStep(step++, totalSteps, "Adding Antigravity plugin...");
|
|
15208
|
-
|
|
15209
|
-
|
|
15210
|
-
|
|
16756
|
+
if (resolvedConfig.dryRun) {
|
|
16757
|
+
printInfo("Dry run mode - skipping Antigravity plugin");
|
|
16758
|
+
} else {
|
|
16759
|
+
const antigravityPluginResult = addAntigravityPlugin();
|
|
16760
|
+
if (!handleStepResult(antigravityPluginResult, "Antigravity plugin added"))
|
|
16761
|
+
return 1;
|
|
16762
|
+
}
|
|
15211
16763
|
printStep(step++, totalSteps, "Configuring Google Provider...");
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
16764
|
+
if (resolvedConfig.dryRun) {
|
|
16765
|
+
printInfo("Dry run mode - skipping Google Provider setup");
|
|
16766
|
+
} else {
|
|
16767
|
+
const googleProviderResult = addGoogleProvider();
|
|
16768
|
+
if (!handleStepResult(googleProviderResult, "Google Provider configured"))
|
|
16769
|
+
return 1;
|
|
16770
|
+
}
|
|
15215
16771
|
}
|
|
15216
|
-
if (resolvedConfig.hasChutes) {
|
|
15217
|
-
printStep(step++, totalSteps, "
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
16772
|
+
if (!modelsOnly && resolvedConfig.hasChutes) {
|
|
16773
|
+
printStep(step++, totalSteps, "Enabling Chutes auth flow...");
|
|
16774
|
+
if (resolvedConfig.dryRun) {
|
|
16775
|
+
printInfo("Dry run mode - skipping Chutes auth flow");
|
|
16776
|
+
} else {
|
|
16777
|
+
const chutesProviderResult = addChutesProvider();
|
|
16778
|
+
if (!handleStepResult(chutesProviderResult, "Chutes auth flow ready"))
|
|
16779
|
+
return 1;
|
|
16780
|
+
}
|
|
15221
16781
|
}
|
|
15222
16782
|
if (hasAnyEnabledProvider) {
|
|
15223
16783
|
printStep(step++, totalSteps, "Resolving dynamic model assignments...");
|
|
@@ -15225,7 +16785,14 @@ async function runInstall(config2) {
|
|
|
15225
16785
|
if (catalogDiscovery.models.length === 0) {
|
|
15226
16786
|
printWarning(catalogDiscovery.error ?? "Unable to discover model catalog. Falling back to static mappings.");
|
|
15227
16787
|
} else {
|
|
15228
|
-
const
|
|
16788
|
+
const { signals, warnings } = await fetchExternalModelSignals({
|
|
16789
|
+
artificialAnalysisApiKey: resolvedConfig.artificialAnalysisApiKey,
|
|
16790
|
+
openRouterApiKey: resolvedConfig.openRouterApiKey
|
|
16791
|
+
});
|
|
16792
|
+
for (const warning of warnings) {
|
|
16793
|
+
printInfo(warning);
|
|
16794
|
+
}
|
|
16795
|
+
const dynamicPlan = buildDynamicModelPlan(catalogDiscovery.models, resolvedConfig, signals);
|
|
15229
16796
|
if (!dynamicPlan) {
|
|
15230
16797
|
printWarning("Dynamic planner found no suitable models. Using static mappings.");
|
|
15231
16798
|
} else {
|
|
@@ -15234,41 +16801,69 @@ async function runInstall(config2) {
|
|
|
15234
16801
|
}
|
|
15235
16802
|
}
|
|
15236
16803
|
}
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
16804
|
+
if (!modelsOnly) {
|
|
16805
|
+
printStep(step++, totalSteps, "Disabling OpenCode default agents...");
|
|
16806
|
+
if (resolvedConfig.dryRun) {
|
|
16807
|
+
printInfo("Dry run mode - skipping agent disabling");
|
|
16808
|
+
} else {
|
|
16809
|
+
const agentResult = disableDefaultAgents();
|
|
16810
|
+
if (!handleStepResult(agentResult, "Default agents disabled"))
|
|
16811
|
+
return 1;
|
|
16812
|
+
}
|
|
16813
|
+
}
|
|
15241
16814
|
printStep(step++, totalSteps, "Writing oh-my-opencode-slim configuration...");
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
16815
|
+
if (resolvedConfig.dryRun) {
|
|
16816
|
+
const liteConfig = generateLiteConfig(resolvedConfig);
|
|
16817
|
+
printInfo("Dry run mode - configuration that would be written:");
|
|
16818
|
+
console.log(`
|
|
16819
|
+
${JSON.stringify(liteConfig, null, 2)}
|
|
16820
|
+
`);
|
|
16821
|
+
} else {
|
|
16822
|
+
const liteResult = writeLiteConfig(resolvedConfig);
|
|
16823
|
+
if (!handleStepResult(liteResult, "Config written"))
|
|
16824
|
+
return 1;
|
|
16825
|
+
}
|
|
16826
|
+
if (!modelsOnly && resolvedConfig.installSkills) {
|
|
15246
16827
|
printStep(step++, totalSteps, "Installing recommended skills...");
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15254
|
-
|
|
16828
|
+
if (resolvedConfig.dryRun) {
|
|
16829
|
+
printInfo("Dry run mode - would install skills:");
|
|
16830
|
+
for (const skill of RECOMMENDED_SKILLS) {
|
|
16831
|
+
printInfo(` - ${skill.name}`);
|
|
16832
|
+
}
|
|
16833
|
+
} else {
|
|
16834
|
+
let skillsInstalled = 0;
|
|
16835
|
+
for (const skill of RECOMMENDED_SKILLS) {
|
|
16836
|
+
printInfo(`Installing ${skill.name}...`);
|
|
16837
|
+
if (installSkill(skill)) {
|
|
16838
|
+
printSuccess(`Installed: ${skill.name}`);
|
|
16839
|
+
skillsInstalled++;
|
|
16840
|
+
} else {
|
|
16841
|
+
printWarning(`Failed to install: ${skill.name}`);
|
|
16842
|
+
}
|
|
15255
16843
|
}
|
|
16844
|
+
printSuccess(`${skillsInstalled}/${RECOMMENDED_SKILLS.length} skills installed`);
|
|
15256
16845
|
}
|
|
15257
|
-
printSuccess(`${skillsInstalled}/${RECOMMENDED_SKILLS.length} skills installed`);
|
|
15258
16846
|
}
|
|
15259
|
-
if (resolvedConfig.installCustomSkills) {
|
|
16847
|
+
if (!modelsOnly && resolvedConfig.installCustomSkills) {
|
|
15260
16848
|
printStep(step++, totalSteps, "Installing custom skills...");
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
16849
|
+
if (resolvedConfig.dryRun) {
|
|
16850
|
+
printInfo("Dry run mode - would install custom skills:");
|
|
16851
|
+
for (const skill of CUSTOM_SKILLS) {
|
|
16852
|
+
printInfo(` - ${skill.name}`);
|
|
16853
|
+
}
|
|
16854
|
+
} else {
|
|
16855
|
+
let customSkillsInstalled = 0;
|
|
16856
|
+
for (const skill of CUSTOM_SKILLS) {
|
|
16857
|
+
printInfo(`Installing ${skill.name}...`);
|
|
16858
|
+
if (installCustomSkill(skill)) {
|
|
16859
|
+
printSuccess(`Installed: ${skill.name}`);
|
|
16860
|
+
customSkillsInstalled++;
|
|
16861
|
+
} else {
|
|
16862
|
+
printWarning(`Failed to install: ${skill.name}`);
|
|
16863
|
+
}
|
|
15269
16864
|
}
|
|
16865
|
+
printSuccess(`${customSkillsInstalled}/${CUSTOM_SKILLS.length} custom skills installed`);
|
|
15270
16866
|
}
|
|
15271
|
-
printSuccess(`${customSkillsInstalled}/${CUSTOM_SKILLS.length} custom skills installed`);
|
|
15272
16867
|
}
|
|
15273
16868
|
console.log();
|
|
15274
16869
|
console.log(formatConfigSummary(resolvedConfig));
|
|
@@ -15307,7 +16902,7 @@ async function runInstall(config2) {
|
|
|
15307
16902
|
}
|
|
15308
16903
|
if (resolvedConfig.hasChutes) {
|
|
15309
16904
|
console.log();
|
|
15310
|
-
console.log(` Then
|
|
16905
|
+
console.log(` Then select ${BOLD}chutes${RESET} provider.`);
|
|
15311
16906
|
}
|
|
15312
16907
|
console.log();
|
|
15313
16908
|
}
|
|
@@ -15340,20 +16935,27 @@ async function install(args) {
|
|
|
15340
16935
|
console.log(` ${SYMBOLS.bullet} --${flagName}=<yes|no>`);
|
|
15341
16936
|
}
|
|
15342
16937
|
console.log();
|
|
15343
|
-
printInfo("Usage: bunx oh-my-opencode-slim install --no-tui --kimi=<yes|no> --openai=<yes|no> --anthropic=<yes|no> --copilot=<yes|no> --zai-plan=<yes|no> --antigravity=<yes|no> --chutes=<yes|no> --tmux=<yes|no>");
|
|
16938
|
+
printInfo("Usage: bunx oh-my-opencode-slim install --no-tui --kimi=<yes|no> --openai=<yes|no> --anthropic=<yes|no> --copilot=<yes|no> --zai-plan=<yes|no> --antigravity=<yes|no> --chutes=<yes|no> --balanced-spend=<yes|no> --tmux=<yes|no>");
|
|
15344
16939
|
console.log();
|
|
15345
16940
|
return 1;
|
|
15346
16941
|
}
|
|
15347
|
-
|
|
16942
|
+
const nonInteractiveConfig = argsToConfig(args);
|
|
16943
|
+
return runInstall(nonInteractiveConfig);
|
|
15348
16944
|
}
|
|
15349
16945
|
const detected = detectCurrentConfig();
|
|
15350
16946
|
printHeader(detected.isInstalled);
|
|
15351
16947
|
printStep(1, 1, "Checking OpenCode installation...");
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
16948
|
+
if (args.dryRun) {
|
|
16949
|
+
printInfo("Dry run mode - skipping OpenCode check");
|
|
16950
|
+
} else {
|
|
16951
|
+
const { ok } = await checkOpenCodeInstalled();
|
|
16952
|
+
if (!ok)
|
|
16953
|
+
return 1;
|
|
16954
|
+
}
|
|
15355
16955
|
console.log();
|
|
15356
|
-
const config2 = await runInteractiveMode(detected);
|
|
16956
|
+
const config2 = await runInteractiveMode(detected, args.modelsOnly === true);
|
|
16957
|
+
config2.dryRun = args.dryRun;
|
|
16958
|
+
config2.modelsOnly = args.modelsOnly;
|
|
15357
16959
|
return runInstall(config2);
|
|
15358
16960
|
}
|
|
15359
16961
|
|
|
@@ -15385,8 +16987,18 @@ function parseArgs(args) {
|
|
|
15385
16987
|
result.skills = arg.split("=")[1];
|
|
15386
16988
|
} else if (arg.startsWith("--opencode-free=")) {
|
|
15387
16989
|
result.opencodeFree = arg.split("=")[1];
|
|
16990
|
+
} else if (arg.startsWith("--balanced-spend=")) {
|
|
16991
|
+
result.balancedSpend = arg.split("=")[1];
|
|
15388
16992
|
} else if (arg.startsWith("--opencode-free-model=")) {
|
|
15389
16993
|
result.opencodeFreeModel = arg.split("=")[1];
|
|
16994
|
+
} else if (arg.startsWith("--aa-key=")) {
|
|
16995
|
+
result.aaKey = arg.slice("--aa-key=".length);
|
|
16996
|
+
} else if (arg.startsWith("--openrouter-key=")) {
|
|
16997
|
+
result.openrouterKey = arg.slice("--openrouter-key=".length);
|
|
16998
|
+
} else if (arg === "--dry-run") {
|
|
16999
|
+
result.dryRun = true;
|
|
17000
|
+
} else if (arg === "--models-only") {
|
|
17001
|
+
result.modelsOnly = true;
|
|
15390
17002
|
} else if (arg === "-h" || arg === "--help") {
|
|
15391
17003
|
printHelp();
|
|
15392
17004
|
process.exit(0);
|
|
@@ -15399,6 +17011,7 @@ function printHelp() {
|
|
|
15399
17011
|
oh-my-opencode-slim installer
|
|
15400
17012
|
|
|
15401
17013
|
Usage: bunx oh-my-opencode-slim install [OPTIONS]
|
|
17014
|
+
bunx oh-my-opencode-slim models [OPTIONS]
|
|
15402
17015
|
|
|
15403
17016
|
Options:
|
|
15404
17017
|
--kimi=yes|no Kimi API access (yes/no)
|
|
@@ -15409,21 +17022,31 @@ Options:
|
|
|
15409
17022
|
--antigravity=yes|no Antigravity/Google models (yes/no)
|
|
15410
17023
|
--chutes=yes|no Chutes models (yes/no)
|
|
15411
17024
|
--opencode-free=yes|no Use OpenCode free models (opencode/*)
|
|
17025
|
+
--balanced-spend=yes|no Evenly spread usage across selected providers when score gaps are within tolerance
|
|
15412
17026
|
--opencode-free-model Preferred OpenCode model id or "auto"
|
|
17027
|
+
--aa-key Artificial Analysis API key (optional)
|
|
17028
|
+
--openrouter-key OpenRouter API key (optional)
|
|
15413
17029
|
--tmux=yes|no Enable tmux integration (yes/no)
|
|
15414
17030
|
--skills=yes|no Install recommended skills (yes/no)
|
|
15415
17031
|
--no-tui Non-interactive mode (requires all flags)
|
|
17032
|
+
--dry-run Simulate install without writing files or requiring OpenCode
|
|
17033
|
+
--models-only Update model assignments only (skip plugin/auth/skills)
|
|
15416
17034
|
-h, --help Show this help message
|
|
15417
17035
|
|
|
15418
17036
|
Examples:
|
|
15419
17037
|
bunx oh-my-opencode-slim install
|
|
15420
|
-
bunx oh-my-opencode-slim
|
|
17038
|
+
bunx oh-my-opencode-slim models
|
|
17039
|
+
bunx oh-my-opencode-slim install --no-tui --kimi=yes --openai=yes --anthropic=yes --copilot=no --zai-plan=no --antigravity=yes --chutes=no --opencode-free=yes --balanced-spend=yes --opencode-free-model=auto --aa-key=YOUR_AA_KEY --openrouter-key=YOUR_OR_KEY --tmux=no --skills=yes
|
|
15421
17040
|
`);
|
|
15422
17041
|
}
|
|
15423
17042
|
async function main() {
|
|
15424
17043
|
const args = process.argv.slice(2);
|
|
15425
|
-
if (args.length === 0 || args[0] === "install") {
|
|
15426
|
-
const
|
|
17044
|
+
if (args.length === 0 || args[0] === "install" || args[0] === "models") {
|
|
17045
|
+
const hasSubcommand = args[0] === "install" || args[0] === "models";
|
|
17046
|
+
const installArgs = parseArgs(args.slice(hasSubcommand ? 1 : 0));
|
|
17047
|
+
if (args[0] === "models") {
|
|
17048
|
+
installArgs.modelsOnly = true;
|
|
17049
|
+
}
|
|
15427
17050
|
const exitCode = await install(installArgs);
|
|
15428
17051
|
process.exit(exitCode);
|
|
15429
17052
|
} else if (args[0] === "-h" || args[0] === "--help") {
|