@codeproxy/core 0.1.5 → 0.1.7

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.
package/README.md CHANGED
@@ -62,8 +62,11 @@ Creates a `fetch` wrapper that translates Responses API traffic to the configure
62
62
  | `dropImages` | `boolean` | Strip image parts from messages (text-only models) |
63
63
  | `timeoutMs` | `number` | Upstream request timeout |
64
64
  | `onCacheStats` | `(stats) => void` | Receive cache usage stats |
65
+ | `fallbackThoughtSignature` | `string` | Fallback Gemini thought signature for OpenAI-compatible tool call histories |
65
66
  | `fallbackUpstream` | `object` | When `dropImages: true` and the request contains images, automatically route to this upstream instead (e.g. a vision-capable model) |
66
67
 
68
+ Gemini OpenAI-compatible tool calls preserve `thought_signature` through Responses function-call items. When migrating old histories that do not include a returned signature, pass `fallbackThoughtSignature` so the next upstream tool-call request can still include `extra_content.google.thought_signature`.
69
+
67
70
  ### Translators
68
71
 
69
72
  Low-level translators are also available by namespace:
package/README.zh-CN.md CHANGED
@@ -62,8 +62,11 @@ npm install @codeproxy/core
62
62
  | `dropImages` | `boolean` | 从消息中移除图片部分(纯文本模型) |
63
63
  | `timeoutMs` | `number` | 上游请求超时 |
64
64
  | `onCacheStats` | `(stats) => void` | 接收缓存使用统计 |
65
+ | `fallbackThoughtSignature` | `string` | Gemini OpenAI 兼容工具调用历史的兜底 thought signature |
65
66
  | `fallbackUpstream` | `object` | 当 `dropImages: true` 且请求包含图片时,自动切换到该上游(如支持视觉的模型) |
66
67
 
68
+ Gemini OpenAI 兼容工具调用会在 Responses function-call item 中保留 `thought_signature`。迁移旧历史时,如果历史里没有上游返回的 signature,可以传入 `fallbackThoughtSignature`,让下一次上游工具调用请求继续带上 `extra_content.google.thought_signature`。
69
+
67
70
  ### 转换器
68
71
 
69
72
  底层转换器也按命名空间导出:
