coder-agent 2.9.12 → 2.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import * as fs from "fs/promises";
4
4
  import { TOOL_DEFINITIONS, dispatchTool } from "./tools.js";
5
5
  import { Memory, getAgentMemoryEntrypoint } from "./memory.js";
6
+ import { getProviderApiKey } from "./config.js";
6
7
  // ─── Loading Spinner ──────────────────────────────────────────────────────────
7
8
  let spinnerTimer = null;
8
9
  let currentFrame = 0;
@@ -405,21 +406,110 @@ function extractTextToolCalls(content) {
405
406
  return calls;
406
407
  }
407
408
  // ─── Gemini API client with Auto-Rotation Fallback ────────────────────────────
408
- async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initialDelayMs = 1500, signal, silent = false) {
409
- const rotationList = [
410
- "gemini-2.0-flash",
411
- "gemini-2.0-pro-exp",
409
+ const lastRequestTimestamps = {};
410
+ function getProviderAndModel(modelName) {
411
+ const lowercase = modelName.toLowerCase();
412
+ if (lowercase.startsWith("groq/")) {
413
+ return { provider: "groq", model: modelName.slice(5) };
414
+ }
415
+ if (lowercase.startsWith("mistral/")) {
416
+ return { provider: "mistral", model: modelName.slice(8) };
417
+ }
418
+ if (lowercase.startsWith("openrouter/")) {
419
+ return { provider: "openrouter", model: modelName.slice(11) };
420
+ }
421
+ if (lowercase.startsWith("nvidia/")) {
422
+ return { provider: "nvidia", model: modelName.slice(7) };
423
+ }
424
+ if (lowercase.startsWith("github/")) {
425
+ return { provider: "github", model: modelName.slice(7) };
426
+ }
427
+ return { provider: "gemini", model: modelName };
428
+ }
429
+ function getProviderEndpoint(provider) {
430
+ switch (provider) {
431
+ case "gemini":
432
+ return "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
433
+ case "groq":
434
+ return "https://api.groq.com/openai/v1/chat/completions";
435
+ case "mistral":
436
+ return "https://api.mistral.ai/v1/chat/completions";
437
+ case "openrouter":
438
+ return "https://openrouter.ai/api/v1/chat/completions";
439
+ case "nvidia":
440
+ return "https://integrate.api.nvidia.com/v1/chat/completions";
441
+ case "github":
442
+ return "https://models.inference.ai.azure.com/chat/completions";
443
+ }
444
+ return "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
445
+ }
446
+ function getRequiredDelayMs(provider, model) {
447
+ const lowercaseModel = model.toLowerCase();
448
+ switch (provider) {
449
+ case "gemini":
450
+ if (lowercaseModel.includes("pro")) {
451
+ return 30000; // 30s for Pro (2 RPM)
452
+ }
453
+ return 4000; // 4s for Flash (15 RPM)
454
+ case "groq":
455
+ return 2000; // 2s (30 RPM)
456
+ case "mistral":
457
+ return 1500; // 1.5s (1 request/second + buffer)
458
+ case "openrouter":
459
+ return 5000; // 5s for free/shared queue
460
+ case "nvidia":
461
+ return 2000; // 2s (40 RPM)
462
+ case "github":
463
+ if (lowercaseModel.includes("sonnet") || lowercaseModel.includes("gpt-4o")) {
464
+ return 30000; // 30s for flagship (50 RPD)
465
+ }
466
+ return 6000; // 6s for open-weights (150 RPD)
467
+ }
468
+ return 2000; // default fallback
469
+ }
470
+ const DEFAULT_ROTATION_MAP = {
471
+ gemini: [
412
472
  "gemini-2.5-flash",
413
473
  "gemini-2.5-pro",
414
474
  "gemini-3.5-flash",
415
475
  "gemini-3.1-flash-lite",
416
476
  "gemma-4-31b-it",
417
- "gemma-4-26b-a4b-it"
418
- ];
477
+ "gemma-4-26b-a4b-it",
478
+ "gemini-2.0-flash",
479
+ "gemini-2.0-pro-exp"
480
+ ],
481
+ groq: [
482
+ "groq/llama-3.3-70b-specdec",
483
+ "groq/gemma-2-9b-it"
484
+ ],
485
+ mistral: [
486
+ "mistral/codestral",
487
+ "mistral/mistral-large-latest"
488
+ ],
489
+ openrouter: [
490
+ "openrouter/qwen/qwen-2.5-coder-32b-instruct:free",
491
+ "openrouter/deepseek/deepseek-r1:free",
492
+ "openrouter/meta-llama/llama-3.3-70b-instruct:free"
493
+ ],
494
+ nvidia: [
495
+ "nvidia/qwen-2.5-coder-32b-instruct",
496
+ "nvidia/deepseek-r1",
497
+ "nvidia/meta/llama-3.3-70b-instruct"
498
+ ],
499
+ github: [
500
+ "github/claude-3.5-sonnet",
501
+ "github/gpt-4o",
502
+ "github/meta-llama-3.3-70b-instruct"
503
+ ]
504
+ };
505
+ async function callAPIWithRotationAndThrottling(defaultApiKey, params, maxRetries = 3, initialDelayMs = 1500, signal, silent = false) {
506
+ const { provider: initialProvider } = getProviderAndModel(params.model);
507
+ let rotationList = DEFAULT_ROTATION_MAP[initialProvider] || DEFAULT_ROTATION_MAP.gemini;
419
508
  let currentModel = params.model;
420
509
  let modelIndex = rotationList.indexOf(currentModel);
421
510
  if (modelIndex === -1) {
422
- modelIndex = 0; // Default if not in list
511
+ rotationList = [currentModel, ...rotationList];
512
+ modelIndex = 0;
423
513
  }
424
514
  let attempts = 0;
425
515
  while (attempts < maxRetries) {
@@ -428,19 +518,53 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
428
518
  abortErr.name = "AbortError";
429
519
  throw abortErr;
430
520
  }
521
+ const { provider, model: actualModelName } = getProviderAndModel(currentModel);
522
+ const providerKey = await getProviderApiKey(provider) || defaultApiKey;
523
+ if (!providerKey) {
524
+ throw new Error(`API key for provider '${provider}' is missing. Please set it using: /key ${provider} <api_key>`);
525
+ }
526
+ // Apply rate-limiting throttle cooldown
527
+ const requiredDelay = getRequiredDelayMs(provider, actualModelName);
528
+ const lastRequestTime = lastRequestTimestamps[provider] || 0;
529
+ const elapsed = Date.now() - lastRequestTime;
530
+ if (elapsed < requiredDelay) {
531
+ const waitMs = requiredDelay - elapsed;
532
+ if (!silent) {
533
+ updateSpinner("thinking...");
534
+ }
535
+ await new Promise((resolve, reject) => {
536
+ const timer = setTimeout(resolve, waitMs);
537
+ if (signal) {
538
+ const onAbort = () => {
539
+ clearTimeout(timer);
540
+ const abortErr = new Error("The user aborted a request.");
541
+ abortErr.name = "AbortError";
542
+ reject(abortErr);
543
+ };
544
+ signal.addEventListener("abort", onAbort);
545
+ }
546
+ });
547
+ }
548
+ lastRequestTimestamps[provider] = Date.now();
431
549
  try {
432
- const res = await fetch("https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", {
550
+ const endpoint = getProviderEndpoint(provider);
551
+ const headers = {
552
+ "Content-Type": "application/json",
553
+ "Authorization": `Bearer ${providerKey}`
554
+ };
555
+ if (provider === "openrouter") {
556
+ headers["HTTP-Referer"] = "https://github.com/google/antigravity";
557
+ headers["X-Title"] = "Coder Agent";
558
+ }
559
+ const res = await fetch(endpoint, {
433
560
  method: "POST",
434
- headers: {
435
- "Content-Type": "application/json",
436
- "Authorization": `Bearer ${apiKey}`
437
- },
438
- body: JSON.stringify({ ...params, model: currentModel }),
561
+ headers,
562
+ body: JSON.stringify({ ...params, model: actualModelName }),
439
563
  signal
440
564
  });
441
565
  if (!res.ok) {
442
566
  const errText = await res.text();
443
- const err = new Error(`Gemini API error (${res.status}): ${errText}`);
567
+ const err = new Error(`${provider.toUpperCase()} API error (${res.status}): ${errText}`);
444
568
  err.status = res.status;
445
569
  throw err;
446
570
  }
@@ -609,7 +733,6 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
609
733
  const isRetryableError = status === 429 || status === 503 || (status >= 500 && status < 600) || !status;
610
734
  const isModelError = status === 404 || status === 400;
611
735
  if (isRetryableError || isModelError) {
612
- // Rotate model immediately if rate limit or model error occurred
613
736
  if (modelIndex + 1 < rotationList.length) {
614
737
  modelIndex++;
615
738
  const nextModel = rotationList[modelIndex];
@@ -843,7 +966,7 @@ export class Agent {
843
966
  }
844
967
  iterations++;
845
968
  updateSpinner("thinking...");
846
- const responseObj = await callGeminiAPIWithRotation(this.apiKey, {
969
+ const responseObj = await callAPIWithRotationAndThrottling(this.apiKey, {
847
970
  model: this.model,
848
971
  messages: this.memory.getAll(),
849
972
  tools: TOOL_DEFINITIONS,
@@ -1062,7 +1185,7 @@ Instructions:
1062
1185
  3. Integrate these new learnings and file catalog updates into the existing memory structure. Keep existing useful learnings/preferences, but clean up duplicates or obsolete info.
1063
1186
  4. Keep the content concise, clean, and formatted in Markdown.
1064
1187
  5. If there are no new learnings, setup details, file updates, or instructions in the conversation, output EXACTLY the existing memory content. Do not add conversational text, just output the updated/existing markdown content.`;
1065
- const responseObj = await callGeminiAPIWithRotation(this.apiKey, {
1188
+ const responseObj = await callAPIWithRotationAndThrottling(this.apiKey, {
1066
1189
  model: this.model,
1067
1190
  messages: [{ role: "user", content: prompt }],
1068
1191
  temperature: 0.1,
package/dist/config.js CHANGED
@@ -53,15 +53,57 @@ async function writeConfig(config) {
53
53
  console.error(`\n ⚠️ Failed to save config to ${CONFIG_FILE}: ${err.message}`);
54
54
  }
55
55
  }
56
- export async function getStoredApiKey() {
56
+ export async function getStoredApiKey(provider) {
57
57
  const config = await readConfig();
58
- return config.geminiApiKey;
58
+ const p = provider?.toLowerCase();
59
+ if (!p || p === "gemini")
60
+ return config.geminiApiKey;
61
+ if (p === "groq")
62
+ return config.groqApiKey;
63
+ if (p === "mistral")
64
+ return config.mistralApiKey;
65
+ if (p === "openrouter")
66
+ return config.openrouterApiKey;
67
+ if (p === "nvidia")
68
+ return config.nvidiaApiKey;
69
+ if (p === "github")
70
+ return config.githubApiKey;
71
+ return undefined;
59
72
  }
60
- export async function saveApiKey(apiKey) {
73
+ export async function saveApiKey(apiKey, provider) {
61
74
  const config = await readConfig();
62
- config.geminiApiKey = apiKey.trim();
75
+ const trimmed = apiKey.trim();
76
+ const p = provider?.toLowerCase();
77
+ if (!p || p === "gemini")
78
+ config.geminiApiKey = trimmed;
79
+ else if (p === "groq")
80
+ config.groqApiKey = trimmed;
81
+ else if (p === "mistral")
82
+ config.mistralApiKey = trimmed;
83
+ else if (p === "openrouter")
84
+ config.openrouterApiKey = trimmed;
85
+ else if (p === "nvidia")
86
+ config.nvidiaApiKey = trimmed;
87
+ else if (p === "github")
88
+ config.githubApiKey = trimmed;
63
89
  await writeConfig(config);
64
90
  }
91
+ export async function getProviderApiKey(provider) {
92
+ const p = provider.toLowerCase();
93
+ if (p === "gemini")
94
+ return process.env.GEMINI_API_KEY || await getStoredApiKey("gemini");
95
+ if (p === "groq")
96
+ return process.env.GROQ_API_KEY || await getStoredApiKey("groq");
97
+ if (p === "mistral")
98
+ return process.env.MISTRAL_API_KEY || await getStoredApiKey("mistral");
99
+ if (p === "openrouter")
100
+ return process.env.OPENROUTER_API_KEY || await getStoredApiKey("openrouter");
101
+ if (p === "nvidia")
102
+ return process.env.NVIDIA_API_KEY || await getStoredApiKey("nvidia");
103
+ if (p === "github")
104
+ return process.env.GITHUB_API_KEY || process.env.GITHUB_TOKEN || await getStoredApiKey("github");
105
+ return undefined;
106
+ }
65
107
  export async function getStoredModel() {
66
108
  const config = await readConfig();
67
109
  return config.defaultModel;
@@ -72,13 +114,10 @@ export async function saveModel(model) {
72
114
  await writeConfig(config);
73
115
  }
74
116
  export async function getStoredGeminiApiKey() {
75
- const config = await readConfig();
76
- return config.geminiApiKey;
117
+ return getStoredApiKey("gemini");
77
118
  }
78
119
  export async function saveGeminiApiKey(geminiApiKey) {
79
- const config = await readConfig();
80
- config.geminiApiKey = geminiApiKey.trim();
81
- await writeConfig(config);
120
+ await saveApiKey(geminiApiKey, "gemini");
82
121
  }
83
122
  export async function getLastUsedInfo() {
84
123
  const config = await readConfig();
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import * as readline from "readline";
4
4
  import chalk from "chalk";
5
5
  import figlet from "figlet";
6
6
  import { Agent } from "./agent.js";
7
- import { getStoredApiKey, saveApiKey, getStoredModel, saveModel, getLastUsedInfo, saveLastUsedInfo } from "./config.js";
7
+ import { getStoredApiKey, saveApiKey, getStoredModel, saveModel, getLastUsedInfo, saveLastUsedInfo, getProviderApiKey } from "./config.js";
8
8
  const require = createRequire(import.meta.url);
9
9
  const packageJson = require("../package.json");
10
10
  const CURRENT_VERSION = packageJson.version;
@@ -18,6 +18,12 @@ const VALID_MODELS = [
18
18
  "gemma-4-31b-it",
19
19
  "gemma-4-26b-a4b-it"
20
20
  ];
21
+ function isValidModel(modelName) {
22
+ if (VALID_MODELS.includes(modelName))
23
+ return true;
24
+ const prefixes = ["groq/", "mistral/", "openrouter/", "nvidia/", "github/"];
25
+ return prefixes.some(prefix => modelName.toLowerCase().startsWith(prefix));
26
+ }
21
27
  function printBanner(modelName) {
22
28
  console.clear();
23
29
  console.log('');
@@ -83,6 +89,14 @@ function printHelp() {
83
89
  console.log(chalk.white(" gemini-2.0-flash — (Deprecated) Ultra-fast, lightweight"));
84
90
  console.log(chalk.white(" gemini-2.0-pro-exp — (Deprecated) Experimental reasoning"));
85
91
  console.log("");
92
+ console.log(chalk.gray(" Popular Multi-Provider Models (prefixed with provider/):"));
93
+ console.log(chalk.white(" groq/llama-3.3-70b-specdec — Groq Llama 3.3 70B (extremely fast)"));
94
+ console.log(chalk.white(" mistral/codestral — Mistral Codestral (highly rated coding model)"));
95
+ console.log(chalk.white(" openrouter/qwen/qwen-2.5-coder-32b-instruct:free — OpenRouter Qwen 2.5 Coder Free"));
96
+ console.log(chalk.white(" openrouter/deepseek/deepseek-r1:free — OpenRouter DeepSeek R1 Free"));
97
+ console.log(chalk.white(" nvidia/qwen-2.5-coder-32b-instruct — NVIDIA NIM Qwen 2.5 Coder 32B"));
98
+ console.log(chalk.white(" github/claude-3.5-sonnet — GitHub Claude 3.5 Sonnet (requires GitHub token)"));
99
+ console.log("");
86
100
  }
87
101
  // ─── API Key Bootstrap ────────────────────────────────────────────────────────
88
102
  async function promptApiKey() {
@@ -171,12 +185,12 @@ async function main() {
171
185
  }
172
186
  const storedModel = await getStoredModel();
173
187
  let defaultModel = "gemini-2.5-flash";
174
- if (storedModel && VALID_MODELS.includes(storedModel)) {
188
+ if (storedModel && isValidModel(storedModel)) {
175
189
  defaultModel = storedModel;
176
190
  }
177
- if (tempModel && !VALID_MODELS.includes(tempModel)) {
191
+ if (tempModel && !isValidModel(tempModel)) {
178
192
  console.log(chalk.hex('#ff453a')('✕ error'));
179
- console.log(chalk.dim(` Invalid model: ${tempModel}. Choose one of: ${VALID_MODELS.join(", ")}`));
193
+ console.log(chalk.dim(` Invalid model: ${tempModel}. Choose one of: ${VALID_MODELS.join(", ")} or prefix with groq/, mistral/, openrouter/, nvidia/, github/`));
180
194
  process.exit(1);
181
195
  }
182
196
  const modelToUse = tempModel || defaultModel;
@@ -322,7 +336,7 @@ async function main() {
322
336
  console.log();
323
337
  console.log(chalk.dim(" Available Commands:"));
324
338
  console.log(` ${chalk.hex('#0a84ff')('/model')} ${chalk.gray('[name]')} ${chalk.dim('— View active model or switch to [name]')}`);
325
- console.log(` ${chalk.hex('#0a84ff')('/key')} ${chalk.gray('[api_key]')} ${chalk.dim('— Set/save your Gemini API Key globally')}`);
339
+ console.log(` ${chalk.hex('#0a84ff')('/key')} ${chalk.gray('[provider] [api_key]')} ${chalk.dim('— Set/save API keys globally')}`);
326
340
  console.log(` ${chalk.hex('#0a84ff')('/clear')} ${chalk.dim('— Wipe conversation memory')}`);
327
341
  console.log(` ${chalk.hex('#0a84ff')('/status')} ${chalk.dim('— Show active model and memory usage')}`);
328
342
  console.log(` ${chalk.hex('#0a84ff')('/help')} ${chalk.dim('— Show help screen')}`);
@@ -378,17 +392,17 @@ async function main() {
378
392
  const parts = trimmed.split(/\s+/);
379
393
  if (parts.length === 1) {
380
394
  console.log(chalk.dim(`model `) + chalk.gray(agent.getModel()));
381
- console.log(chalk.gray(`options `) + chalk.gray(VALID_MODELS.join(" · ")));
395
+ console.log(chalk.gray(`options `) + chalk.gray(VALID_MODELS.join(" · ") + " · groq/... · mistral/... · openrouter/... · nvidia/... · github/..."));
382
396
  }
383
397
  else {
384
398
  const newModel = parts[1];
385
- if (VALID_MODELS.includes(newModel)) {
399
+ if (isValidModel(newModel)) {
386
400
  agent.setModel(newModel);
387
401
  console.log(chalk.hex('#30d158')('✓') + ' ' + chalk.gray(`Switched model to: ${newModel}`));
388
402
  }
389
403
  else {
390
404
  console.log(chalk.hex('#ff453a')('✕ error'));
391
- console.log(chalk.dim(` Model must be one of: ${VALID_MODELS.join(" · ")}`));
405
+ console.log(chalk.dim(` Model must be one of: ${VALID_MODELS.join(" · ")} or prefixed with groq/, mistral/, openrouter/, nvidia/, github/`));
392
406
  }
393
407
  }
394
408
  if (!isRlClosed) {
@@ -403,26 +417,47 @@ async function main() {
403
417
  if (trimmed.startsWith("/key")) {
404
418
  const parts = trimmed.split(/\s+/);
405
419
  if (parts.length === 1) {
406
- const keyToCheck = agent.getApiKey();
407
- if (keyToCheck) {
408
- const masked = keyToCheck.slice(0, 7) + "..." + keyToCheck.slice(-4);
409
- console.log(chalk.dim(` Gemini API Key is set: `) + chalk.gray(masked));
410
- }
411
- else {
412
- console.log(chalk.hex('#ff453a')('✕ error') + chalk.dim(' — Gemini API Key is missing. Set it using: /key <your_api_key>'));
420
+ const providers = ["gemini", "groq", "mistral", "openrouter", "nvidia", "github"];
421
+ console.log(chalk.white.bold("\n Stored API Keys:"));
422
+ for (const p of providers) {
423
+ const keyToCheck = await getProviderApiKey(p);
424
+ if (keyToCheck) {
425
+ const masked = keyToCheck.slice(0, 7) + "..." + keyToCheck.slice(-4);
426
+ console.log(` ${chalk.dim(p.toUpperCase().padEnd(10))} : ${chalk.gray(masked)}`);
427
+ }
428
+ else {
429
+ console.log(` ${chalk.dim(p.toUpperCase().padEnd(10))} : ${chalk.hex('#ff453a')('missing')}`);
430
+ }
413
431
  }
432
+ console.log(chalk.dim("\n To set a key, use: /key <provider> <api_key> (e.g. /key groq gsk_...)"));
433
+ console.log(chalk.dim(" Or set it in environment (e.g. GEMINI_API_KEY, GROQ_API_KEY, etc.)\n"));
414
434
  }
415
- else {
435
+ else if (parts.length === 2) {
416
436
  const newKey = parts[1].trim();
417
437
  if (!newKey) {
418
438
  console.log(chalk.hex('#ff453a')('✕ error') + chalk.dim(' — API Key cannot be empty.'));
419
439
  }
420
440
  else {
421
- await saveApiKey(newKey);
441
+ await saveApiKey(newKey, "gemini");
422
442
  agent.setApiKey(newKey);
423
443
  console.log(chalk.hex('#30d158')('✓') + ' ' + chalk.gray('Gemini API Key saved and set successfully.'));
424
444
  }
425
445
  }
446
+ else {
447
+ const provider = parts[1].toLowerCase();
448
+ const newKey = parts[2].trim();
449
+ const validProviders = ["gemini", "groq", "mistral", "openrouter", "nvidia", "github"];
450
+ if (!validProviders.includes(provider)) {
451
+ console.log(chalk.hex('#ff453a')('✕ error') + chalk.dim(` — Invalid provider. Choose one of: ${validProviders.join(", ")}`));
452
+ }
453
+ else {
454
+ await saveApiKey(newKey, provider);
455
+ if (provider === "gemini") {
456
+ agent.setApiKey(newKey);
457
+ }
458
+ console.log(chalk.hex('#30d158')('✓') + ' ' + chalk.gray(`${provider.toUpperCase()} API Key saved and set successfully.`));
459
+ }
460
+ }
426
461
  if (!isRlClosed) {
427
462
  try {
428
463
  rl.resume();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-agent",
3
- "version": "2.9.12",
3
+ "version": "2.9.13",
4
4
  "description": "CLI coding agent powered by Google Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",