oh-my-opencode-slim 0.7.0 → 0.8.1

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