open-agents-ai 0.16.2 → 0.16.4

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 (3) hide show
  1. package/README.md +42 -0
  2. package/dist/index.js +268 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -276,6 +276,48 @@ oa --backend vllm --backend-url http://localhost:8000/v1 "add tests"
276
276
  oa --backend-url http://10.0.0.5:11434 "refactor auth"
277
277
  ```
278
278
 
279
+ ## Supported Inference Providers
280
+
281
+ Open Agents auto-detects your provider from the endpoint URL and configures auth + health checks accordingly. All providers use standard `Authorization: Bearer <key>` authentication.
282
+
283
+ | Provider | Endpoint URL | API Key | Notes |
284
+ |----------|-------------|---------|-------|
285
+ | **Ollama** (local) | `http://localhost:11434` | None | Default. Auto-detects, auto-expands context window |
286
+ | **vLLM** (local) | `http://localhost:8000` | Optional | Self-hosted OpenAI-compatible server |
287
+ | **LM Studio** (local) | `http://localhost:1234` | None | Local model server with GUI |
288
+ | **Chutes AI** | `https://llm.chutes.ai` | `cpk_...` | Bearer auth. Fast cloud inference |
289
+ | **Together AI** | `https://api.together.xyz` | Required | Large model catalog |
290
+ | **Groq** | `https://api.groq.com/openai` | `gsk_...` | Ultra-fast LPU inference |
291
+ | **OpenRouter** | `https://openrouter.ai/api` | `sk-or-...` | Multi-provider routing |
292
+ | **Fireworks AI** | `https://api.fireworks.ai/inference` | `fw_...` | Fast serverless inference |
293
+ | **DeepInfra** | `https://api.deepinfra.com` | Required | Cost-effective inference |
294
+ | **Mistral AI** | `https://api.mistral.ai` | Required | Mistral models |
295
+ | **Cerebras** | `https://api.cerebras.ai` | `csk-...` | Wafer-scale inference |
296
+ | **SambaNova** | `https://api.sambanova.ai` | Required | RDU-accelerated inference |
297
+ | **NVIDIA NIM** | `https://integrate.api.nvidia.com` | `nvapi-...` | NVIDIA cloud inference |
298
+ | **Hyperbolic** | `https://api.hyperbolic.xyz` | Required | GPU cloud inference |
299
+ | **OpenAI** | `https://api.openai.com` | `sk-...` | GPT models (tool calling) |
300
+
301
+ ### Connecting to a Provider
302
+
303
+ Use `/endpoint` in the TUI or pass via CLI:
304
+
305
+ ```bash
306
+ # Chutes AI
307
+ /endpoint https://llm.chutes.ai --auth cpk_your_key_here
308
+
309
+ # Groq
310
+ /endpoint https://api.groq.com/openai --auth gsk_your_key_here
311
+
312
+ # Together AI
313
+ /endpoint https://api.together.xyz --auth your_key_here
314
+
315
+ # Self-hosted vLLM on LAN
316
+ /endpoint http://10.0.0.5:8000
317
+ ```
318
+
319
+ The agent auto-detects the provider, normalizes the URL (strips `/v1/chat/completions` if pasted), tests connectivity, and saves the configuration. You can paste full endpoint URLs — they'll be cleaned up automatically.
320
+
279
321
  ## Evaluation Suite
280
322
 
281
323
  23 evaluation tasks test the agent's autonomous capabilities across coding, web research, SDLC analysis, and tool creation:
package/dist/index.js CHANGED
@@ -497,13 +497,100 @@ var init_sleep = __esm({
497
497
  function normalizeBaseUrl(url) {
498
498
  let u = url.trim();
499
499
  u = u.replace(/\/+$/, "");
500
- u = u.replace(/\/v1(?:\/.*)?$/, "");
500
+ u = u.replace(/\/chat\/completions$/, "");
501
+ u = u.replace(/\/completions$/, "");
502
+ u = u.replace(/\/embeddings$/, "");
503
+ u = u.replace(/\/models(?:\/.*)?$/, "");
504
+ u = u.replace(/\/+$/, "");
505
+ u = u.replace(/\/v1\/openai$/, "");
506
+ u = u.replace(/\/+$/, "");
507
+ u = u.replace(/\/v1$/, "");
501
508
  u = u.replace(/\/+$/, "");
502
509
  return u;
503
510
  }
511
+ function detectProvider(url) {
512
+ const normalized = url.trim().toLowerCase();
513
+ for (const { match, info } of PROVIDERS) {
514
+ if (match(normalized))
515
+ return info;
516
+ }
517
+ const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/i.test(normalized);
518
+ return {
519
+ id: "unknown",
520
+ label: isLocal ? "Local OpenAI-compatible" : "OpenAI-compatible",
521
+ local: isLocal,
522
+ authRequired: !isLocal,
523
+ modelsPath: isLocal ? "/v1/models" : "/v1/models"
524
+ };
525
+ }
526
+ var PROVIDERS;
504
527
  var init_normalizeUrl = __esm({
505
528
  "packages/backend-vllm/dist/normalizeUrl.js"() {
506
529
  "use strict";
530
+ PROVIDERS = [
531
+ // --- Cloud providers (specific domains) ---
532
+ {
533
+ match: (u) => /api\.openai\.com/i.test(u),
534
+ info: { id: "openai", label: "OpenAI", local: false, authRequired: true, keyPrefix: "sk-", modelsPath: "/v1/models" }
535
+ },
536
+ {
537
+ match: (u) => /api\.together\.xyz/i.test(u),
538
+ info: { id: "together", label: "Together AI", local: false, authRequired: true, modelsPath: "/v1/models" }
539
+ },
540
+ {
541
+ match: (u) => /api\.groq\.com/i.test(u),
542
+ info: { id: "groq", label: "Groq", local: false, authRequired: true, keyPrefix: "gsk_", modelsPath: "/v1/models" }
543
+ },
544
+ {
545
+ match: (u) => /openrouter\.ai/i.test(u),
546
+ info: { id: "openrouter", label: "OpenRouter", local: false, authRequired: true, keyPrefix: "sk-or-", modelsPath: "/v1/models" }
547
+ },
548
+ {
549
+ match: (u) => /api\.fireworks\.ai/i.test(u),
550
+ info: { id: "fireworks", label: "Fireworks AI", local: false, authRequired: true, keyPrefix: "fw_", modelsPath: "/v1/models" }
551
+ },
552
+ {
553
+ match: (u) => /api\.deepinfra\.com/i.test(u),
554
+ info: { id: "deepinfra", label: "DeepInfra", local: false, authRequired: true, modelsPath: "/v1/models" }
555
+ },
556
+ {
557
+ match: (u) => /api\.mistral\.ai/i.test(u),
558
+ info: { id: "mistral", label: "Mistral AI", local: false, authRequired: true, modelsPath: "/v1/models" }
559
+ },
560
+ {
561
+ match: (u) => /llm\.chutes\.ai|chutes\.ai/i.test(u),
562
+ info: { id: "chutes", label: "Chutes AI", local: false, authRequired: true, keyPrefix: "cpk_", modelsPath: "/v1/models" }
563
+ },
564
+ {
565
+ match: (u) => /api\.cerebras\.ai/i.test(u),
566
+ info: { id: "cerebras", label: "Cerebras", local: false, authRequired: true, keyPrefix: "csk-", modelsPath: "/v1/models" }
567
+ },
568
+ {
569
+ match: (u) => /api\.sambanova\.ai/i.test(u),
570
+ info: { id: "sambanova", label: "SambaNova", local: false, authRequired: true, modelsPath: "/v1/models" }
571
+ },
572
+ {
573
+ match: (u) => /integrate\.api\.nvidia\.com/i.test(u),
574
+ info: { id: "nvidia", label: "NVIDIA NIM", local: false, authRequired: true, keyPrefix: "nvapi-", modelsPath: "/v1/models" }
575
+ },
576
+ {
577
+ match: (u) => /api\.hyperbolic\.xyz/i.test(u),
578
+ info: { id: "hyperbolic", label: "Hyperbolic", local: false, authRequired: true, modelsPath: "/v1/models" }
579
+ },
580
+ // --- Local providers (port-based detection) ---
581
+ {
582
+ match: (u) => /(?:localhost|127\.0\.0\.1|0\.0\.0\.0):11434/i.test(u),
583
+ info: { id: "ollama", label: "Ollama (local)", local: true, authRequired: false, modelsPath: "/api/tags" }
584
+ },
585
+ {
586
+ match: (u) => /(?:localhost|127\.0\.0\.1|0\.0\.0\.0):1234/i.test(u),
587
+ info: { id: "lmstudio", label: "LM Studio (local)", local: true, authRequired: false, modelsPath: "/v1/models" }
588
+ },
589
+ {
590
+ match: (u) => /(?:localhost|127\.0\.0\.1|0\.0\.0\.0):8000/i.test(u),
591
+ info: { id: "vllm", label: "vLLM (local)", local: true, authRequired: false, modelsPath: "/v1/models" }
592
+ }
593
+ ];
507
594
  }
508
595
  });
509
596
 
@@ -9094,11 +9181,11 @@ ${newerSummary}` : newerSummary;
9094
9181
  this.model = model;
9095
9182
  this.apiKey = apiKey ?? "";
9096
9183
  }
9097
- /** Build auth headers — handles Chutes (cpk_ raw), Bearer, or none. */
9184
+ /** Build auth headers — all providers use standard Bearer token auth. */
9098
9185
  authHeaders() {
9099
9186
  const headers = { "Content-Type": "application/json" };
9100
9187
  if (this.apiKey) {
9101
- headers["Authorization"] = this.apiKey.startsWith("cpk_") ? this.apiKey : `Bearer ${this.apiKey}`;
9188
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
9102
9189
  }
9103
9190
  return headers;
9104
9191
  }
@@ -9820,6 +9907,84 @@ function getColorsEnabled() {
9820
9907
  function getTermWidth() {
9821
9908
  return process.stdout.columns ?? 80;
9822
9909
  }
9910
+ function formatMarkdownLine(line) {
9911
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
9912
+ if (headingMatch) {
9913
+ const level = headingMatch[1].length;
9914
+ const text = headingMatch[2];
9915
+ const colors = [MD.heading1, MD.heading2, MD.heading3, MD.heading3, 183, 183];
9916
+ return c2.bold(fg256(colors[level - 1] ?? 147, formatInlineMarkdown(text)));
9917
+ }
9918
+ if (/^[-*_]{3,}\s*$/.test(line)) {
9919
+ const w = getTermWidth() - 10;
9920
+ return fg256(MD.hr, "\u2500".repeat(Math.min(w, 60)));
9921
+ }
9922
+ if (/^>\s?/.test(line)) {
9923
+ const content = line.replace(/^>\s?/, "");
9924
+ return fg256(MD.blockquote, "\u2502 ") + c2.italic(fg256(MD.blockquote, formatInlineMarkdown(content)));
9925
+ }
9926
+ if (/^\|(.+)\|/.test(line)) {
9927
+ if (/^\|[\s:_-]+\|/.test(line)) {
9928
+ return fg256(MD.tableBar, line);
9929
+ }
9930
+ return line.replace(/([^|]+)/g, (cell) => {
9931
+ const trimmed = cell.trim();
9932
+ if (!trimmed)
9933
+ return cell;
9934
+ const leading = cell.match(/^(\s*)/)?.[1] ?? "";
9935
+ const trailing = cell.match(/(\s*)$/)?.[1] ?? "";
9936
+ return leading + formatInlineMarkdown(trimmed) + trailing;
9937
+ });
9938
+ }
9939
+ const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)/);
9940
+ if (ulMatch) {
9941
+ return ulMatch[1] + fg256(MD.listBullet, "\u2022") + " " + formatInlineMarkdown(ulMatch[3]);
9942
+ }
9943
+ const olMatch = line.match(/^(\s*)(\d+[.)])\s+(.*)/);
9944
+ if (olMatch) {
9945
+ return olMatch[1] + fg256(MD.listBullet, olMatch[2]) + " " + formatInlineMarkdown(olMatch[3]);
9946
+ }
9947
+ return formatInlineMarkdown(line);
9948
+ }
9949
+ function formatInlineMarkdown(text) {
9950
+ let result = text;
9951
+ result = result.replace(/`([^`]+)`/g, (_m, code) => fg256(MD.inlineCode, code));
9952
+ result = result.replace(/\*{3}([^*]+)\*{3}/g, (_m, t) => c2.bold(c2.italic(t)));
9953
+ result = result.replace(/\*{2}([^*]+)\*{2}/g, (_m, t) => c2.bold(t));
9954
+ result = result.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_m, t) => c2.italic(t));
9955
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => c2.bold(fg256(MD.link, label)) + " " + c2.dim(fg256(MD.link, `(${url})`)));
9956
+ result = result.replace(/__([^_]+)__/g, (_m, t) => c2.bold(t));
9957
+ result = result.replace(/(?<!_)_([^_]+)_(?!_)/g, (_m, t) => c2.italic(t));
9958
+ result = result.replace(/~~([^~]+)~~/g, (_m, t) => c2.dim(t));
9959
+ return result;
9960
+ }
9961
+ function formatMarkdownBlock(text) {
9962
+ const lines = text.split("\n");
9963
+ const result = [];
9964
+ let inCodeBlock = false;
9965
+ let codeLang = "";
9966
+ for (const line of lines) {
9967
+ const trimmedLine = line.trimStart();
9968
+ if (trimmedLine.startsWith("```")) {
9969
+ if (inCodeBlock) {
9970
+ result.push(c2.dim(" ```"));
9971
+ inCodeBlock = false;
9972
+ codeLang = "";
9973
+ } else {
9974
+ codeLang = trimmedLine.slice(3).trim();
9975
+ result.push(c2.dim(" ```" + codeLang));
9976
+ inCodeBlock = true;
9977
+ }
9978
+ continue;
9979
+ }
9980
+ if (inCodeBlock) {
9981
+ result.push(" " + c2.dim(line));
9982
+ } else {
9983
+ result.push(formatMarkdownLine(line));
9984
+ }
9985
+ }
9986
+ return result.join("\n");
9987
+ }
9823
9988
  function renderUserMessage(text) {
9824
9989
  process.stdout.write(`
9825
9990
  ${c2.bold(c2.blue("> "))}${c2.bold(text)}
@@ -9828,7 +9993,8 @@ ${c2.bold(c2.blue("> "))}${c2.bold(text)}
9828
9993
  function renderAssistantText(text) {
9829
9994
  if (!text.trim())
9830
9995
  return;
9831
- const lines = text.split("\n");
9996
+ const formatted = formatMarkdownBlock(text);
9997
+ const lines = formatted.split("\n");
9832
9998
  for (const line of lines) {
9833
9999
  process.stdout.write(` ${line}
9834
10000
  `);
@@ -9925,8 +10091,9 @@ function renderToolResult(toolName, success, output) {
9925
10091
  `);
9926
10092
  return;
9927
10093
  }
