@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.
@@ -50,7 +50,7 @@ import { join as join2 } from "path";
50
50
  // package.json
51
51
  var package_default = {
52
52
  name: "@jacobbd/relay-ai",
53
- version: "0.9.2",
53
+ version: "0.9.3",
54
54
  publishConfig: {
55
55
  access: "public"
56
56
  },
@@ -328,9 +328,7 @@ function createResponsesLiteNormalizeState() {
328
328
  textDeltaForwarded: false,
329
329
  messageAddedIds: /* @__PURE__ */ new Set(),
330
330
  messageDoneIds: /* @__PURE__ */ new Set(),
331
- functionAddedIndexes: /* @__PURE__ */ new Set(),
332
- functionDeltaIndexes: /* @__PURE__ */ new Set(),
333
- functionDoneCallIds: /* @__PURE__ */ new Set()
331
+ functionCalls: []
334
332
  };
335
333
  }
336
334
  function nextId(state, prefix) {
@@ -354,20 +352,92 @@ function normalizeErrorEvent(event) {
354
352
  }
355
353
  };
356
354
  }
357
- function normalizeFunctionItem(item, state, forDone = false) {
358
- const callId = asString(item.call_id) ?? asString(item.id) ?? nextId(state, "call");
359
- const id = asString(item.id) ?? nextId(state, "fc");
360
- state.lastFunctionItemId = id;
355
+ function resolveFunctionCall(state, hint) {
356
+ if (hint.callId) {
357
+ const byCallId = state.functionCalls.find((entry2) => entry2.callId === hint.callId);
358
+ if (byCallId) return byCallId;
359
+ }
360
+ if (hint.itemId) {
361
+ const byItemId = state.functionCalls.find((entry2) => entry2.itemId === hint.itemId);
362
+ if (byItemId) return byItemId;
363
+ }
364
+ if (hint.outputIndex !== void 0) {
365
+ const open3 = [...state.functionCalls].reverse().find((entry2) => entry2.outputIndex === hint.outputIndex && !entry2.done && !(hint.callId && entry2.callId !== hint.callId));
366
+ if (open3) return open3;
367
+ }
368
+ if (hint.callId === void 0 && hint.itemId === void 0 && hint.outputIndex === void 0 && state.lastFunctionCall) {
369
+ return state.lastFunctionCall;
370
+ }
371
+ const entry = {
372
+ itemId: hint.itemId ?? nextId(state, "fc"),
373
+ callId: hint.callId ?? hint.itemId ?? nextId(state, "call"),
374
+ name: "",
375
+ args: "",
376
+ upstream: {},
377
+ outputIndex: hint.outputIndex ?? state.lastOutputIndex,
378
+ added: false,
379
+ deltaForwarded: false,
380
+ doneSeen: false,
381
+ done: false
382
+ };
383
+ state.functionCalls.push(entry);
384
+ return entry;
385
+ }
386
+ function absorbFunctionItem(entry, item, authoritative) {
387
+ entry.upstream = { ...entry.upstream, ...item };
388
+ const name = asString(item.name);
389
+ if (name) entry.name = name;
390
+ const callId = asString(item.call_id);
391
+ if (callId) entry.callId = callId;
392
+ if (authoritative && typeof item.arguments === "string") entry.upstreamArgs = item.arguments;
393
+ }
394
+ function resolveFunctionArgs(entry) {
395
+ if (entry.upstreamArgs) return entry.upstreamArgs;
396
+ if (entry.args) return entry.args;
397
+ return entry.upstreamArgs;
398
+ }
399
+ function functionItemPayload(entry, extra) {
361
400
  return {
362
- ...item,
401
+ ...entry.upstream,
363
402
  type: "function_call",
364
- id,
365
- call_id: callId,
366
- name: asString(item.name) ?? "",
367
- arguments: typeof item.arguments === "string" ? item.arguments : "",
368
- ...forDone ? { status: "completed" } : {}
403
+ id: entry.itemId,
404
+ call_id: entry.callId,
405
+ name: entry.name,
406
+ ...extra
407
+ };
408
+ }
409
+ function functionAddedEvent(entry) {
410
+ entry.added = true;
411
+ return {
412
+ type: "response.output_item.added",
413
+ output_index: entry.outputIndex,
414
+ item: functionItemPayload(entry, { arguments: "" })
415
+ };
416
+ }
417
+ function functionDoneEvent(entry, args) {
418
+ entry.done = true;
419
+ return {
420
+ type: "response.output_item.done",
421
+ output_index: entry.outputIndex,
422
+ item: functionItemPayload(entry, { arguments: args, status: "completed" })
369
423
  };
370
424
  }
425
+ function completeFunctionCall(entry, args) {
426
+ if (entry.done) return [];
427
+ const events = [];
428
+ if (!entry.added) events.push(functionAddedEvent(entry));
429
+ if (!entry.deltaForwarded && args.length > 0) {
430
+ entry.deltaForwarded = true;
431
+ events.push({
432
+ type: "response.function_call_arguments.delta",
433
+ item_id: entry.itemId,
434
+ output_index: entry.outputIndex,
435
+ delta: args
436
+ });
437
+ }
438
+ events.push(functionDoneEvent(entry, args));
439
+ return events;
440
+ }
371
441
  function messageText(item) {
372
442
  if (typeof item.text === "string") return item.text;
373
443
  if (!Array.isArray(item.content)) return "";
@@ -394,47 +464,43 @@ function synthesizeMessage(item, outputIndex, state) {
394
464
  ];
395
465
  }
396
466
  function synthesizeFunctionCall(item, outputIndex, state) {
397
- const normalized = normalizeFunctionItem(item, state);
398
- const callId = String(normalized.call_id);
399
- if (state.functionDoneCallIds.has(callId)) return [];
400
- const events = [];
401
- if (!state.functionAddedIndexes.has(outputIndex)) {
402
- events.push({
403
- type: "response.output_item.added",
404
- output_index: outputIndex,
405
- item: { ...normalized, arguments: "" }
406
- });
407
- state.functionAddedIndexes.add(outputIndex);
408
- }
409
- if (!state.functionDeltaIndexes.has(outputIndex) && typeof normalized.arguments === "string" && normalized.arguments.length > 0) {
410
- events.push({
411
- type: "response.function_call_arguments.delta",
412
- item_id: normalized.id,
413
- output_index: outputIndex,
414
- delta: normalized.arguments
415
- });
416
- state.functionDeltaIndexes.add(outputIndex);
417
- }
418
- events.push({
419
- type: "response.output_item.done",
420
- output_index: outputIndex,
421
- item: { ...normalized, status: "completed" }
467
+ const entry = resolveFunctionCall(state, {
468
+ itemId: asString(item.id),
469
+ callId: asString(item.call_id),
470
+ outputIndex
422
471
  });
423
- state.functionDoneCallIds.add(callId);
424
- state.lastOutputIndex = outputIndex;
425
- return events;
472
+ absorbFunctionItem(entry, item, true);
473
+ state.lastFunctionCall = entry;
474
+ state.lastOutputIndex = entry.outputIndex;
475
+ const args = resolveFunctionArgs(entry);
476
+ if (args === void 0) {
477
+ entry.doneSeen = true;
478
+ return [];
479
+ }
480
+ return completeFunctionCall(entry, args);
426
481
  }
427
482
  function recoverFromCompletedOutput(response, state) {
428
- if (!Array.isArray(response.output)) return [];
429
483
  const recovered = [];
430
- response.output.forEach((item, index) => {
431
- if (!isRecord(item) || typeof item.type !== "string") return;
432
- if (item.type === "message" && !state.textDeltaForwarded) {
433
- recovered.push(...synthesizeMessage(item, index, state));
434
- } else if (item.type === "function_call") {
435
- recovered.push(...synthesizeFunctionCall(item, index, state));
436
- }
437
- });
484
+ if (Array.isArray(response.output)) {
485
+ response.output.forEach((item, index) => {
486
+ if (!isRecord(item) || typeof item.type !== "string") return;
487
+ if (item.type === "message" && !state.textDeltaForwarded) {
488
+ recovered.push(...synthesizeMessage(item, index, state));
489
+ } else if (item.type === "function_call") {
490
+ recovered.push(...synthesizeFunctionCall(item, index, state));
491
+ }
492
+ });
493
+ }
494
+ for (const entry of state.functionCalls) {
495
+ if (entry.done || !entry.doneSeen) continue;
496
+ recovered.push(normalizeErrorEvent({
497
+ error: {
498
+ type: "invalid_response",
499
+ code: "incomplete_function_call",
500
+ message: `Provider ended the response without arguments for function call "${entry.callId}"${entry.name ? ` (${entry.name})` : ""}.`
501
+ }
502
+ }));
503
+ }
438
504
  return recovered;
439
505
  }
440
506
  function normalizeResponsesLiteEvent(event, state) {
@@ -450,19 +516,40 @@ function normalizeResponsesLiteEvent(event, state) {
450
516
  return [{ ...event, output_index: outputIndex, item: { ...event.item, id } }];
451
517
  }
452
518
  if (event.item.type === "function_call") {
453
- const item = normalizeFunctionItem(event.item, state);
454
- state.functionAddedIndexes.add(outputIndex);
455
- return [{ ...event, output_index: outputIndex, item }];
519
+ const entry = resolveFunctionCall(state, {
520
+ itemId: asString(event.item.id),
521
+ callId: asString(event.item.call_id),
522
+ outputIndex
523
+ });
524
+ absorbFunctionItem(entry, event.item, false);
525
+ entry.outputIndex = outputIndex;
526
+ entry.added = true;
527
+ state.lastFunctionCall = entry;
528
+ return [{ ...event, output_index: outputIndex, item: functionItemPayload(entry, { arguments: "" }) }];
456
529
  }
457
530
  return [{ ...event, output_index: outputIndex }];
458
531
  }
459
532
  if (event.type === "response.output_item.done" && isRecord(event.item)) {
460
533
  const outputIndex = typeof event.output_index === "number" ? event.output_index : state.lastOutputIndex;
461
534
  if (event.item.type === "function_call") {
462
- const item = normalizeFunctionItem(event.item, state, true);
463
- const callId = String(item.call_id);
464
- state.functionDoneCallIds.add(callId);
465
- return [{ ...event, output_index: outputIndex, item }];
535
+ const entry = resolveFunctionCall(state, {
536
+ itemId: asString(event.item.id),
537
+ callId: asString(event.item.call_id),
538
+ outputIndex
539
+ });
540
+ absorbFunctionItem(entry, event.item, true);
541
+ entry.outputIndex = outputIndex;
542
+ entry.doneSeen = true;
543
+ state.lastFunctionCall = entry;
544
+ state.lastOutputIndex = outputIndex;
545
+ const args = resolveFunctionArgs(entry);
546
+ if (args === void 0 || !entry.name) return [];
547
+ if (entry.done) return [];
548
+ const events = [];
549
+ if (!entry.added) events.push(functionAddedEvent(entry));
550
+ events.push({ ...event, output_index: outputIndex, item: functionItemPayload(entry, { arguments: args, status: "completed" }) });
551
+ entry.done = true;
552
+ return events;
466
553
  }
467
554
  if (event.item.type === "message") {
468
555
  const id = asString(event.item.id) ?? state.lastMessageItemId ?? nextId(state, "msg");
@@ -489,12 +576,16 @@ function normalizeResponsesLiteEvent(event, state) {
489
576
  return events;
490
577
  }
491
578
  if (event.type === "response.function_call_arguments.delta") {
492
- const itemId = asString(event.item_id) ?? state.lastFunctionItemId ?? nextId(state, "fc");
493
- const outputIndex = typeof event.output_index === "number" ? event.output_index : state.lastOutputIndex;
494
- state.lastFunctionItemId = itemId;
495
- state.lastOutputIndex = outputIndex;
496
- state.functionDeltaIndexes.add(outputIndex);
497
- return [{ ...event, item_id: itemId, output_index: outputIndex, delta: typeof event.delta === "string" ? event.delta : "" }];
579
+ const entry = resolveFunctionCall(state, {
580
+ itemId: asString(event.item_id),
581
+ outputIndex: typeof event.output_index === "number" ? event.output_index : void 0
582
+ });
583
+ const delta = typeof event.delta === "string" ? event.delta : "";
584
+ entry.args += delta;
585
+ entry.deltaForwarded = true;
586
+ state.lastFunctionCall = entry;
587
+ state.lastOutputIndex = entry.outputIndex;
588
+ return [{ ...event, item_id: entry.itemId, output_index: entry.outputIndex, delta }];
498
589
  }
499
590
  if (event.type === "response.completed" || event.type === "response.incomplete") {
500
591
  const response = isRecord(event.response) ? event.response : {};
@@ -922,10 +1013,11 @@ async function createLanguageModel(spec) {
922
1013
  return model;
923
1014
  }
924
1015
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
925
- var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
1016
+ var OPENAI_EFFORT_LEVELS = ["low", "medium", "high"];
926
1017
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
927
1018
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
928
- var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
1019
+ var XAI_CHAT_EFFORT_LEVELS = ["low", "high"];
1020
+ var XAI_RESPONSES_EFFORT_LEVELS = ["low", "medium", "high"];
929
1021
  var OPENROUTER_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
930
1022
  var DEEPSEEK_EFFORT_LEVELS = ["high", "max", "off"];
931
1023
  var GLM_52_EFFORT_LEVELS = ["high", "xhigh"];
@@ -937,6 +1029,15 @@ var EMPTY_REASONING = {
937
1029
  source: "none",
938
1030
  confidence: "inferred"
939
1031
  };
1032
+ var GEMINI_25_BUDGETS = {
1033
+ low: 1024,
1034
+ medium: 4096,
1035
+ high: 8192,
1036
+ xhigh: 16384,
1037
+ max: 16384,
1038
+ minimal: 512,
1039
+ none: 0
1040
+ };
940
1041
  function isClaudeReasoningModel(modelId) {
941
1042
  const lower = modelId.toLowerCase();
942
1043
  if (!lower.startsWith("claude-")) return false;
@@ -951,6 +1052,10 @@ function isGeminiReasoningModel(modelId) {
951
1052
  const lower = modelId.toLowerCase();
952
1053
  return lower.startsWith("gemini-2.5-") || lower.startsWith("gemini-3") || lower.startsWith("gemini-3.");
953
1054
  }
1055
+ function isGemini3Model(modelId) {
1056
+ const lower = modelId.toLowerCase();
1057
+ return lower.startsWith("gemini-3") || lower.startsWith("gemini-3.");
1058
+ }
954
1059
  function isMistralReasoningModel(modelId) {
955
1060
  const lower = modelId.toLowerCase();
956
1061
  return lower.startsWith("mistral-") || lower.startsWith("magistral-") || lower.startsWith("ministral-") || lower.includes("reasoning");
@@ -983,6 +1088,9 @@ function isGlm52ReasoningModel(modelId) {
983
1088
  const lower = modelId.toLowerCase();
984
1089
  return lower === "glm-5.2" || lower === "z-ai/glm-5.2" || lower === "zai/glm-5.2" || lower === "zai-org/glm-5.2" || lower === "zai-org/glm5.2" || lower === "glm5.2";
985
1090
  }
1091
+ function toCamelCase(str) {
1092
+ return str.replace(/[-_]([a-z])/g, (_, g) => g.toUpperCase());
1093
+ }
986
1094
  function hasSupportedParameter(metadata, param) {
987
1095
  return (metadata?.supportedParameters ?? []).some((p) => p === param);
988
1096
  }
@@ -1018,7 +1126,179 @@ function openRouterReasoningCapabilities(metadata) {
1018
1126
  }
1019
1127
  return EMPTY_REASONING;
1020
1128
  }
1129
+ function mapCodexEffortToDeepSeek(effort) {
1130
+ switch (effort) {
1131
+ case "off":
1132
+ case "none":
1133
+ return "off";
1134
+ case "low":
1135
+ case "medium":
1136
+ case "high":
1137
+ return "high";
1138
+ case "xhigh":
1139
+ case "max":
1140
+ return "max";
1141
+ default:
1142
+ if (effort === "high" || effort === "max") return effort;
1143
+ return void 0;
1144
+ }
1145
+ }
1146
+ function deepSeekEffortProviderOptions(effort) {
1147
+ const mapped = mapCodexEffortToDeepSeek(effort);
1148
+ if (!mapped) return void 0;
1149
+ const thinking = { type: mapped === "off" ? "disabled" : "enabled" };
1150
+ const spread = { thinking };
1151
+ if (mapped === "off") {
1152
+ return {
1153
+ deepseek: spread,
1154
+ openaiCompatible: spread
1155
+ };
1156
+ }
1157
+ return {
1158
+ openaiCompatible: { reasoningEffort: mapped, ...spread },
1159
+ deepseek: spread
1160
+ };
1161
+ }
1162
+ function mapCodexEffortToAnthropic(effort) {
1163
+ switch (effort) {
1164
+ case "none":
1165
+ case "minimal":
1166
+ case "low":
1167
+ return "low";
1168
+ case "medium":
1169
+ return "medium";
1170
+ case "high":
1171
+ case "xhigh":
1172
+ case "max":
1173
+ return effort === "xhigh" ? "high" : effort === "max" ? "max" : "high";
1174
+ default:
1175
+ if (ANTHROPIC_EFFORT_LEVELS.includes(effort)) {
1176
+ return effort;
1177
+ }
1178
+ return void 0;
1179
+ }
1180
+ }
1181
+ var OPENAI_MODEL_REASONING = {
1182
+ "gpt-5-pro": { levels: ["high"], defaultLevel: "high" },
1183
+ "gpt-5.1": { levels: ["none", "low", "medium", "high"], defaultLevel: "none" },
1184
+ "gpt-5.1-codex-max": { levels: ["low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1185
+ "gpt-5.2": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1186
+ "gpt-5.2-codex": { levels: ["low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1187
+ "gpt-5.2-pro": { levels: ["medium", "high", "xhigh"], defaultLevel: "medium" },
1188
+ "gpt-5.3-codex": { levels: ["low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1189
+ "gpt-5.4": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1190
+ "gpt-5.4-mini": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1191
+ "gpt-5.4-nano": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "none" },
1192
+ "gpt-5.4-pro": { levels: ["medium", "high", "xhigh"], defaultLevel: "medium" },
1193
+ "gpt-5.5": { levels: ["none", "low", "medium", "high", "xhigh"], defaultLevel: "medium" },
1194
+ "gpt-5.5-pro": { levels: ["medium", "high", "xhigh"], defaultLevel: "high" },
1195
+ "gpt-5.6": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" },
1196
+ "gpt-5.6-luna": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" },
1197
+ "gpt-5.6-sol": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" },
1198
+ "gpt-5.6-terra": { levels: ["none", "low", "medium", "high", "xhigh", "max"], defaultLevel: "medium" }
1199
+ };
1200
+ var OPENAI_NON_REASONING_MODELS = /* @__PURE__ */ new Set([
1201
+ "chat-latest",
1202
+ "gpt-5-chat-latest",
1203
+ "gpt-5.1-chat-latest",
1204
+ "gpt-5.2-chat-latest",
1205
+ "gpt-5.3-chat-latest"
1206
+ ]);
1207
+ var OPENAI_DATED_SNAPSHOT_SUFFIX = /-\d{4}-\d{2}-\d{2}$/;
1208
+ function canonicalOpenAiModelId(modelId, metadata) {
1209
+ return (metadata?.upstreamModelId ?? modelId ?? "").toLowerCase();
1210
+ }
1211
+ function openAiReasoningProfile(modelId, metadata) {
1212
+ const id = canonicalOpenAiModelId(modelId, metadata);
1213
+ if (!id) return void 0;
1214
+ return OPENAI_MODEL_REASONING[id] ?? OPENAI_MODEL_REASONING[id.replace(OPENAI_DATED_SNAPSHOT_SUFFIX, "")];
1215
+ }
1216
+ function openAiModelReasons(modelId, metadata) {
1217
+ const id = canonicalOpenAiModelId(modelId, metadata);
1218
+ if (OPENAI_NON_REASONING_MODELS.has(id.replace(OPENAI_DATED_SNAPSHOT_SUFFIX, ""))) return false;
1219
+ return !!openAiReasoningProfile(modelId, metadata) || modelPrefersResponsesApi(id) || !!metadata?.reasoning;
1220
+ }
1221
+ function mapCodexEffortToOpenAI(effort, allowed) {
1222
+ return allowed.includes(effort) ? effort : void 0;
1223
+ }
1224
+ function mapCodexEffortToOpenAICompatible(effort) {
1225
+ if (effort === "xhigh") return "high";
1226
+ const allowed = ["low", "medium", "high"];
1227
+ return allowed.includes(effort) ? effort : void 0;
1228
+ }
1229
+ function mapCodexEffortToGlm52(effort) {
1230
+ switch (effort) {
1231
+ case "high":
1232
+ return "high";
1233
+ case "xhigh":
1234
+ case "max":
1235
+ return "max";
1236
+ default:
1237
+ return void 0;
1238
+ }
1239
+ }
1240
+ function mapCodexEffortToXai(effort, supportsMedium) {
1241
+ switch (effort) {
1242
+ case "low":
1243
+ return "low";
1244
+ case "medium":
1245
+ return supportsMedium ? "medium" : void 0;
1246
+ case "high":
1247
+ case "xhigh":
1248
+ case "max":
1249
+ return "high";
1250
+ default:
1251
+ return void 0;
1252
+ }
1253
+ }
1254
+ function mapCodexEffortToGeminiLevel(effort) {
1255
+ switch (effort) {
1256
+ case "none":
1257
+ case "minimal":
1258
+ case "low":
1259
+ return "low";
1260
+ case "medium":
1261
+ return "medium";
1262
+ case "high":
1263
+ case "xhigh":
1264
+ case "max":
1265
+ return "high";
1266
+ default:
1267
+ return GEMINI_EFFORT_LEVELS.includes(effort) ? effort : void 0;
1268
+ }
1269
+ }
1270
+ function mapCodexEffortToGeminiBudget(effort) {
1271
+ const direct = GEMINI_25_BUDGETS[effort];
1272
+ if (direct !== void 0) return direct > 0 ? direct : void 0;
1273
+ const level = mapCodexEffortToGeminiLevel(effort);
1274
+ if (!level) return void 0;
1275
+ return GEMINI_25_BUDGETS[level];
1276
+ }
1277
+ function withMappableLevels(caps, npm, modelId, metadata) {
1278
+ if (caps.mode !== "controllable") return caps;
1279
+ const seen = /* @__PURE__ */ new Set();
1280
+ const levels = caps.levels.filter((level) => {
1281
+ const mapped = effortProviderOptions(npm, level, modelId, metadata);
1282
+ if (mapped === void 0) return false;
1283
+ const wire = JSON.stringify(mapped);
1284
+ if (seen.has(wire)) return false;
1285
+ seen.add(wire);
1286
+ return true;
1287
+ });
1288
+ if (levels.length === caps.levels.length) return caps;
1289
+ if (levels.length === 0) {
1290
+ return { ...caps, levels: [], defaultLevel: "", mode: "internal-only" };
1291
+ }
1292
+ return {
1293
+ ...caps,
1294
+ levels,
1295
+ defaultLevel: levels.includes(caps.defaultLevel) ? caps.defaultLevel : levels[levels.length - 1]
1296
+ };
1297
+ }
1021
1298
  function getReasoningCapabilities(npm, modelId, metadata) {
1299
+ return withMappableLevels(resolveRawReasoningCapabilities(npm, modelId, metadata), npm, modelId, metadata);
1300
+ }
1301
+ function resolveRawReasoningCapabilities(npm, modelId, metadata) {
1022
1302
  const id = modelId.toLowerCase();
1023
1303
  if (isOpenRouterRoute(npm, metadata)) {
1024
1304
  return openRouterReasoningCapabilities(metadata);
@@ -1039,15 +1319,18 @@ function getReasoningCapabilities(npm, modelId, metadata) {
1039
1319
  return EMPTY_REASONING;
1040
1320
  }
1041
1321
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
1042
- const prefersResponses = modelPrefersResponsesApi(modelId);
1043
- if (prefersResponses || metadata?.reasoning) {
1322
+ const canonicalId = canonicalOpenAiModelId(modelId, metadata);
1323
+ const profile = openAiReasoningProfile(modelId, metadata);
1324
+ const prefersResponses = modelPrefersResponsesApi(canonicalId);
1325
+ if (openAiModelReasons(modelId, metadata) && shouldUseOpenAiResponsesEndpoint(canonicalId)) {
1326
+ const levels = profile?.levels ?? [...OPENAI_EFFORT_LEVELS];
1044
1327
  return {
1045
- levels: [...OPENAI_EFFORT_LEVELS],
1046
- defaultLevel: "medium",
1328
+ levels: [...levels],
1329
+ defaultLevel: profile?.defaultLevel ?? (levels.includes("medium") ? "medium" : levels[levels.length - 1]),
1047
1330
  supportsSummaries: true,
1331
+ source: profile || prefersResponses ? "provider-rule" : "model-metadata",
1332
+ confidence: profile || prefersResponses ? "documented" : "inferred",
1048
1333
  mode: "controllable",
1049
- source: prefersResponses ? "provider-rule" : "model-metadata",
1050
- confidence: prefersResponses ? "documented" : "inferred",
1051
1334
  wireFormat: { kind: "openai-reasoning-effort" }
1052
1335
  };
1053
1336
  }
@@ -1083,7 +1366,7 @@ function getReasoningCapabilities(npm, modelId, metadata) {
1083
1366
  }
1084
1367
  if (npm === "@ai-sdk/xai") {
1085
1368
  if (isXaiReasoningEffortModel(modelId)) {
1086
- const levels = modelPrefersResponsesApi(modelId) ? ["low", "medium", "high", "xhigh"] : [...XAI_EFFORT_LEVELS];
1369
+ const levels = modelPrefersResponsesApi(modelId) ? [...XAI_RESPONSES_EFFORT_LEVELS] : [...XAI_CHAT_EFFORT_LEVELS];
1087
1370
  return {
1088
1371
  levels,
1089
1372
  defaultLevel: xaiDefaultReasoningEffort(modelId),
@@ -1164,6 +1447,91 @@ function getReasoningCapabilities(npm, modelId, metadata) {
1164
1447
  }
1165
1448
  return EMPTY_REASONING;
1166
1449
  }
1450
+ function effortProviderOptions(npm, effort, modelId, metadata) {
1451
+ if (!effort) return void 0;
1452
+ if (isOpenRouterRoute(npm, metadata)) {
1453
+ const caps = openRouterReasoningCapabilities(metadata);
1454
+ if (caps.mode !== "controllable") return void 0;
1455
+ const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
1456
+ const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
1457
+ return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
1458
+ }
1459
+ if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
1460
+ if (!modelId || !shouldUseOpenAiResponsesEndpoint(canonicalOpenAiModelId(modelId, metadata))) return void 0;
1461
+ if (!openAiModelReasons(modelId, metadata)) return void 0;
1462
+ const allowed = openAiReasoningProfile(modelId, metadata)?.levels ?? OPENAI_EFFORT_LEVELS;
1463
+ const reasoningEffort = mapCodexEffortToOpenAI(effort, allowed);
1464
+ return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
1465
+ }
1466
+ if (npm === "@ai-sdk/xai") {
1467
+ if (!modelId || !isXaiReasoningEffortModel(modelId)) return void 0;
1468
+ const reasoningEffort = mapCodexEffortToXai(effort, modelPrefersResponsesApi(modelId));
1469
+ return reasoningEffort ? { xai: { reasoningEffort } } : void 0;
1470
+ }
1471
+ if (npm === "@ai-sdk/anthropic" || npm === VERTEX_ANTHROPIC_NPM) {
1472
+ if (!modelId || !isClaudeReasoningModel(modelId)) return void 0;
1473
+ const mapped = mapCodexEffortToAnthropic(effort);
1474
+ return mapped ? { anthropic: { thinking: { type: "adaptive", effort: mapped } } } : void 0;
1475
+ }
1476
+ if (npm === "@ai-sdk/google") {
1477
+ const id = modelId ?? "";
1478
+ if (isGemini3Model(id)) {
1479
+ const thinkingLevel = mapCodexEffortToGeminiLevel(effort);
1480
+ return thinkingLevel ? { google: { thinkingConfig: { thinkingLevel, includeThoughts: true } } } : void 0;
1481
+ }
1482
+ const thinkingBudget = mapCodexEffortToGeminiBudget(effort);
1483
+ return thinkingBudget ? { google: { thinkingConfig: { thinkingBudget, includeThoughts: true } } } : void 0;
1484
+ }
1485
+ if (npm === "@ai-sdk/mistral") {
1486
+ if (!modelId || !isMistralReasoningModel(modelId)) return void 0;
1487
+ const reasoningEffort = effort === "off" || effort === "none" ? "none" : "high";
1488
+ return { mistral: { reasoningEffort } };
1489
+ }
1490
+ if (npm === "@ai-sdk/openai-compatible" || npm === "@ai-sdk/openai") {
1491
+ if (!modelId) return void 0;
1492
+ if (isDeepSeekReasoningModel(modelId)) {
1493
+ return deepSeekEffortProviderOptions(effort);
1494
+ }
1495
+ if (isKimiReasoningModel(modelId)) {
1496
+ const reasoningEffort = mapCodexEffortToOpenAICompatible(effort);
1497
+ if (reasoningEffort) {
1498
+ const key = metadata?.providerId ? toCamelCase(metadata.providerId) : "openaiCompatible";
1499
+ return { [key]: { reasoningEffort } };
1500
+ }
1501
+ return void 0;
1502
+ }
1503
+ if (isGlm52ReasoningModel(modelId)) {
1504
+ const reasoningEffort = mapCodexEffortToGlm52(effort);
1505
+ if (reasoningEffort) {
1506
+ const key = metadata?.providerId ? toCamelCase(metadata.providerId) : "openaiCompatible";
1507
+ return { [key]: { reasoningEffort } };
1508
+ }
1509
+ return void 0;
1510
+ }
1511
+ if (hasSupportedParameter(metadata, "reasoning_effort")) {
1512
+ const reasoningEffort = mapCodexEffortToOpenAICompatible(effort);
1513
+ return reasoningEffort ? { openai: { reasoningEffort }, openaiCompatible: { reasoningEffort } } : void 0;
1514
+ }
1515
+ if (hasSupportedParameter(metadata, "reasoning")) {
1516
+ const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
1517
+ const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
1518
+ return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
1519
+ }
1520
+ return void 0;
1521
+ }
1522
+ return void 0;
1523
+ }
1524
+ function deepMergeProviderOptions(a, b) {
1525
+ if (!a && !b) return void 0;
1526
+ if (!a) return b;
1527
+ if (!b) return a;
1528
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
1529
+ const out = {};
1530
+ for (const key of keys) {
1531
+ out[key] = { ...a[key] ?? {}, ...b[key] ?? {} };
1532
+ }
1533
+ return out;
1534
+ }
1167
1535
 
1168
1536
  // src/registry/io.ts
1169
1537
  import {
@@ -1379,6 +1747,7 @@ var DEFAULT_RETRYABLE = {
1379
1747
  CREDENTIAL_UNAVAILABLE: false,
1380
1748
  OAUTH_REFRESH_FAILED: true,
1381
1749
  UNSUPPORTED_MODEL: false,
1750
+ UNSUPPORTED_REASONING_LEVEL: false,
1382
1751
  UNSUPPORTED_REGISTRY_VERSION: false,
1383
1752
  PROVIDER_LOAD_FAILED: true
1384
1753
  };
@@ -1410,6 +1779,80 @@ function isRelayCoreError(err) {
1410
1779
  return err instanceof RelayCoreError;
1411
1780
  }
1412
1781
 
1782
+ // src/core/reasoning.ts
1783
+ var RELAY_REASONING_LEVELS = [
1784
+ "off",
1785
+ "none",
1786
+ "minimal",
1787
+ "low",
1788
+ "medium",
1789
+ "high",
1790
+ "xhigh",
1791
+ "max"
1792
+ ];
1793
+ function isRelayReasoningLevel(value) {
1794
+ return typeof value === "string" && RELAY_REASONING_LEVELS.includes(value);
1795
+ }
1796
+ function reasoningNpmForRoute(provider, model) {
1797
+ if (model.modelFormat === "cloud-code") return "@ai-sdk/google";
1798
+ return model.npm ?? provider.api.npm ?? "";
1799
+ }
1800
+ function resolveReasoningProviderOptions(level, provider, model, routeId) {
1801
+ if (!isRelayReasoningLevel(level)) {
1802
+ throw new RelayCoreError(
1803
+ "UNSUPPORTED_REASONING_LEVEL",
1804
+ `Unknown reasoning level "${String(level)}" \u2014 expected one of: ${RELAY_REASONING_LEVELS.join(", ")}.`,
1805
+ { providerId: provider.id, routeId }
1806
+ );
1807
+ }
1808
+ const npm = reasoningNpmForRoute(provider, model);
1809
+ const upstreamModelId = model.upstreamModelId ?? model.id;
1810
+ const metadata = {
1811
+ providerId: provider.id,
1812
+ upstreamModelId,
1813
+ ...model.apiUrl ?? provider.api.url ? { apiBaseUrl: model.apiUrl ?? provider.api.url } : {},
1814
+ ...model.supportedParameters ? { supportedParameters: model.supportedParameters } : {},
1815
+ ...model.reasoning !== void 0 ? { reasoning: model.reasoning } : {},
1816
+ ...model.interleavedReasoningField ? { interleavedReasoningField: model.interleavedReasoningField } : {}
1817
+ };
1818
+ const caps = getReasoningCapabilities(npm, upstreamModelId, metadata);
1819
+ if (caps.mode !== "controllable" || !caps.levels.includes(level)) {
1820
+ const available = caps.levels.length > 0 ? caps.levels.join(", ") : "none";
1821
+ throw new RelayCoreError(
1822
+ "UNSUPPORTED_REASONING_LEVEL",
1823
+ `Model "${model.id}" on provider "${provider.name}" does not support reasoning level "${level}" \u2014 available levels: ${available}. See capabilities.reasoningLevels from listRelayModels().`,
1824
+ { providerId: provider.id, routeId }
1825
+ );
1826
+ }
1827
+ const resolved = effortProviderOptions(npm, level, upstreamModelId, metadata);
1828
+ if (!resolved) {
1829
+ throw new RelayCoreError(
1830
+ "UNSUPPORTED_REASONING_LEVEL",
1831
+ `Model "${model.id}" on provider "${provider.name}" advertises reasoning level "${level}" but Relay has no request mapping for it \u2014 this is a relay-ai bug, please report it.`,
1832
+ { providerId: provider.id, routeId }
1833
+ );
1834
+ }
1835
+ return resolved;
1836
+ }
1837
+ async function withReasoningProviderOptions(model, providerOptions) {
1838
+ const { wrapLanguageModel: wrapLanguageModel2 } = await import("ai");
1839
+ return wrapLanguageModel2({
1840
+ // `LanguageModel` also admits a bare model-id string and the legacy v2
1841
+ // interface; everything Core builds is a concrete current-spec model.
1842
+ model,
1843
+ middleware: {
1844
+ specificationVersion: "v3",
1845
+ transformParams: async ({ params }) => ({
1846
+ ...params,
1847
+ providerOptions: deepMergeProviderOptions(
1848
+ providerOptions,
1849
+ params.providerOptions
1850
+ )
1851
+ })
1852
+ }
1853
+ });
1854
+ }
1855
+
1413
1856
  // src/core/route-id.ts
1414
1857
  var SEPARATOR = "::";
1415
1858
  function toRelayRouteId(providerId, modelId) {
@@ -1447,7 +1890,7 @@ function loadCoreRegistry(path) {
1447
1890
  }
1448
1891
  function mapReasoning(provider, model) {
1449
1892
  const base = { tools: "unknown", vision: "unknown" };
1450
- const npm = model.npm ?? provider.api.npm ?? "";
1893
+ const npm = reasoningNpmForRoute(provider, model);
1451
1894
  const upstreamModelId = model.upstreamModelId ?? model.id;
1452
1895
  try {
1453
1896
  const caps = getReasoningCapabilities(npm, upstreamModelId, {
@@ -1463,13 +1906,16 @@ function mapReasoning(provider, model) {
1463
1906
  return { ...base, reasoning: "none" };
1464
1907
  case "internal-only":
1465
1908
  return { ...base, reasoning: "fixed" };
1466
- case "controllable":
1909
+ case "controllable": {
1910
+ const levels = caps.levels.filter(isRelayReasoningLevel);
1911
+ if (levels.length === 0) return { ...base, reasoning: "fixed" };
1467
1912
  return {
1468
1913
  ...base,
1469
1914
  reasoning: "adjustable",
1470
- reasoningLevels: [...caps.levels],
1471
- defaultReasoningLevel: caps.defaultLevel
1915
+ reasoningLevels: levels,
1916
+ ...isRelayReasoningLevel(caps.defaultLevel) ? { defaultReasoningLevel: caps.defaultLevel } : {}
1472
1917
  };
1918
+ }
1473
1919
  default:
1474
1920
  return { ...base, reasoning: "unknown" };
1475
1921
  }
@@ -2637,10 +3083,21 @@ function providerRefreshToken(providerId, authType, authRef) {
2637
3083
 
2638
3084
  // src/core/antigravity-model.ts
2639
3085
  import { randomUUID as randomUUID2 } from "crypto";
2640
- var CLOUD_CODE_BASE = ANTIGRAVITY_BASE_URLS[0].replace(/\/+$/, "");
2641
- var STREAM_URL = `${CLOUD_CODE_BASE}/${ANTIGRAVITY_API_VERSION}:streamGenerateContent?alt=sse`;
2642
- var UNARY_URL = `${CLOUD_CODE_BASE}/${ANTIGRAVITY_API_VERSION}:generateContent`;
3086
+ var CLOUD_CODE_BASES = ANTIGRAVITY_BASE_URLS.map((base) => base.replace(/\/+$/, ""));
3087
+ var CLOUD_CODE_BASE = CLOUD_CODE_BASES[0];
3088
+ var STREAM_URLS = CLOUD_CODE_BASES.map((base) => `${base}/${ANTIGRAVITY_API_VERSION}:streamGenerateContent?alt=sse`);
3089
+ var UNARY_URLS = CLOUD_CODE_BASES.map((base) => `${base}/${ANTIGRAVITY_API_VERSION}:generateContent`);
2643
3090
  var SDK_BASE_URL = `${CLOUD_CODE_BASE}/v1beta`;
3091
+ var ENDPOINT_FAILOVER_STATUSES = /* @__PURE__ */ new Set([404, 408, 429]);
3092
+ function shouldTryNextEndpoint(status) {
3093
+ return ENDPOINT_FAILOVER_STATUSES.has(status) || status >= 500;
3094
+ }
3095
+ function discardResponse(response) {
3096
+ try {
3097
+ void response.body?.cancel();
3098
+ } catch {
3099
+ }
3100
+ }
2644
3101
  function unwrapCloudCodeSsePayload(payload) {
2645
3102
  const trimmed = payload.trim();
2646
3103
  if (trimmed === "" || trimmed === "[DONE]") return payload;
@@ -2699,6 +3156,12 @@ function createCloudCodeSseUnwrapper() {
2699
3156
  }
2700
3157
  function createCloudCodeFetch(options, fetchImpl) {
2701
3158
  let accessToken = options.accessToken;
3159
+ const debug = (msg) => {
3160
+ try {
3161
+ options.onDebug?.(`cloud-code: ${msg}`);
3162
+ } catch {
3163
+ }
3164
+ };
2702
3165
  return async (input, init) => {
2703
3166
  const url = requestUrl(input);
2704
3167
  const streaming = url.includes("streamGenerateContent");
@@ -2714,35 +3177,66 @@ function createCloudCodeFetch(options, fetchImpl) {
2714
3177
  request: geminiBody
2715
3178
  };
2716
3179
  const body = JSON.stringify(envelope);
2717
- const upstreamUrl = streaming ? STREAM_URL : UNARY_URL;
3180
+ const bodyByteLength = Buffer.byteLength(body, "utf8");
3181
+ const upstreamUrls = streaming ? STREAM_URLS : UNARY_URLS;
2718
3182
  const doFetch = fetchImpl ?? ((input2, init2) => globalThis.fetch(input2, init2));
2719
- const send = (token) => doFetch(upstreamUrl, {
2720
- method: "POST",
2721
- headers: {
2722
- "Content-Type": "application/json",
2723
- Authorization: `Bearer ${token}`,
2724
- "User-Agent": ANTIGRAVITY_USER_AGENT
2725
- },
2726
- body,
2727
- signal
2728
- });
2729
- let response;
2730
- try {
2731
- response = await send(accessToken);
2732
- } catch (err) {
2733
- if (isAbortError(err, signal)) throw abortError(signal, err);
2734
- throw err;
2735
- }
2736
- if (response.status === 401 && options.refreshToken && !signal?.aborted) {
2737
- const refreshed = await options.refreshToken().catch(() => null);
2738
- if (refreshed && refreshed !== accessToken && !signal?.aborted) {
2739
- accessToken = refreshed;
3183
+ const send = async (url2, token) => {
3184
+ try {
3185
+ return await doFetch(url2, {
3186
+ method: "POST",
3187
+ headers: {
3188
+ "Content-Type": "application/json",
3189
+ Authorization: `Bearer ${token}`,
3190
+ "User-Agent": ANTIGRAVITY_USER_AGENT
3191
+ },
3192
+ body,
3193
+ signal
3194
+ });
3195
+ } catch (err) {
3196
+ if (isAbortError(err, signal)) throw abortError(signal, err);
3197
+ throw err;
3198
+ }
3199
+ };
3200
+ const sendWithFailover = async (token, startIndex = 0) => {
3201
+ let lastError;
3202
+ for (let i = startIndex; i < upstreamUrls.length; i += 1) {
3203
+ const url2 = upstreamUrls[i];
3204
+ const isLast = i === upstreamUrls.length - 1;
3205
+ const where = `endpoint=${i + 1}/${upstreamUrls.length} host=${endpointHost(url2)}`;
3206
+ let response2;
2740
3207
  try {
2741
- response = await send(accessToken);
3208
+ debug(`request ${where} kind=${streaming ? "stream" : "unary"} payloadBytes=${bodyByteLength}`);
3209
+ response2 = await send(url2, token);
2742
3210
  } catch (err) {
2743
- if (isAbortError(err, signal)) throw abortError(signal, err);
2744
- throw err;
3211
+ if (isAbortError(err, signal)) throw err;
3212
+ lastError = err;
3213
+ debug(`network failure ${where} errorName=${errorName(err)}`);
2745
3214
  }
3215
+ if (response2) {
3216
+ if (isLast || !shouldTryNextEndpoint(response2.status)) {
3217
+ debug(`response ${where} status=${response2.status}`);
3218
+ return { response: response2, url: url2, index: i };
3219
+ }
3220
+ discardResponse(response2);
3221
+ lastError = new Error(`Cloud Code Assist endpoint returned ${response2.status}`);
3222
+ debug(`retryable status=${response2.status} ${where} \u2014 trying next endpoint`);
3223
+ }
3224
+ if (signal?.aborted) throw abortError(signal);
3225
+ }
3226
+ throw lastError ?? new Error("All Cloud Code Assist endpoints failed");
3227
+ };
3228
+ const tokenUsed = accessToken;
3229
+ let { response, index: servedByIndex } = await sendWithFailover(tokenUsed);
3230
+ if (response.status === 401 && options.refreshToken && !signal?.aborted) {
3231
+ debug("status=401 \u2014 refreshing credential");
3232
+ const refreshed = await options.refreshToken().catch(() => null);
3233
+ if (refreshed && refreshed !== tokenUsed && !signal?.aborted) {
3234
+ accessToken = refreshed;
3235
+ discardResponse(response);
3236
+ ({ response } = await sendWithFailover(refreshed, servedByIndex));
3237
+ debug(`retry after refresh status=${response.status}`);
3238
+ } else {
3239
+ debug(`refresh did not yield a new credential (refreshed=${refreshed ? "same" : "none"})`);
2746
3240
  }
2747
3241
  }
2748
3242
  return adaptUpstreamResponse(response, streaming);
@@ -2757,6 +3251,17 @@ async function createAntigravityCloudCodeModel(options) {
2757
3251
  });
2758
3252
  return google(options.modelId);
2759
3253
  }
3254
+ function endpointHost(url) {
3255
+ try {
3256
+ return new URL(url).host;
3257
+ } catch {
3258
+ return "unknown";
3259
+ }
3260
+ }
3261
+ function errorName(err) {
3262
+ if (err instanceof Error) return err.name || "Error";
3263
+ return typeof err;
3264
+ }
2760
3265
  function isWrappedCloudCodeBody(parsed) {
2761
3266
  return !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && "response" in parsed && parsed.response !== null && typeof parsed.response === "object";
2762
3267
  }
@@ -2858,6 +3363,8 @@ async function createRelayModel(routeId, options) {
2858
3363
  const { providerId, modelId } = parseRelayRouteId(routeId);
2859
3364
  const registry = loadCoreRegistry();
2860
3365
  const { provider, model } = findRoute(registry, providerId, modelId, routeId);
3366
+ const reasoningOptions = options?.reasoning === void 0 ? void 0 : resolveReasoningProviderOptions(options.reasoning, provider, model, routeId);
3367
+ const finish = (built) => reasoningOptions ? withReasoningProviderOptions(built, reasoningOptions) : Promise.resolve(built);
2861
3368
  if (isAntigravityCloudCodeRoute(provider, model)) {
2862
3369
  const apiKey2 = await resolveCredential(provider, routeId);
2863
3370
  const providerData2 = await resolveProviderOAuthProviderData(provider.authRef);
@@ -2870,12 +3377,13 @@ async function createRelayModel(routeId, options) {
2870
3377
  );
2871
3378
  }
2872
3379
  try {
2873
- return await createAntigravityCloudCodeModel({
3380
+ return await finish(await createAntigravityCloudCodeModel({
2874
3381
  modelId: model.upstreamModelId ?? model.id,
2875
3382
  accessToken: apiKey2,
2876
3383
  projectId,
2877
- refreshToken: providerRefreshToken(provider.id, provider.authType, provider.authRef)
2878
- });
3384
+ refreshToken: providerRefreshToken(provider.id, provider.authType, provider.authRef),
3385
+ ...options?.onDebug ? { onDebug: options.onDebug } : {}
3386
+ }));
2879
3387
  } catch (err) {
2880
3388
  if (isRelayCoreError(err)) throw err;
2881
3389
  throw new RelayCoreError(
@@ -2912,7 +3420,7 @@ async function createRelayModel(routeId, options) {
2912
3420
  ...options?.onDebug ? { onDebug: options.onDebug } : {}
2913
3421
  };
2914
3422
  try {
2915
- return await createLanguageModel(spec);
3423
+ return await finish(await createLanguageModel(spec));
2916
3424
  } catch (err) {
2917
3425
  if (isRelayCoreError(err)) throw err;
2918
3426
  throw new RelayCoreError(