@jacobbd/relay-ai 0.6.1 → 0.6.2

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.
@@ -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.6.1",
14
+ version: "0.6.2",
15
15
  publishConfig: {
16
16
  access: "public"
17
17
  },
@@ -4271,6 +4271,16 @@ function gatewayProviderId(model) {
4271
4271
  function gatewayAliasId(model) {
4272
4272
  return aliasModelId(model.id, gatewayProviderId(model));
4273
4273
  }
4274
+ function openAiIdCollisions(models) {
4275
+ const counts = /* @__PURE__ */ new Map();
4276
+ for (const model of models) counts.set(model.id, (counts.get(model.id) ?? 0) + 1);
4277
+ const collisions = /* @__PURE__ */ new Set();
4278
+ for (const [id, count] of counts) if (count > 1) collisions.add(id);
4279
+ return collisions;
4280
+ }
4281
+ function openAiExposedId(model, collisions) {
4282
+ return collisions.has(model.id) ? `${gatewayProviderId(model)}/${model.id}` : model.id;
4283
+ }
4274
4284
  function exposedGatewayAliasId(model, opts) {
4275
4285
  const alias = gatewayAliasId(model);
4276
4286
  return opts?.maskGatewayIds ? maskGatewayModelId(alias) : alias;
@@ -4290,8 +4300,11 @@ function formatGatewayAnthropicModels(models, opts) {
4290
4300
  }
4291
4301
  function createGatewayModelCatalog(models, opts) {
4292
4302
  const byId = /* @__PURE__ */ new Map();
4303
+ const collisions = openAiIdCollisions(models);
4293
4304
  for (const model of models) {
4294
- byId.set(model.id, model);
4305
+ if (!byId.has(model.id)) byId.set(model.id, model);
4306
+ const scopedId = openAiExposedId(model, collisions);
4307
+ if (scopedId !== model.id) byId.set(scopedId, model);
4295
4308
  const alias = exposedGatewayAliasId(model, opts);
4296
4309
  if (alias !== model.id) byId.set(alias, model);
4297
4310
  if (opts?.maskGatewayIds) {
@@ -4308,14 +4321,14 @@ function upstreamModelId(model) {
4308
4321
  const id = model.upstreamModelId ?? model.id;
4309
4322
  return id.replace(/\[1m\]$/i, "");
4310
4323
  }
4311
- function buildDedupedModelRows(models, opts) {
4324
+ function buildDedupedModelRows(models, opts, collisions = openAiIdCollisions(models)) {
4312
4325
  const seen = /* @__PURE__ */ new Set();
4313
4326
  const rows = [];
4314
4327
  for (const model of [...models].sort((a, b) => a.name.localeCompare(b.name))) {
4315
4328
  const row = {
4316
4329
  name: model.name,
4317
4330
  anthropicId: exposedGatewayAliasId(model, opts),
4318
- openaiId: model.id
4331
+ openaiId: openAiExposedId(model, collisions)
4319
4332
  };
4320
4333
  const key = `${row.name}\0${row.anthropicId}\0${row.openaiId}`;
4321
4334
  if (seen.has(key)) continue;
@@ -4328,10 +4341,11 @@ function supportsDirectOpenAIChatCompletions(model) {
4328
4341
  return model.modelFormat === "openai" && (!!model.completionsUrl || model.sourceBackend === "zen" || model.sourceBackend === "go");
4329
4342
  }
4330
4343
  function formatOpenAIModels(models) {
4344
+ const collisions = openAiIdCollisions(models);
4331
4345
  return {
4332
4346
  object: "list",
4333
4347
  data: models.map((model) => ({
4334
- id: model.id,
4348
+ id: openAiExposedId(model, collisions),
4335
4349
  object: "model",
4336
4350
  created: CREATED_AT_UNIX,
4337
4351
  owned_by: model.sourceBackend
@@ -8721,8 +8735,9 @@ function translateOpenAiRequest(body) {
8721
8735
  break;
8722
8736
  case "assistant": {
8723
8737
  const parts = [];
8724
- if (typeof msg.content === "string" && msg.content) {
8725
- parts.push({ type: "text", text: msg.content });
8738
+ const assistantText = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.filter((p8) => p8?.type === "text" && typeof p8.text === "string").map((p8) => p8.text).join("") : "";
8739
+ if (assistantText) {
8740
+ parts.push({ type: "text", text: assistantText });
8726
8741
  }
8727
8742
  for (const tc of msg.tool_calls ?? []) {
8728
8743
  parts.push({
@@ -8783,14 +8798,31 @@ function translateOpenAiRequest(body) {
8783
8798
  maxOutputTokens: body.max_completion_tokens ?? body.max_tokens
8784
8799
  };
8785
8800
  }
8801
+ function toOpenAiFinishReason(reason) {
8802
+ switch (reason) {
8803
+ case "tool-calls":
8804
+ return "tool_calls";
8805
+ case "content-filter":
8806
+ return "content_filter";
8807
+ case "length":
8808
+ return "length";
8809
+ case "stop":
8810
+ return "stop";
8811
+ default:
8812
+ return "stop";
8813
+ }
8814
+ }
8786
8815
  async function generateOpenAiResponse(model, params, responseModelId) {
8787
8816
  const result = await generateText2({ model, ...params });
8788
8817
  const message = { role: "assistant", content: result.text || null };
8818
+ if (result.reasoningText || result.reasoning) {
8819
+ message.reasoning_content = result.reasoningText ?? result.reasoning;
8820
+ }
8789
8821
  if (result.toolCalls?.length) {
8790
8822
  message.tool_calls = result.toolCalls.map((tc) => ({
8791
8823
  id: tc.toolCallId,
8792
8824
  type: "function",
8793
- function: { name: tc.toolName, arguments: JSON.stringify(tc.args) }
8825
+ function: { name: tc.toolName, arguments: JSON.stringify(tc.input ?? tc.args ?? {}) }
8794
8826
  }));
8795
8827
  }
8796
8828
  return {
@@ -8798,7 +8830,7 @@ async function generateOpenAiResponse(model, params, responseModelId) {
8798
8830
  object: "chat.completion",
8799
8831
  created: Math.floor(Date.now() / 1e3),
8800
8832
  model: responseModelId,
8801
- choices: [{ index: 0, message, finish_reason: result.finishReason || "stop" }],
8833
+ choices: [{ index: 0, message, finish_reason: toOpenAiFinishReason(result.finishReason) }],
8802
8834
  usage: {
8803
8835
  prompt_tokens: result.usage?.promptTokens ?? 0,
8804
8836
  completion_tokens: result.usage?.completionTokens ?? 0,
@@ -8806,7 +8838,7 @@ async function generateOpenAiResponse(model, params, responseModelId) {
8806
8838
  }
8807
8839
  };
8808
8840
  }
8809
- async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
8841
+ async function streamOpenAiResponse(model, params, responseModelId, onChunk, log7) {
8810
8842
  const { fullStream } = streamText2({ model, ...params });
8811
8843
  const baseData = {
8812
8844
  id: `chatcmpl-${Date.now()}`,
@@ -8817,23 +8849,68 @@ async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
8817
8849
  const send = (delta, finish_reason = null) => onChunk(`data: ${JSON.stringify({ ...baseData, choices: [{ index: 0, delta, finish_reason }] })}
8818
8850
 
8819
8851
  `);
8852
+ const streamedToolIndex = /* @__PURE__ */ new Map();
8853
+ let nextToolIndex = 0;
8854
+ const seenPartTypes = /* @__PURE__ */ new Set();
8855
+ let toolCallChunksEmitted = 0;
8820
8856
  for await (const part of fullStream) {
8821
8857
  const p8 = part;
8858
+ seenPartTypes.add(p8.type);
8822
8859
  switch (p8.type) {
8823
8860
  case "text-delta":
8824
8861
  send({ role: "assistant", content: p8.textDelta ?? p8.text ?? "" });
8825
8862
  break;
8863
+ case "reasoning-delta":
8864
+ send({ role: "assistant", reasoning_content: p8.text ?? p8.delta ?? "" });
8865
+ break;
8826
8866
  case "tool-input-start":
8827
- case "tool-call-streaming-start":
8828
- send({ role: "assistant", tool_calls: [{ index: 0, id: p8.id ?? p8.toolCallId, type: "function", function: { name: p8.toolName, arguments: "" } }] });
8867
+ case "tool-call-streaming-start": {
8868
+ const id = p8.id ?? p8.toolCallId ?? "";
8869
+ const index = nextToolIndex++;
8870
+ streamedToolIndex.set(id, index);
8871
+ send({ role: "assistant", tool_calls: [{ index, id, type: "function", function: { name: p8.toolName, arguments: "" } }] });
8872
+ toolCallChunksEmitted++;
8829
8873
  break;
8874
+ }
8830
8875
  case "tool-input-delta":
8831
- case "tool-call-delta":
8832
- send({ tool_calls: [{ index: 0, function: { arguments: p8.delta ?? p8.text ?? p8.argsTextDelta ?? "" } }] });
8876
+ case "tool-call-delta": {
8877
+ const id = p8.id ?? p8.toolCallId ?? "";
8878
+ const index = streamedToolIndex.get(id) ?? 0;
8879
+ send({ tool_calls: [{ index, function: { arguments: p8.delta ?? p8.text ?? p8.argsTextDelta ?? "" } }] });
8833
8880
  break;
8881
+ }
8882
+ case "tool-call": {
8883
+ const id = p8.toolCallId ?? "";
8884
+ if (streamedToolIndex.has(id)) break;
8885
+ const index = nextToolIndex++;
8886
+ send({
8887
+ role: "assistant",
8888
+ tool_calls: [{ index, id, type: "function", function: { name: p8.toolName, arguments: JSON.stringify(p8.input ?? {}) } }]
8889
+ });
8890
+ toolCallChunksEmitted++;
8891
+ break;
8892
+ }
8834
8893
  case "finish":
8835
- send({}, p8.finishReason || "stop");
8894
+ log7?.(() => `openai stream parts=[${[...seenPartTypes].join(",")}] toolCallChunks=${toolCallChunksEmitted} finishReason=${p8.finishReason}`);
8895
+ send({}, toOpenAiFinishReason(p8.finishReason));
8836
8896
  break;
8897
+ case "error": {
8898
+ const errMsg = typeof p8.error === "string" ? p8.error : formatUpstreamError(p8.error);
8899
+ log7?.(() => `openai stream error parts=[${[...seenPartTypes].join(",")}]: ${errMsg}`);
8900
+ log7?.(() => {
8901
+ try {
8902
+ return `openai stream error raw: ${JSON.stringify(p8.error, Object.getOwnPropertyNames(p8.error ?? {})).slice(0, 3e3)}`;
8903
+ } catch {
8904
+ return `openai stream error raw: (unserializable) ${String(p8.error)}`;
8905
+ }
8906
+ });
8907
+ send({ role: "assistant", content: `
8908
+
8909
+ [relay-ai upstream error: ${errMsg}]` });
8910
+ send({}, "stop");
8911
+ onChunk("data: [DONE]\n\n");
8912
+ return;
8913
+ }
8837
8914
  }
8838
8915
  }
8839
8916
  onChunk("data: [DONE]\n\n");
@@ -9023,6 +9100,25 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9023
9100
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
9024
9101
  return;
9025
9102
  }
9103
+ plog(() => `openai-chat-completions raw body: ${JSON.stringify(body).slice(0, 6e3)}`);
9104
+ plog(() => {
9105
+ const messages = Array.isArray(body.messages) ? body.messages : [];
9106
+ const summary = messages.map((m, i) => {
9107
+ const msg = m;
9108
+ const contentType = msg.content === null ? "null" : Array.isArray(msg.content) ? "array" : typeof msg.content;
9109
+ let partShapes = "";
9110
+ if (Array.isArray(msg.content)) {
9111
+ partShapes = " parts=[" + msg.content.map((p8) => {
9112
+ const part = p8;
9113
+ const keys = Object.keys(part).join(",");
9114
+ const textLen = typeof part.text === "string" ? part.text.length : void 0;
9115
+ return `{type=${part.type} keys=${keys}${textLen !== void 0 ? ` textLen=${textLen}` : ""}}`;
9116
+ }).join(",") + "]";
9117
+ }
9118
+ return `#${i} role=${msg.role} hasContent=${"content" in msg} contentType=${contentType} toolCalls=${Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0}${partShapes}`;
9119
+ });
9120
+ return `openai-chat-completions message shapes: [${summary.join(" | ")}]`;
9121
+ });
9026
9122
  const model = lookupModel(res, options.catalog, body.model);
9027
9123
  if (!model) return;
9028
9124
  if (supportsDirectOpenAIChatCompletions(model)) {
@@ -9032,7 +9128,9 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9032
9128
  }
9033
9129
  const completionsUrl = model.completionsUrl ? model.completionsUrl : `${backendFor(options, model).baseUrl}/v1/chat/completions`;
9034
9130
  const apiKey2 = model.apiKey ?? options.apiKey;
9035
- await relayAnthropicMessages(res, completionsUrl, body, apiKey2, Boolean(body.stream));
9131
+ const forwardBody = { ...body, model: upstreamModelId(model) };
9132
+ plog(() => `openai-direct-passthrough \u2192 ${completionsUrl} model=${forwardBody.model} stream=${Boolean(body.stream)}`);
9133
+ await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), void 0, void 0, (message) => plog(message));
9036
9134
  return;
9037
9135
  }
9038
9136
  const npm = model.npm || (model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : void 0);
@@ -9054,7 +9152,7 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9054
9152
  "Cache-Control": "no-cache",
9055
9153
  "Connection": "keep-alive"
9056
9154
  });
9057
- await streamOpenAiResponse(languageModel, params, responseModelId, (chunk) => res.write(chunk));
9155
+ await streamOpenAiResponse(languageModel, params, responseModelId, (chunk) => res.write(chunk), plog);
9058
9156
  res.end();
9059
9157
  } else {
9060
9158
  const response = await generateOpenAiResponse(languageModel, params, responseModelId);
@@ -9395,10 +9493,11 @@ function formatModelCatalogLines(models, gateway) {
9395
9493
  }
9396
9494
  list.push(model);
9397
9495
  }
9496
+ const collisions = openAiIdCollisions(models);
9398
9497
  const lines = ["Model catalog:", ""];
9399
9498
  const sortedGroups = [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]));
9400
9499
  for (const [label, groupModels] of sortedGroups) {
9401
- const rows = buildDedupedModelRows(groupModels, gateway);
9500
+ const rows = buildDedupedModelRows(groupModels, gateway, collisions);
9402
9501
  const hiddenDuplicates = groupModels.length - rows.length;
9403
9502
  const duplicateNote = hiddenDuplicates > 0 ? `, ${hiddenDuplicates} duplicate${hiddenDuplicates !== 1 ? "s" : ""} hidden` : "";
9404
9503
  const nameWidth = cappedWidth(rows.map((row) => row.name), "Model", 28);
@@ -10877,6 +10976,7 @@ export {
10877
10976
  appendCodexBodyDump,
10878
10977
  getGeminiProxyDebugLogPath,
10879
10978
  getUiDebugLogPath,
10979
+ getServerDebugLogPath,
10880
10980
  makeTraceLogger,
10881
10981
  writeSecureLogLine,
10882
10982
  printTraceLog,
@@ -10892,6 +10992,7 @@ export {
10892
10992
  extractApiKey,
10893
10993
  sendJson,
10894
10994
  gatewayProviderLabel,
10995
+ openAiIdCollisions,
10895
10996
  createGatewayModelCatalog,
10896
10997
  buildDedupedModelRows,
10897
10998
  grabRoundTripSignature,
@@ -10962,4 +11063,4 @@ export {
10962
11063
  supportsClaudeTransparentMode,
10963
11064
  buildHttpProxyRoutes
10964
11065
  };
10965
- //# sourceMappingURL=chunk-P4S42QJK.js.map
11066
+ //# sourceMappingURL=chunk-I3I5LKZI.js.map