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/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
- }).strict();
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 { config: parsedConfig, error: error48 } = parseConfig(configPath);
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 add chutes provider: ${err}`
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 baseScore(model) {
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, 12);
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
- const versionBoost = tokenScore(lowered, /gpt-5\.3/i, 12) + tokenScore(lowered, /gpt-5\.2/i, 8) + tokenScore(lowered, /k2\.5/i, 6);
14522
- return statusScore(model.status) + context + output + deep + fast + code + versionBoost + (model.toolcall ? 25 : 0);
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 roleScore(agent, model) {
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
- return score + reasoning * 40 + toolcall * 25 + deep * 10 + code * 8 + context;
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
- return score + reasoning * 55 + deep * 18 + context * 1.2 + toolcall * 10;
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
- return score + attachment * 25 + reasoning * 18 + toolcall * 15 + context * 0.8 + output;
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
- return score + fast * 35 + toolcall * 28 + reasoning * 8 + context * 0.7;
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
- return score + context * 30 + toolcall * 22 + reasoning * 15 + output * 10;
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 score + code * 28 + toolcall * 24 + fast * 18 + reasoning * 14 + output * 8;
15186
+ return counts;
14557
15187
  }
14558
- function rankModels2(models, agent) {
14559
- return [...models].sort((a, b) => roleScore(agent, b) - roleScore(agent, a));
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 dedupe(models) {
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 buildDynamicModelPlan(catalog, config2) {
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 providerCandidates = catalog.filter((m) => enabledProviders.has(m.providerID));
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
- for (const agent of AGENTS) {
14581
- const ranked = rankModels2(providerCandidates, agent);
14582
- const primary = ranked[0];
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
- const providerOrder = dedupe(ranked.map((m) => m.providerID));
14586
- const perProviderBest = providerOrder.map((providerID) => ranked.find((m) => m.providerID === providerID)?.model).filter((m) => Boolean(m));
14587
- const selectedOpencode = agent === "explorer" || agent === "librarian" || agent === "fixer" ? config2.selectedOpenCodeSecondaryModel ?? config2.selectedOpenCodePrimaryModel : config2.selectedOpenCodePrimaryModel;
14588
- const selectedChutes = agent === "explorer" || agent === "librarian" || agent === "fixer" ? config2.selectedChutesSecondaryModel ?? config2.selectedChutesPrimaryModel : config2.selectedChutesPrimaryModel;
14589
- const chain = dedupe([
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
- ...perProviderBest,
15483
+ ...nonFreePerProviderBest,
14592
15484
  selectedChutes,
14593
15485
  selectedOpencode,
14594
- "opencode/big-pickle"
14595
- ]).slice(0, 7);
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: chain[0] ?? primary.model,
15514
+ model: finalModel,
14598
15515
  variant: ROLE_VARIANT[agent]
14599
15516
  };
14600
- chains[agent] = chain;
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 { agents, chains };
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
- const isModelHeader = /^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(line);
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 (/^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(lines[search]?.trim() ?? "")) {
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 discoverFreeModelsByProvider(providerID) {
15937
+ async function discoverModelsByProvider(providerID, freeOnly = true) {
14703
15938
  try {
14704
- const proc = Bun.spawn(["opencode", "models", "--refresh", "--verbose"], {
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, true)
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 proc = Bun.spawn(["opencode", "models", "--refresh", "--verbose"], {
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
- return discoverFreeModelsByProvider("opencode");
15990
+ const result = await discoverModelsByProvider("opencode", true);
15991
+ return { models: result.models, error: result.error };
14754
15992
  }
14755
- async function discoverProviderFreeModels(providerID) {
14756
- return discoverFreeModelsByProvider(providerID);
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 statSync2
16032
+ statSync as statSync3
14821
16033
  } from "fs";
14822
16034
  import { homedir as homedir2 } from "os";
14823
- import { dirname, join as join2 } from "path";
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 join2(homedir2(), ".config", "opencode", "skills");
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 = join2(src, entry);
14843
- const destPath = join2(dest, entry);
14844
- const stat = statSync2(srcPath);
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 = join2(packageRoot, skill.sourcePath);
14860
- const targetPath = join2(getCustomSkillsDir(), skill.name);
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
- printSuccess(`OpenCode ${version2 ?? ""} detected`);
14921
- return { ok: true, version: version2 ?? undefined };
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 runInteractiveMode(detected) {
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 useOpenCodeFree = await askYesNo(rl, "Use only OpenCode free models (opencode/*) with live refresh?", "yes");
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 availableChutesFreeModels;
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
- selectedOpenCodeSecondaryModel = await askModelSelection(rl, discovery.models, recommendedSecondary, "Choose support model for explorer/librarian/fixer");
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 anthropic = await askYesNo(rl, "Do you have access to Anthropic models?", detected.hasAnthropic ? "yes" : "no");
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 copilot = await askYesNo(rl, "Do you have access to GitHub Copilot models?", detected.hasCopilot ? "yes" : "no");
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 zaiPlan = await askYesNo(rl, "Do you have access to ZAI Coding Plan models?", detected.hasZaiPlan ? "yes" : "no");
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 antigravity = await askYesNo(rl, "Enable Antigravity authentication for Google models?", detected.hasAntigravity ? "yes" : "no");
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 chutes = await askYesNo(rl, "Enable Chutes provider with free daily capped models?", detected.hasChutes ? "yes" : "no");
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 discoverProviderFreeModels("chutes");
16600
+ const discovery = await discoverProviderModels("chutes");
15084
16601
  if (discovery.models.length === 0) {
15085
- printWarning(discovery.error ?? "No free Chutes models found. Continuing without Chutes dynamic assignment.");
16602
+ printWarning(discovery.error ?? "No Chutes models found. Continuing without Chutes dynamic assignment.");
15086
16603
  } else {
15087
- availableChutesFreeModels = discovery.models;
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 Free Models:${RESET}`);
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
- selectedChutesSecondaryModel = await askModelSelection(rl, discovery.models, recommendedSecondary, "Choose Chutes support model for explorer/librarian/fixer");
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}Recommended Skills:${RESET}`);
15101
- for (const skill of RECOMMENDED_SKILLS) {
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
- console.log(`${BOLD}Custom Skills:${RESET}`);
15108
- for (const skill of CUSTOM_SKILLS) {
15109
- console.log(` ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
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
- availableChutesFreeModels,
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
- let totalSteps = 4;
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
- const { ok } = await checkOpenCodeInstalled();
15162
- if (!ok)
15163
- return 1;
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.availableChutesFreeModels?.length ?? 0) === 0) {
15185
- printStep(step++, totalSteps, "Refreshing Chutes free models (chutes/*)...");
15186
- const discovery = await discoverProviderFreeModels("chutes");
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 free Chutes models found. Continuing with fallback Chutes mapping.");
16730
+ printWarning(discovery.error ?? "No Chutes models found. Continuing with fallback Chutes mapping.");
15189
16731
  } else {
15190
- resolvedConfig.availableChutesFreeModels = discovery.models;
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.availableChutesFreeModels?.length ?? 0) > 0) {
15196
- const availableChutes = resolvedConfig.availableChutesFreeModels ?? [];
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 free model list...");
16741
+ printStep(step++, totalSteps, "Using previously refreshed Chutes model list...");
15200
16742
  printSuccess(`Chutes models ready (${availableChutes.length} models found)`);
15201
16743
  }
15202
- printStep(step++, totalSteps, "Adding oh-my-opencode-slim plugin...");
15203
- const pluginResult = await addPluginToOpenCodeConfig();
15204
- if (!handleStepResult(pluginResult, "Plugin added"))
15205
- return 1;
15206
- if (resolvedConfig.hasAntigravity) {
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
- const antigravityPluginResult = addAntigravityPlugin();
15209
- if (!handleStepResult(antigravityPluginResult, "Antigravity plugin added"))
15210
- return 1;
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
- const googleProviderResult = addGoogleProvider();
15213
- if (!handleStepResult(googleProviderResult, "Google Provider configured"))
15214
- return 1;
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, "Configuring Chutes Provider...");
15218
- const chutesProviderResult = addChutesProvider();
15219
- if (!handleStepResult(chutesProviderResult, "Chutes Provider configured"))
15220
- return 1;
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 dynamicPlan = buildDynamicModelPlan(catalogDiscovery.models, resolvedConfig);
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
- printStep(step++, totalSteps, "Disabling OpenCode default agents...");
15238
- const agentResult = disableDefaultAgents();
15239
- if (!handleStepResult(agentResult, "Default agents disabled"))
15240
- return 1;
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
- const liteResult = writeLiteConfig(resolvedConfig);
15243
- if (!handleStepResult(liteResult, "Config written"))
15244
- return 1;
15245
- if (resolvedConfig.installSkills) {
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
- let skillsInstalled = 0;
15248
- for (const skill of RECOMMENDED_SKILLS) {
15249
- printInfo(`Installing ${skill.name}...`);
15250
- if (installSkill(skill)) {
15251
- printSuccess(`Installed: ${skill.name}`);
15252
- skillsInstalled++;
15253
- } else {
15254
- printWarning(`Failed to install: ${skill.name}`);
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
- let customSkillsInstalled = 0;
15262
- for (const skill of CUSTOM_SKILLS) {
15263
- printInfo(`Installing ${skill.name}...`);
15264
- if (installCustomSkill(skill)) {
15265
- printSuccess(`Installed: ${skill.name}`);
15266
- customSkillsInstalled++;
15267
- } else {
15268
- printWarning(`Failed to install: ${skill.name}`);
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 set ${BOLD}CHUTES_API_KEY${RESET} in your shell.`);
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
- return runInstall(argsToConfig(args));
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
- const { ok } = await checkOpenCodeInstalled();
15353
- if (!ok)
15354
- return 1;
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 install --no-tui --kimi=yes --openai=yes --anthropic=yes --copilot=no --zai-plan=no --antigravity=yes --chutes=no --opencode-free=yes --opencode-free-model=auto --tmux=no --skills=yes
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 installArgs = parseArgs(args.slice(args[0] === "install" ? 1 : 0));
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") {