@jacobbd/relay-ai 0.9.2 → 0.9.3

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.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getTemplateById,
4
4
  init_provider_templates
5
- } from "./chunk-Q2FTCICO.js";
5
+ } from "./chunk-NYKVDBQC.js";
6
6
 
7
7
  // src/constants.ts
8
8
  import { homedir } from "os";
@@ -11,7 +11,7 @@ import { join } from "path";
11
11
  // package.json
12
12
  var package_default = {
13
13
  name: "@jacobbd/relay-ai",
14
- version: "0.9.2",
14
+ version: "0.9.3",
15
15
  publishConfig: {
16
16
  access: "public"
17
17
  },
@@ -385,9 +385,7 @@ function createResponsesLiteNormalizeState() {
385
385
  textDeltaForwarded: false,
386
386
  messageAddedIds: /* @__PURE__ */ new Set(),
387
387
  messageDoneIds: /* @__PURE__ */ new Set(),
388
- functionAddedIndexes: /* @__PURE__ */ new Set(),
389
- functionDeltaIndexes: /* @__PURE__ */ new Set(),
390
- functionDoneCallIds: /* @__PURE__ */ new Set()
388
+ functionCalls: []
391
389
  };
392
390
  }
393
391
  function nextId(state, prefix) {
@@ -411,20 +409,92 @@ function normalizeErrorEvent(event) {
411
409
  }
412
410
  };
413
411
  }
