copilot-reverse 0.0.1
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/GUIDE.md +142 -0
- package/README.md +60 -0
- package/dist/cli/auth.js +9 -0
- package/dist/cli/index.js +133 -0
- package/dist/core/anthropic-inbound.js +63 -0
- package/dist/core/canonical.js +6 -0
- package/dist/core/fuzzy.js +35 -0
- package/dist/core/openai-inbound.js +83 -0
- package/dist/core/tokens.js +22 -0
- package/dist/daemon/lifecycle.js +32 -0
- package/dist/providers/copilot/adapter.js +146 -0
- package/dist/providers/copilot/auth.js +30 -0
- package/dist/providers/copilot/models.js +49 -0
- package/dist/providers/copilot/token.js +53 -0
- package/dist/providers/types.js +1 -0
- package/dist/shared/client-setup.js +19 -0
- package/dist/shared/config.js +20 -0
- package/dist/shared/control-types.js +1 -0
- package/dist/shared/creds.js +14 -0
- package/dist/shared/format.js +28 -0
- package/dist/shared/ipc.js +1 -0
- package/dist/shared/open-url.js +17 -0
- package/dist/shared/paths.js +11 -0
- package/dist/shared/prefs.js +28 -0
- package/dist/supervisor/api.js +24 -0
- package/dist/supervisor/dashboard.js +110 -0
- package/dist/supervisor/db.js +35 -0
- package/dist/supervisor/events.js +6 -0
- package/dist/supervisor/index.js +66 -0
- package/dist/supervisor/monitor.js +91 -0
- package/dist/tui/app.js +184 -0
- package/dist/tui/assistant/on-chat.js +25 -0
- package/dist/tui/assistant/runtime.js +104 -0
- package/dist/tui/assistant/tools.js +24 -0
- package/dist/tui/components/select.js +30 -0
- package/dist/tui/daemon-client.js +16 -0
- package/dist/tui/panels/metrics-agg.js +22 -0
- package/dist/tui/panels/metrics.js +7 -0
- package/dist/tui/repl.js +45 -0
- package/dist/tui/report.js +35 -0
- package/dist/tui/screens/config.js +13 -0
- package/dist/tui/screens/model.js +16 -0
- package/dist/tui/setup/apply.js +119 -0
- package/dist/tui/setup/clients.js +38 -0
- package/dist/tui/setup/codex-toml.js +47 -0
- package/dist/tui/setup/status.js +35 -0
- package/dist/tui/setup/wizard.js +37 -0
- package/dist/tui/slash/commands.js +68 -0
- package/dist/tui/slash/registry.js +16 -0
- package/dist/tui/theme.js +16 -0
- package/dist/worker/anthropic-server.js +108 -0
- package/dist/worker/errors.js +12 -0
- package/dist/worker/index.js +30 -0
- package/dist/worker/openai-server.js +44 -0
- package/dist/worker/router.js +34 -0
- package/dist/worker/server.js +11 -0
- package/images/dashboard.png +0 -0
- package/package.json +69 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { openaiRequestToCanonical, canonicalToOpenAIResponse, canonicalChunkToOpenAISSE } from "../core/openai-inbound.js";
|
|
3
|
+
import { errorHint } from "./errors.js";
|
|
4
|
+
import { CopilotAuthError } from "../providers/copilot/token.js";
|
|
5
|
+
export function mountOpenAI(app, router, onMetric) {
|
|
6
|
+
app.post("/v1/chat/completions", async (req, res) => {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const canon = openaiRequestToCanonical(req.body);
|
|
9
|
+
canon.model = router.resolveModel(canon.model);
|
|
10
|
+
const provider = router.pick(canon.model);
|
|
11
|
+
const metric = (status, error) => onMetric({ endpoint: "/v1/chat/completions", model: canon.model, status, latencyMs: Date.now() - start, error });
|
|
12
|
+
try {
|
|
13
|
+
if (canon.stream) {
|
|
14
|
+
res.setHeader("content-type", "text/event-stream");
|
|
15
|
+
res.setHeader("cache-control", "no-cache");
|
|
16
|
+
const id = `chatcmpl-${randomUUID().replace(/-/g, "")}`; // unique per response, not constant
|
|
17
|
+
for await (const chunk of provider.stream(canon))
|
|
18
|
+
res.write(canonicalChunkToOpenAISSE(chunk, id, canon.model));
|
|
19
|
+
res.end();
|
|
20
|
+
metric(200);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
res.json(canonicalToOpenAIResponse(await provider.complete(canon)));
|
|
24
|
+
metric(200);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
29
|
+
const hint = errorHint(raw);
|
|
30
|
+
const message = hint ? `${raw}\n${hint}` : raw;
|
|
31
|
+
const status = err instanceof CopilotAuthError ? 401 : 502;
|
|
32
|
+
if (!res.headersSent) {
|
|
33
|
+
res.status(status).json({ error: { message } });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Stream already opened: surface the failure as a final error chunk so the client
|
|
37
|
+
// sees it instead of a silently truncated response, then close the stream.
|
|
38
|
+
res.write(`data: ${JSON.stringify({ error: { message } })}\n\n`);
|
|
39
|
+
res.end();
|
|
40
|
+
}
|
|
41
|
+
metric(status, message);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { bestModelMatch } from "../core/fuzzy.js";
|
|
2
|
+
// M1: single provider. Model name is remapped to the provider's actual id.
|
|
3
|
+
export class Router {
|
|
4
|
+
providers;
|
|
5
|
+
modelMap;
|
|
6
|
+
available = [];
|
|
7
|
+
constructor(providers, modelMap) {
|
|
8
|
+
this.providers = providers;
|
|
9
|
+
this.modelMap = modelMap;
|
|
10
|
+
}
|
|
11
|
+
// The live Copilot model list, used for fuzzy matching (set once fetched at worker startup).
|
|
12
|
+
setAvailableModels(ids) { this.available = ids; }
|
|
13
|
+
resolveModel(requested) {
|
|
14
|
+
// Claude Code appends [1m] to signal its 1M context window; Copilot doesn't know that id, so
|
|
15
|
+
// strip it back to the real model before mapping/forwarding.
|
|
16
|
+
requested = requested.endsWith("[1m]") ? requested.slice(0, -4) : requested;
|
|
17
|
+
const mapped = this.modelMap[requested];
|
|
18
|
+
if (mapped)
|
|
19
|
+
return mapped;
|
|
20
|
+
// Fuzzy-match a near-miss id (e.g. claude-opus-4-8-20251101 -> claude-opus-4.8) to a real model.
|
|
21
|
+
if (this.available.length) {
|
|
22
|
+
const match = bestModelMatch(requested, this.available);
|
|
23
|
+
if (match)
|
|
24
|
+
return match;
|
|
25
|
+
}
|
|
26
|
+
return this.modelMap["*"] ?? requested;
|
|
27
|
+
}
|
|
28
|
+
pick(_model) {
|
|
29
|
+
const p = this.providers[0];
|
|
30
|
+
if (!p)
|
|
31
|
+
throw new Error("no provider registered");
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { mountOpenAI } from "./openai-server.js";
|
|
3
|
+
import { mountAnthropic } from "./anthropic-server.js";
|
|
4
|
+
export function createWorkerApp(router, onMetric) {
|
|
5
|
+
const app = express();
|
|
6
|
+
app.use(express.json({ limit: "20mb" }));
|
|
7
|
+
app.get("/healthz", (_req, res) => res.json({ ok: true }));
|
|
8
|
+
mountOpenAI(app, router, onMetric);
|
|
9
|
+
mountAnthropic(app, router, onMetric);
|
|
10
|
+
return app;
|
|
11
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "copilot-reverse",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Interactive terminal app that exposes your GitHub Copilot subscription as local OpenAI- and Anthropic-compatible endpoints, with a self-healing daemon and a built-in assistant.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/wangcansunking/copilot-reverse.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/wangcansunking/copilot-reverse#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/wangcansunking/copilot-reverse/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"copilot-reverse": "dist/cli/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"GUIDE.md",
|
|
22
|
+
"images"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"github-copilot",
|
|
26
|
+
"openai",
|
|
27
|
+
"anthropic",
|
|
28
|
+
"proxy",
|
|
29
|
+
"cli",
|
|
30
|
+
"tui",
|
|
31
|
+
"ink",
|
|
32
|
+
"claude",
|
|
33
|
+
"llm"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:coverage": "vitest run --coverage",
|
|
39
|
+
"test:e2e": "vitest run e2e/copilot-reverse.e2e.test.ts tests/e2e",
|
|
40
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
41
|
+
"dev": "tsx src/cli/index.ts",
|
|
42
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
|
49
|
+
"better-sqlite3": "^12.11.1",
|
|
50
|
+
"commander": "^12.0.0",
|
|
51
|
+
"express": "^4.19.0",
|
|
52
|
+
"ink": "^5.0.0",
|
|
53
|
+
"react": "^18.3.0",
|
|
54
|
+
"zod": "^3.23.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
58
|
+
"@types/express": "^4.17.0",
|
|
59
|
+
"@types/node": "^20.0.0",
|
|
60
|
+
"@types/react": "^18.3.0",
|
|
61
|
+
"@types/supertest": "^6.0.0",
|
|
62
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
63
|
+
"ink-testing-library": "^4.0.0",
|
|
64
|
+
"supertest": "^7.0.0",
|
|
65
|
+
"tsx": "^4.0.0",
|
|
66
|
+
"typescript": "^5.5.0",
|
|
67
|
+
"vitest": "^2.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|