package/dist/index.cjs CHANGED
@@ -45,13 +45,21 @@ var DEFAULT_REASONING_BUDGETS = {
45
45
  function translateRequest(data, options = {}) {
46
46
  const model = data.model;
47
47
  const maxTokens = typeof data.max_output_tokens === "number" && data.max_output_tokens || typeof data.max_tokens === "number" && data.max_tokens || options.defaultMaxTokens || 8192;
48
- const systemBlocks = extractSystemBlocks(data.instructions);
48
+ let systemBlocks = extractSystemBlocks(data.instructions);
49
49
  const built = buildMessages(data, systemBlocks);
50
50
  let messages = built.messages;
51
- const hasPromptCache = built.hasPromptCache;
51
+ let hasPromptCache = built.hasPromptCache;
52
+ if (data.prompt_cache_key) {
53
+ hasPromptCache = true;
54
+ systemBlocks = markBlocksForCache(systemBlocks);
55
+ messages = markCacheBreakpoint(messages);
56
+ }
52
57
  messages = repairToolAdjacency(messages);
53
58
  messages = sanitizeMessages(messages);
54
59
  messages = ensureEndsWithUser(messages);
60
+ if (data.prompt_cache_key) {
61
+ messages = markCacheBreakpoint(messages);
62
+ }
55
63
  const request = {
56
64
  model,
57
65
  messages,
@@ -192,7 +200,15 @@ function buildMessages(data, systemBlocks) {
192
200
  } else if (part && typeof part === "object") {
193
201
  const contentPart = part;
194
202
  if (contentPart.type === "input_text" || contentPart.type === "text" || contentPart.type === "output_text") {
195
- contentBlocks.push({ type: "text", text: String(contentPart.text ?? "") });
203
+ const textBlock = {
204
+ type: "text",
205
+ text: String(contentPart.text ?? "")
206
+ };
207
+ const cc = contentPart.cache_control;
208
+ if (cc) {
209
+ textBlock.cache_control = cc;
210
+ }
211
+ contentBlocks.push(textBlock);
196
212
  } else if (contentPart.type === "input_image" || contentPart.type === "image" || contentPart.type === "image_url") {
197
213
  const imgUrlPart = contentPart;
198
214
  const imgUrl = imgUrlPart.image_url;
@@ -510,6 +526,48 @@ function ensureEndsWithUser(messages) {
510
526
  }
511
527
  return [...messages, { role: "user", content: [{ type: "text", text: "Continue." }] }];
512
528
  }
529
+ function markBlocksForCache(blocks) {
530
+ let count = 0;
531
+ for (const block of blocks) {
532
+ if (!block.cache_control) {
533
+ block.cache_control = { type: "ephemeral" };
534
+ count++;
535
+ if (count >= 3) {
536
+ break;
537
+ }
538
+ }
539
+ }
540
+ return blocks;
541
+ }
542
+ function markCacheBreakpoint(messages) {
543
+ for (const msg of messages) {
544
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
545
+ for (let j = msg.content.length - 1; j >= 0; j--) {
546
+ const block = msg.content[j];
547
+ if (block.type === "text") {
548
+ if (!block.cache_control) {
549
+ block.cache_control = { type: "ephemeral" };
550
+ }
551
+ return messages;
552
+ }
553
+ }
554
+ }
555
+ }
556
+ for (const msg of messages) {
557
+ if (msg.role === "user" && Array.isArray(msg.content)) {
558
+ for (let j = msg.content.length - 1; j >= 0; j--) {
559
+ const block = msg.content[j];
560
+ if (block.type === "text") {
561
+ if (!block.cache_control) {
562
+ block.cache_control = { type: "ephemeral" };
563
+ }
564
+ return messages;
565
+ }
566
+ }
567
+ }
568
+ }
569
+ return messages;
570
+ }
513
571
 
514
572
  // src/utils/json.ts
515
573
  function safeJsonParse(text) {
@@ -785,6 +843,8 @@ var StreamTranslator = class {
785
843
  *finalize() {
786
844
  const items = [];
787
845
  if (this.textItem) {
846
+ this.textItem.status = "completed";
847
+ this.textItem.content[0].text = this.textBuffer;
788
848
  items.push({ index: this.textItemIndex, item: this.textItem });
789
849
  }
790
850
  for (const block of this.blocks.values()) {
@@ -896,15 +956,22 @@ var StreamTranslator = class {
896
956
  if (btype === "tool_use") {
897
957
  const outputIndex = this.outputCounter++;
898
958
  const callId = block.id ?? makeId("call");
959
+ const initialInput = typeof block.input === "object" && block.input !== null ? jsonStringifySafe(block.input) : "";
960
+ const hasInitialInput = initialInput !== "" && initialInput !== "{}";
899
961
  const item = {
900
962
  id: callId,
901
963
  type: "function_call",
902
964
  status: "in_progress",
903
965
  name: block.name ?? "",
904
- arguments: "",
966
+ arguments: hasInitialInput ? initialInput : "",
905
967
  call_id: callId
906
968
  };
907
- this.blocks.set(index, { type: "tool_use", outputIndex, item, buffer: "" });
969
+ this.blocks.set(index, {
970
+ type: "tool_use",
971
+ outputIndex,
972
+ item,
973
+ buffer: hasInitialInput ? initialInput : ""
974
+ });
908
975
  yield this.makeEvent("response.output_item.added", {
909
976
  response_id: this.responseId,
910
977
  output_index: outputIndex,
@@ -1023,7 +1090,7 @@ function translateRequest2(data, options = {}) {
1023
1090
  continue;
1024
1091
  }
1025
1092
  const rawItem = raw;
1026
- processInputItem(rawItem, messages, options.dropImages);
1093
+ processInputItem(rawItem, messages, options);
1027
1094
  }
1028
1095
  const request = {
1029
1096
  model: data.model,
@@ -1083,7 +1150,7 @@ function buildSystemContent(instructions) {
1083
1150
  }
1084
1151
  return out;
1085
1152
  }
1086
- function processInputItem(item, messages, dropImages) {
1153
+ function processInputItem(item, messages, options) {
1087
1154
  const itemType = String(item.type) || "message";
1088
1155
  const getLastAssistant = () => {
1089
1156
  const last = messages[messages.length - 1];
@@ -1145,7 +1212,7 @@ function processInputItem(item, messages, dropImages) {
1145
1212
  } else if (contentPart.type === "reasoning_text") {
1146
1213
  reasoningContent += String(contentPart.text ?? "");
1147
1214
  } else if (contentPart.type === "input_image" || contentPart.type === "image" || contentPart.type === "image_url") {
1148
- if (dropImages) {
1215
+ if (options.dropImages) {
1149
1216
  continue;
1150
1217
  }
1151
1218
  let url = "";
@@ -1220,7 +1287,7 @@ function processInputItem(item, messages, dropImages) {
1220
1287
  return;
1221
1288
  }
1222
1289
  if (itemType === "function_call" || itemType === "commandExecution" || itemType === "local_shell_call" || itemType === "fileChange" || itemType === "custom_tool_call" || itemType === "web_search_call") {
1223
- processToolCall(item, messages, getLastAssistant);
1290
+ processToolCall(item, messages, getLastAssistant, options.fallbackThoughtSignature);
1224
1291
  return;
1225
1292
  }
1226
1293
  if (itemType === "function_call_output" || itemType === "commandExecutionOutput" || itemType === "fileChangeOutput" || itemType === "custom_tool_call_output") {
@@ -1228,7 +1295,7 @@ function processInputItem(item, messages, dropImages) {
1228
1295
  return;
1229
1296
  }
1230
1297
  }
1231
- function processToolCall(item, messages, getLastAssistant) {
1298
+ function processToolCall(item, messages, getLastAssistant, fallbackThoughtSignature) {
1232
1299
  const callId = String(item.call_id ?? "") || String(item.id ?? "") || makeId("call");
1233
1300
  let name = item.name === void 0 ? void 0 : String(item.name);
1234
1301
  const itemType = item.type === void 0 ? void 0 : String(item.type);
@@ -1274,16 +1341,18 @@ function processToolCall(item, messages, getLastAssistant) {
1274
1341
  if (!amsg.tool_calls) {
1275
1342
  amsg.tool_calls = [];
1276
1343
  }
1277
- amsg.tool_calls.push({
1344
+ const toolCall = {
1278
1345
  id: callId,
1279
1346
  type: "function",
1280
1347
  function: { name, arguments: argsStr }
1281
- });
1282
- const sig = item.thought_signature;
1348
+ };
1349
+ const sig = item.thought_signature ?? fallbackThoughtSignature;
1283
1350
  const thought = item.thought;
1284
1351
  if (typeof sig === "string" && sig) {
1352
+ toolCall.extra_content = { google: { thought_signature: sig } };
1285
1353
  amsg.thought_signature = sig;
1286
1354
  }
1355
+ amsg.tool_calls.push(toolCall);
1287
1356
  if (typeof thought === "string" && thought) {
1288
1357
  amsg.reasoning_content = (amsg.reasoning_content ?? "") + thought;
1289
1358
  }
@@ -1487,6 +1556,10 @@ function mapToolCallToOutput(tc) {
1487
1556
  arguments: args,
1488
1557
  call_id: callId
1489
1558
  };
1559
+ const thoughtSignature = getThoughtSignature(tc);
1560
+ if (thoughtSignature) {
1561
+ item.thought_signature = thoughtSignature;
1562
+ }
1490
1563
  if (SHELL_TOOL_NAMES3.has(name)) {
1491
1564
  item.type = "local_shell_call";
1492
1565
  const parsed = safeJsonParse(args);
@@ -1494,6 +1567,10 @@ function mapToolCallToOutput(tc) {
1494
1567
  }
1495
1568
  return item;
1496
1569
  }
1570
+ function getThoughtSignature(tc) {
1571
+ const sig = tc.extra_content?.google?.thought_signature ?? tc.thought_signature;
1572
+ return typeof sig === "string" && sig ? sig : void 0;
1573
+ }
1497
1574
 
1498
1575
  // src/translate/openai/translateStream.ts
1499
1576
  var SHELL_TOOL_NAMES4 = /* @__PURE__ */ new Set(["shell", "container.exec", "shell_command"]);
@@ -1596,6 +1673,10 @@ var StreamTranslator2 = class {
1596
1673
  });
1597
1674
  }
1598
1675
  const fn = tc.function;
1676
+ const thoughtSignature = getThoughtSignature2(tc);
1677
+ if (thoughtSignature) {
1678
+ state.item.thought_signature = thoughtSignature;
1679
+ }
1599
1680
  if (fn?.name) {
1600
1681
  state.item.name = (state.item.name ?? "") + fn.name;
1601
1682
  }
@@ -1707,6 +1788,10 @@ var StreamTranslator2 = class {
1707
1788
  };
1708
1789
  }
1709
1790
  };
1791
+ function getThoughtSignature2(tc) {
1792
+ const sig = tc.extra_content?.google?.thought_signature ?? tc.thought_signature;
1793
+ return typeof sig === "string" && sig ? sig : void 0;
1794
+ }
1710
1795
 
1711
1796
  // src/fetch.ts
1712
1797
  function createResponsesFetch(options) {
@@ -1714,7 +1799,7 @@ function createResponsesFetch(options) {
1714
1799
  throw new Error("baseUrl is required");
1715
1800
  }
1716
1801
  const rawFormat = options.upstreamFormat;
1717
- const format = rawFormat ? normalizeFormat(rawFormat) : inferFormatFromUrl(options.baseUrl) ?? "openai-chat";
1802
+ const format = rawFormat ? normalizeFormat(rawFormat) : inferFormatFromUrl(options.baseUrl) ?? inferFormatFromModel(options.model) ?? "openai-chat";
1718
1803
  if (!format) {
1719
1804
  throw new Error(
1720
1805
  `Unsupported upstream format: ${options.upstreamFormat}. Use 'anthropic' or 'openai-chat'`
@@ -1885,7 +1970,8 @@ async function handleResponses(request, format, options, baseFetch, incomingHead
1885
1970
  options.baseUrl,
1886
1971
  dropImages,
1887
1972
  options.reasoning_effort,
1888
- options.thinking
1973
+ options.thinking,
1974
+ options.fallbackThoughtSignature
1889
1975
  );
1890
1976
  const upstreamHeaders = buildUpstreamHeaders(format, options, incomingHeaders);
1891
1977
  const upstream = await baseFetch(resolvedUrl, {
@@ -1941,7 +2027,7 @@ function buildRequestMetadata(request, temperature, top_p) {
1941
2027
  metadata: request.metadata ?? {}
1942
2028
  };
1943
2029
  }
1944
- function buildUpstreamBody(request, format, streaming, baseUrl, dropImages, reasoning_effort, thinking) {
2030
+ function buildUpstreamBody(request, format, streaming, baseUrl, dropImages, reasoning_effort, thinking, fallbackThoughtSignature) {
1945
2031
  if (format === "anthropic") {
1946
2032
  const { request: ar } = translateRequest(request);
1947
2033
  ar.stream = streaming;
@@ -1972,7 +2058,10 @@ function buildUpstreamBody(request, format, streaming, baseUrl, dropImages, reas
1972
2058
  requestMetadata: buildRequestMetadata(request, ar.temperature, ar.top_p)
1973
2059
  };
1974
2060
  }
1975
- const { request: cr } = translateRequest2(request, { dropImages });
2061
+ const { request: cr } = translateRequest2(request, {
2062
+ dropImages,
2063
+ fallbackThoughtSignature
2064
+ });
1976
2065
  cr.stream = streaming;
1977
2066
  if (streaming) {
1978
2067
  cr.stream_options = { include_usage: true };