414
- function normalizeFunctionItem(item, state, forDone = false) {
415
- const callId = asString(item.call_id) ?? asString(item.id) ?? nextId(state, "call");
416
- const id = asString(item.id) ?? nextId(state, "fc");
417
- state.lastFunctionItemId = id;
412
+ function resolveFunctionCall(state, hint) {
413
+ if (hint.callId) {
414
+ const byCallId = state.functionCalls.find((entry2) => entry2.callId === hint.callId);
415
+ if (byCallId) return byCallId;
416
+ }
417
+ if (hint.itemId) {
418
+ const byItemId = state.functionCalls.find((entry2) => entry2.itemId === hint.itemId);
419
+ if (byItemId) return byItemId;
420
+ }
421
+ if (hint.outputIndex !== void 0) {
422
+ const open4 = [...state.functionCalls].reverse().find((entry2) => entry2.outputIndex === hint.outputIndex && !entry2.done && !(hint.callId && entry2.callId !== hint.callId));
423
+ if (open4) return open4;
424
+ }
425
+ if (hint.callId === void 0 && hint.itemId === void 0 && hint.outputIndex === void 0 && state.lastFunctionCall) {
426
+ return state.lastFunctionCall;
427
+ }
428
+ const entry = {
429
+ itemId: hint.itemId ?? nextId(state, "fc"),
430
+ callId: hint.callId ?? hint.itemId ?? nextId(state, "call"),
431
+ name: "",
432
+ args: "",
433
+ upstream: {},
434
+ outputIndex: hint.outputIndex ?? state.lastOutputIndex,
435
+ added: false,
436
+ deltaForwarded: false,
437
+ doneSeen: false,
438
+ done: false
439
+ };
440
+ state.functionCalls.push(entry);
441
+ return entry;
442
+ }
443
+ function absorbFunctionItem(entry, item, authoritative) {
444
+ entry.upstream = { ...entry.upstream, ...item };
445
+ const name = asString(item.name);
446
+ if (name) entry.name = name;
447
+ const callId = asString(item.call_id);
448
+ if (callId) entry.callId = callId;
449
+ if (authoritative && typeof item.arguments === "string") entry.upstreamArgs = item.arguments;
450
+ }
451
+ function resolveFunctionArgs(entry) {
452
+ if (entry.upstreamArgs) return entry.upstreamArgs;
453
+ if (entry.args) return entry.args;
454
+ return entry.upstreamArgs;
455
+ }
456
+ function functionItemPayload(entry, extra) {
418
457
  return {
419
- ...item,
458
+ ...entry.upstream,
420
459
  type: "function_call",
421
- id,
422
- call_id: callId,
423
- name: asString(item.name) ?? "",
424
- arguments: typeof item.arguments === "string" ? item.arguments : "",
425
- ...forDone ? { status: "completed" } : {}
460
+ id: entry.itemId,
461
+ call_id: entry.callId,
462
+ name: entry.name,
463
+ ...extra
464
+ };
465
+ }
466
+ function functionAddedEvent(entry) {
467
+ entry.added = true;
468
+ return {
469
+ type: "response.output_item.added",
470
+ output_index: entry.outputIndex,
471
+ item: functionItemPayload(entry, { arguments: "" })
472
+ };
473
+ }
474
+ function functionDoneEvent(entry, args) {
475
+ entry.done = true;
476
+ return {
477
+ type: "response.output_item.done",
478
+ output_index: entry.outputIndex,
479
+ item: functionItemPayload(entry, { arguments: args, status: "completed" })
426
480
  };
427
481
  }
482
+ function completeFunctionCall(entry, args) {
483
+ if (entry.done) return [];
484
+ const events = [];
485
+ if (!entry.added) events.push(functionAddedEvent(entry));
486
+ if (!entry.deltaForwarded && args.length > 0) {
487
+ entry.deltaForwarded = true;
488
+ events.push({
489
+ type: "response.function_call_arguments.delta",
490
+ item_id: entry.itemId,
491
+ output_index: entry.outputIndex,
492
+ delta: args
493
+ });
494
+ }
495
+ events.push(functionDoneEvent(entry, args));
496
+ return events;
497
+ }
428
498
  function messageText(item) {
429
499
  if (typeof item.text === "string") return item.text;
430
500
  if (!Array.isArray(item.content)) return "";
@@ -451,47 +521,43 @@ function synthesizeMessage(item, outputIndex, state) {
451
521
  ];
452
522
  }
453
523
  function synthesizeFunctionCall(item, outputIndex, state) {
454
- const normalized = normalizeFunctionItem(item, state);
455
- const callId = String(normalized.call_id);
456
- if (state.functionDoneCallIds.has(callId)) return [];
457
- const events = [];
458
- if (!state.functionAddedIndexes.has(outputIndex)) {
459
- events.push({
460
- type: "response.output_item.added",
461
- output_index: outputIndex,
462
- item: { ...normalized, arguments: "" }
463
- });
464
- state.functionAddedIndexes.add(outputIndex);
465
- }
466
- if (!state.functionDeltaIndexes.has(outputIndex) && typeof normalized.arguments === "string" && normalized.arguments.length > 0) {
467
- events.push({
468
- type: "response.function_call_arguments.delta",
469
- item_id: normalized.id,
470
- output_index: outputIndex,
471
- delta: normalized.arguments
472
- });
473
- state.functionDeltaIndexes.add(outputIndex);
474
- }
475
- events.push({
476
- type: "response.output_item.done",
477
- output_index: outputIndex,
478
- item: { ...normalized, status: "completed" }
524
+ const entry = resolveFunctionCall(state, {
525
+ itemId: asString(item.id),
526
+ callId: asString(item.call_id),
527
+ outputIndex
479
528
  });
480
- state.functionDoneCallIds.add(callId);
481
- state.lastOutputIndex = outputIndex;
482
- return events;
529
+ absorbFunctionItem(entry, item, true);
530
+ state.lastFunctionCall = entry;
531
+ state.lastOutputIndex = entry.outputIndex;
532
+ const args = resolveFunctionArgs(entry);
533
+ if (args === void 0) {
534
+ entry.doneSeen = true;
535
+ return [];
536
+ }
537
+ return completeFunctionCall(entry, args);
483
538
  }
484
539
  function recoverFromCompletedOutput(response, state) {
485
- if (!Array.isArray(response.output)) return [];
486
540
  const recovered = [];
487
- response.output.forEach((item, index) => {
488
- if (!isRecord(item) || typeof item.type !== "string") return;
489
- if (item.type === "message" && !state.textDeltaForwarded) {
490
- recovered.push(...synthesizeMessage(item, index, state));
491
- } else if (item.type === "function_call") {
492
- recovered.push(...synthesizeFunctionCall(item, index, state));
493
- }
494
- });
541
+ if (Array.isArray(response.output)) {
542
+ response.output.forEach((item, index) => {
543
+ if (!isRecord(item) || typeof item.type !== "string") return;
544
+ if (item.type === "message" && !state.textDeltaForwarded) {
545
+ recovered.push(...synthesizeMessage(item, index, state));
546
+ } else if (item.type === "function_call") {
547
+ recovered.push(...synthesizeFunctionCall(item, index, state));
548
+ }
549
+ });
550
+ }
551
+ for (const entry of state.functionCalls) {
552
+ if (entry.done || !entry.doneSeen) continue;
553
+ recovered.push(normalizeErrorEvent({
554
+ error: {
555
+ type: "invalid_response",
556
+ code: "incomplete_function_call",
557
+ message: `Provider ended the response without arguments for function call "${entry.callId}"${entry.name ? ` (${entry.name})` : ""}.`
558
+ }
559
+ }));
560
+ }
495
561
  return recovered;
496
562
  }
497
563
  function normalizeResponsesLiteEvent(event, state) {
@@ -507,19 +573,40 @@ function normalizeResponsesLiteEvent(event, state) {
507
573
  return [{ ...event, output_index: outputIndex, item: { ...event.item, id } }];
508
574
  }
509
575
  if (event.item.type === "function_call") {
510
- const item = normalizeFunctionItem(event.item, state);
511
- state.functionAddedIndexes.add(outputIndex);
512
- return [{ ...event, output_index: outputIndex, item }];
576
+ const entry = resolveFunctionCall(state, {
577
+ itemId: asString(event.item.id),
578
+ callId: asString(event.item.call_id),
579
+ outputIndex
580
+ });
581
+ absorbFunctionItem(entry, event.item, false);
582
+ entry.outputIndex = outputIndex;
583
+ entry.added = true;
584
+ state.lastFunctionCall = entry;
585
+ return [{ ...event, output_index: outputIndex, item: functionItemPayload(entry, { arguments: "" }) }];
513
586
  }
514
587
  return [{ ...event, output_index: outputIndex }];
515
588
  }
516
589
  if (event.type === "response.output_item.done" && isRecord(event.item)) {
517
590
  const outputIndex = typeof event.output_index === "number" ? event.output_index : state.lastOutputIndex;
518
591
  if (event.item.type === "function_call") {
519
- const item = normalizeFunctionItem(event.item, state, true);
520
- const callId = String(item.call_id);
521
- state.functionDoneCallIds.add(callId);
522
- return [{ ...event, output_index: outputIndex, item }];
592
+ const entry = resolveFunctionCall(state, {
593
+ itemId: asString(event.item.id),
594
+ callId: asString(event.item.call_id),
595
+ outputIndex
596
+ });
597
+ absorbFunctionItem(entry, event.item, true);
598
+ entry.outputIndex = outputIndex;
599
+ entry.doneSeen = true;
600
+ state.lastFunctionCall = entry;
601
+ state.lastOutputIndex = outputIndex;
602
+ const args = resolveFunctionArgs(entry);
603
+ if (args === void 0 || !entry.name) return [];
604
+ if (entry.done) return [];
605
+ const events = [];
606
+ if (!entry.added) events.push(functionAddedEvent(entry));
607
+ events.push({ ...event, output_index: outputIndex, item: functionItemPayload(entry, { arguments: args, status: "completed" }) });
608
+ entry.done = true;
609
+ return events;
523
610
  }
524
611
  if (event.item.type === "message") {
525
612
  const id = asString(event.item.id) ?? state.lastMessageItemId ?? nextId(state, "msg");
@@ -546,12 +633,16 @@ function normalizeResponsesLiteEvent(event, state) {
546
633
  return events;
547
634
  }
548
635
  if (event.type === "response.function_call_arguments.delta") {
549
- const itemId = asString(event.item_id) ?? state.lastFunctionItemId ?? nextId(state, "fc");
550
- const outputIndex = typeof event.output_index === "number" ? event.output_index : state.lastOutputIndex;
551
- state.lastFunctionItemId = itemId;
552
- state.lastOutputIndex = outputIndex;
553
- state.functionDeltaIndexes.add(outputIndex);
554
- return [{ ...event, item_id: itemId, output_index: outputIndex, delta: typeof event.delta === "string" ? event.delta : "" }];
636
+ const entry = resolveFunctionCall(state, {
637
+ itemId: asString(event.item_id),
638
+ outputIndex: typeof event.output_index === "number" ? event.output_index : void 0
639
+ });
640
+ const delta = typeof event.delta === "string" ? event.delta : "";
641
+ entry.args += delta;
642
+ entry.deltaForwarded = true;
643
+ state.lastFunctionCall = entry;
644
+ state.lastOutputIndex = entry.outputIndex;
645
+ return [{ ...event, item_id: entry.itemId, output_index: entry.outputIndex, delta }];
555
646
  }
556
647
  if (event.type === "response.completed" || event.type === "response.incomplete") {
557
648
  const response = isRecord(event.response) ? event.response : {};
@@ -1051,10 +1142,11 @@ async function createLanguageModel(spec) {
1051
1142
  return model;
1052
1143
  }
1053
1144
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
1054
- var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
1145
+ var OPENAI_EFFORT_LEVELS = ["low", "medium", "high"];
1055
1146
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
1056
1147
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
1057
- var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
1148
+ var XAI_CHAT_EFFORT_LEVELS = ["low", "high"];
1149
+ var XAI_RESPONSES_EFFORT_LEVELS = ["low", "medium", "high"];
1058
1150
  var OPENROUTER_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
1059
1151
  var DEEPSEEK_EFFORT_LEVELS = ["high", "max", "off"];
1060
1152
  var GLM_52_EFFORT_LEVELS = ["high", "xhigh"];
@@ -1225,7 +1317,50 @@ function mapCodexEffortToAnthropic(effort) {
1225
1317
  return void 0;
1226
1318
  }
1227
1319
  }
1228
- function mapCodexEffortToOpenAI(effort) {
1320
+ var OPENAI_MODEL_REASONING = {
1321
+ "gpt-5-pro": { levels: ["high"], defaultLevel: "high" },
1322
+ "gpt-5.1": { levels: ["none", "low", "medium", "high"], defaultLevel: "none" },
1323
+ "gpt-5.1-codex-max": { levels: ["low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1324
+ "gpt-5.2": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1325
+ "gpt-5.2-codex": { levels: ["low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1326
+ "gpt-5.2-pro": { levels: ["medium", "high", "xhigh"], defaultLevel: "medium" },
1327
+ "gpt-5.3-codex": { levels: ["low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1328
+ "gpt-5.4": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1329
+ "gpt-5.4-mini": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1330
+ "gpt-5.4-nano": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1331
+ "gpt-5.4-pro": { levels: ["medium", "high", "xhigh"], defaultLevel: "medium" },
1332
+ "gpt-5.5": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1333
+ "gpt-5.5-pro": { levels: ["medium", "high", "xhigh"], defaultLevel: "high" },
1334
+ "gpt-5.6": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" },
1335
+ "gpt-5.6-luna": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" },
1336
+ "gpt-5.6-sol": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" },
1337
+ "gpt-5.6-terra": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" }
1338
+ };
1339
+ var OPENAI_NON_REASONING_MODELS = /* @__PURE__ */ new Set([
1340
+ "chat-latest",
1341
+ "gpt-5-chat-latest",
1342
+ "gpt-5.1-chat-latest",
1343
+ "gpt-5.2-chat-latest",
1344
+ "gpt-5.3-chat-latest"
1345
+ ]);
1346
+ var OPENAI_DATED_SNAPSHOT_SUFFIX = /-\d{4}-\d{2}-\d{2}$/;
1347
+ function canonicalOpenAiModelId(modelId, metadata) {
1348
+ return (metadata?.upstreamModelId ?? modelId ?? "").toLowerCase();
1349
+ }
1350
+ function openAiReasoningProfile(modelId, metadata) {
1351
+ const id = canonicalOpenAiModelId(modelId, metadata);
1352
+ if (!id) return void 0;
1353
+ return OPENAI_MODEL_REASONING[id] ?? OPENAI_MODEL_REASONING[id.replace(OPENAI_DATED_SNAPSHOT_SUFFIX, "")];
1354
+ }
1355
+ function openAiModelReasons(modelId, metadata) {
1356
+ const id = canonicalOpenAiModelId(modelId, metadata);
1357
+ if (OPENAI_NON_REASONING_MODELS.has(id.replace(OPENAI_DATED_SNAPSHOT_SUFFIX, ""))) return false;
1358
+ return !!openAiReasoningProfile(modelId, metadata) || modelPrefersResponsesApi(id) || !!metadata?.reasoning;
1359
+ }
1360
+ function mapCodexEffortToOpenAI(effort, allowed) {
1361
+ return allowed.includes(effort) ? effort : void 0;
1362
+ }
1363
+ function mapCodexEffortToOpenAICompatible(effort) {
1229
1364
  if (effort === "xhigh") return "high";
1230
1365
  const allowed = ["low", "medium", "high"];
1231
1366
  return allowed.includes(effort) ? effort : void 0;
@@ -1241,16 +1376,12 @@ function mapCodexEffortToGlm52(effort) {
1241
1376
  return void 0;
1242
1377
  }
1243
1378
  }
1244
- function mapCodexEffortToXai(effort) {
1379
+ function mapCodexEffortToXai(effort, supportsMedium) {
1245
1380
  switch (effort) {
1246
- case "none":
1247
- case "minimal":
1248
- return void 0;
1249
- // xAI SDK only accepts 'low'|'high'; omit param for 'none'
1250
1381
  case "low":
1251
- case "medium":
1252
1382
  return "low";
1253
- // 'medium' has no xAI equivalent — nearest valid value
1383
+ case "medium":
1384
+ return supportsMedium ? "medium" : void 0;
1254
1385
  case "high":
1255
1386
  case "xhigh":
1256
1387
  case "max":
@@ -1282,7 +1413,31 @@ function mapCodexEffortToGeminiBudget(effort) {
1282
1413
  if (!level) return void 0;
1283
1414
  return GEMINI_25_BUDGETS[level];
1284
1415
  }
1416
+ function withMappableLevels(caps, npm, modelId, metadata) {
1417
+ if (caps.mode !== "controllable") return caps;
1418
+ const seen = /* @__PURE__ */ new Set();
1419
+ const levels = caps.levels.filter((level) => {
1420
+ const mapped = effortProviderOptions(npm, level, modelId, metadata);
1421
+ if (mapped === void 0) return false;
1422
+ const wire = JSON.stringify(mapped);
1423
+ if (seen.has(wire)) return false;
1424
+ seen.add(wire);
1425
+ return true;
1426
+ });
1427
+ if (levels.length === caps.levels.length) return caps;
1428
+ if (levels.length === 0) {
1429
+ return { ...caps, levels: [], defaultLevel: "", mode: "internal-only" };
1430
+ }
1431
+ return {
1432
+ ...caps,
1433
+ levels,
1434
+ defaultLevel: levels.includes(caps.defaultLevel) ? caps.defaultLevel : levels[levels.length - 1]
1435
+ };
1436
+ }
1285
1437
  function getReasoningCapabilities(npm, modelId, metadata) {
1438
+ return withMappableLevels(resolveRawReasoningCapabilities(npm, modelId, metadata), npm, modelId, metadata);
1439
+ }
1440
+ function resolveRawReasoningCapabilities(npm, modelId, metadata) {
1286
1441
  const id = modelId.toLowerCase();
1287
1442
  if (isOpenRouterRoute(npm, metadata)) {
1288
1443
  return openRouterReasoningCapabilities(metadata);
@@ -1303,15 +1458,18 @@ function getReasoningCapabilities(npm, modelId, metadata) {
1303
1458
  return EMPTY_REASONING;
1304
1459
  }
1305
1460
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
1306
- const prefersResponses = modelPrefersResponsesApi(modelId);
1307
- if (prefersResponses || metadata?.reasoning) {
1461
+ const canonicalId = canonicalOpenAiModelId(modelId, metadata);
1462
+ const profile = openAiReasoningProfile(modelId, metadata);
1463
+ const prefersResponses = modelPrefersResponsesApi(canonicalId);
1464
+ if (openAiModelReasons(modelId, metadata) && shouldUseOpenAiResponsesEndpoint(canonicalId)) {
1465
+ const levels = profile?.levels ?? [...OPENAI_EFFORT_LEVELS];
1308
1466
  return {
1309
- levels: [...OPENAI_EFFORT_LEVELS],
1310
- defaultLevel: "medium",
1467
+ levels: [...levels],
1468
+ defaultLevel: profile?.defaultLevel ?? (levels.includes("medium") ? "medium" : levels[levels.length - 1]),
1311
1469
  supportsSummaries: true,
1470
+ source: profile || prefersResponses ? "provider-rule" : "model-metadata",
1471
+ confidence: profile || prefersResponses ? "documented" : "inferred",
1312
1472
  mode: "controllable",
1313
- source: prefersResponses ? "provider-rule" : "model-metadata",
1314
- confidence: prefersResponses ? "documented" : "inferred",
1315
1473
  wireFormat: { kind: "openai-reasoning-effort" }
1316
1474
  };
1317
1475
  }
@@ -1347,7 +1505,7 @@ function getReasoningCapabilities(npm, modelId, metadata) {
1347
1505
  }
1348
1506
  if (npm === "@ai-sdk/xai") {
1349
1507
  if (isXaiReasoningEffortModel(modelId)) {
1350
- const levels = modelPrefersResponsesApi(modelId) ? ["low", "medium", "high", "xhigh"] : [...XAI_EFFORT_LEVELS];
1508
+ const levels = modelPrefersResponsesApi(modelId) ? [...XAI_RESPONSES_EFFORT_LEVELS] : [...XAI_CHAT_EFFORT_LEVELS];
1351
1509
  return {
1352
1510
  levels,
1353
1511
  defaultLevel: xaiDefaultReasoningEffort(modelId),
@@ -1444,13 +1602,15 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
1444
1602
  return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
1445
1603
  }
1446
1604
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
1447
- if (!modelId || !modelPrefersResponsesApi(modelId)) return void 0;
1448
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
1605
+ if (!modelId || !shouldUseOpenAiResponsesEndpoint(canonicalOpenAiModelId(modelId, metadata))) return void 0;
1606
+ if (!openAiModelReasons(modelId, metadata)) return void 0;
1607
+ const allowed = openAiReasoningProfile(modelId, metadata)?.levels ?? OPENAI_EFFORT_LEVELS;
1608
+ const reasoningEffort = mapCodexEffortToOpenAI(effort, allowed);
1449
1609
  return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
1450
1610
  }
1451
1611
  if (npm === "@ai-sdk/xai") {
1452
1612
  if (!modelId || !isXaiReasoningEffortModel(modelId)) return void 0;
1453
- const reasoningEffort = mapCodexEffortToXai(effort);
1613
+ const reasoningEffort = mapCodexEffortToXai(effort, modelPrefersResponsesApi(modelId));
1454
1614
  return reasoningEffort ? { xai: { reasoningEffort } } : void 0;
1455
1615
  }
1456
1616
  if (npm === "@ai-sdk/anthropic" || npm === VERTEX_ANTHROPIC_NPM) {
@@ -1478,7 +1638,7 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
1478
1638
  return deepSeekEffortProviderOptions(effort);
1479
1639
  }
1480
1640
  if (isKimiReasoningModel(modelId)) {
1481
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
1641
+ const reasoningEffort = mapCodexEffortToOpenAICompatible(effort);
1482
1642
  if (reasoningEffort) {
1483
1643
  const key = metadata?.providerId ? toCamelCase(metadata.providerId) : "openaiCompatible";
1484
1644
  return { [key]: { reasoningEffort } };
@@ -1494,7 +1654,7 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
1494
1654
  return void 0;
1495
1655
  }
1496
1656
  if (hasSupportedParameter(metadata, "reasoning_effort")) {
1497
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
1657
+ const reasoningEffort = mapCodexEffortToOpenAICompatible(effort);
1498
1658
  return reasoningEffort ? { openai: { reasoningEffort }, openaiCompatible: { reasoningEffort } } : void 0;
1499
1659
  }
1500
1660
  if (hasSupportedParameter(metadata, "reasoning")) {
@@ -6929,8 +7089,8 @@ function anthropicError(res, status, message) {
6929
7089
  });
6930
7090
  }
6931
7091
  function aliasModelId(realId, providerId) {
6932
- if (realId.startsWith("claude-")) return realId;
6933
7092
  const sanitized = providerId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7093
+ if (realId.startsWith("claude-") && !sanitized.startsWith("custom-")) return realId;
6934
7094
  return `anthropic-${sanitized}__${realId}`;
6935
7095
  }
6936
7096
  function buildProxySubagentModelRouting(routes, parentRoute) {
@@ -9244,6 +9404,80 @@ function goRegistryStub() {
9244
9404
  };
9245
9405
  }
9246
9406
 
9407
+ // src/registry/fetch-anthropic-models.ts
9408
+ async function fetchAnthropicModels(baseUrl, apiKey, extraHeaders) {
9409
+ const root = baseUrl.replace(/\/v1\/?$/, "").replace(/\/$/, "");
9410
+ const modelsUrl2 = `${root}/v1/models`;
9411
+ const controller = new AbortController();
9412
+ const timer = setTimeout(() => controller.abort(), 1e4);
9413
+ try {
9414
+ const response = await fetch(modelsUrl2, {
9415
+ method: "GET",
9416
+ headers: {
9417
+ "x-api-key": apiKey,
9418
+ "anthropic-version": "2023-06-01",
9419
+ Accept: "application/json",
9420
+ ...extraHeaders
9421
+ },
9422
+ redirect: "manual",
9423
+ signal: controller.signal
9424
+ });
9425
+ let logTrace;
9426
+ if (process.env.RELAY_AI_TRACE === "1") {
9427
+ logTrace = makeTraceLogger(getProviderDebugLogPath());
9428
+ }
9429
+ const rawBodyText = await response.text().catch(() => "");
9430
+ if (logTrace) {
9431
+ logTrace(`[fetchAnthropicModels] HTTP ${response.status} from ${modelsUrl2}`);
9432
+ logTrace(`[fetchAnthropicModels] Body: ${rawBodyText}`);
9433
+ }
9434
+ if (response.ok) {
9435
+ let json = {};
9436
+ try {
9437
+ if (rawBodyText.trim()) {
9438
+ json = JSON.parse(rawBodyText);
9439
+ }
9440
+ } catch {
9441
+ }
9442
+ const models = [];
9443
+ for (const row of json.data ?? []) {
9444
+ const id = row.id?.trim();
9445
+ if (!id) continue;
9446
+ models.push({
9447
+ id,
9448
+ name: row.name?.trim() || id,
9449
+ upstreamModelId: id,
9450
+ family: id.split("-")[0] ?? id,
9451
+ brand: deriveBrand(id),
9452
+ contextWindow: resolveContextWindow(id),
9453
+ modelFormat: "anthropic",
9454
+ npm: "@ai-sdk/anthropic",
9455
+ apiUrl: root
9456
+ });
9457
+ }
9458
+ if (models.length > 0) return { models, baseUrl: root };
9459
+ }
9460
+ if (response.status === 401 || response.status === 403) {
9461
+ return { models: [], baseUrl: root, error: "API key was rejected.", hint: "Check your Anthropic-compatible API key." };
9462
+ }
9463
+ return {
9464
+ models: [],
9465
+ baseUrl: root,
9466
+ error: `Could not list models (HTTP ${response.status}).`,
9467
+ hint: "Verify the base URL supports Anthropic-compatible /v1/models or try the OpenAI-compatible option instead."
9468
+ };
9469
+ } catch {
9470
+ return {
9471
+ models: [],
9472
+ baseUrl: root,
9473
+ error: "Could not reach the Anthropic-compatible server.",
9474
+ hint: "Check the base URL and that the server is running."
9475
+ };
9476
+ } finally {
9477
+ clearTimeout(timer);
9478
+ }
9479
+ }
9480
+
9247
9481
  // src/registry/fetch-template-models.ts
9248
9482
  var TEST_TIMEOUT_MS = 1e4;
9249
9483
  function modelFormatForNpm(npm) {
@@ -9604,77 +9838,28 @@ function npmForKind(kind) {
9604
9838
  function modelFormatForKind(kind) {
9605
9839
  return kind === "anthropic" ? "anthropic" : "openai";
9606
9840
  }
9607
- async function fetchAnthropicModels(baseUrl, apiKey, extraHeaders) {
9608
- const root = baseUrl.replace(/\/v1\/?$/, "").replace(/\/$/, "");
9609
- const modelsUrl2 = `${root}/v1/models`;
9610
- const controller = new AbortController();
9611
- const timer = setTimeout(() => controller.abort(), 1e4);
9612
- try {
9613
- const response = await fetch(modelsUrl2, {
9614
- method: "GET",
9615
- headers: {
9616
- "x-api-key": apiKey,
9617
- "anthropic-version": "2023-06-01",
9618
- Accept: "application/json",
9619
- ...extraHeaders
9620
- },
9621
- redirect: "manual",
9622
- signal: controller.signal
9623
- });
9624
- let logTrace;
9625
- if (process.env.RELAY_AI_TRACE === "1") {
9626
- logTrace = makeTraceLogger(getProviderDebugLogPath());
9627
- }
9628
- const rawBodyText = await response.text().catch(() => "");
9629
- if (logTrace) {
9630
- logTrace(`[fetchAnthropicModels] HTTP ${response.status} from ${modelsUrl2}`);
9631
- logTrace(`[fetchAnthropicModels] Body: ${rawBodyText}`);
9632
- }
9633
- if (response.ok) {
9634
- let json = {};
9635
- try {
9636
- if (rawBodyText.trim()) {
9637
- json = JSON.parse(rawBodyText);
9638
- }
9639
- } catch {
9640
- }
9641
- const models = [];
9642
- for (const row of json.data ?? []) {
9643
- const id = row.id?.trim();
9644
- if (!id) continue;
9645
- models.push({
9646
- id,
9647
- name: row.name?.trim() || id,
9648
- upstreamModelId: id,
9649
- family: id.split("-")[0] ?? id,
9650
- brand: deriveBrand(id),
9651
- contextWindow: resolveContextWindow(id),
9652
- modelFormat: "anthropic",
9653
- npm: "@ai-sdk/anthropic",
9654
- apiUrl: root
9655
- });
9656
- }
9657
- if (models.length > 0) return { models, baseUrl: root };
9658
- }
9659
- if (response.status === 401 || response.status === 403) {
9660
- return { models: [], baseUrl: root, error: "API key was rejected.", hint: "Check your Anthropic-compatible API key." };
9661
- }
9662
- return {
9663
- models: [],
9664
- baseUrl: root,
9665
- error: `Could not list models (HTTP ${response.status}).`,
9666
- hint: "Verify the base URL supports Anthropic-compatible /v1/models or try the OpenAI-compatible option instead."
9667
- };
9668
- } catch {
9669
- return {
9670
- models: [],
9671
- baseUrl: root,
9672
- error: "Could not reach the Anthropic-compatible server.",
9673
- hint: "Check the base URL and that the server is running."
9674
- };
9675
- } finally {
9676
- clearTimeout(timer);
9841
+ function customEndpointKind(provider) {
9842
+ if (provider.templateId === "custom-anthropic") return "anthropic";
9843
+ if (provider.templateId === "custom-openai") return "openai";
9844
+ return null;
9845
+ }
9846
+ function sameHeaders(a, b) {
9847
+ const norm = (h) => JSON.stringify(Object.entries(h ?? {}).sort(([x], [y]) => x.localeCompare(y)));
9848
+ return norm(a) === norm(b);
9849
+ }
9850
+ function compareableUrl(url) {
9851
+ return url.replace(/\/v1\/?$/, "").replace(/\/$/, "");
9852
+ }
9853
+ async function findDuplicateCustomProvider(registry, normalizedUrl, apiKey, headers) {
9854
+ const target = compareableUrl(normalizedUrl);
9855
+ for (const existing of registry.providers) {
9856
+ if (!customEndpointKind(existing)) continue;
9857
+ if (compareableUrl(existing.api.url ?? "") !== target) continue;
9858
+ if (!sameHeaders(existing.api.headers, headers)) continue;
9859
+ const storedKey = await readStoredProviderCredential(existing.authRef);
9860
+ if ((storedKey ?? "") === apiKey) return existing.id;
9677
9861
  }
9862
+ return null;
9678
9863
  }
9679
9864
  function uniqueProviderId(displayName, registry) {
9680
9865
  let base = customProviderId(displayName);
@@ -9689,6 +9874,25 @@ function uniqueProviderId(displayName, registry) {
9689
9874
  }
9690
9875
  return `${base}-${Date.now()}`;
9691
9876
  }
9877
+ async function fetchCustomEndpointModels(input) {
9878
+ if (input.kind === "anthropic") {
9879
+ return fetchAnthropicModels(input.normalizedBaseUrl, input.apiKey, input.headers);
9880
+ }
9881
+ return fetchTemplateModels(
9882
+ {
9883
+ id: input.providerId,
9884
+ name: input.displayName,
9885
+ authType: input.apiKey === "local" ? "none" : "api",
9886
+ npm: npmForKind(input.kind),
9887
+ defaultBaseUrl: input.normalizedBaseUrl,
9888
+ modelSource: "api-list",
9889
+ supported: true
9890
+ },
9891
+ input.apiKey,
9892
+ input.normalizedBaseUrl,
9893
+ input.headers
9894
+ );
9895
+ }
9692
9896
  async function addCustomEndpointProvider(input) {
9693
9897
  const urlCheck = await validateCustomEndpointUrl(input.baseUrl, {
9694
9898
  allowInsecureLocal: input.allowInsecureLocal
@@ -9697,29 +9901,34 @@ async function addCustomEndpointProvider(input) {
9697
9901
  return { added: false, error: urlCheck.error, hint: urlCheck.hint };
9698
9902
  }
9699
9903
  const registry = loadRegistry();
9700
- const providerId = uniqueProviderId(input.displayName.trim(), registry);
9701
- const npm = npmForKind(input.kind);
9702
9904
  const apiKey = input.apiKey.trim() || "local";
9703
9905
  const headers = input.headers && Object.keys(input.headers).length > 0 ? input.headers : void 0;
9704
- let fetched;
9705
- if (input.kind === "anthropic") {
9706
- fetched = await fetchAnthropicModels(urlCheck.normalizedUrl, apiKey, headers);
9707
- } else {
9708
- fetched = await fetchTemplateModels(
9709
- {
9710
- id: providerId,
9711
- name: input.displayName,
9712
- authType: apiKey === "local" ? "none" : "api",
9713
- npm,
9714
- defaultBaseUrl: urlCheck.normalizedUrl,
9715
- modelSource: "api-list",
9716
- supported: true
9717
- },
9718
- apiKey,
9906
+ if (!input.confirmDuplicate) {
9907
+ const duplicateOf = await findDuplicateCustomProvider(
9908
+ registry,
9719
9909
  urlCheck.normalizedUrl,
9910
+ apiKey,
9720
9911
  headers
9721
9912
  );
9913
+ if (duplicateOf) {
9914
+ return {
9915
+ added: false,
9916
+ duplicateOf,
9917
+ error: `A backend with the same URL, key and headers already exists (${duplicateOf}).`,
9918
+ hint: "Add it anyway to keep both, or cancel."
9919
+ };
9920
+ }
9722
9921
  }
9922
+ const providerId = uniqueProviderId(input.displayName.trim(), registry);
9923
+ const npm = npmForKind(input.kind);
9924
+ const fetched = await fetchCustomEndpointModels({
9925
+ providerId,
9926
+ displayName: input.displayName,
9927
+ kind: input.kind,
9928
+ normalizedBaseUrl: urlCheck.normalizedUrl,
9929
+ apiKey,
9930
+ headers
9931
+ });
9723
9932
  if (fetched.error || fetched.models.length === 0) {
9724
9933
  return { added: false, error: fetched.error ?? "No models returned.", hint: fetched.hint };
9725
9934
  }
@@ -9756,6 +9965,115 @@ async function addCustomEndpointProvider(input) {
9756
9965
  saveRegistry(registry);
9757
9966
  return { added: true, provider: entry, modelCount: fetched.models.length };
9758
9967
  }
9968
+ async function updateCustomEndpointProvider(input) {
9969
+ const registry = loadRegistry();
9970
+ const provider = registry.providers.find((pr) => pr.id === input.providerId);
9971
+ if (!provider) {
9972
+ return { updated: false, error: `Provider not found: ${input.providerId}` };
9973
+ }
9974
+ const kind = customEndpointKind(provider);
9975
+ if (!kind) {
9976
+ return {
9977
+ updated: false,
9978
+ error: "Edit is only available for custom backends.",
9979
+ hint: "Template providers can only change their API key."
9980
+ };
9981
+ }
9982
+ const nextName = input.displayName?.trim();
9983
+ let nextBaseUrl = provider.api.url ?? "";
9984
+ let urlChanged = false;
9985
+ const requestedUrl = input.baseUrl?.trim();
9986
+ if (requestedUrl) {
9987
+ const urlCheck = await validateCustomEndpointUrl(requestedUrl, {
9988
+ allowInsecureLocal: input.allowInsecureLocal
9989
+ });
9990
+ if (!urlCheck.ok || !urlCheck.normalizedUrl) {
9991
+ return { updated: false, error: urlCheck.error, hint: urlCheck.hint };
9992
+ }
9993
+ urlChanged = urlCheck.normalizedUrl !== nextBaseUrl;
9994
+ nextBaseUrl = urlCheck.normalizedUrl;
9995
+ }
9996
+ const newKey = input.apiKey?.trim();
9997
+ const headersChanged = input.headers !== void 0 && !sameHeaders(input.headers, provider.api.headers);
9998
+ const nextHeaders = input.headers !== void 0 ? Object.keys(input.headers).length > 0 ? input.headers : void 0 : provider.api.headers;
9999
+ const nameChanged = Boolean(nextName) && nextName !== provider.name;
10000
+ const needsTest = urlChanged || Boolean(newKey) || headersChanged;
10001
+ if (!needsTest) {
10002
+ if (!nameChanged) return { updated: false, error: "Nothing to change." };
10003
+ provider.name = nextName;
10004
+ saveRegistry(registry);
10005
+ return {
10006
+ updated: true,
10007
+ provider,
10008
+ modelCount: provider.modelsCache?.models.length ?? 0
10009
+ };
10010
+ }
10011
+ const apiKey = newKey || await readStoredProviderCredential(provider.authRef) || "";
10012
+ if (!apiKey) {
10013
+ return {
10014
+ updated: false,
10015
+ error: "No stored API key was found for this backend.",
10016
+ hint: "Enter an API key to continue."
10017
+ };
10018
+ }
10019
+ const fetched = await fetchCustomEndpointModels({
10020
+ providerId: provider.id,
10021
+ displayName: nextName || provider.name,
10022
+ kind,
10023
+ normalizedBaseUrl: nextBaseUrl,
10024
+ apiKey,
10025
+ headers: nextHeaders
10026
+ });
10027
+ const testFailed = Boolean(fetched.error) || fetched.models.length === 0;
10028
+ if (testFailed && !input.saveAnyway) {
10029
+ return {
10030
+ updated: false,
10031
+ error: fetched.error ?? "No models returned.",
10032
+ hint: fetched.hint,
10033
+ canSaveAnyway: true
10034
+ };
10035
+ }
10036
+ if (newKey) {
10037
+ const saved = await saveProviderCredential(provider.authRef, newKey);
10038
+ if (!saved) {
10039
+ return {
10040
+ updated: false,
10041
+ error: "Could not save API key to credential store.",
10042
+ hint: "Grant Keychain access, or ensure RELAY_AI_HOME is writable (file fallback)."
10043
+ };
10044
+ }
10045
+ }
10046
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10047
+ if (nameChanged) provider.name = nextName;
10048
+ const storedBaseUrl = kind === "anthropic" ? nextBaseUrl.replace(/\/v1\/?$/, "").replace(/\/$/, "") : nextBaseUrl;
10049
+ provider.api.url = testFailed ? storedBaseUrl : fetched.baseUrl || storedBaseUrl;
10050
+ if (nextHeaders) provider.api.headers = nextHeaders;
10051
+ else delete provider.api.headers;
10052
+ if (!testFailed) {
10053
+ provider.modelsCache = {
10054
+ fetchedAt: now,
10055
+ models: fetched.models.map((m) => ({
10056
+ ...m,
10057
+ modelFormat: modelFormatForKind(kind),
10058
+ npm: npmForKind(kind),
10059
+ apiUrl: fetched.baseUrl || storedBaseUrl
10060
+ }))
10061
+ };
10062
+ provider.refreshedAt = now;
10063
+ } else if (provider.modelsCache) {
10064
+ provider.modelsCache = {
10065
+ ...provider.modelsCache,
10066
+ models: provider.modelsCache.models.map((m) => ({ ...m, apiUrl: storedBaseUrl }))
10067
+ };
10068
+ }
10069
+ saveRegistry(registry);
10070
+ return {
10071
+ updated: true,
10072
+ provider,
10073
+ modelCount: provider.modelsCache?.models.length ?? 0,
10074
+ ...testFailed ? { modelsStale: true } : {}
10075
+ };
10076
+ }
9759
10077
 
9760
10078
  // src/registry/fetch-cline-pass-models.ts
9761
10079
  var REQUEST_TIMEOUT_MS = 1e4;
@@ -10283,7 +10601,7 @@ async function refreshApiListProvider(provider, apiKey) {
10283
10601
  const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
10284
10602
  if (configuredUrl && configuredUrl !== templateDefault) {
10285
10603
  const urlCheck = await validateCustomEndpointUrl(baseUrl, {
10286
- allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
10604
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true || customEndpointKind(provider) !== null
10287
10605
  });
10288
10606
  if (!urlCheck.ok || !urlCheck.normalizedUrl) {
10289
10607
  return { models: [], error: `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim() };
@@ -10291,8 +10609,9 @@ async function refreshApiListProvider(provider, apiKey) {
10291
10609
  safeBaseUrl = urlCheck.normalizedUrl;
10292
10610
  }
10293
10611
  const template = catalogTemplate ?? syntheticTemplate(provider, safeBaseUrl);
10612
+ const extraHeaders = provider.api.headers && Object.keys(provider.api.headers).length > 0 ? provider.api.headers : void 0;
10294
10613
  if (npm === "@ai-sdk/anthropic") {
10295
- const fetched2 = await fetchAnthropicModels(safeBaseUrl, apiKey);
10614
+ const fetched2 = await fetchAnthropicModels(safeBaseUrl, apiKey, extraHeaders);
10296
10615
  if (fetched2.error || fetched2.models.length === 0) {
10297
10616
  return { models: [], error: fetched2.error ?? "No models returned.", baseUrl: fetched2.baseUrl };
10298
10617
  }
@@ -10301,7 +10620,7 @@ async function refreshApiListProvider(provider, apiKey) {
10301
10620
  baseUrl: fetched2.baseUrl
10302
10621
  };
10303
10622
  }
10304
- const fetched = await fetchTemplateModels(template, apiKey, safeBaseUrl);
10623
+ const fetched = await fetchTemplateModels(template, apiKey, safeBaseUrl, extraHeaders);
10305
10624
  if (fetched.error || fetched.models.length === 0) {
10306
10625
  return { models: [], error: fetched.error ?? "No models returned." };
10307
10626
  }
@@ -12234,7 +12553,7 @@ function removeFavorite(list, fav) {
12234
12553
  // src/favorite-provider-display.ts
12235
12554
  var OAUTH_FAVORITE_NAMES = {
12236
12555
  "claude-code": "Claude Code OAuth (Anthropic subscription)",
12237
- antigravity: "Antigravity OAuth (Google Cloud Code Assist)",
12556
+ antigravity: "Cloud Code Assist OAuth (Google)",
12238
12557
  "openai-oauth": "OpenAI OAuth (ChatGPT)",
12239
12558
  "xai-oauth": "xAI OAuth (SuperGrok)"
12240
12559
  };
@@ -12437,7 +12756,7 @@ var PROVIDER_DISPLAY = {
12437
12756
  "openai-oauth": OPENAI_DISPLAY,
12438
12757
  "github-copilot": "GitHub Copilot",
12439
12758
  "claude-code": "Claude Code (Anthropic subscription)",
12440
- antigravity: "Antigravity (Google Cloud Code Assist)",
12759
+ antigravity: "Cloud Code Assist OAuth (Google)",
12441
12760
  "cline-pass": "ClinePass"
12442
12761
  };
12443
12762
  function openBrowser(url) {
@@ -12540,7 +12859,7 @@ async function runNativeBrowserOAuth(providerId) {
12540
12859
  p5.log.info(`Opening: ${pc6.cyan(url)}`);
12541
12860
  spinner3.start("Waiting for authorization\u2026");
12542
12861
  });
12543
- spinner3.stop(pc6.green("Signed in to Antigravity"));
12862
+ spinner3.stop(pc6.green("Signed in to Cloud Code Assist"));
12544
12863
  const providerData = {};
12545
12864
  if (projectId) providerData.projectId = projectId;
12546
12865
  if (tierId) providerData.tier = tierId;
@@ -13414,14 +13733,13 @@ export {
13414
13733
  makeTraceLogger,
13415
13734
  writeSecureLogLine,
13416
13735
  printTraceLog,
13417
- fetchTemplateModels,
13418
- validateCustomEndpointUrl,
13419
13736
  fetchAnthropicModels,
13420
- addCustomEndpointProvider,
13737
+ fetchTemplateModels,
13421
13738
  resolveProviderTemplate,
13422
13739
  effectiveProviderBaseUrl,
13423
13740
  syntheticTemplate,
13424
13741
  resolveModelSource,
13742
+ validateCustomEndpointUrl,
13425
13743
  readBody,
13426
13744
  extractApiKey,
13427
13745
  sendJson,
@@ -13476,6 +13794,9 @@ export {
13476
13794
  routableModelsForTarget,
13477
13795
  providersForTarget,
13478
13796
  providersForCodexSubagents,
13797
+ customEndpointKind,
13798
+ addCustomEndpointProvider,
13799
+ updateCustomEndpointProvider,
13479
13800
  refreshProviderModels,
13480
13801
  refreshAllProviderModels,
13481
13802
  removeProviderFromRegistry,
@@ -13520,4 +13841,4 @@ export {
13520
13841
  supportsClaudeTransparentMode,
13521
13842
  buildHttpProxyRoutes
13522
13843
  };
13523
- //# sourceMappingURL=chunk-GQCFLSEM.js.map
13844
+ //# sourceMappingURL=chunk-PVGAE7HA.js.map