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 +1 -1
- package/index.js +161 -23
- package/package.json +1 -1
- package/tools/__pycache__/coder.cpython-314.pyc +0 -0
- package/tools/__pycache__/qa.cpython-314.pyc +0 -0
- package/tools/coder.py +13 -2
- package/tools/qa.py +14 -2
package/README.md
CHANGED
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
|
-
|
|
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: {
|
|
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://
|
|
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.
|
|
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
|
Binary file
|
|
Binary file
|
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
|
-
|
|
895
|
+
chorus_free = os.environ.get("CHORUS_FREE", "")
|
|
896
|
+
default_headers = {}
|
|
891
897
|
if machine_id:
|
|
892
|
-
|
|
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
|
-
|
|
411
|
+
chorus_free = os.environ.get("CHORUS_FREE", "")
|
|
412
|
+
default_headers = {}
|
|
412
413
|
if machine_id:
|
|
413
|
-
|
|
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:
|