@dianshuv/copilot-api 0.10.0 → 0.11.0
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 +16 -10
- package/dist/main.mjs +530 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ make down
|
|
|
57
57
|
| Option | Description | Default |
|
|
58
58
|
|--------|-------------|---------|
|
|
59
59
|
| `--port`, `-p` | Port to listen on | 4141 |
|
|
60
|
-
| `--host`, `-H` | Host/interface to bind to
|
|
60
|
+
| `--host`, `-H` | Host/interface to bind to (`0.0.0.0` = all interfaces) | `127.0.0.1` |
|
|
61
61
|
| `--verbose`, `-v` | Enable verbose logging | false |
|
|
62
62
|
| `--account-type`, `-a` | Account type (individual, business, enterprise) | individual |
|
|
63
63
|
| `--manual` | Manual request approval mode | false |
|
|
@@ -200,17 +200,23 @@ The startup banner prints whether auth is on/off and, when on, the source
|
|
|
200
200
|
|
|
201
201
|
### Network binding
|
|
202
202
|
|
|
203
|
-
The proxy **binds
|
|
204
|
-
instance is reachable from
|
|
205
|
-
|
|
203
|
+
The proxy **binds loopback (`127.0.0.1`) by default**, so an unconfigured
|
|
204
|
+
instance is reachable only from the local machine. To expose it on your network
|
|
205
|
+
— behind auth, a tunnel, or inside a container — opt into an all-interfaces
|
|
206
|
+
bind explicitly:
|
|
206
207
|
|
|
207
208
|
```sh
|
|
208
|
-
copilot-api start --host
|
|
209
|
+
copilot-api start --host 0.0.0.0
|
|
209
210
|
```
|
|
210
211
|
|
|
211
|
-
The
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
The bind host resolves with **`--host` flag > `HOST` env > `127.0.0.1`**. Note
|
|
213
|
+
that a `HOST` env var — inherited from your shell or a container — also widens
|
|
214
|
+
the bind: `HOST=0.0.0.0` opens all interfaces just like the flag. A *blank*
|
|
215
|
+
`HOST` (`HOST=`, or `HOST=$UNSET`) is ignored and falls back to the loopback
|
|
216
|
+
default, so an accidentally-empty env var can't silently expose the proxy. The
|
|
217
|
+
startup banner always reports the **real** bind address (`127.0.0.1` for the
|
|
218
|
+
loopback default, `0.0.0.0` for an all-interfaces bind, or the narrowed host you
|
|
219
|
+
passed), so a wide-open bind is never hidden.
|
|
214
220
|
|
|
215
221
|
### Choosing a key
|
|
216
222
|
|
|
@@ -277,8 +283,8 @@ a custom header, so when auth is on they stop working:
|
|
|
277
283
|
|
|
278
284
|
Workarounds:
|
|
279
285
|
|
|
280
|
-
- Run a **separate local, unauthenticated instance** (`--
|
|
281
|
-
|
|
286
|
+
- Run a **separate local, unauthenticated instance** (no `--api-key`; the
|
|
287
|
+
default `127.0.0.1` bind keeps it local) for the browser UI, or
|
|
282
288
|
- Hit the history JSON API directly with a key, e.g.
|
|
283
289
|
`curl -H "Authorization: Bearer $COPILOT_API_KEY" http://127.0.0.1:4141/history/api/entries`.
|
|
284
290
|
|
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.
|
|
1351
|
+
var version = "0.11.0";
|
|
1352
1352
|
|
|
1353
1353
|
//#endregion
|
|
1354
1354
|
//#region src/lib/adaptive-rate-limiter.ts
|
|
@@ -1762,6 +1762,44 @@ function resolveProxyApiKey(sources) {
|
|
|
1762
1762
|
};
|
|
1763
1763
|
}
|
|
1764
1764
|
/**
|
|
1765
|
+
* Resolve the hostname the server will *actually* bind to, decided at the CLI
|
|
1766
|
+
* edge with flag-over-env precedence and a **safe loopback default** — the same
|
|
1767
|
+
* flag-over-env shape this codebase already uses to reconcile a CLI flag with
|
|
1768
|
+
* its env twin (`--api-key`/`COPILOT_API_KEY`, `--github-token`/`GH_TOKEN`).
|
|
1769
|
+
*
|
|
1770
|
+
* - `--host` flag wins when present; otherwise the `HOST` env; otherwise the
|
|
1771
|
+
* default `127.0.0.1`.
|
|
1772
|
+
* - The default is **loopback, not all-interfaces**: an unconfigured instance
|
|
1773
|
+
* must not expose `/token` (which echoes the plaintext Copilot token) and the
|
|
1774
|
+
* otherwise-unauthenticated API to the whole network. Binding every interface
|
|
1775
|
+
* is now an explicit opt-in — pass `--host 0.0.0.0` (or `HOST=0.0.0.0`).
|
|
1776
|
+
* - **flag vs env asymmetry on a blank value** (the security-critical part): an
|
|
1777
|
+
* explicit `--host` flag is the operator's deliberate choice, so a blank flag
|
|
1778
|
+
* (`--host ""` / whitespace) is taken as the wildcard-bind escape hatch and
|
|
1779
|
+
* canonicalized to an explicit `0.0.0.0` (rather than left as `""` to lean on
|
|
1780
|
+
* srvx's undocumented empty-string handling). But a *set-but-blank* `HOST` env
|
|
1781
|
+
* (`HOST=`, or `HOST=$UNSET` in a shell / compose where the var is unset →
|
|
1782
|
+
* empty — NOT a deliberate keystroke) is accidental plumbing, so it is treated
|
|
1783
|
+
* as **not provided** and falls through to the loopback default. This mirrors
|
|
1784
|
+
* `resolveProxyApiKey` trimming `""` to not-provided, so an empty `HOST` can't
|
|
1785
|
+
* silently reopen the all-interfaces-unauthenticated exposure the loopback
|
|
1786
|
+
* default exists to prevent.
|
|
1787
|
+
* - **Both sources are trimmed**: a padded `--host " 10.0.0.5 "` or
|
|
1788
|
+
* `HOST=" 10.0.0.5 "` would otherwise reach the socket bind verbatim and fail
|
|
1789
|
+
* with `ENOTFOUND`. Trimming also decides blank-ness for the rules above.
|
|
1790
|
+
*
|
|
1791
|
+
* `env` is passed in (not read here) to keep the function pure and unit-testable.
|
|
1792
|
+
*/
|
|
1793
|
+
function resolveBindHost(flag, env) {
|
|
1794
|
+
if (flag !== void 0) {
|
|
1795
|
+
const trimmed = flag.trim();
|
|
1796
|
+
return trimmed === "" ? "0.0.0.0" : trimmed;
|
|
1797
|
+
}
|
|
1798
|
+
const envTrimmed = env?.trim() ?? "";
|
|
1799
|
+
if (envTrimmed !== "") return envTrimmed;
|
|
1800
|
+
return "127.0.0.1";
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1765
1803
|
* Resolve the address the server will *actually* bind to for the startup banner
|
|
1766
1804
|
* (Issue 04).
|
|
1767
1805
|
*
|
|
@@ -8011,6 +8049,472 @@ function translateErrorToAnthropicErrorEvent(error) {
|
|
|
8011
8049
|
};
|
|
8012
8050
|
}
|
|
8013
8051
|
|
|
8052
|
+
//#endregion
|
|
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="`);
|
|
8059
|
+
const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
|
|
8060
|
+
const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
|
|
8061
|
+
function coerceParamValue(raw) {
|
|
8062
|
+
const trimmed = raw.trim();
|
|
8063
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
|
|
8064
|
+
return JSON.parse(trimmed);
|
|
8065
|
+
} catch {
|
|
8066
|
+
return raw;
|
|
8067
|
+
}
|
|
8068
|
+
return raw;
|
|
8069
|
+
}
|
|
8070
|
+
function parseRegionInvokes(region, knownTools) {
|
|
8071
|
+
const calls = [];
|
|
8072
|
+
for (const invokeMatch of region.matchAll(INVOKE_RE)) {
|
|
8073
|
+
const name = invokeMatch[1];
|
|
8074
|
+
if (knownTools && !knownTools.has(name)) continue;
|
|
8075
|
+
const input = {};
|
|
8076
|
+
for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
|
|
8077
|
+
calls.push({
|
|
8078
|
+
name,
|
|
8079
|
+
input
|
|
8080
|
+
});
|
|
8081
|
+
}
|
|
8082
|
+
return calls;
|
|
8083
|
+
}
|
|
8084
|
+
/**
|
|
8085
|
+
* Split assistant text into ordered segments — dropping leaked envelope markup
|
|
8086
|
+
* (and undeclared-tool invokes) while preserving the natural-language on either
|
|
8087
|
+
* side and the pre/tool/post ordering. Returns a single text segment when there
|
|
8088
|
+
* is no leak. Shared by both response recovery paths so they cannot diverge.
|
|
8089
|
+
*/
|
|
8090
|
+
function recoverSegments(text, knownTools) {
|
|
8091
|
+
const segments = [];
|
|
8092
|
+
let cursor = 0;
|
|
8093
|
+
let sawRegion = false;
|
|
8094
|
+
for (const region of text.matchAll(LEAKED_REGION_RE)) {
|
|
8095
|
+
sawRegion = true;
|
|
8096
|
+
const pre = text.slice(cursor, region.index);
|
|
8097
|
+
if (pre.trim() !== "") segments.push({
|
|
8098
|
+
kind: "text",
|
|
8099
|
+
text: pre
|
|
8100
|
+
});
|
|
8101
|
+
for (const call of parseRegionInvokes(region[0], knownTools)) segments.push({
|
|
8102
|
+
kind: "tool",
|
|
8103
|
+
call
|
|
8104
|
+
});
|
|
8105
|
+
cursor = region.index + region[0].length;
|
|
8106
|
+
}
|
|
8107
|
+
if (!sawRegion) return [{
|
|
8108
|
+
kind: "text",
|
|
8109
|
+
text
|
|
8110
|
+
}];
|
|
8111
|
+
const post = text.slice(cursor);
|
|
8112
|
+
if (post.trim() !== "") segments.push({
|
|
8113
|
+
kind: "text",
|
|
8114
|
+
text: post
|
|
8115
|
+
});
|
|
8116
|
+
return segments;
|
|
8117
|
+
}
|
|
8118
|
+
/** True when `text` contains at least one complete, envelope-wrapped leak. */
|
|
8119
|
+
function containsLeakedToolCall(text) {
|
|
8120
|
+
if (!text.includes("invoke")) return false;
|
|
8121
|
+
for (const _region of text.matchAll(LEAKED_REGION_RE)) return true;
|
|
8122
|
+
return false;
|
|
8123
|
+
}
|
|
8124
|
+
/**
|
|
8125
|
+
* Remove complete leaked tool-call regions from assistant text, preserving the
|
|
8126
|
+
* surrounding natural-language on both sides.
|
|
8127
|
+
*/
|
|
8128
|
+
function stripLeakedToolCalls(text) {
|
|
8129
|
+
if (!text.includes("invoke")) return text;
|
|
8130
|
+
const stripped = text.replaceAll(LEAKED_REGION_RE, "");
|
|
8131
|
+
if (stripped === text) return text;
|
|
8132
|
+
return stripped.replace(/[ \t\n]+$/, "");
|
|
8133
|
+
}
|
|
8134
|
+
/**
|
|
8135
|
+
* Declared CLIENT tool names for a payload. Server-side tools are excluded (they
|
|
8136
|
+
* are not client-executable, so a leaked server-tool invoke must not become a
|
|
8137
|
+
* client tool_use). Always returns a set — an empty set means "no declared
|
|
8138
|
+
* tools", which correctly drops every leaked invoke rather than trusting it.
|
|
8139
|
+
*/
|
|
8140
|
+
function toolNameSet(tools) {
|
|
8141
|
+
return new Set((tools ?? []).filter((tool) => !isServerToolType(tool.type)).map((tool) => tool.name));
|
|
8142
|
+
}
|
|
8143
|
+
const EMPTIED_ASSISTANT_PLACEHOLDER = "[malformed tool call removed by proxy]";
|
|
8144
|
+
function historyHasLeak(text) {
|
|
8145
|
+
if (!text.includes("invoke")) return false;
|
|
8146
|
+
return containsLeakedToolCall(text) || INCOMPLETE_LEAK_TAIL_RE.test(text);
|
|
8147
|
+
}
|
|
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]+$/, "");
|
|
8152
|
+
}
|
|
8153
|
+
/**
|
|
8154
|
+
* Request-side de-poison. Strips leaked tool-call markup (complete and truncated)
|
|
8155
|
+
* from assistant text in the inbound history so the model never sees a
|
|
8156
|
+
* text-format exemplar to imitate — breaking the self-reinforcing poisoning
|
|
8157
|
+
* loop. When stripping empties an assistant message, the message is kept with a
|
|
8158
|
+
* short placeholder rather than dropped, so role alternation is preserved.
|
|
8159
|
+
*/
|
|
8160
|
+
function dePoisonAssistantMessages(payload) {
|
|
8161
|
+
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;
|
|
8162
|
+
let changed = false;
|
|
8163
|
+
const messages = [];
|
|
8164
|
+
for (const msg of payload.messages) {
|
|
8165
|
+
if (msg.role !== "assistant") {
|
|
8166
|
+
messages.push(msg);
|
|
8167
|
+
continue;
|
|
8168
|
+
}
|
|
8169
|
+
if (typeof msg.content === "string") {
|
|
8170
|
+
if (!historyHasLeak(msg.content)) {
|
|
8171
|
+
messages.push(msg);
|
|
8172
|
+
continue;
|
|
8173
|
+
}
|
|
8174
|
+
changed = true;
|
|
8175
|
+
const cleaned = scrubHistoryText(msg.content);
|
|
8176
|
+
messages.push({
|
|
8177
|
+
...msg,
|
|
8178
|
+
content: cleaned.trim() === "" ? EMPTIED_ASSISTANT_PLACEHOLDER : cleaned
|
|
8179
|
+
});
|
|
8180
|
+
continue;
|
|
8181
|
+
}
|
|
8182
|
+
if (!msg.content.some((b) => b.type === "text" && historyHasLeak(b.text))) {
|
|
8183
|
+
messages.push(msg);
|
|
8184
|
+
continue;
|
|
8185
|
+
}
|
|
8186
|
+
changed = true;
|
|
8187
|
+
const content = msg.content.flatMap((b) => {
|
|
8188
|
+
if (b.type !== "text" || !historyHasLeak(b.text)) return [b];
|
|
8189
|
+
const cleaned = scrubHistoryText(b.text);
|
|
8190
|
+
return cleaned.trim() === "" ? [] : [{
|
|
8191
|
+
...b,
|
|
8192
|
+
text: cleaned
|
|
8193
|
+
}];
|
|
8194
|
+
});
|
|
8195
|
+
messages.push(content.length > 0 ? {
|
|
8196
|
+
...msg,
|
|
8197
|
+
content
|
|
8198
|
+
} : {
|
|
8199
|
+
...msg,
|
|
8200
|
+
content: EMPTIED_ASSISTANT_PLACEHOLDER
|
|
8201
|
+
});
|
|
8202
|
+
}
|
|
8203
|
+
return changed ? {
|
|
8204
|
+
...payload,
|
|
8205
|
+
messages
|
|
8206
|
+
} : payload;
|
|
8207
|
+
}
|
|
8208
|
+
let recoveryCounter = 0;
|
|
8209
|
+
function nextToolUseId() {
|
|
8210
|
+
recoveryCounter += 1;
|
|
8211
|
+
return `toolu_recovered_${Date.now().toString(36)}_${recoveryCounter}`;
|
|
8212
|
+
}
|
|
8213
|
+
/**
|
|
8214
|
+
* Non-streaming recovery: rewrites any assistant text block that contains a
|
|
8215
|
+
* leaked tool call into ordered [pre-text, tool_use(s), post-text] blocks,
|
|
8216
|
+
* dropping the leak markup and undeclared-tool invokes. stop_reason is flipped to
|
|
8217
|
+
* tool_use only when a real call was recovered AND upstream reported a plain
|
|
8218
|
+
* end-of-turn (so max_tokens / refusal / pause_turn survive).
|
|
8219
|
+
*/
|
|
8220
|
+
function recoverLeakedToolCallsInResponse(response, knownTools) {
|
|
8221
|
+
let changed = false;
|
|
8222
|
+
let recoveredCall = false;
|
|
8223
|
+
const content = [];
|
|
8224
|
+
for (const block of response.content) {
|
|
8225
|
+
if (block.type !== "text" || !containsLeakedToolCall(block.text)) {
|
|
8226
|
+
content.push(block);
|
|
8227
|
+
continue;
|
|
8228
|
+
}
|
|
8229
|
+
changed = true;
|
|
8230
|
+
for (const segment of recoverSegments(block.text, knownTools)) if (segment.kind === "text") content.push({
|
|
8231
|
+
type: "text",
|
|
8232
|
+
text: segment.text
|
|
8233
|
+
});
|
|
8234
|
+
else {
|
|
8235
|
+
recoveredCall = true;
|
|
8236
|
+
content.push({
|
|
8237
|
+
type: "tool_use",
|
|
8238
|
+
id: nextToolUseId(),
|
|
8239
|
+
name: segment.call.name,
|
|
8240
|
+
input: segment.call.input
|
|
8241
|
+
});
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
if (!changed) return response;
|
|
8245
|
+
const flip = recoveredCall && (response.stop_reason === "end_turn" || response.stop_reason === null);
|
|
8246
|
+
return {
|
|
8247
|
+
...response,
|
|
8248
|
+
content,
|
|
8249
|
+
stop_reason: flip ? "tool_use" : response.stop_reason
|
|
8250
|
+
};
|
|
8251
|
+
}
|
|
8252
|
+
function out(event) {
|
|
8253
|
+
return {
|
|
8254
|
+
event,
|
|
8255
|
+
data: JSON.stringify(event)
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
const TAIL_GUARD = 48;
|
|
8259
|
+
const MAX_CAPTURE = 65536;
|
|
8260
|
+
/**
|
|
8261
|
+
* Per-response streaming transformer. Feed it each parsed upstream Anthropic
|
|
8262
|
+
* event (plus the original `data` string); forward whatever it returns; call
|
|
8263
|
+
* `flush()` once the upstream stream ends.
|
|
8264
|
+
*
|
|
8265
|
+
* Identity passthrough until an envelope appears in a text block; from there it
|
|
8266
|
+
* suppresses the leaked markup, emits structured tool_use block(s) (declared
|
|
8267
|
+
* tools only) plus any trailing prose, shifts the indices of later blocks, and
|
|
8268
|
+
* flips a plain end-of-turn stop_reason to tool_use.
|
|
8269
|
+
*/
|
|
8270
|
+
var LeakedToolCallStreamRecovery = class {
|
|
8271
|
+
extraBlocks = 0;
|
|
8272
|
+
text = null;
|
|
8273
|
+
converted = false;
|
|
8274
|
+
knownTools;
|
|
8275
|
+
constructor(knownTools) {
|
|
8276
|
+
this.knownTools = knownTools;
|
|
8277
|
+
}
|
|
8278
|
+
process(event, rawData) {
|
|
8279
|
+
switch (event.type) {
|
|
8280
|
+
case "content_block_start": return this.onBlockStart(event, rawData);
|
|
8281
|
+
case "content_block_delta": return this.onBlockDelta(event, rawData);
|
|
8282
|
+
case "content_block_stop": return this.onBlockStop(event, rawData);
|
|
8283
|
+
case "message_delta": return this.onMessageDelta(event, rawData);
|
|
8284
|
+
default: return [{
|
|
8285
|
+
event,
|
|
8286
|
+
data: rawData
|
|
8287
|
+
}];
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
/** Flush any pending (buffered/capturing) text block at end of stream. */
|
|
8291
|
+
flush() {
|
|
8292
|
+
return this.flushPending();
|
|
8293
|
+
}
|
|
8294
|
+
reindexed(event, rawData) {
|
|
8295
|
+
if (this.extraBlocks === 0) return {
|
|
8296
|
+
event,
|
|
8297
|
+
data: rawData
|
|
8298
|
+
};
|
|
8299
|
+
const shifted = {
|
|
8300
|
+
...event,
|
|
8301
|
+
index: event.index + this.extraBlocks
|
|
8302
|
+
};
|
|
8303
|
+
return {
|
|
8304
|
+
event: shifted,
|
|
8305
|
+
data: JSON.stringify(shifted)
|
|
8306
|
+
};
|
|
8307
|
+
}
|
|
8308
|
+
onBlockStart(event, rawData) {
|
|
8309
|
+
if (event.content_block.type === "text") {
|
|
8310
|
+
this.text = {
|
|
8311
|
+
upstreamIndex: event.index,
|
|
8312
|
+
opened: false,
|
|
8313
|
+
buffer: "",
|
|
8314
|
+
forwarded: 0,
|
|
8315
|
+
capturing: false,
|
|
8316
|
+
captureStart: 0,
|
|
8317
|
+
abandoned: false
|
|
8318
|
+
};
|
|
8319
|
+
return [];
|
|
8320
|
+
}
|
|
8321
|
+
return [this.reindexed(event, rawData)];
|
|
8322
|
+
}
|
|
8323
|
+
onBlockDelta(event, rawData) {
|
|
8324
|
+
const t = this.text;
|
|
8325
|
+
if (!t || event.index !== t.upstreamIndex || event.delta.type !== "text_delta") return [this.reindexed(event, rawData)];
|
|
8326
|
+
t.buffer += event.delta.text;
|
|
8327
|
+
if (t.capturing) {
|
|
8328
|
+
if (t.buffer.length - t.captureStart > MAX_CAPTURE) return this.abandonCapture(t);
|
|
8329
|
+
return [];
|
|
8330
|
+
}
|
|
8331
|
+
if (!t.abandoned) {
|
|
8332
|
+
const open = LEAK_OPEN_RE.exec(t.buffer);
|
|
8333
|
+
if (open) return this.beginCapture(t, open.index);
|
|
8334
|
+
}
|
|
8335
|
+
return this.flushSafe(t);
|
|
8336
|
+
}
|
|
8337
|
+
onBlockStop(event, rawData) {
|
|
8338
|
+
const t = this.text;
|
|
8339
|
+
if (!t || event.index !== t.upstreamIndex) return [this.reindexed(event, rawData)];
|
|
8340
|
+
return this.flushPending();
|
|
8341
|
+
}
|
|
8342
|
+
onMessageDelta(event, rawData) {
|
|
8343
|
+
const flushed = this.flushPending();
|
|
8344
|
+
const isNaturalEnd = event.delta.stop_reason === "end_turn" || event.delta.stop_reason === null;
|
|
8345
|
+
if (!this.converted || !isNaturalEnd) return [...flushed, {
|
|
8346
|
+
event,
|
|
8347
|
+
data: rawData
|
|
8348
|
+
}];
|
|
8349
|
+
const rewritten = {
|
|
8350
|
+
...event,
|
|
8351
|
+
delta: {
|
|
8352
|
+
...event.delta,
|
|
8353
|
+
stop_reason: "tool_use"
|
|
8354
|
+
}
|
|
8355
|
+
};
|
|
8356
|
+
return [...flushed, out(rewritten)];
|
|
8357
|
+
}
|
|
8358
|
+
emitText(t, chunk) {
|
|
8359
|
+
const idx = t.upstreamIndex + this.extraBlocks;
|
|
8360
|
+
const events = [];
|
|
8361
|
+
if (!t.opened) {
|
|
8362
|
+
events.push(out({
|
|
8363
|
+
type: "content_block_start",
|
|
8364
|
+
index: idx,
|
|
8365
|
+
content_block: {
|
|
8366
|
+
type: "text",
|
|
8367
|
+
text: ""
|
|
8368
|
+
}
|
|
8369
|
+
}));
|
|
8370
|
+
t.opened = true;
|
|
8371
|
+
}
|
|
8372
|
+
events.push(out({
|
|
8373
|
+
type: "content_block_delta",
|
|
8374
|
+
index: idx,
|
|
8375
|
+
delta: {
|
|
8376
|
+
type: "text_delta",
|
|
8377
|
+
text: chunk
|
|
8378
|
+
}
|
|
8379
|
+
}));
|
|
8380
|
+
return events;
|
|
8381
|
+
}
|
|
8382
|
+
flushSafe(t) {
|
|
8383
|
+
const safeEnd = t.buffer.length - TAIL_GUARD;
|
|
8384
|
+
if (safeEnd <= t.forwarded) return [];
|
|
8385
|
+
const chunk = t.buffer.slice(t.forwarded, safeEnd);
|
|
8386
|
+
t.forwarded = safeEnd;
|
|
8387
|
+
return this.emitText(t, chunk);
|
|
8388
|
+
}
|
|
8389
|
+
beginCapture(t, start) {
|
|
8390
|
+
t.captureStart = start;
|
|
8391
|
+
t.capturing = true;
|
|
8392
|
+
if (start > t.forwarded) {
|
|
8393
|
+
const preamble = t.buffer.slice(t.forwarded, start);
|
|
8394
|
+
t.forwarded = start;
|
|
8395
|
+
return this.emitText(t, preamble);
|
|
8396
|
+
}
|
|
8397
|
+
return [];
|
|
8398
|
+
}
|
|
8399
|
+
abandonCapture(t) {
|
|
8400
|
+
t.capturing = false;
|
|
8401
|
+
t.abandoned = true;
|
|
8402
|
+
const events = this.emitText(t, t.buffer.slice(t.forwarded));
|
|
8403
|
+
t.forwarded = t.buffer.length;
|
|
8404
|
+
return events;
|
|
8405
|
+
}
|
|
8406
|
+
emitTextBlock(index, text) {
|
|
8407
|
+
return [
|
|
8408
|
+
out({
|
|
8409
|
+
type: "content_block_start",
|
|
8410
|
+
index,
|
|
8411
|
+
content_block: {
|
|
8412
|
+
type: "text",
|
|
8413
|
+
text: ""
|
|
8414
|
+
}
|
|
8415
|
+
}),
|
|
8416
|
+
out({
|
|
8417
|
+
type: "content_block_delta",
|
|
8418
|
+
index,
|
|
8419
|
+
delta: {
|
|
8420
|
+
type: "text_delta",
|
|
8421
|
+
text
|
|
8422
|
+
}
|
|
8423
|
+
}),
|
|
8424
|
+
out({
|
|
8425
|
+
type: "content_block_stop",
|
|
8426
|
+
index
|
|
8427
|
+
})
|
|
8428
|
+
];
|
|
8429
|
+
}
|
|
8430
|
+
emitToolBlock(index, call) {
|
|
8431
|
+
return [
|
|
8432
|
+
out({
|
|
8433
|
+
type: "content_block_start",
|
|
8434
|
+
index,
|
|
8435
|
+
content_block: {
|
|
8436
|
+
type: "tool_use",
|
|
8437
|
+
id: nextToolUseId(),
|
|
8438
|
+
name: call.name,
|
|
8439
|
+
input: {}
|
|
8440
|
+
}
|
|
8441
|
+
}),
|
|
8442
|
+
out({
|
|
8443
|
+
type: "content_block_delta",
|
|
8444
|
+
index,
|
|
8445
|
+
delta: {
|
|
8446
|
+
type: "input_json_delta",
|
|
8447
|
+
partial_json: JSON.stringify(call.input)
|
|
8448
|
+
}
|
|
8449
|
+
}),
|
|
8450
|
+
out({
|
|
8451
|
+
type: "content_block_stop",
|
|
8452
|
+
index
|
|
8453
|
+
})
|
|
8454
|
+
];
|
|
8455
|
+
}
|
|
8456
|
+
flushPending() {
|
|
8457
|
+
const t = this.text;
|
|
8458
|
+
if (!t) return [];
|
|
8459
|
+
this.text = null;
|
|
8460
|
+
if (t.capturing) return this.finishCapture(t);
|
|
8461
|
+
const events = [];
|
|
8462
|
+
if (t.buffer.length > t.forwarded) events.push(...this.emitText(t, t.buffer.slice(t.forwarded)));
|
|
8463
|
+
const idx = t.upstreamIndex + this.extraBlocks;
|
|
8464
|
+
if (!t.opened) {
|
|
8465
|
+
events.push(out({
|
|
8466
|
+
type: "content_block_start",
|
|
8467
|
+
index: idx,
|
|
8468
|
+
content_block: {
|
|
8469
|
+
type: "text",
|
|
8470
|
+
text: ""
|
|
8471
|
+
}
|
|
8472
|
+
}));
|
|
8473
|
+
t.opened = true;
|
|
8474
|
+
}
|
|
8475
|
+
events.push(out({
|
|
8476
|
+
type: "content_block_stop",
|
|
8477
|
+
index: idx
|
|
8478
|
+
}));
|
|
8479
|
+
return events;
|
|
8480
|
+
}
|
|
8481
|
+
finishCapture(t) {
|
|
8482
|
+
const segments = recoverSegments(t.buffer.slice(t.captureStart), this.knownTools);
|
|
8483
|
+
const hasTool = segments.some((s) => s.kind === "tool");
|
|
8484
|
+
const base = t.upstreamIndex + this.extraBlocks;
|
|
8485
|
+
const events = [];
|
|
8486
|
+
if (!hasTool) {
|
|
8487
|
+
const text = segments.map((s) => s.kind === "text" ? s.text : "").join("");
|
|
8488
|
+
if (text !== "") events.push(...this.emitText(t, text), out({
|
|
8489
|
+
type: "content_block_stop",
|
|
8490
|
+
index: base
|
|
8491
|
+
}));
|
|
8492
|
+
else if (t.opened) events.push(out({
|
|
8493
|
+
type: "content_block_stop",
|
|
8494
|
+
index: base
|
|
8495
|
+
}));
|
|
8496
|
+
else this.extraBlocks -= 1;
|
|
8497
|
+
return events;
|
|
8498
|
+
}
|
|
8499
|
+
this.converted = true;
|
|
8500
|
+
let cursor = base;
|
|
8501
|
+
if (t.opened) {
|
|
8502
|
+
events.push(out({
|
|
8503
|
+
type: "content_block_stop",
|
|
8504
|
+
index: base
|
|
8505
|
+
}));
|
|
8506
|
+
cursor = base + 1;
|
|
8507
|
+
}
|
|
8508
|
+
for (const [i, segment] of segments.entries()) {
|
|
8509
|
+
const idx = cursor + i;
|
|
8510
|
+
events.push(...segment.kind === "text" ? this.emitTextBlock(idx, segment.text) : this.emitToolBlock(idx, segment.call));
|
|
8511
|
+
}
|
|
8512
|
+
const clientBlocks = (t.opened ? 1 : 0) + segments.length;
|
|
8513
|
+
this.extraBlocks += clientBlocks - 1;
|
|
8514
|
+
return events;
|
|
8515
|
+
}
|
|
8516
|
+
};
|
|
8517
|
+
|
|
8014
8518
|
//#endregion
|
|
8015
8519
|
//#region src/routes/messages/direct-anthropic-handler.ts
|
|
8016
8520
|
/**
|
|
@@ -8069,7 +8573,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
8069
8573
|
});
|
|
8070
8574
|
});
|
|
8071
8575
|
}
|
|
8072
|
-
return handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, effectivePayload);
|
|
8576
|
+
return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
|
|
8073
8577
|
} catch (error) {
|
|
8074
8578
|
if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
|
|
8075
8579
|
recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
|
|
@@ -8187,6 +8691,19 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
8187
8691
|
const acc = createAnthropicStreamAccumulator();
|
|
8188
8692
|
const checkRepetition = createStreamRepetitionChecker(`anthropic:${anthropicPayload.model}`);
|
|
8189
8693
|
const serverToolFilter = createServerToolBlockFilter();
|
|
8694
|
+
const recovery = new LeakedToolCallStreamRecovery(toolNameSet(anthropicPayload.tools));
|
|
8695
|
+
const forward = async (recovered) => {
|
|
8696
|
+
const outEvent = recovered.event;
|
|
8697
|
+
processAnthropicEvent(outEvent, acc);
|
|
8698
|
+
if (outEvent.type === "content_block_start") logServerToolBlock(outEvent.content_block);
|
|
8699
|
+
const forwardData = serverToolFilter.rewriteEvent(outEvent, recovered.data);
|
|
8700
|
+
if (forwardData === null) return;
|
|
8701
|
+
const echoedData = echoForwardData(forwardData, outEvent.type, ctx);
|
|
8702
|
+
await stream.writeSSE({
|
|
8703
|
+
event: outEvent.type,
|
|
8704
|
+
data: echoedData
|
|
8705
|
+
});
|
|
8706
|
+
};
|
|
8190
8707
|
try {
|
|
8191
8708
|
for await (const rawEvent of response) {
|
|
8192
8709
|
consola.debug("Direct Anthropic raw stream event:", JSON.stringify(rawEvent));
|
|
@@ -8199,17 +8716,10 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
8199
8716
|
consola.error("Failed to parse Anthropic stream event:", parseError, rawEvent.data);
|
|
8200
8717
|
continue;
|
|
8201
8718
|
}
|
|
8202
|
-
processAnthropicEvent(event, acc);
|
|
8203
|
-
if (event.type === "content_block_start") logServerToolBlock(event.content_block);
|
|
8204
8719
|
if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
|
|
8205
|
-
const
|
|
8206
|
-
if (forwardData === null) continue;
|
|
8207
|
-
const echoedData = echoForwardData(forwardData, event.type, ctx);
|
|
8208
|
-
await stream.writeSSE({
|
|
8209
|
-
event: rawEvent.event || event.type,
|
|
8210
|
-
data: echoedData
|
|
8211
|
-
});
|
|
8720
|
+
for (const recovered of recovery.process(event, rawEvent.data)) await forward(recovered);
|
|
8212
8721
|
}
|
|
8722
|
+
for (const recovered of recovery.flush()) await forward(recovered);
|
|
8213
8723
|
recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
|
|
8214
8724
|
completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
|
|
8215
8725
|
model: acc.model || anthropicPayload.model,
|
|
@@ -8536,12 +9046,13 @@ async function handleCompletion(c) {
|
|
|
8536
9046
|
system: extractSystemPrompt(p.system)
|
|
8537
9047
|
})
|
|
8538
9048
|
});
|
|
8539
|
-
|
|
8540
|
-
|
|
9049
|
+
const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
|
|
9050
|
+
logToolInfo(sanitizedPayload);
|
|
9051
|
+
const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
|
|
8541
9052
|
const initiatorOverride = subagentMarker ? "agent" : void 0;
|
|
8542
9053
|
if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
|
|
8543
|
-
if (supportsDirectAnthropicApi(
|
|
8544
|
-
return handleTranslatedCompletion(c,
|
|
9054
|
+
if (supportsDirectAnthropicApi(sanitizedPayload.model)) return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
|
|
9055
|
+
return handleTranslatedCompletion(c, sanitizedPayload, ctx, initiatorOverride);
|
|
8545
9056
|
}
|
|
8546
9057
|
/**
|
|
8547
9058
|
* Log tool-related information for debugging
|
|
@@ -9283,7 +9794,7 @@ async function runServer(options) {
|
|
|
9283
9794
|
const visibleModels = allModels.filter((m) => !isHiddenModel(m.id, state.showAllModels));
|
|
9284
9795
|
if (visibleModels.length === 0) consola.warn("All upstream models are filtered by the hardcoded blacklist. /v1/models will return an empty list, but explicit POSTs with a hidden id still pass through to upstream. Restart with --show-all-models to see the full catalogue.");
|
|
9285
9796
|
else consola.info(`Available models:\n${visibleModels.map((m) => formatModelInfo(m)).join("\n")}`);
|
|
9286
|
-
const serverUrl = `http://${resolveClientHost(options.host,
|
|
9797
|
+
const serverUrl = `http://${resolveClientHost(options.host, void 0)}:${options.port}`;
|
|
9287
9798
|
if (options.claudeCode) {
|
|
9288
9799
|
if (visibleModels.length === 0) {
|
|
9289
9800
|
consola.error("--claude-code interactive setup needs at least one visible model. Restart with --show-all-models or update src/lib/hidden-models.ts.");
|
|
@@ -9319,7 +9830,7 @@ async function runServer(options) {
|
|
|
9319
9830
|
consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage${options.history ? `\n📜 History UI: ${serverUrl}/history` : ""}`);
|
|
9320
9831
|
for (const line of buildStartupAuthLines({
|
|
9321
9832
|
source: options.apiKeySource,
|
|
9322
|
-
bindAddress: resolveBindAddress(options.host,
|
|
9833
|
+
bindAddress: resolveBindAddress(options.host, void 0)
|
|
9323
9834
|
})) process.stdout.write(`${line}\n`);
|
|
9324
9835
|
setupShutdownHandlers();
|
|
9325
9836
|
setServerInstance(serve({
|
|
@@ -9369,7 +9880,7 @@ const start = defineCommand({
|
|
|
9369
9880
|
host: {
|
|
9370
9881
|
alias: "H",
|
|
9371
9882
|
type: "string",
|
|
9372
|
-
description: "Host/interface to bind to
|
|
9883
|
+
description: "Host/interface to bind to. Default: 127.0.0.1 (loopback only); pass 0.0.0.0 to bind all interfaces. Falls back to the HOST env var when the flag is omitted."
|
|
9373
9884
|
},
|
|
9374
9885
|
verbose: {
|
|
9375
9886
|
alias: "v",
|
|
@@ -9495,7 +10006,7 @@ const start = defineCommand({
|
|
|
9495
10006
|
});
|
|
9496
10007
|
return runServer({
|
|
9497
10008
|
port: Number.parseInt(args.port, 10),
|
|
9498
|
-
host: args.host,
|
|
10009
|
+
host: resolveBindHost(args.host, process.env.HOST),
|
|
9499
10010
|
verbose: args.verbose,
|
|
9500
10011
|
accountType: args["account-type"],
|
|
9501
10012
|
manual: args.manual,
|