chorus-cli 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,7 +72,7 @@ chorus run 4464 --super --qa 'John Doe'
72
72
 
73
73
  ## Docs
74
74
 
75
- Full documentation at [getchorusai.com/docs](https://getchorusai.com/docs)
75
+ Full documentation at [choruscli.com/docs](https://choruscli.com/docs)
76
76
 
77
77
  ## License
78
78
 
package/index.js CHANGED
@@ -62,7 +62,7 @@ async function getMachineId() {
62
62
  }
63
63
 
64
64
  // Run coder.py with real-time stderr streaming so progress is visible
65
- function runCoder(prompt) {
65
+ function runCoder(prompt, { model } = {}) {
66
66
  return new Promise((resolve, reject) => {
67
67
  const env = { ...process.env };
68
68
  if (CONFIG.ai.chorusApiKey) {
@@ -72,7 +72,12 @@ function runCoder(prompt) {
72
72
  if (CONFIG.ai.machineId) {
73
73
  env.CHORUS_MACHINE_ID = CONFIG.ai.machineId;
74
74
  }
75
- const proc = spawn(CONFIG.ai.venvPython, [CONFIG.ai.coderPath, '--prompt', prompt], {
75
+ if (isFreeModel(model)) {
76
+ env.CHORUS_FREE = '1';
77
+ }
78
+ const coderArgs = [CONFIG.ai.coderPath, '--prompt', prompt];
79
+ if (model) { coderArgs.push('--model', model); }
80
+ const proc = spawn(CONFIG.ai.venvPython, coderArgs, {
76
81
  cwd: process.cwd(),
77
82
  env,
78
83
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -103,7 +108,7 @@ function runCoder(prompt) {
103
108
  }
104
109
 
105
110
  // Run qa.py with issue context on stdin, capture JSON from stdout
106
- function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
111
+ function runQAChat(issue, enrichedDetails, qaName, useSuper = false, { model } = {}) {
107
112
  return new Promise((resolve, reject) => {
108
113
  const input = JSON.stringify({
109
114
  issue_number: issue.number,
@@ -117,6 +122,7 @@ function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
117
122
  args.push('--auth', CONFIG.teams.authPath);
118
123
  }
119
124
  if (useSuper) {args.push('--super');}
125
+ if (model) {args.push('--model', model);}
120
126
 
121
127
  const env = { ...process.env };
122
128
  if (CONFIG.ai.chorusApiKey) {
@@ -126,6 +132,9 @@ function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
126
132
  if (CONFIG.ai.machineId) {
127
133
  env.CHORUS_MACHINE_ID = CONFIG.ai.machineId;
128
134
  }
135
+ if (isFreeModel(model)) {
136
+ env.CHORUS_FREE = '1';
137
+ }
129
138
  if (CONFIG.messenger === 'slack' && CONFIG.slack.botToken) {
130
139
  env.SLACK_BOT_TOKEN = CONFIG.slack.botToken;
131
140
  }
@@ -202,7 +211,7 @@ const CONFIG = {
202
211
  // Use createProvider(CONFIG, issueArg) to get the right provider.
203
212
 
204
213
  // ===== AI ENRICHMENT =====
205
- async function enrichWithAI(issue) {
214
+ async function enrichWithAI(issue, { model } = {}) {
206
215
  const prompt = `Analyze this GitHub issue and write questions for QA clarification.
207
216
 
208
217
  ISSUE DETAILS:
@@ -253,7 +262,7 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
253
262
  const openai = new OpenAI(openaiOpts);
254
263
 
255
264
  const response = await openai.chat.completions.create({
256
- model: 'chorus-default',
265
+ model: model || 'chorus-default',
257
266
  max_tokens: 2000,
258
267
  messages: [
259
268
  {
@@ -262,7 +271,10 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
262
271
  }
263
272
  ]
264
273
  }, {
265
- headers: { 'X-Chorus-Mode': 'enrich' },
274
+ headers: {
275
+ 'X-Chorus-Mode': 'enrich',
276
+ ...(isFreeModel(model) && { 'X-Chorus-Free': '1' }),
277
+ },
266
278
  });
267
279
 
268
280
  if (response.usage) {
@@ -279,7 +291,7 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
279
291
  }
280
292
 
281
293
  // ===== CODE GENERATION =====
282
- async function generateCode(issue, enrichedDetails, qaResponse) {
294
+ async function generateCode(issue, enrichedDetails, qaResponse, { model } = {}) {
283
295
  const tool = CONFIG.ai.codingTool;
284
296
 
285
297
  if (tool === 'coder') {
@@ -304,7 +316,7 @@ Instructions:
304
316
 
305
317
  console.log('🔨 Generating code with Coder agent...');
306
318
 
307
- return await runCoder(prompt);
319
+ return await runCoder(prompt, { model });
308
320
  }
309
321
 
310
322
  // Fallback: kimi
@@ -423,7 +435,7 @@ async function getCodeRabbitReview(solution, issue, provider) {
423
435
  }
424
436
  }
425
437
 
426
- async function refineCode(solution, review) {
438
+ async function refineCode(solution, review, { model } = {}) {
427
439
  const tool = CONFIG.ai.codingTool;
428
440
 
429
441
  if (tool === 'coder') {
@@ -448,7 +460,7 @@ Instructions:
448
460
 
449
461
  console.log('🔄 Refining code with Coder agent...');
450
462
 
451
- return await runCoder(prompt);
463
+ return await runCoder(prompt, { model });
452
464
  }
453
465
 
454
466
  // Fallback: kimi
@@ -535,6 +547,10 @@ ${lintOutput.slice(0, 5000)}`;
535
547
 
536
548
  // ===== TOKEN LIMIT =====
537
549
 
550
+ function isFreeModel(model) {
551
+ return model && FREE_MODELS.has(model);
552
+ }
553
+
538
554
  function isTokenLimitError(err) {
539
555
  const msg = typeof err === 'string' ? err : (err?.message || err?.error || '');
540
556
  return msg.includes('token limit exceeded') || msg.includes('rate_limit_error');
@@ -545,19 +561,38 @@ async function fetchAccountEmail() {
545
561
  return null;
546
562
  }
547
563
 
564
+ async function fetchCreditBalance(email) {
565
+ if (!email || !CONFIG.ai.chorusApiKey) return null;
566
+ try {
567
+ const baseUrl = CONFIG.ai.chorusApiUrl.replace(/\/v1\/?$/, '');
568
+ const res = await fetch(`${baseUrl}/tokens/balance?email=${encodeURIComponent(email)}&client=true`);
569
+ if (!res.ok) return null;
570
+ const data = await res.json();
571
+ return data.tokenBalance ?? null;
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+
548
577
  async function printTokenLimitMessage() {
549
578
  const email = await fetchAccountEmail();
550
- const base = 'https://getchorusai.com/pricing';
579
+ const base = 'https://choruscli.com/pricing';
551
580
  const url = email ? `${base}?email=${encodeURIComponent(email)}` : base;
552
581
 
553
582
  console.error('\n⚠️ You\'ve run out of Chorus tokens for this month.\n');
583
+
584
+ const balance = await fetchCreditBalance(email);
585
+ if (balance !== null) {
586
+ console.error(` Remaining credit balance: ${balance}\n`);
587
+ }
588
+
554
589
  console.error(' To keep going, purchase more tokens at:');
555
590
  console.error(` ${url}\n`);
556
591
  console.error(' Or wait for your monthly allowance to reset.\n');
557
592
  }
558
593
 
559
594
  // ===== MAIN WORKFLOW =====
560
- async function processTicket(issueArg, { useSuper = false, skipQA = false, qaName: qaNameOverride } = {}) {
595
+ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaName: qaNameOverride, model } = {}) {
561
596
  try {
562
597
  console.log('🚀 Starting ticket processing...\n');
563
598
 
@@ -581,7 +616,25 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
581
616
  efs(CONFIG.ai.venvPython, ['-m', 'pip', 'install', '-r', reqFile], { stdio: 'inherit' });
582
617
  }
583
618
 
584
- // 0a. Verify no modified tracked files (untracked files like .chorus/ are fine)
619
+ // 0a. Check token balance free models can proceed with zero balance
620
+ const usingFree = isFreeModel(model);
621
+ const email = await fetchAccountEmail();
622
+ const balance = await fetchCreditBalance(email);
623
+
624
+ if (balance !== null && balance <= 0 && !usingFree) {
625
+ await printTokenLimitMessage();
626
+ console.log(' Tip: Use --free to run with a free model\n');
627
+ process.exit(1);
628
+ }
629
+ if (usingFree) {
630
+ if (balance !== null && balance <= 0) {
631
+ console.log(`🆓 Using free model "${model}"\n`);
632
+ } else {
633
+ console.log(`🆓 Using free model "${model}"\n`);
634
+ }
635
+ }
636
+
637
+ // 0b. Verify no modified tracked files (untracked files like .chorus/ are fine)
585
638
  const { stdout: gitStatus } = await execPromise('git status --porcelain --untracked-files=no');
586
639
  if (gitStatus.trim()) {
587
640
  console.error('⚠️ Working directory has uncommitted changes. Commit or stash first:');
@@ -621,7 +674,7 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
621
674
  console.log(`Found issue #${issue.number}: ${issue.title}\n`);
622
675
 
623
676
  // 2. Enrich with AI
624
- const enrichedDetails = await enrichWithAI(issue);
677
+ const enrichedDetails = await enrichWithAI(issue, { model });
625
678
  console.log('Enrichment complete\n', enrichedDetails);
626
679
 
627
680
  // 3. Multi-turn QA conversation via qa.py
@@ -631,12 +684,13 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
631
684
  } else {
632
685
  const qaName = qaNameOverride || await provider.getUserDisplayName(issue.user.login);
633
686
  console.log(`💬 Starting QA conversation with ${qaName?.login}...`);
634
- const qaResult = await runQAChat(issue, enrichedDetails, qaName, useSuper);
687
+ const qaResult = await runQAChat(issue, enrichedDetails, qaName, useSuper, { model });
635
688
  qaResponse = qaResult.requirements;
636
689
 
637
690
  if (!qaResult.completed) {
638
- if (isTokenLimitError(qaResult.error)) {
691
+ if (isTokenLimitError(qaResult.error) && !usingFree) {
639
692
  await printTokenLimitMessage();
693
+ console.log(' Tip: Use --free to run with a free model\n');
640
694
  process.exit(1);
641
695
  }
642
696
  console.warn('⚠️ QA chat did not complete successfully:', qaResult.error || 'unknown');
@@ -659,7 +713,7 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
659
713
 
660
714
  for (let attempt = 1; attempt <= maxCodeAttempts; attempt++) {
661
715
  if (attempt === 1) {
662
- solution = await generateCode(issue, enrichedDetails, qaResponse);
716
+ solution = await generateCode(issue, enrichedDetails, qaResponse, { model });
663
717
  } else {
664
718
  // Reprompt with explicit instruction that files must be written
665
719
  const retryPrompt = `You previously attempted to implement this issue but DID NOT write any files. Your task is NOT complete until you have actually created or modified files using write_file or edit_file.
@@ -680,13 +734,14 @@ ${qaResponse}
680
734
  CRITICAL: You MUST write code to actual files. Do not just describe changes — use write_file or edit_file to make them. If you are unsure where to make changes, explore the codebase first, then write the code.`;
681
735
 
682
736
  console.log(`🔁 Reprompting coder (attempt ${attempt}/${maxCodeAttempts})...`);
683
- solution = await runCoder(retryPrompt);
737
+ solution = await runCoder(retryPrompt, { model });
684
738
  }
685
739
 
686
740
  if (solution.completed === false) {
687
741
  const errs = solution.errors || [solution.summary || ''];
688
- if (errs.some(e => isTokenLimitError(e))) {
742
+ if (errs.some(e => isTokenLimitError(e)) && !usingFree) {
689
743
  printTokenLimitMessage();
744
+ console.log(' Tip: Use --free to run with a free model\n');
690
745
  process.exit(1);
691
746
  }
692
747
  console.error('❌ Code generation failed:', errs);
@@ -746,7 +801,7 @@ CRITICAL: You MUST write code to actual files. Do not just describe changes —
746
801
  while (review.needsChanges && iterations < maxIterations) {
747
802
  console.log(`Iteration ${iterations + 1}/${maxIterations}...`);
748
803
 
749
- const refined = await refineCode(solution, review);
804
+ const refined = await refineCode(solution, review, { model });
750
805
 
751
806
  if (refined.completed === false) {
752
807
  console.warn('⚠️ Refinement had errors:', refined.errors);
@@ -824,8 +879,9 @@ Instructions:
824
879
 
825
880
  if (revised.completed === false) {
826
881
  const errs = revised.errors || [revised.summary || ''];
827
- if (errs.some(e => isTokenLimitError(e))) {
882
+ if (errs.some(e => isTokenLimitError(e)) && !usingFree) {
828
883
  printTokenLimitMessage();
884
+ console.log(' Tip: Use --free to run with a free model\n');
829
885
  rl.close();
830
886
  process.exit(1);
831
887
  }
@@ -855,8 +911,9 @@ Instructions:
855
911
  console.log('\n✨ Ticket processing complete!');
856
912
 
857
913
  } catch (error) {
858
- if (isTokenLimitError(error)) {
914
+ if (isTokenLimitError(error) && !isFreeModel(model)) {
859
915
  await printTokenLimitMessage();
916
+ console.log(' Tip: Use --free to run with a free model\n');
860
917
  process.exit(1);
861
918
  }
862
919
  console.error('❌ Error processing ticket:', error);
@@ -1252,21 +1309,91 @@ function printZEP() {
1252
1309
  const command = process.argv[2];
1253
1310
  const _envExists = require('fs').existsSync(path.join(os.homedir(), '.config', 'chorus', '.env'));
1254
1311
 
1312
+ const FREE_MODELS = new Set([
1313
+ 'deepseek', 'gemini', 'gemini-flash-lite', 'gpt-oss', 'gpt-nano',
1314
+ 'gpt-mini', 'gpt-5.2', 'gpt-4.1-mini', 'gpt-4o-mini', 'grok',
1315
+ 'kimi', 'glm', 'minimax', 'step'
1316
+ ]);
1317
+
1318
+ const PAID_MODELS = new Set([
1319
+ 'claude', 'chatgpt', 'qwen', 'llama', 'mistral', 'command-r'
1320
+ ]);
1321
+
1322
+ const ALLOWED_MODELS = new Set([...FREE_MODELS, ...PAID_MODELS]);
1323
+
1324
+ const DEFAULT_FREE_MODEL = 'deepseek';
1325
+
1326
+ function printModelTable(out = console.error) {
1327
+ out('\n Free models:');
1328
+ out(' ┌─────────────────────┬──────────────────────────────────┐');
1329
+ out(' │ Flag value │ Model │');
1330
+ out(' ├─────────────────────┼──────────────────────────────────┤');
1331
+ out(' │ deepseek │ DeepSeek V3.2 (default) │');
1332
+ out(' │ gemini │ Gemini 2.5 Flash │');
1333
+ out(' │ gemini-flash-lite │ Gemini 2.5 Flash Lite │');
1334
+ out(' │ gpt-oss │ GPT-OSS 120B │');
1335
+ out(' │ gpt-nano │ GPT-5 Nano │');
1336
+ out(' │ gpt-mini │ GPT-5 Mini │');
1337
+ out(' │ gpt-5.2 │ GPT-5.2 │');
1338
+ out(' │ gpt-4.1-mini │ GPT-4.1 Mini │');
1339
+ out(' │ gpt-4o-mini │ GPT-4o Mini │');
1340
+ out(' │ grok │ Grok 4.1 Fast │');
1341
+ out(' │ kimi │ Kimi K2.5 │');
1342
+ out(' │ glm │ GLM-5 │');
1343
+ out(' │ minimax │ Minimax M2.5 │');
1344
+ out(' │ step │ Step 3.5 Flash │');
1345
+ out(' └─────────────────────┴──────────────────────────────────┘');
1346
+ out('\n Paid models:');
1347
+ out(' ┌─────────────────────┬──────────────────────────────────┐');
1348
+ out(' │ Flag value │ Model │');
1349
+ out(' ├─────────────────────┼──────────────────────────────────┤');
1350
+ out(' │ claude │ Claude │');
1351
+ out(' │ chatgpt │ ChatGPT │');
1352
+ out(' │ qwen │ Qwen │');
1353
+ out(' │ llama │ Llama │');
1354
+ out(' │ mistral │ Mistral │');
1355
+ out(' │ command-r │ Command R │');
1356
+ out(' └─────────────────────┴──────────────────────────────────┘');
1357
+ }
1358
+
1255
1359
  function parseRunArgs() {
1256
1360
  const args = process.argv.slice(3);
1257
- const opts = { useSuper: false, skipQA: false, qaName: null, issueArg: null };
1361
+ const opts = { useSuper: false, skipQA: false, qaName: null, issueArg: null, model: null, free: false };
1258
1362
 
1259
1363
  for (let i = 0; i < args.length; i++) {
1260
1364
  if (args[i] === '--super') {
1261
1365
  opts.useSuper = true;
1262
1366
  } else if (args[i] === '--skip-qa') {
1263
1367
  opts.skipQA = true;
1368
+ } else if (args[i] === '--free') {
1369
+ opts.free = true;
1264
1370
  } else if (args[i] === '--qa' && i + 1 < args.length) {
1265
1371
  opts.qaName = args[++i];
1372
+ } else if (args[i] === '--model' && i + 1 < args.length) {
1373
+ const val = args[++i];
1374
+ if (!ALLOWED_MODELS.has(val)) {
1375
+ console.error(`Error: invalid model "${val}". Allowed models: ${[...ALLOWED_MODELS].join(', ')}`);
1376
+ printModelTable();
1377
+ process.exit(1);
1378
+ }
1379
+ opts.model = val;
1266
1380
  } else if (!args[i].startsWith('--')) {
1267
1381
  opts.issueArg = args[i];
1268
1382
  }
1269
1383
  }
1384
+
1385
+ // --free: default to best free model, or validate that --model is free
1386
+ if (opts.free) {
1387
+ if (opts.model && !FREE_MODELS.has(opts.model)) {
1388
+ console.error(`Error: "${opts.model}" is a paid model and cannot be used with --free.`);
1389
+ printModelTable();
1390
+ process.exit(1);
1391
+ }
1392
+ if (!opts.model) {
1393
+ opts.model = DEFAULT_FREE_MODEL;
1394
+ }
1395
+ }
1396
+
1270
1397
  return opts;
1271
1398
  }
1272
1399
 
@@ -1284,6 +1411,13 @@ if (command === 'setup') {
1284
1411
  const { issueArg, ...opts } = parseRunArgs();
1285
1412
  processTicket(issueArg, opts).catch(console.error);
1286
1413
  }
1414
+ } else if (command === 'models') {
1415
+ console.log('\nChorus — Available Models');
1416
+ printModelTable(console.log);
1417
+ console.log('\nUsage:');
1418
+ console.log(' chorus run 4464 --model deepseek Use a specific model');
1419
+ console.log(' chorus run 4464 --free Use the best free model (DeepSeek V3.2)');
1420
+ console.log(' chorus run 4464 --free --model kimi Use a specific free model\n');
1287
1421
  } else if (command === 'zep') {
1288
1422
  printZEP()
1289
1423
  } else {
@@ -1302,10 +1436,14 @@ Chorus — AI-powered ticket automation (GitHub & Azure DevOps)
1302
1436
 
1303
1437
  Usage:
1304
1438
  chorus setup - Set up provider, Chorus auth + messenger
1439
+ chorus models - List all available models (free & paid)
1305
1440
  chorus run - Process latest assigned issue
1306
1441
  chorus run 4464 - Process specific issue by number
1307
1442
  chorus run <url> - Process issue from full URL (auto-detects provider)
1308
1443
  chorus run 4464 --super - Use Opus 4.6 for QA evaluation
1444
+ chorus run 4464 --model deepseek - Use a specific model (see --free for free options)
1445
+ chorus run 4464 --free - Use the best free model (DeepSeek V3.2)
1446
+ chorus run 4464 --free --model kimi - Use a specific free model
1309
1447
  chorus run 4464 --qa 'John Doe' - Specify QA contact name for chat
1310
1448
  chorus run 4464 --skip-qa - Skip QA conversation, go straight to coding
1311
1449
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chorus-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Automated ticket resolution with AI, Teams, and Slack integration",
5
5
  "main": "index.js",
6
6
  "bin": {
package/tools/coder.py CHANGED
@@ -878,8 +878,13 @@ def run_prompt(client, prompt, chorus_context):
878
878
  def main():
879
879
  parser = argparse.ArgumentParser(description="Coder — AI coding agent powered by Claude via Chorus")
880
880
  parser.add_argument("-p", "--prompt", help="Run a single prompt headlessly and output JSON")
881
+ parser.add_argument("--model", help="Override the model sent to the proxy")
881
882
  args = parser.parse_args()
882
883
 
884
+ global MODEL
885
+ if args.model:
886
+ MODEL = args.model
887
+
883
888
  api_key = os.environ.get("CHORUS_API_KEY")
884
889
  if not api_key:
885
890
  print(f"{C.RED}Error: CHORUS_API_KEY not set. Run 'chorus setup' to configure.{C.RESET}", file=sys.stderr)
@@ -887,9 +892,15 @@ def main():
887
892
 
888
893
  base_url = os.environ.get("CHORUS_API_URL", "https://chorus-bad0f.web.app/v1")
889
894
  machine_id = os.environ.get("CHORUS_MACHINE_ID")
890
- client_kwargs = {"api_key": api_key, "base_url": base_url}
895
+ chorus_free = os.environ.get("CHORUS_FREE", "")
896
+ default_headers = {}
891
897
  if machine_id:
892
- client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
898
+ default_headers["X-Machine-Id"] = machine_id
899
+ if chorus_free:
900
+ default_headers["X-Chorus-Free"] = "1"
901
+ client_kwargs = {"api_key": api_key, "base_url": base_url}
902
+ if default_headers:
903
+ client_kwargs["default_headers"] = default_headers
893
904
  client = OpenAI(**client_kwargs)
894
905
  cwd = os.getcwd()
895
906
 
package/tools/qa.py CHANGED
@@ -408,9 +408,15 @@ def run_qa_chat(issue_context, messenger, qa_name):
408
408
  api_key = os.environ.get("CHORUS_API_KEY")
409
409
  base_url = os.environ.get("CHORUS_API_URL", "https://chorus-bad0f.web.app/v1")
410
410
  machine_id = os.environ.get("CHORUS_MACHINE_ID")
411
- client_kwargs = {"api_key": api_key, "base_url": base_url}
411
+ chorus_free = os.environ.get("CHORUS_FREE", "")
412
+ default_headers = {}
412
413
  if machine_id:
413
- client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
414
+ default_headers["X-Machine-Id"] = machine_id
415
+ if chorus_free:
416
+ default_headers["X-Chorus-Free"] = "1"
417
+ client_kwargs = {"api_key": api_key, "base_url": base_url}
418
+ if default_headers:
419
+ client_kwargs["default_headers"] = default_headers
414
420
  client = OpenAI(**client_kwargs)
415
421
  conversation = []
416
422
  raw_responses = []
@@ -483,8 +489,14 @@ def main():
483
489
  parser.add_argument("--auth", help="Path to Teams auth state JSON (required for --messenger teams)")
484
490
  parser.add_argument("--qa", required=True, help="QA person's name")
485
491
  parser.add_argument("--super", action="store_true", help="Use Opus 4.6 instead of Sonnet")
492
+ parser.add_argument("--model", help="Override the model sent to the proxy")
486
493
  args = parser.parse_args()
487
494
 
495
+ # Override model if specified
496
+ global MODEL
497
+ if args.model:
498
+ MODEL = args.model
499
+
488
500
  # chorus_mode tells the proxy which model to use
489
501
  global QA_CHORUS_MODE
490
502
  if args.super: