@dianshuv/copilot-api 0.11.0 → 0.11.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.
Files changed (2) hide show
  1. package/dist/main.mjs +78 -34
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -1348,7 +1348,7 @@ const patchClaude = defineCommand({
1348
1348
 
1349
1349
  //#endregion
1350
1350
  //#region package.json
1351
- var version = "0.11.0";
1351
+ var version = "0.11.2";
1352
1352
 
1353
1353
  //#endregion
1354
1354
  //#region src/lib/adaptive-rate-limiter.ts
@@ -8051,11 +8051,12 @@ function translateErrorToAnthropicErrorEvent(error) {
8051
8051
 
8052
8052
  //#endregion
8053
8053
  //#region src/routes/messages/tool-call-recovery.ts
8054
- const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call)`;
8055
- const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">[\s\S]*?</(?:antml:)?invoke>`;
8056
- const LEAKED_REGION_RE = new RegExp(String.raw`(?:^|\n)[ \t]*` + ENVELOPE + String.raw`[ \t\n]*(?:` + INVOKE_BODY + String.raw`\s*)+(?:</(?:antml:)?function_calls>)?`, "g");
8057
- const INCOMPLETE_LEAK_TAIL_RE = new RegExp(String.raw`(?:^|\n)[ \t]*` + ENVELOPE + String.raw`[ \t\n]*<(?:antml:)?invoke\b[\s\S]*$`);
8058
- const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*` + ENVELOPE + String.raw`[ \t\n]*<(?:antml:)?invoke\s+name="`);
8054
+ const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
8055
+ const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
8056
+ const LEAKED_REGION_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*(?:` + INVOKE_BODY + String.raw`\s*)+(?:</(?:antml:)?function_calls>)?|` + INVOKE_BODY + String.raw`)`, "g");
8057
+ const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
8058
+ const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
8059
+ const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
8059
8060
  const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
8060
8061
  const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
8061
8062
  function coerceParamValue(raw) {
@@ -8081,19 +8082,43 @@ function parseRegionInvokes(region, knownTools) {
8081
8082
  }
8082
8083
  return calls;
8083
8084
  }
8085
+ const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
8086
+ function insideFence(text, pos) {
8087
+ let openAt = -1;
8088
+ for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
8089
+ if (m.index >= pos) break;
8090
+ openAt = m.index;
8091
+ } else if (m.index > pos) return true;
8092
+ else openAt = -1;
8093
+ return false;
8094
+ }
8095
+ function regionIsLeak(region, offset, fullText, knownTools) {
8096
+ if (insideFence(fullText, offset)) return false;
8097
+ if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
8098
+ if (!knownTools) return false;
8099
+ return parseRegionInvokes(region, knownTools).length > 0;
8100
+ }
8084
8101
  /**
8085
8102
  * Split assistant text into ordered segments — dropping leaked envelope markup
8086
8103
  * (and undeclared-tool invokes) while preserving the natural-language on either
8087
8104
  * side and the pre/tool/post ordering. Returns a single text segment when there
8088
8105
  * is no leak. Shared by both response recovery paths so they cannot diverge.
8106
+ *
8107
+ * `from` restricts the EMITTED window to `text.slice(from)` while still
8108
+ * classifying against the FULL `text` — so a streaming capture that started just
8109
+ * after an opening ``` fence still sees that fence (and the now-arrived closing
8110
+ * one) when deciding whether the region is documentation.
8089
8111
  */
8090
- function recoverSegments(text, knownTools) {
8112
+ function recoverSegments(text, knownTools, from = 0) {
8091
8113
  const segments = [];
8092
- let cursor = 0;
8114
+ let cursor = from;
8093
8115
  let sawRegion = false;
8094
8116
  for (const region of text.matchAll(LEAKED_REGION_RE)) {
8117
+ const idx = region.index;
8118
+ if (idx < from) continue;
8119
+ if (!regionIsLeak(region[0], idx, text, knownTools)) continue;
8095
8120
  sawRegion = true;
8096
- const pre = text.slice(cursor, region.index);
8121
+ const pre = text.slice(cursor, idx);
8097
8122
  if (pre.trim() !== "") segments.push({
8098
8123
  kind: "text",
8099
8124
  text: pre
@@ -8102,11 +8127,11 @@ function recoverSegments(text, knownTools) {
8102
8127
  kind: "tool",
8103
8128
  call
8104
8129
  });
8105
- cursor = region.index + region[0].length;
8130
+ cursor = idx + region[0].length;
8106
8131
  }
8107
8132
  if (!sawRegion) return [{
8108
8133
  kind: "text",
8109
- text
8134
+ text: text.slice(from)
8110
8135
  }];
8111
8136
  const post = text.slice(cursor);
8112
8137
  if (post.trim() !== "") segments.push({
@@ -8115,19 +8140,20 @@ function recoverSegments(text, knownTools) {
8115
8140
  });
8116
8141
  return segments;
8117
8142
  }
8118
- /** True when `text` contains at least one complete, envelope-wrapped leak. */
8119
- function containsLeakedToolCall(text) {
8143
+ /** True when `text` contains at least one real leak (see `regionIsLeak`). */
8144
+ function containsLeakedToolCall(text, knownTools) {
8120
8145
  if (!text.includes("invoke")) return false;
8121
- for (const _region of text.matchAll(LEAKED_REGION_RE)) return true;
8146
+ for (const region of text.matchAll(LEAKED_REGION_RE)) if (regionIsLeak(region[0], region.index, text, knownTools)) return true;
8122
8147
  return false;
8123
8148
  }
8124
8149
  /**
8125
- * Remove complete leaked tool-call regions from assistant text, preserving the
8126
- * surrounding natural-language on both sides.
8150
+ * Remove real leaked tool-call regions from assistant text, preserving the
8151
+ * surrounding natural-language on both sides. Regions that are not leaks (an
8152
+ * envelope-less <invoke> that names no declared tool) round-trip untouched.
8127
8153
  */
8128
- function stripLeakedToolCalls(text) {
8154
+ function stripLeakedToolCalls(text, knownTools) {
8129
8155
  if (!text.includes("invoke")) return text;
8130
- const stripped = text.replaceAll(LEAKED_REGION_RE, "");
8156
+ const stripped = text.replaceAll(LEAKED_REGION_RE, (match, offset) => regionIsLeak(match, offset, text, knownTools) ? "" : match);
8131
8157
  if (stripped === text) return text;
8132
8158
  return stripped.replace(/[ \t\n]+$/, "");
8133
8159
  }
@@ -8141,14 +8167,27 @@ function toolNameSet(tools) {
8141
8167
  return new Set((tools ?? []).filter((tool) => !isServerToolType(tool.type)).map((tool) => tool.name));
8142
8168
  }
8143
8169
  const EMPTIED_ASSISTANT_PLACEHOLDER = "[malformed tool call removed by proxy]";
8144
- function historyHasLeak(text) {
8170
+ const INVOKE_NAME_RE = /<(?:antml:)?invoke\s+name="([^"]+)"/;
8171
+ function findIncompleteLeakTail(text, knownTools) {
8172
+ let last;
8173
+ for (const m of text.matchAll(INVOKE_OPENER_RE)) last = m;
8174
+ if (!last || last.index === void 0) return -1;
8175
+ if (text.includes("</invoke>", last.index + last[0].length)) return -1;
8176
+ if (insideFence(text, last.index)) return -1;
8177
+ if (STARTS_WITH_ENVELOPE_RE.test(last[0])) return last.index;
8178
+ if (!knownTools) return -1;
8179
+ const name = INVOKE_NAME_RE.exec(last[0]);
8180
+ return name && knownTools.has(name[1]) ? last.index : -1;
8181
+ }
8182
+ function historyHasLeak(text, knownTools) {
8145
8183
  if (!text.includes("invoke")) return false;
8146
- return containsLeakedToolCall(text) || INCOMPLETE_LEAK_TAIL_RE.test(text);
8184
+ return containsLeakedToolCall(text, knownTools) || findIncompleteLeakTail(text, knownTools) !== -1;
8147
8185
  }
8148
- function scrubHistoryText(text) {
8149
- const stripped = stripLeakedToolCalls(text);
8150
- const final = stripped.replace(INCOMPLETE_LEAK_TAIL_RE, "");
8151
- return final === stripped ? stripped : final.replace(/[ \t\n]+$/, "");
8186
+ function scrubHistoryText(text, knownTools) {
8187
+ const stripped = stripLeakedToolCalls(text, knownTools);
8188
+ const tailStart = findIncompleteLeakTail(stripped, knownTools);
8189
+ if (tailStart === -1) return stripped;
8190
+ return stripped.slice(0, tailStart).replace(/[ \t\n]+$/, "");
8152
8191
  }
8153
8192
  /**
8154
8193
  * Request-side de-poison. Strips leaked tool-call markup (complete and truncated)
@@ -8159,6 +8198,7 @@ function scrubHistoryText(text) {
8159
8198
  */
8160
8199
  function dePoisonAssistantMessages(payload) {
8161
8200
  if (!payload.messages.some((m) => m.role === "assistant" && (typeof m.content === "string" ? m.content.includes("invoke") : m.content.some((b) => b.type === "text" && b.text.includes("invoke"))))) return payload;
8201
+ const knownTools = toolNameSet(payload.tools);
8162
8202
  let changed = false;
8163
8203
  const messages = [];
8164
8204
  for (const msg of payload.messages) {
@@ -8167,26 +8207,26 @@ function dePoisonAssistantMessages(payload) {
8167
8207
  continue;
8168
8208
  }
8169
8209
  if (typeof msg.content === "string") {
8170
- if (!historyHasLeak(msg.content)) {
8210
+ if (!historyHasLeak(msg.content, knownTools)) {
8171
8211
  messages.push(msg);
8172
8212
  continue;
8173
8213
  }
8174
8214
  changed = true;
8175
- const cleaned = scrubHistoryText(msg.content);
8215
+ const cleaned = scrubHistoryText(msg.content, knownTools);
8176
8216
  messages.push({
8177
8217
  ...msg,
8178
8218
  content: cleaned.trim() === "" ? EMPTIED_ASSISTANT_PLACEHOLDER : cleaned
8179
8219
  });
8180
8220
  continue;
8181
8221
  }
8182
- if (!msg.content.some((b) => b.type === "text" && historyHasLeak(b.text))) {
8222
+ if (!msg.content.some((b) => b.type === "text" && historyHasLeak(b.text, knownTools))) {
8183
8223
  messages.push(msg);
8184
8224
  continue;
8185
8225
  }
8186
8226
  changed = true;
8187
8227
  const content = msg.content.flatMap((b) => {
8188
- if (b.type !== "text" || !historyHasLeak(b.text)) return [b];
8189
- const cleaned = scrubHistoryText(b.text);
8228
+ if (b.type !== "text" || !historyHasLeak(b.text, knownTools)) return [b];
8229
+ const cleaned = scrubHistoryText(b.text, knownTools);
8190
8230
  return cleaned.trim() === "" ? [] : [{
8191
8231
  ...b,
8192
8232
  text: cleaned
@@ -8222,7 +8262,7 @@ function recoverLeakedToolCallsInResponse(response, knownTools) {
8222
8262
  let recoveredCall = false;
8223
8263
  const content = [];
8224
8264
  for (const block of response.content) {
8225
- if (block.type !== "text" || !containsLeakedToolCall(block.text)) {
8265
+ if (block.type !== "text" || !containsLeakedToolCall(block.text, knownTools)) {
8226
8266
  content.push(block);
8227
8267
  continue;
8228
8268
  }
@@ -8328,12 +8368,16 @@ var LeakedToolCallStreamRecovery = class {
8328
8368
  if (t.buffer.length - t.captureStart > MAX_CAPTURE) return this.abandonCapture(t);
8329
8369
  return [];
8330
8370
  }
8331
- if (!t.abandoned) {
8332
- const open = LEAK_OPEN_RE.exec(t.buffer);
8333
- if (open) return this.beginCapture(t, open.index);
8371
+ if (!t.abandoned && t.buffer.includes("invoke")) {
8372
+ for (const open of t.buffer.matchAll(LEAK_OPEN_RE)) if (this.shouldArmCapture(open[0], t.buffer, open.index)) return this.beginCapture(t, open.index);
8334
8373
  }
8335
8374
  return this.flushSafe(t);
8336
8375
  }
8376
+ shouldArmCapture(opener, buffer, openerIndex) {
8377
+ if (insideFence(buffer, openerIndex)) return false;
8378
+ if (STARTS_WITH_ENVELOPE_RE.test(opener)) return true;
8379
+ return this.knownTools !== void 0 && this.knownTools.size > 0;
8380
+ }
8337
8381
  onBlockStop(event, rawData) {
8338
8382
  const t = this.text;
8339
8383
  if (!t || event.index !== t.upstreamIndex) return [this.reindexed(event, rawData)];
@@ -8479,7 +8523,7 @@ var LeakedToolCallStreamRecovery = class {
8479
8523
  return events;
8480
8524
  }
8481
8525
  finishCapture(t) {
8482
- const segments = recoverSegments(t.buffer.slice(t.captureStart), this.knownTools);
8526
+ const segments = recoverSegments(t.buffer, this.knownTools, t.captureStart);
8483
8527
  const hasTool = segments.some((s) => s.kind === "tool");
8484
8528
  const base = t.upstreamIndex + this.extraBlocks;
8485
8529
  const events = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",