@jacobbd/relay-ai 0.9.1 → 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.1",
14
+ version: "0.9.3",
15
15
  publishConfig: {
16
16
  access: "public"
17
17
  },
@@ -345,7 +345,320 @@ async function runOpenAiDeviceCodeFlow(onDeviceCode, opts) {
345
345
 
346
346
  // src/oauth/responses-websocket.ts
347
347
  var RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
348
- var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set(["response.completed", "response.failed", "response.incomplete"]);
348
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set(["response.completed", "response.failed", "response.incomplete", "error"]);
349
+ function isRecord(value) {
350
+ return !!value && typeof value === "object" && !Array.isArray(value);
351
+ }
352
+ function recordKeys(value) {
353
+ return isRecord(value) ? Object.keys(value).sort().join(",") : "";
354
+ }
355
+ function summarizeResponsesLiteEvent(event) {
356
+ if (!isRecord(event)) return `kind=${event == null ? "null" : typeof event}`;
357
+ const parts = [`type=${typeof event.type === "string" ? event.type : "unknown"}`, `keys=${recordKeys(event)}`];
358
+ if (typeof event.delta === "string") parts.push(`deltaChars=${event.delta.length}`);
359
+ if (typeof event.output_index === "number") parts.push(`hasOutputIndex=1`);
360
+ if (typeof event.item_id === "string") parts.push(`hasItemId=1`);
361
+ if (isRecord(event.item)) {
362
+ parts.push(`itemType=${typeof event.item.type === "string" ? event.item.type : "unknown"}`);
363
+ parts.push(`itemKeys=${recordKeys(event.item)}`);
364
+ if (typeof event.item.arguments === "string") parts.push(`argumentsChars=${event.item.arguments.length}`);
365
+ }
366
+ if (isRecord(event.response)) {
367
+ parts.push(`responseKeys=${recordKeys(event.response)}`);
368
+ if (Array.isArray(event.response.output)) {
369
+ parts.push(`outputCount=${event.response.output.length}`);
370
+ parts.push(`outputTypes=${event.response.output.map((item) => isRecord(item) && typeof item.type === "string" ? item.type : "unknown").join(",")}`);
371
+ }
372
+ if (isRecord(event.response.usage)) parts.push(`usageKeys=${recordKeys(event.response.usage)}`);
373
+ if (typeof event.response.status === "string") parts.push(`status=${event.response.status}`);
374
+ }
375
+ if (isRecord(event.error)) {
376
+ parts.push(`errorKeys=${recordKeys(event.error)}`);
377
+ if (typeof event.error.message === "string") parts.push(`messageChars=${event.error.message.length}`);
378
+ }
379
+ return parts.join(" ");
380
+ }
381
+ function createResponsesLiteNormalizeState() {
382
+ return {
383
+ nextId: 1,
384
+ lastOutputIndex: 0,
385
+ textDeltaForwarded: false,
386
+ messageAddedIds: /* @__PURE__ */ new Set(),
387
+ messageDoneIds: /* @__PURE__ */ new Set(),
388
+ functionCalls: []
389
+ };
390
+ }
391
+ function nextId(state, prefix) {
392
+ const id = `${prefix}_${state.nextId}`;
393
+ state.nextId += 1;
394
+ return id;
395
+ }
396
+ function asString(value) {
397
+ return typeof value === "string" && value.length > 0 ? value : void 0;
398
+ }
399
+ function normalizeErrorEvent(event) {
400
+ const raw = isRecord(event.error) ? event.error : { message: typeof event.error === "string" ? event.error : "upstream error" };
401
+ return {
402
+ type: "error",
403
+ sequence_number: typeof event.sequence_number === "number" ? event.sequence_number : 0,
404
+ error: {
405
+ type: asString(raw.type) ?? "server_error",
406
+ code: asString(raw.code) ?? "unknown",
407
+ message: asString(raw.message) ?? "upstream error",
408
+ ...raw.param == null ? {} : { param: raw.param }
409
+ }
410
+ };
411
+ }
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) {
457
+ return {
458
+ ...entry.upstream,
459
+ type: "function_call",
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" })
480
+ };
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
+ }
498
+ function messageText(item) {
499
+ if (typeof item.text === "string") return item.text;
500
+ if (!Array.isArray(item.content)) return "";
501
+ let out = "";
502
+ for (const part of item.content) {
503
+ if (isRecord(part) && typeof part.text === "string" && (part.type === "output_text" || part.type === "text")) {
504
+ out += part.text;
505
+ }
506
+ }
507
+ return out;
508
+ }
509
+ function synthesizeMessage(item, outputIndex, state) {
510
+ const text4 = messageText(item);
511
+ if (!text4) return [];
512
+ const id = asString(item.id) ?? nextId(state, "msg");
513
+ state.lastMessageItemId = id;
514
+ state.textDeltaForwarded = true;
515
+ state.messageAddedIds.add(id);
516
+ state.messageDoneIds.add(id);
517
+ return [
518
+ { type: "response.output_item.added", output_index: outputIndex, item: { type: "message", id } },
519
+ { type: "response.output_text.delta", item_id: id, delta: text4 },
520
+ { type: "response.output_item.done", output_index: outputIndex, item: { type: "message", id } }
521
+ ];
522
+ }
523
+ function synthesizeFunctionCall(item, outputIndex, state) {
524
+ const entry = resolveFunctionCall(state, {
525
+ itemId: asString(item.id),
526
+ callId: asString(item.call_id),
527
+ outputIndex
528
+ });
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);
538
+ }
539
+ function recoverFromCompletedOutput(response, state) {
540
+ const recovered = [];
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
+ }
561
+ return recovered;
562
+ }
563
+ function normalizeResponsesLiteEvent(event, state) {
564
+ if (!isRecord(event) || typeof event.type !== "string") return [event];
565
+ if (event.type === "error") return [normalizeErrorEvent(event)];
566
+ if (event.type === "response.output_item.added" && isRecord(event.item)) {
567
+ const outputIndex = typeof event.output_index === "number" ? event.output_index : state.lastOutputIndex;
568
+ state.lastOutputIndex = outputIndex;
569
+ if (event.item.type === "message") {
570
+ const id = asString(event.item.id) ?? nextId(state, "msg");
571
+ state.lastMessageItemId = id;
572
+ state.messageAddedIds.add(id);
573
+ return [{ ...event, output_index: outputIndex, item: { ...event.item, id } }];
574
+ }
575
+ if (event.item.type === "function_call") {
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: "" }) }];
586
+ }
587
+ return [{ ...event, output_index: outputIndex }];
588
+ }
589
+ if (event.type === "response.output_item.done" && isRecord(event.item)) {
590
+ const outputIndex = typeof event.output_index === "number" ? event.output_index : state.lastOutputIndex;
591
+ if (event.item.type === "function_call") {
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;
610
+ }
611
+ if (event.item.type === "message") {
612
+ const id = asString(event.item.id) ?? state.lastMessageItemId ?? nextId(state, "msg");
613
+ state.lastMessageItemId = id;
614
+ state.messageDoneIds.add(id);
615
+ return [{ ...event, output_index: outputIndex, item: { ...event.item, id } }];
616
+ }
617
+ return [{ ...event, output_index: outputIndex }];
618
+ }
619
+ if (event.type === "response.output_text.delta") {
620
+ const itemId = asString(event.item_id) ?? state.lastMessageItemId ?? nextId(state, "msg");
621
+ state.lastMessageItemId = itemId;
622
+ state.textDeltaForwarded = true;
623
+ const events = [];
624
+ if (!state.messageAddedIds.has(itemId)) {
625
+ events.push({
626
+ type: "response.output_item.added",
627
+ output_index: state.lastOutputIndex,
628
+ item: { type: "message", id: itemId }
629
+ });
630
+ state.messageAddedIds.add(itemId);
631
+ }
632
+ events.push({ ...event, item_id: itemId, delta: typeof event.delta === "string" ? event.delta : "" });
633
+ return events;
634
+ }
635
+ if (event.type === "response.function_call_arguments.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 }];
646
+ }
647
+ if (event.type === "response.completed" || event.type === "response.incomplete") {
648
+ const response = isRecord(event.response) ? event.response : {};
649
+ const recovered = recoverFromCompletedOutput(response, state);
650
+ if (state.lastMessageItemId && state.textDeltaForwarded && !state.messageDoneIds.has(state.lastMessageItemId)) {
651
+ recovered.push({
652
+ type: "response.output_item.done",
653
+ output_index: state.lastOutputIndex,
654
+ item: { type: "message", id: state.lastMessageItemId }
655
+ });
656
+ state.messageDoneIds.add(state.lastMessageItemId);
657
+ }
658
+ return [...recovered, event];
659
+ }
660
+ return [event];
661
+ }
349
662
  function toHeaderRecord(headers) {
350
663
  const out = {};
351
664
  if (!headers) return out;
@@ -403,10 +716,14 @@ function createResponsesWebSocketFetch(wsUrl, log7) {
403
716
  if (hasResponsesLiteHeader(headers)) {
404
717
  payload = applyResponsesLiteShape(payload);
405
718
  }
719
+ debug(
720
+ `request type=response.create keys=${Object.keys(payload).sort().join(",")} toolCount=${Array.isArray(payload.tools) ? payload.tools.length : 0} store=${String(payload.store)} parallelToolCalls=${String(payload.parallel_tool_calls)} reasoningKeys=${recordKeys(payload.reasoning)}`
721
+ );
406
722
  const outgoing = JSON.stringify({ type: "response.create", ...payload });
407
723
  const encoder = new TextEncoder();
408
724
  let socket;
409
725
  let frameCount = 0;
726
+ const normalizeState = createResponsesLiteNormalizeState();
410
727
  const stream = new ReadableStream({
411
728
  start(controller) {
412
729
  let closed = false;
@@ -424,13 +741,12 @@ function createResponsesWebSocketFetch(wsUrl, log7) {
424
741
  };
425
742
  const fail = (message) => {
426
743
  if (closed) return;
427
- debug(`fail: ${message}`);
744
+ debug(`fail messageChars=${message.length}`);
428
745
  try {
429
- controller.enqueue(encoder.encode(
430
- `data: ${JSON.stringify({ type: "error", error: { message } })}
746
+ const [errorEvent] = normalizeResponsesLiteEvent({ type: "error", error: { message } }, normalizeState);
747
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}
431
748
 
432
- `
433
- ));
749
+ `));
434
750
  } catch {
435
751
  }
436
752
  close();
@@ -446,28 +762,31 @@ function createResponsesWebSocketFetch(wsUrl, log7) {
446
762
  socket.on("message", (data) => {
447
763
  const text4 = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
448
764
  frameCount += 1;
449
- if (frameCount <= 3) debug(`frame#${frameCount}: ${text4.slice(0, 200)}`);
450
765
  let event;
451
766
  try {
452
767
  event = JSON.parse(text4);
453
768
  } catch {
769
+ debug(`frame#${frameCount} non-json chars=${text4.length}`);
454
770
  controller.enqueue(encoder.encode(`data: ${text4.replace(/\r?\n/g, " ")}
455
771
 
456
772
  `));
457
773
  return;
458
774
  }
459
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}
775
+ if (frameCount <= 8) debug(`frame#${frameCount} ${summarizeResponsesLiteEvent(event)}`);
776
+ for (const next of normalizeResponsesLiteEvent(event, normalizeState)) {
777
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(next)}
460
778
 
461
779
  `));
462
- const type = event.type;
463
- if (typeof type === "string" && TERMINAL_EVENT_TYPES.has(type)) {
780
+ }
781
+ const type = isRecord(event) && typeof event.type === "string" ? event.type : void 0;
782
+ if (type && TERMINAL_EVENT_TYPES.has(type)) {
464
783
  debug(`terminal event: ${type} (after ${frameCount} frames)`);
465
784
  close();
466
785
  }
467
786
  });
468
787
  socket.on("error", (err) => fail(err.message));
469
788
  socket.on("close", (code, reason) => {
470
- debug(`close code=${code} frames=${frameCount}${reason?.length ? ` reason=${reason.toString("utf8").slice(0, 200)}` : ""}`);
789
+ debug(`close code=${code} frames=${frameCount}${reason?.length ? ` reasonChars=${reason.length}` : ""}`);
471
790
  if (closed) return;
472
791
  if (code === 1e3 || code === 1005) {
473
792
  close();
@@ -823,10 +1142,11 @@ async function createLanguageModel(spec) {
823
1142
  return model;
824
1143
  }
825
1144
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
826
- var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
1145
+ var OPENAI_EFFORT_LEVELS = ["low", "medium", "high"];
827
1146
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
828
1147
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
829
- 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"];
830
1150
  var OPENROUTER_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
831
1151
  var DEEPSEEK_EFFORT_LEVELS = ["high", "max", "off"];
832
1152
  var GLM_52_EFFORT_LEVELS = ["high", "xhigh"];
@@ -997,7 +1317,50 @@ function mapCodexEffortToAnthropic(effort) {
997
1317
  return void 0;
998
1318
  }
999
1319
  }
1000
- 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) {
1001
1364
  if (effort === "xhigh") return "high";
1002
1365
  const allowed = ["low", "medium", "high"];
1003
1366
  return allowed.includes(effort) ? effort : void 0;
@@ -1013,16 +1376,12 @@ function mapCodexEffortToGlm52(effort) {
1013
1376
  return void 0;
1014
1377
  }
1015
1378
  }
1016
- function mapCodexEffortToXai(effort) {
1379
+ function mapCodexEffortToXai(effort, supportsMedium) {
1017
1380
  switch (effort) {
1018
- case "none":
1019
- case "minimal":
1020
- return void 0;
1021
- // xAI SDK only accepts 'low'|'high'; omit param for 'none'
1022
1381
  case "low":
1023
- case "medium":
1024
1382
  return "low";
1025
- // 'medium' has no xAI equivalent — nearest valid value
1383
+ case "medium":
1384
+ return supportsMedium ? "medium" : void 0;
1026
1385
  case "high":
1027
1386
  case "xhigh":
1028
1387
  case "max":
@@ -1054,7 +1413,31 @@ function mapCodexEffortToGeminiBudget(effort) {
1054
1413
  if (!level) return void 0;
1055
1414
  return GEMINI_25_BUDGETS[level];
1056
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
+ }
1057
1437
  function getReasoningCapabilities(npm, modelId, metadata) {
1438
+ return withMappableLevels(resolveRawReasoningCapabilities(npm, modelId, metadata), npm, modelId, metadata);
1439
+ }
1440
+ function resolveRawReasoningCapabilities(npm, modelId, metadata) {
1058
1441
  const id = modelId.toLowerCase();
1059
1442
  if (isOpenRouterRoute(npm, metadata)) {
1060
1443
  return openRouterReasoningCapabilities(metadata);
@@ -1075,15 +1458,18 @@ function getReasoningCapabilities(npm, modelId, metadata) {
1075
1458
  return EMPTY_REASONING;
1076
1459
  }
1077
1460
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
1078
- const prefersResponses = modelPrefersResponsesApi(modelId);
1079
- 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];
1080
1466
  return {
1081
- levels: [...OPENAI_EFFORT_LEVELS],
1082
- defaultLevel: "medium",
1467
+ levels: [...levels],
1468
+ defaultLevel: profile?.defaultLevel ?? (levels.includes("medium") ? "medium" : levels[levels.length - 1]),
1083
1469
  supportsSummaries: true,
1470
+ source: profile || prefersResponses ? "provider-rule" : "model-metadata",
1471
+ confidence: profile || prefersResponses ? "documented" : "inferred",
1084
1472
  mode: "controllable",
1085
- source: prefersResponses ? "provider-rule" : "model-metadata",
1086
- confidence: prefersResponses ? "documented" : "inferred",
1087
1473
  wireFormat: { kind: "openai-reasoning-effort" }
1088
1474
  };
1089
1475
  }
@@ -1119,7 +1505,7 @@ function getReasoningCapabilities(npm, modelId, metadata) {
1119
1505
  }
1120
1506
  if (npm === "@ai-sdk/xai") {
1121
1507
  if (isXaiReasoningEffortModel(modelId)) {
1122
- const levels = modelPrefersResponsesApi(modelId) ? ["low", "medium", "high", "xhigh"] : [...XAI_EFFORT_LEVELS];
1508
+ const levels = modelPrefersResponsesApi(modelId) ? [...XAI_RESPONSES_EFFORT_LEVELS] : [...XAI_CHAT_EFFORT_LEVELS];
1123
1509
  return {
1124
1510
  levels,
1125
1511
  defaultLevel: xaiDefaultReasoningEffort(modelId),
@@ -1216,13 +1602,15 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
1216
1602
  return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
1217
1603
  }
1218
1604
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
1219
- if (!modelId || !modelPrefersResponsesApi(modelId)) return void 0;
1220
- 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);
1221
1609
  return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
1222
1610
  }
1223
1611
  if (npm === "@ai-sdk/xai") {
1224
1612
  if (!modelId || !isXaiReasoningEffortModel(modelId)) return void 0;
1225
- const reasoningEffort = mapCodexEffortToXai(effort);
1613
+ const reasoningEffort = mapCodexEffortToXai(effort, modelPrefersResponsesApi(modelId));
1226
1614
  return reasoningEffort ? { xai: { reasoningEffort } } : void 0;
1227
1615
  }
1228
1616
  if (npm === "@ai-sdk/anthropic" || npm === VERTEX_ANTHROPIC_NPM) {
@@ -1250,7 +1638,7 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
1250
1638
  return deepSeekEffortProviderOptions(effort);
1251
1639
  }
1252
1640
  if (isKimiReasoningModel(modelId)) {
1253
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
1641
+ const reasoningEffort = mapCodexEffortToOpenAICompatible(effort);
1254
1642
  if (reasoningEffort) {
1255
1643
  const key = metadata?.providerId ? toCamelCase(metadata.providerId) : "openaiCompatible";
1256
1644
  return { [key]: { reasoningEffort } };
@@ -1266,7 +1654,7 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
1266
1654
  return void 0;
1267
1655
  }
1268
1656
  if (hasSupportedParameter(metadata, "reasoning_effort")) {
1269
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
1657
+ const reasoningEffort = mapCodexEffortToOpenAICompatible(effort);
1270
1658
  return reasoningEffort ? { openai: { reasoningEffort }, openaiCompatible: { reasoningEffort } } : void 0;
1271
1659
  }
1272
1660
  if (hasSupportedParameter(metadata, "reasoning")) {
@@ -2348,7 +2736,7 @@ var ANTIGRAVITY_BASE_URLS = [
2348
2736
  "https://cloudcode-pa.googleapis.com",
2349
2737
  "https://daily-cloudcode-pa.sandbox.googleapis.com"
2350
2738
  ];
2351
- var API_VERSION = "v1internal";
2739
+ var ANTIGRAVITY_API_VERSION = "v1internal";
2352
2740
  async function buildAntigravityAuthUrl(redirectUri) {
2353
2741
  const { verifier, challenge } = await generatePkce();
2354
2742
  const state = generateOAuthState();
@@ -2470,7 +2858,7 @@ function resolveAntigravityOnboardTierId(data) {
2470
2858
  return pickTierId(sub.currentTier) ?? "legacy-tier";
2471
2859
  }
2472
2860
  async function loadCodeAssist(accessToken) {
2473
- const endpoints = ANTIGRAVITY_BASE_URLS.map((b) => `${b}/${API_VERSION}:loadCodeAssist`);
2861
+ const endpoints = ANTIGRAVITY_BASE_URLS.map((b) => `${b}/${ANTIGRAVITY_API_VERSION}:loadCodeAssist`);
2474
2862
  const res = await fetchFirstOk(endpoints, {
2475
2863
  method: "POST",
2476
2864
  headers: apiHeaders(accessToken),
@@ -2487,7 +2875,7 @@ async function loadCodeAssist(accessToken) {
2487
2875
  };
2488
2876
  }
2489
2877
  async function onboardUser(accessToken, tierId, maxAttempts = 10) {
2490
- const endpoints = ANTIGRAVITY_BASE_URLS.map((b) => `${b}/${API_VERSION}:onboardUser`);
2878
+ const endpoints = ANTIGRAVITY_BASE_URLS.map((b) => `${b}/${ANTIGRAVITY_API_VERSION}:onboardUser`);
2491
2879
  let finalProjectId = "";
2492
2880
  for (let i = 0; i < maxAttempts; i++) {
2493
2881
  const res = await fetchFirstOk(endpoints, {
@@ -4742,7 +5130,7 @@ var SubagentRouteRegistry = class {
4742
5130
  // src/subagent-model-routing.ts
4743
5131
  var CLAUDE_MODEL_FAMILIES = ["sonnet", "opus", "haiku", "fable"];
4744
5132
  var CLAUDE_MODEL_FAMILY_SET = new Set(CLAUDE_MODEL_FAMILIES);
4745
- function isRecord(value) {
5133
+ function isRecord2(value) {
4746
5134
  return typeof value === "object" && value !== null && !Array.isArray(value);
4747
5135
  }
4748
5136
  function claudeModelFamily(modelId) {
@@ -4751,10 +5139,10 @@ function claudeModelFamily(modelId) {
4751
5139
  return CLAUDE_MODEL_FAMILIES.find((family) => normalized.includes(family));
4752
5140
  }
4753
5141
  function isClaudeAgentTool(tool4) {
4754
- if (tool4.name !== "Agent" || !isRecord(tool4.input_schema)) return false;
5142
+ if (tool4.name !== "Agent" || !isRecord2(tool4.input_schema)) return false;
4755
5143
  const properties = tool4.input_schema.properties;
4756
- if (!isRecord(properties)) return false;
4757
- return ["description", "prompt", "subagent_type"].every((name) => isRecord(properties[name]));
5144
+ if (!isRecord2(properties)) return false;
5145
+ return ["description", "prompt", "subagent_type"].every((name) => isRecord2(properties[name]));
4758
5146
  }
4759
5147
  var UnavailableSubagentModelError = class extends Error {
4760
5148
  constructor(selector, routing) {
@@ -4771,7 +5159,7 @@ var UnavailableSubagentModelError = class extends Error {
4771
5159
  statusCode = 400;
4772
5160
  };
4773
5161
  function normalizeClaudeAgentInput(input, routing) {
4774
- const source = isRecord(input) ? input : {};
5162
+ const source = isRecord2(input) ? input : {};
4775
5163
  const normalized = { ...source };
4776
5164
  if (source.subagent_type === "fork") {
4777
5165
  return { input: normalized, decision: { kind: "fork" } };
@@ -4843,9 +5231,9 @@ function prepareClaudeAgentInput(input, routing) {
4843
5231
  return { input: clientInput, decision };
4844
5232
  }
4845
5233
  function augmentClaudeAgentTool(tool4, routing) {
4846
- const inputSchema = isRecord(tool4.input_schema) ? tool4.input_schema : {};
4847
- const properties = isRecord(inputSchema.properties) ? inputSchema.properties : {};
4848
- const originalModel = isRecord(properties.model) ? properties.model : {};
5234
+ const inputSchema = isRecord2(tool4.input_schema) ? tool4.input_schema : {};
5235
+ const properties = isRecord2(inputSchema.properties) ? inputSchema.properties : {};
5236
+ const originalModel = isRecord2(properties.model) ? properties.model : {};
4849
5237
  const smallCatalog = routing.models.length <= MAX_MODEL_CATALOG;
4850
5238
  const modelProperty = {
4851
5239
  ...originalModel,
@@ -6701,8 +7089,8 @@ function anthropicError(res, status, message) {
6701
7089
  });
6702
7090
  }
6703
7091
  function aliasModelId(realId, providerId) {
6704
- if (realId.startsWith("claude-")) return realId;
6705
7092
  const sanitized = providerId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7093
+ if (realId.startsWith("claude-") && !sanitized.startsWith("custom-")) return realId;
6706
7094
  return `anthropic-${sanitized}__${realId}`;
6707
7095
  }
6708
7096
  function buildProxySubagentModelRouting(routes, parentRoute) {
@@ -9016,6 +9404,80 @@ function goRegistryStub() {
9016
9404
  };
9017
9405
  }
9018
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
+
9019
9481
  // src/registry/fetch-template-models.ts
9020
9482
  var TEST_TIMEOUT_MS = 1e4;
9021
9483
  function modelFormatForNpm(npm) {
@@ -9376,77 +9838,28 @@ function npmForKind(kind) {
9376
9838
  function modelFormatForKind(kind) {
9377
9839
  return kind === "anthropic" ? "anthropic" : "openai";
9378
9840
  }
9379
- async function fetchAnthropicModels(baseUrl, apiKey, extraHeaders) {
9380
- const root = baseUrl.replace(/\/v1\/?$/, "").replace(/\/$/, "");
9381
- const modelsUrl2 = `${root}/v1/models`;
9382
- const controller = new AbortController();
9383
- const timer = setTimeout(() => controller.abort(), 1e4);
9384
- try {
9385
- const response = await fetch(modelsUrl2, {
9386
- method: "GET",
9387
- headers: {
9388
- "x-api-key": apiKey,
9389
- "anthropic-version": "2023-06-01",
9390
- Accept: "application/json",
9391
- ...extraHeaders
9392
- },
9393
- redirect: "manual",
9394
- signal: controller.signal
9395
- });
9396
- let logTrace;
9397
- if (process.env.RELAY_AI_TRACE === "1") {
9398
- logTrace = makeTraceLogger(getProviderDebugLogPath());
9399
- }
9400
- const rawBodyText = await response.text().catch(() => "");
9401
- if (logTrace) {
9402
- logTrace(`[fetchAnthropicModels] HTTP ${response.status} from ${modelsUrl2}`);
9403
- logTrace(`[fetchAnthropicModels] Body: ${rawBodyText}`);
9404
- }
9405
- if (response.ok) {
9406
- let json = {};
9407
- try {
9408
- if (rawBodyText.trim()) {
9409
- json = JSON.parse(rawBodyText);
9410
- }
9411
- } catch {
9412
- }
9413
- const models = [];
9414
- for (const row of json.data ?? []) {
9415
- const id = row.id?.trim();
9416
- if (!id) continue;
9417
- models.push({
9418
- id,
9419
- name: row.name?.trim() || id,
9420
- upstreamModelId: id,
9421
- family: id.split("-")[0] ?? id,
9422
- brand: deriveBrand(id),
9423
- contextWindow: resolveContextWindow(id),
9424
- modelFormat: "anthropic",
9425
- npm: "@ai-sdk/anthropic",
9426
- apiUrl: root
9427
- });
9428
- }
9429
- if (models.length > 0) return { models, baseUrl: root };
9430
- }
9431
- if (response.status === 401 || response.status === 403) {
9432
- return { models: [], baseUrl: root, error: "API key was rejected.", hint: "Check your Anthropic-compatible API key." };
9433
- }
9434
- return {
9435
- models: [],
9436
- baseUrl: root,
9437
- error: `Could not list models (HTTP ${response.status}).`,
9438
- hint: "Verify the base URL supports Anthropic-compatible /v1/models or try the OpenAI-compatible option instead."
9439
- };
9440
- } catch {
9441
- return {
9442
- models: [],
9443
- baseUrl: root,
9444
- error: "Could not reach the Anthropic-compatible server.",
9445
- hint: "Check the base URL and that the server is running."
9446
- };
9447
- } finally {
9448
- 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;
9449
9861
  }
9862
+ return null;
9450
9863
  }
9451
9864
  function uniqueProviderId(displayName, registry) {
9452
9865
  let base = customProviderId(displayName);
@@ -9461,6 +9874,25 @@ function uniqueProviderId(displayName, registry) {
9461
9874
  }
9462
9875
  return `${base}-${Date.now()}`;
9463
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
+ }
9464
9896
  async function addCustomEndpointProvider(input) {
9465
9897
  const urlCheck = await validateCustomEndpointUrl(input.baseUrl, {
9466
9898
  allowInsecureLocal: input.allowInsecureLocal
@@ -9469,29 +9901,34 @@ async function addCustomEndpointProvider(input) {
9469
9901
  return { added: false, error: urlCheck.error, hint: urlCheck.hint };
9470
9902
  }
9471
9903
  const registry = loadRegistry();
9472
- const providerId = uniqueProviderId(input.displayName.trim(), registry);
9473
- const npm = npmForKind(input.kind);
9474
9904
  const apiKey = input.apiKey.trim() || "local";
9475
9905
  const headers = input.headers && Object.keys(input.headers).length > 0 ? input.headers : void 0;
9476
- let fetched;
9477
- if (input.kind === "anthropic") {
9478
- fetched = await fetchAnthropicModels(urlCheck.normalizedUrl, apiKey, headers);
9479
- } else {
9480
- fetched = await fetchTemplateModels(
9481
- {
9482
- id: providerId,
9483
- name: input.displayName,
9484
- authType: apiKey === "local" ? "none" : "api",
9485
- npm,
9486
- defaultBaseUrl: urlCheck.normalizedUrl,
9487
- modelSource: "api-list",
9488
- supported: true
9489
- },
9490
- apiKey,
9906
+ if (!input.confirmDuplicate) {
9907
+ const duplicateOf = await findDuplicateCustomProvider(
9908
+ registry,
9491
9909
  urlCheck.normalizedUrl,
9910
+ apiKey,
9492
9911
  headers
9493
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
+ }
9494
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
+ });
9495
9932
  if (fetched.error || fetched.models.length === 0) {
9496
9933
  return { added: false, error: fetched.error ?? "No models returned.", hint: fetched.hint };
9497
9934
  }
@@ -9528,6 +9965,115 @@ async function addCustomEndpointProvider(input) {
9528
9965
  saveRegistry(registry);
9529
9966
  return { added: true, provider: entry, modelCount: fetched.models.length };
9530
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
+ }
9531
10077
 
9532
10078
  // src/registry/fetch-cline-pass-models.ts
9533
10079
  var REQUEST_TIMEOUT_MS = 1e4;
@@ -10055,7 +10601,7 @@ async function refreshApiListProvider(provider, apiKey) {
10055
10601
  const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
10056
10602
  if (configuredUrl && configuredUrl !== templateDefault) {
10057
10603
  const urlCheck = await validateCustomEndpointUrl(baseUrl, {
10058
- allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
10604
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true || customEndpointKind(provider) !== null
10059
10605
  });
10060
10606
  if (!urlCheck.ok || !urlCheck.normalizedUrl) {
10061
10607
  return { models: [], error: `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim() };
@@ -10063,8 +10609,9 @@ async function refreshApiListProvider(provider, apiKey) {
10063
10609
  safeBaseUrl = urlCheck.normalizedUrl;
10064
10610
  }
10065
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;
10066
10613
  if (npm === "@ai-sdk/anthropic") {
10067
- const fetched2 = await fetchAnthropicModels(safeBaseUrl, apiKey);
10614
+ const fetched2 = await fetchAnthropicModels(safeBaseUrl, apiKey, extraHeaders);
10068
10615
  if (fetched2.error || fetched2.models.length === 0) {
10069
10616
  return { models: [], error: fetched2.error ?? "No models returned.", baseUrl: fetched2.baseUrl };
10070
10617
  }
@@ -10073,7 +10620,7 @@ async function refreshApiListProvider(provider, apiKey) {
10073
10620
  baseUrl: fetched2.baseUrl
10074
10621
  };
10075
10622
  }
10076
- const fetched = await fetchTemplateModels(template, apiKey, safeBaseUrl);
10623
+ const fetched = await fetchTemplateModels(template, apiKey, safeBaseUrl, extraHeaders);
10077
10624
  if (fetched.error || fetched.models.length === 0) {
10078
10625
  return { models: [], error: fetched.error ?? "No models returned." };
10079
10626
  }
@@ -12006,7 +12553,7 @@ function removeFavorite(list, fav) {
12006
12553
  // src/favorite-provider-display.ts
12007
12554
  var OAUTH_FAVORITE_NAMES = {
12008
12555
  "claude-code": "Claude Code OAuth (Anthropic subscription)",
12009
- antigravity: "Antigravity OAuth (Google Cloud Code Assist)",
12556
+ antigravity: "Cloud Code Assist OAuth (Google)",
12010
12557
  "openai-oauth": "OpenAI OAuth (ChatGPT)",
12011
12558
  "xai-oauth": "xAI OAuth (SuperGrok)"
12012
12559
  };
@@ -12209,7 +12756,7 @@ var PROVIDER_DISPLAY = {
12209
12756
  "openai-oauth": OPENAI_DISPLAY,
12210
12757
  "github-copilot": "GitHub Copilot",
12211
12758
  "claude-code": "Claude Code (Anthropic subscription)",
12212
- antigravity: "Antigravity (Google Cloud Code Assist)",
12759
+ antigravity: "Cloud Code Assist OAuth (Google)",
12213
12760
  "cline-pass": "ClinePass"
12214
12761
  };
12215
12762
  function openBrowser(url) {
@@ -12312,7 +12859,7 @@ async function runNativeBrowserOAuth(providerId) {
12312
12859
  p5.log.info(`Opening: ${pc6.cyan(url)}`);
12313
12860
  spinner3.start("Waiting for authorization\u2026");
12314
12861
  });
12315
- spinner3.stop(pc6.green("Signed in to Antigravity"));
12862
+ spinner3.stop(pc6.green("Signed in to Cloud Code Assist"));
12316
12863
  const providerData = {};
12317
12864
  if (projectId) providerData.projectId = projectId;
12318
12865
  if (tierId) providerData.tier = tierId;
@@ -13186,14 +13733,13 @@ export {
13186
13733
  makeTraceLogger,
13187
13734
  writeSecureLogLine,
13188
13735
  printTraceLog,
13189
- fetchTemplateModels,
13190
- validateCustomEndpointUrl,
13191
13736
  fetchAnthropicModels,
13192
- addCustomEndpointProvider,
13737
+ fetchTemplateModels,
13193
13738
  resolveProviderTemplate,
13194
13739
  effectiveProviderBaseUrl,
13195
13740
  syntheticTemplate,
13196
13741
  resolveModelSource,
13742
+ validateCustomEndpointUrl,
13197
13743
  readBody,
13198
13744
  extractApiKey,
13199
13745
  sendJson,
@@ -13248,6 +13794,9 @@ export {
13248
13794
  routableModelsForTarget,
13249
13795
  providersForTarget,
13250
13796
  providersForCodexSubagents,
13797
+ customEndpointKind,
13798
+ addCustomEndpointProvider,
13799
+ updateCustomEndpointProvider,
13251
13800
  refreshProviderModels,
13252
13801
  refreshAllProviderModels,
13253
13802
  removeProviderFromRegistry,
@@ -13292,4 +13841,4 @@ export {
13292
13841
  supportsClaudeTransparentMode,
13293
13842
  buildHttpProxyRoutes
13294
13843
  };
13295
- //# sourceMappingURL=chunk-MFKBK6YL.js.map
13844
+ //# sourceMappingURL=chunk-PVGAE7HA.js.map