9928
- const trimmed = line.length > maxW ? line.slice(0, maxW - 3) + "..." : line;
9929
- process.stdout.write(`${prefix}${highlightToolOutput(trimmed)}
10094
+ const cropped = line.length > maxW ? line.slice(0, maxW - 3) + "..." : line;
10095
+ const formatted = formatMarkdownLine(cropped);
10096
+ process.stdout.write(`${prefix}${formatted === cropped ? highlightToolOutput(cropped) : formatted}
9930
10097
  `);
9931
10098
  }
9932
10099
  if (lines.length > maxLines) {
@@ -10010,6 +10177,9 @@ function highlightToolOutput(line) {
10010
10177
  return c2.green(line);
10011
10178
  if (/\bfail(ed|ing|ure)?\b/i.test(line))
10012
10179
  return c2.red(line);
10180
+ const formatted = formatInlineMarkdown(line);
10181
+ if (formatted !== line)
10182
+ return formatted;
10013
10183
  return c2.dim(line);
10014
10184
  }
10015
10185
  function renderTaskComplete(summary, turns, toolCalls, durationMs, tokens) {
@@ -10023,7 +10193,8 @@ ${c2.green("\u2714")} ${c2.bold("Task completed")} ${c2.dim(`(${turns} turns, ${
10023
10193
  `);
10024
10194
  }
10025
10195
  if (summary) {
10026
- const lines = summary.split("\n");
10196
+ const formatted = formatMarkdownBlock(summary);
10197
+ const lines = formatted.split("\n");
10027
10198
  for (const line of lines) {
10028
10199
  process.stdout.write(` ${line}
10029
10200
  `);
@@ -10234,13 +10405,14 @@ function renderConfig(config) {
10234
10405
  process.stdout.write("\n");
10235
10406
  }
10236
10407
  function formatToolArgs(toolName, args) {
10408
+ const maxArg = Math.max(40, getTermWidth() - 20);
10237
10409
  switch (toolName) {
10238
10410
  case "file_read":
10239
10411
  case "file_write":
10240
10412
  case "file_edit":
10241
10413
  return String(args["path"] ?? "");
10242
10414
  case "shell": {
10243
- const cmd = truncStr(String(args["command"] ?? ""), 60);
10415
+ const cmd = truncStr(String(args["command"] ?? ""), maxArg);
10244
10416
  return args["stdin"] ? `${cmd} ${c2.dim("(with stdin)")}` : cmd;
10245
10417
  }
10246
10418
  case "grep_search":
@@ -10250,21 +10422,21 @@ function formatToolArgs(toolName, args) {
10250
10422
  case "list_directory":
10251
10423
  return String(args["path"] ?? ".");
10252
10424
  case "web_search":
10253
- return `"${truncStr(String(args["query"] ?? ""), 50)}"`;
10425
+ return `"${truncStr(String(args["query"] ?? ""), maxArg - 2)}"`;
10254
10426
  case "web_fetch":
10255
- return truncStr(String(args["url"] ?? ""), 60);
10427
+ return truncStr(String(args["url"] ?? ""), maxArg);
10256
10428
  case "memory_read":
10257
10429
  return `${args["topic"]}${args["key"] ? "." + args["key"] : ""}`;
10258
10430
  case "memory_write":
10259
10431
  return `${args["topic"]}.${args["key"]}`;
10260
10432
  case "task_complete":
10261
- return truncStr(String(args["summary"] ?? ""), 60);
10433
+ return truncStr(String(args["summary"] ?? ""), maxArg);
10262
10434
  case "aiwg_setup":
10263
10435
  return String(args["framework"] ?? "sdlc");
10264
10436
  case "aiwg_health":
10265
10437
  return args["detailed"] ? "detailed" : "summary";
10266
10438
  case "aiwg_workflow":
10267
- return truncStr(String(args["command"] ?? ""), 60);
10439
+ return truncStr(String(args["command"] ?? ""), maxArg);
10268
10440
  case "batch_edit": {
10269
10441
  const edits = args["edits"];
10270
10442
  return edits ? `${edits.length} edit(s)` : "";
@@ -10278,14 +10450,14 @@ function formatToolArgs(toolName, args) {
10278
10450
  case "git_info":
10279
10451
  return args["show_diff"] ? "with diff" : "summary";
10280
10452
  case "background_run":
10281
- return truncStr(String(args["command"] ?? ""), 60);
10453
+ return truncStr(String(args["command"] ?? ""), maxArg);
10282
10454
  case "task_status":
10283
10455
  case "task_output":
10284
10456
  case "task_stop":
10285
10457
  return String(args["task_id"] ?? "all");
10286
10458
  case "sub_agent": {
10287
10459
  const bg = args["background"] ? " (background)" : "";
10288
- return truncStr(String(args["task"] ?? ""), 50) + bg;
10460
+ return truncStr(String(args["task"] ?? ""), maxArg - 15) + bg;
10289
10461
  }
10290
10462
  case "image_read":
10291
10463
  return String(args["path"] ?? "");
@@ -10296,9 +10468,9 @@ function formatToolArgs(toolName, args) {
10296
10468
  case "transcribe_file":
10297
10469
  return `${args["path"] ?? ""}${args["model"] ? ` (${args["model"]})` : ""}`;
10298
10470
  case "transcribe_url":
10299
- return truncStr(String(args["url"] ?? ""), 60);
10471
+ return truncStr(String(args["url"] ?? ""), maxArg);
10300
10472
  default:
10301
- return Object.entries(args).map(([k, v]) => `${k}=${truncStr(String(v), 30)}`).join(", ");
10473
+ return Object.entries(args).map(([k, v]) => `${k}=${truncStr(String(v), Math.max(30, maxArg / 3))}`).join(", ");
10302
10474
  }
10303
10475
  }
10304
10476
  function truncStr(s, max) {
@@ -10314,7 +10486,7 @@ function formatDuration2(ms) {
10314
10486
  const secs = Math.floor(totalSecs % 60);
10315
10487
  return `${mins}m ${secs}s`;
10316
10488
  }
10317
- var isTTY2, c2, pastel, _emojisEnabled, _colorsEnabled, TOOL_ICONS, TOOL_LABELS, TOOL_COLORS, _contentWriteHook, HINTS, TOOL_NAMES, COMMAND_NAMES;
10489
+ var isTTY2, c2, pastel, _emojisEnabled, _colorsEnabled, MD, TOOL_ICONS, TOOL_LABELS, TOOL_COLORS, _contentWriteHook, HINTS, TOOL_NAMES, COMMAND_NAMES;
10318
10490
  var init_render = __esm({
10319
10491
  "packages/cli/dist/tui/render.js"() {
10320
10492
  "use strict";
@@ -10347,6 +10519,26 @@ var init_render = __esm({
10347
10519
  };
10348
10520
  _emojisEnabled = true;
10349
10521
  _colorsEnabled = true;
10522
+ MD = {
10523
+ heading1: 75,
10524
+ // blue
10525
+ heading2: 117,
10526
+ // sky blue
10527
+ heading3: 147,
10528
+ // light blue
10529
+ inlineCode: 223,
10530
+ // light peach
10531
+ link: 111,
10532
+ // periwinkle
10533
+ blockquote: 245,
10534
+ // grey
10535
+ hr: 240,
10536
+ // dark grey
10537
+ listBullet: 245,
10538
+ // grey
10539
+ tableBar: 245
10540
+ // grey
10541
+ };
10350
10542
  TOOL_ICONS = {
10351
10543
  file_read: "\u{1F4C4}",
10352
10544
  file_write: "\u{1F4DD}",
@@ -11054,15 +11246,18 @@ async function promptForCustomEndpoint(config, rl) {
11054
11246
  `);
11055
11247
  const modelName = await ask(rl, ` ${c2.bold("Model name")} (Enter for ${c2.dim(config.model)}): `);
11056
11248
  const chosenModel = modelName || config.model;
11249
+ const provider = detectProvider(endpoint);
11057
11250
  process.stdout.write(`
11058
- ${c2.cyan("\u25CF")} Testing endpoint ${c2.bold(cleanUrl)}...
11251
+ ${c2.cyan("\u25CF")} Detected provider: ${c2.bold(provider.label)}
11252
+ `);
11253
+ process.stdout.write(` ${c2.cyan("\u25CF")} Testing endpoint ${c2.bold(cleanUrl)}...
11059
11254
  `);
11060
11255
  let testOk = false;
11061
11256
  try {
11062
- const testUrl = `${cleanUrl}/v1/models`;
11257
+ const testUrl = `${cleanUrl}${provider.modelsPath}`;
11063
11258
  const headers = { "Content-Type": "application/json" };
11064
11259
  if (apiKey) {
11065
- headers["Authorization"] = apiKey.startsWith("cpk_") ? apiKey : `Bearer ${apiKey}`;
11260
+ headers["Authorization"] = `Bearer ${apiKey}`;
11066
11261
  }
11067
11262
  const resp = await fetch(testUrl, { headers, signal: AbortSignal.timeout(1e4) });
11068
11263
  if (resp.ok) {
@@ -11070,14 +11265,16 @@ async function promptForCustomEndpoint(config, rl) {
11070
11265
  `);
11071
11266
  testOk = true;
11072
11267
  } else {
11073
- try {
11074
- const ollamaResp = await fetch(`${cleanUrl}/api/tags`, { signal: AbortSignal.timeout(1e4) });
11075
- if (ollamaResp.ok) {
11076
- process.stdout.write(` ${c2.green("\u2714")} Ollama endpoint detected.
11268
+ if (provider.id !== "ollama") {
11269
+ try {
11270
+ const ollamaResp = await fetch(`${cleanUrl}/api/tags`, { signal: AbortSignal.timeout(1e4) });
11271
+ if (ollamaResp.ok) {
11272
+ process.stdout.write(` ${c2.green("\u2714")} Ollama endpoint detected.
11077
11273
  `);
11078
- testOk = true;
11274
+ testOk = true;
11275
+ }
11276
+ } catch {
11079
11277
  }
11080
- } catch {
11081
11278
  }
11082
11279
  if (!testOk) {
11083
11280
  process.stdout.write(` ${c2.yellow("\u26A0")} Endpoint returned HTTP ${resp.status}
@@ -11089,6 +11286,10 @@ async function promptForCustomEndpoint(config, rl) {
11089
11286
  `);
11090
11287
  }
11091
11288
  if (!testOk) {
11289
+ if (provider.authRequired && !apiKey) {
11290
+ process.stdout.write(` ${c2.dim(`${provider.label} typically requires an API key.`)}
11291
+ `);
11292
+ }
11092
11293
  const startAnyway = await ask(rl, `
11093
11294
  ${c2.bold("Endpoint unreachable. Start anyway?")} (y/n) `);
11094
11295
  if (startAnyway.toLowerCase() !== "y" && startAnyway.toLowerCase() !== "yes") {
@@ -11103,11 +11304,12 @@ async function promptForCustomEndpoint(config, rl) {
11103
11304
  if (apiKey) {
11104
11305
  setConfigValue("apiKey", apiKey);
11105
11306
  }
11106
- const isLocalOllama = /localhost|127\.0\.0\.1/.test(cleanUrl) && cleanUrl.includes("11434");
11107
- const backendType = isLocalOllama ? "ollama" : "vllm";
11307
+ const backendType = provider.id === "ollama" ? "ollama" : "vllm";
11108
11308
  setConfigValue("backendType", backendType);
11109
11309
  process.stdout.write(`
11110
11310
  ${c2.green("\u2714")} Configured: ${c2.bold(chosenModel)} at ${c2.bold(cleanUrl)}
11311
+ `);
11312
+ process.stdout.write(` ${c2.green("\u2714")} Provider: ${c2.bold(provider.label)}
11111
11313
  `);
11112
11314
  if (apiKey)
11113
11315
  process.stdout.write(` ${c2.green("\u2714")} API key saved.
@@ -11728,9 +11930,12 @@ async function showModelPicker(ctx) {
11728
11930
  }
11729
11931
  async function handleEndpoint(arg, ctx, local = false) {
11730
11932
  if (!arg) {
11933
+ const currentProvider = detectProvider(ctx.config.backendUrl);
11731
11934
  process.stdout.write(`
11732
11935
  ${c2.bold("Current endpoint:")}
11733
11936
 
11937
+ `);
11938
+ process.stdout.write(` ${c2.cyan("Provider".padEnd(12))} ${currentProvider.label}
11734
11939
  `);
11735
11940
  process.stdout.write(` ${c2.cyan("URL".padEnd(12))} ${ctx.config.backendUrl}
11736
11941
  `);
@@ -11741,11 +11946,15 @@ async function handleEndpoint(arg, ctx, local = false) {
11741
11946
  process.stdout.write(`
11742
11947
  ${c2.dim("Usage: /endpoint <url> [--auth <token>]")}
11743
11948
  `);
11744
- process.stdout.write(` ${c2.dim(" /endpoint http://localhost:11434 (Ollama, no auth)")}
11949
+ process.stdout.write(` ${c2.dim(" /endpoint http://localhost:11434 Ollama")}
11950
+ `);
11951
+ process.stdout.write(` ${c2.dim(" /endpoint https://llm.chutes.ai --auth cpk_... Chutes AI")}
11745
11952
  `);
11746
- process.stdout.write(` ${c2.dim(" /endpoint http://remote:8000/v1 --auth sk-... (OpenAI-compatible)")}
11953
+ process.stdout.write(` ${c2.dim(" /endpoint https://api.groq.com/openai --auth gsk_... Groq")}
11747
11954
  `);
11748
- process.stdout.write(` ${c2.dim(" /endpoint http://remote:8000/v1 (OpenAI-compatible, no auth)")}
11955
+ process.stdout.write(` ${c2.dim(" /endpoint https://api.together.xyz --auth ... Together AI")}
11956
+ `);
11957
+ process.stdout.write(` ${c2.dim(" /endpoint http://localhost:8000 vLLM")}
11749
11958
 
11750
11959
  `);
11751
11960
  return;
@@ -11764,15 +11973,17 @@ async function handleEndpoint(arg, ctx, local = false) {
11764
11973
  return;
11765
11974
  }
11766
11975
  const normalizedUrl = normalizeBaseUrl(url);
11767
- const isOllama = /localhost|127\.0\.0\.1/.test(url) && !url.includes("/v1") && !apiKey;
11768
- let backendType = isOllama ? "ollama" : "vllm";
11976
+ const provider = detectProvider(url);
11977
+ const backendType = provider.id === "ollama" ? "ollama" : "vllm";
11769
11978
  process.stdout.write(`
11770
- ${c2.dim("Testing connection...")} `);
11979
+ ${c2.dim("Detected:")} ${c2.bold(provider.label)}
11980
+ `);
11981
+ process.stdout.write(` ${c2.dim("Testing connection...")} `);
11771
11982
  try {
11772
- const healthUrl = backendType === "ollama" ? `${normalizedUrl}/api/tags` : `${normalizedUrl}/v1/models`;
11983
+ const healthUrl = `${normalizedUrl}${provider.modelsPath}`;
11773
11984
  const headers = {};
11774
11985
  if (apiKey) {
11775
- headers["Authorization"] = apiKey.startsWith("cpk_") ? apiKey : `Bearer ${apiKey}`;
11986
+ headers["Authorization"] = `Bearer ${apiKey}`;
11776
11987
  }
11777
11988
  const resp = await fetch(healthUrl, {
11778
11989
  headers,
@@ -11786,6 +11997,9 @@ async function handleEndpoint(arg, ctx, local = false) {
11786
11997
  process.stdout.write(`${c2.yellow("\u26A0")} Could not verify
11787
11998
  `);
11788
11999
  renderWarning(`Endpoint may not be reachable: ${err instanceof Error ? err.message : String(err)}`);
12000
+ if (provider.authRequired && !apiKey) {
12001
+ renderInfo(`${provider.label} typically requires an API key. Use: /endpoint ${url} --auth <key>`);
12002
+ }
11789
12003
  renderInfo("Setting endpoint anyway \u2014 it may come online later.");
11790
12004
  }
11791
12005
  ctx.setEndpoint(normalizedUrl, backendType, apiKey);
@@ -11803,15 +12017,17 @@ async function handleEndpoint(arg, ctx, local = false) {
11803
12017
  process.stdout.write(`
11804
12018
  ${c2.green("\u2714")} Endpoint updated and saved${local ? " (project-local)" : ""}:
11805
12019
  `);
11806
- process.stdout.write(` ${c2.cyan("URL".padEnd(8))} ${url}
12020
+ process.stdout.write(` ${c2.cyan("Provider".padEnd(12))} ${provider.label}
12021
+ `);
12022
+ process.stdout.write(` ${c2.cyan("URL".padEnd(12))} ${normalizedUrl}
11807
12023
  `);
11808
- process.stdout.write(` ${c2.cyan("Type".padEnd(8))} ${backendType}
12024
+ process.stdout.write(` ${c2.cyan("Type".padEnd(12))} ${backendType}
11809
12025
  `);
11810
12026
  if (apiKey) {
11811
- process.stdout.write(` ${c2.cyan("Auth".padEnd(8))} Bearer ${apiKey.slice(0, 8)}...
12027
+ process.stdout.write(` ${c2.cyan("Auth".padEnd(12))} Bearer ${apiKey.slice(0, 8)}...
11812
12028
  `);
11813
12029
  } else {
11814
- process.stdout.write(` ${c2.cyan("Auth".padEnd(8))} none
12030
+ process.stdout.write(` ${c2.cyan("Auth".padEnd(12))} ${provider.authRequired ? c2.yellow("none (may be required)") : "none"}
11815
12031
  `);
11816
12032
  }
11817
12033
  process.stdout.write("\n");
@@ -15587,17 +15803,19 @@ async function startInteractive(config, repoPath) {
15587
15803
  if (!isResumed) {
15588
15804
  try {
15589
15805
  const baseUrl = normalizeBaseUrl(config.backendUrl);
15590
- const healthUrl = config.backendType === "ollama" ? `${baseUrl}/api/tags` : `${baseUrl}/v1/models`;
15806
+ const provider = detectProvider(config.backendUrl);
15807
+ const healthUrl = `${baseUrl}${provider.modelsPath}`;
15591
15808
  const headers = {};
15592
15809
  if (config.apiKey) {
15593
- headers["Authorization"] = config.apiKey.startsWith("cpk_") ? config.apiKey : `Bearer ${config.apiKey}`;
15810
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
15594
15811
  }
15595
15812
  const resp = await fetch(healthUrl, { headers, signal: AbortSignal.timeout(1e4) });
15596
15813
  if (!resp.ok)
15597
15814
  throw new Error(`HTTP ${resp.status}`);
15598
15815
  } catch {
15599
- renderWarning(`Cannot reach ${config.backendType} at ${config.backendUrl}`);
15600
- if (config.backendType === "ollama") {
15816
+ const provider = detectProvider(config.backendUrl);
15817
+ renderWarning(`Cannot reach ${provider.label} at ${config.backendUrl}`);
15818
+ if (provider.id === "ollama") {
15601
15819
  renderInfo("Start Ollama with: ollama serve");
15602
15820
  }
15603
15821
  renderInfo("Use /endpoint to configure a different backend. Starting anyway...");
@@ -16205,17 +16423,19 @@ async function runWithTUI(task, config, repoPath) {
16205
16423
  }
16206
16424
  try {
16207
16425
  const baseUrl2 = normalizeBaseUrl(config.backendUrl);
16208
- const healthUrl = config.backendType === "ollama" ? `${baseUrl2}/api/tags` : `${baseUrl2}/v1/models`;
16426
+ const provider2 = detectProvider(config.backendUrl);
16427
+ const healthUrl = `${baseUrl2}${provider2.modelsPath}`;
16209
16428
  const headers = {};
16210
16429
  if (config.apiKey) {
16211
- headers["Authorization"] = config.apiKey.startsWith("cpk_") ? config.apiKey : `Bearer ${config.apiKey}`;
16430
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
16212
16431
  }
16213
16432
  const resp = await fetch(healthUrl, { headers, signal: AbortSignal.timeout(1e4) });
16214
16433
  if (!resp.ok)
16215
16434
  throw new Error(`HTTP ${resp.status}`);
16216
16435
  } catch {
16217
- renderWarning(`Cannot reach ${config.backendType} at ${config.backendUrl}`);
16218
- if (config.backendType === "ollama") {
16436
+ const provider2 = detectProvider(config.backendUrl);
16437
+ renderWarning(`Cannot reach ${provider2.label} at ${config.backendUrl}`);
16438
+ if (provider2.id === "ollama") {
16219
16439
  renderInfo("Start Ollama with: ollama serve");
16220
16440
  }
16221
16441
  renderInfo("The agent will retry when you submit a task. Use /endpoint to reconfigure.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",