copillm 0.2.3 → 0.2.5
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 +1 -0
- package/dist/agentconfig/apply.js +4 -3
- package/dist/agentconfig/load.js +34 -1
- package/dist/agentconfig/schema.js +22 -0
- package/dist/agents/registry.js +80 -6
- package/dist/cli/auth/ensure.js +30 -0
- package/dist/cli/auth/runAuth.js +38 -0
- package/dist/cli/commands/agents/claude.js +47 -0
- package/dist/cli/commands/agents/codex.js +48 -0
- package/dist/cli/commands/agents/copilot.js +49 -0
- package/dist/cli/commands/agents/pi.js +47 -0
- package/dist/cli/commands/agents/shared.js +28 -0
- package/dist/cli/commands/auth.js +99 -0
- package/dist/cli/commands/daemon.js +358 -0
- package/dist/cli/commands/env.js +135 -0
- package/dist/cli/commands/models.js +80 -0
- package/dist/cli/configCommands.js +10 -0
- package/dist/cli/copillmFlags.js +111 -0
- package/dist/cli/daemon/ensureRunning.js +65 -0
- package/dist/cli/daemon/lifecycle.js +61 -0
- package/dist/cli/daemon/probes.js +68 -0
- package/dist/cli/daemon/runDaemon.js +102 -0
- package/dist/cli/daemon/selfSpawn.js +15 -0
- package/dist/cli/daemon/spawnEnv.js +12 -0
- package/dist/cli/index.js +41 -0
- package/dist/cli/integrations/banner.js +51 -0
- package/dist/cli/integrations/claudeExport.js +14 -0
- package/dist/cli/integrations/refreshCodex.js +19 -0
- package/dist/cli/integrations/refreshPi.js +17 -0
- package/dist/cli/packageInfo.js +29 -0
- package/dist/cli/shared/backends.js +31 -0
- package/dist/cli/shared/debug.js +44 -0
- package/dist/cli/shared/deprecation.js +7 -0
- package/dist/cli/shared/exitCodes.js +9 -0
- package/dist/cli/shared/output.js +14 -0
- package/dist/cli/shared/parseAgent.js +6 -0
- package/dist/cli/updateNotifier.js +223 -0
- package/dist/cli.js +1 -1355
- package/dist/server/errors.js +195 -0
- package/dist/server/proxy.js +50 -885
- package/dist/server/routes/debug.js +65 -0
- package/dist/server/routes/health.js +32 -0
- package/dist/server/routes/models.js +41 -0
- package/dist/server/routes/proxyForward.js +108 -0
- package/dist/server/routes/shared.js +161 -0
- package/dist/server/upstream/copilotClient.js +137 -0
- package/dist/server/upstream/streaming.js +146 -0
- package/package.json +7 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { pipeline } from "node:stream/promises";
|
|
3
|
+
import { openAIToAnthropic, ProtocolTranslationError } from "../../translation/openaiAnthropic.js";
|
|
4
|
+
import { translateOpenAIStreamToAnthropic } from "../../translation/streamingOpenAIToAnthropic.js";
|
|
5
|
+
import { isBenignSocketError, safeSendJson } from "../requestLifecycle.js";
|
|
6
|
+
import { buildUpstreamErrorPayload, formatUpstreamErrorMessage, readUpstreamError, upstreamStatusCategory, writeAnthropicSseError } from "../errors.js";
|
|
7
|
+
export function isEventStream(upstream) {
|
|
8
|
+
const contentType = upstream.headers.get("content-type");
|
|
9
|
+
return typeof contentType === "string" && contentType.toLowerCase().includes("text/event-stream");
|
|
10
|
+
}
|
|
11
|
+
export function isStreamingRequestBody(body) {
|
|
12
|
+
return typeof body === "object" && body !== null && body.stream === true;
|
|
13
|
+
}
|
|
14
|
+
export function beginAnthropicSseResponse(res, req) {
|
|
15
|
+
if (res.headersSent) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
res.statusCode = 200;
|
|
19
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
20
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
21
|
+
res.setHeader("Connection", "keep-alive");
|
|
22
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
23
|
+
if (typeof res.flushHeaders === "function") {
|
|
24
|
+
res.flushHeaders();
|
|
25
|
+
}
|
|
26
|
+
const socket = res.socket ?? req?.socket;
|
|
27
|
+
if (socket && typeof socket.setNoDelay === "function") {
|
|
28
|
+
socket.setNoDelay(true);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function forwardResponse(upstream, anthroShape, res, diagnostics) {
|
|
32
|
+
if (!upstream.ok) {
|
|
33
|
+
const upstreamError = await readUpstreamError(upstream);
|
|
34
|
+
const category = upstreamStatusCategory(upstream.status);
|
|
35
|
+
diagnostics.logger.warn({
|
|
36
|
+
event: "upstream_non_ok",
|
|
37
|
+
request_id: diagnostics.requestId,
|
|
38
|
+
status_code: upstream.status,
|
|
39
|
+
error: category,
|
|
40
|
+
upstream_content_type: upstreamError.contentType,
|
|
41
|
+
upstream_error_code: upstreamError.code,
|
|
42
|
+
upstream_error_type: upstreamError.type,
|
|
43
|
+
upstream_error_message: upstreamError.message,
|
|
44
|
+
upstream_response_bytes: upstreamError.responseBytes
|
|
45
|
+
}, "upstream request failed");
|
|
46
|
+
const message = formatUpstreamErrorMessage(category, upstreamError);
|
|
47
|
+
const prelude = diagnostics.prelude ?? null;
|
|
48
|
+
if (prelude) {
|
|
49
|
+
writeAnthropicSseError(res, prelude, message);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
safeSendJson(res, upstream.status, buildUpstreamErrorPayload(category, upstream.status, diagnostics.requestId, upstreamError, anthroShape));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (isEventStream(upstream)) {
|
|
56
|
+
if (anthroShape) {
|
|
57
|
+
if (!upstream.body) {
|
|
58
|
+
if (diagnostics.prelude) {
|
|
59
|
+
writeAnthropicSseError(res, diagnostics.prelude, "invalid_upstream_response");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
safeSendJson(res, 502, { error: "invalid_upstream_response", detail: "Upstream stream body is missing." });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!diagnostics.prelude) {
|
|
66
|
+
beginAnthropicSseResponse(res);
|
|
67
|
+
}
|
|
68
|
+
const upstreamReadable = Readable.fromWeb(upstream.body);
|
|
69
|
+
await translateOpenAIStreamToAnthropic({
|
|
70
|
+
upstream: upstreamReadable,
|
|
71
|
+
downstream: res,
|
|
72
|
+
fallbackModel: diagnostics.requestedModel,
|
|
73
|
+
preEmittedMessageId: diagnostics.prelude?.messageId
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await pipeEventStream(upstream, res);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (diagnostics.prelude) {
|
|
81
|
+
writeAnthropicSseError(res, diagnostics.prelude, "invalid_upstream_response");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let json;
|
|
85
|
+
try {
|
|
86
|
+
json = (await upstream.json());
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
safeSendJson(res, 502, { error: "invalid_upstream_response" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
let payload = json;
|
|
93
|
+
if (anthroShape) {
|
|
94
|
+
try {
|
|
95
|
+
payload = openAIToAnthropic(json);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof ProtocolTranslationError) {
|
|
99
|
+
safeSendJson(res, 502, { error: error.code, detail: error.message });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
safeSendJson(res, 200, payload);
|
|
106
|
+
}
|
|
107
|
+
async function pipeEventStream(upstream, res) {
|
|
108
|
+
if (!upstream.body) {
|
|
109
|
+
safeSendJson(res, 502, { error: "invalid_upstream_response", detail: "Upstream stream body is missing." });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
res.statusCode = upstream.status;
|
|
113
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
114
|
+
const cacheControl = upstream.headers.get("cache-control");
|
|
115
|
+
if (cacheControl) {
|
|
116
|
+
res.setHeader("Cache-Control", cacheControl);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
120
|
+
}
|
|
121
|
+
const connection = upstream.headers.get("connection");
|
|
122
|
+
if (connection) {
|
|
123
|
+
res.setHeader("Connection", connection);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
res.setHeader("Connection", "keep-alive");
|
|
127
|
+
}
|
|
128
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
129
|
+
if (typeof res.flushHeaders === "function") {
|
|
130
|
+
res.flushHeaders();
|
|
131
|
+
}
|
|
132
|
+
if (res.socket && typeof res.socket.setNoDelay === "function") {
|
|
133
|
+
res.socket.setNoDelay(true);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await pipeline(Readable.fromWeb(upstream.body), res);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (isBenignSocketError(error)) {
|
|
140
|
+
// Client went away mid-stream — normal for SSE consumers (Codex,
|
|
141
|
+
// Claude Code, pi) that cancel pending responses on user input.
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copillm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Local Copilot proxy CLI (OpenAI/Anthropic-compatible)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.json",
|
|
28
28
|
"dev": "tsx src/cli.ts",
|
|
29
|
-
"lint": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
|
|
29
|
+
"lint": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit && eslint src",
|
|
30
|
+
"lint:boundaries": "eslint src",
|
|
30
31
|
"test": "vitest run",
|
|
31
32
|
"test:e2e:pr": "npm run build && tsx tests/e2e/pr-gate-runner.ts",
|
|
32
33
|
"test:e2e:release": "npm run build && tsx tests/e2e/release-runner.ts",
|
|
@@ -44,6 +45,10 @@
|
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/node": "^20",
|
|
48
|
+
"@typescript-eslint/parser": "^8.60.0",
|
|
49
|
+
"eslint": "^10.4.1",
|
|
50
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
51
|
+
"eslint-plugin-boundaries": "^6.0.2",
|
|
47
52
|
"tsx": "^4.19.1",
|
|
48
53
|
"typescript": "^5.6.2",
|
|
49
54
|
"vitest": "^2.1.2"
|