copilot-reverse 0.0.2 → 0.1.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 +5 -5
- package/dist/cli/index.js +12 -8
- package/dist/providers/copilot/models.js +3 -3
- package/dist/tui/setup/clients.js +2 -2
- package/dist/tui/slash/commands.js +1 -1
- package/dist/worker/anthropic-server.js +8 -3
- package/dist/worker/openai-server.js +7 -2
- package/dist/worker/router.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,13 +87,13 @@ health, request volume, and (most useful) recent **errors with their real messag
|
|
|
87
87
|
|
|
88
88
|
Already have something that speaks OpenAI or Anthropic? Point it here:
|
|
89
89
|
|
|
90
|
-
- **OpenAI-compatible:** `http://127.0.0.1:7891/
|
|
91
|
-
- **Anthropic-compatible:** `http://127.0.0.1:7891`
|
|
90
|
+
- **OpenAI-compatible:** `http://127.0.0.1:7891/openai`
|
|
91
|
+
- **Anthropic-compatible:** `http://127.0.0.1:7891/anthropic`
|
|
92
92
|
|
|
93
93
|
Any API key value works locally (it's your machine). Example:
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
export ANTHROPIC_BASE_URL=http://127.0.0.1:7891
|
|
96
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:7891/anthropic
|
|
97
97
|
export ANTHROPIC_API_KEY=local
|
|
98
98
|
claude
|
|
99
99
|
```
|
|
@@ -159,8 +159,8 @@ Three processes, one terminal app:
|
|
|
159
159
|
- **TUI** (Ink) — the `copilot-reverse` process: REPL + slash commands + a claude-agent-sdk
|
|
160
160
|
assistant (which dogfoods copilot-reverse's own Anthropic endpoint).
|
|
161
161
|
- **Supervisor** (:7890) — control API + SQLite + self-healing worker supervision.
|
|
162
|
-
- **Worker** (:7891) — OpenAI `/
|
|
163
|
-
with tool-use translation both ways.
|
|
162
|
+
- **Worker** (:7891) — OpenAI `/openai/chat/completions` + Anthropic `/anthropic/v1/messages` → Copilot,
|
|
163
|
+
with tool-use translation both ways. Each protocol also serves a `…/models` discovery endpoint.
|
|
164
164
|
|
|
165
165
|
## Development
|
|
166
166
|
|
package/dist/cli/index.js
CHANGED
|
@@ -49,6 +49,10 @@ async function launchTui() {
|
|
|
49
49
|
const base = `http://${cfg.bindHost}:${cfg.supervisorPort}`;
|
|
50
50
|
const client = new DaemonClient(base);
|
|
51
51
|
const workerBase = `http://${cfg.bindHost}:${cfg.workerPort}`;
|
|
52
|
+
// Per-protocol base URLs the worker now serves under: OpenAI clients -> /openai/*,
|
|
53
|
+
// Anthropic clients (and the assistant's own dogfood SDK) -> /anthropic/*.
|
|
54
|
+
const openaiBase = `${workerBase}/openai`;
|
|
55
|
+
const anthropicBase = `${workerBase}/anthropic`;
|
|
52
56
|
const endpoint = { host: cfg.bindHost, port: cfg.workerPort, apiKey: "copilot-reverse-local" };
|
|
53
57
|
let app;
|
|
54
58
|
const quit = () => { stopSupervisor?.(); app?.unmount(); process.exit(0); };
|
|
@@ -66,7 +70,7 @@ async function launchTui() {
|
|
|
66
70
|
const registry = buildRegistry({ client, quit }, endpoint, {
|
|
67
71
|
dashboardUrl: `http://${cfg.bindHost}:${cfg.supervisorPort}/`,
|
|
68
72
|
reportRepo: cfg.reportRepo,
|
|
69
|
-
appVersion: "0.0
|
|
73
|
+
appVersion: "0.1.0",
|
|
70
74
|
platform: `${process.platform} node-${process.version}`,
|
|
71
75
|
resetClient,
|
|
72
76
|
// Re-run device-code login, then restart the worker so it picks up the new token.
|
|
@@ -102,18 +106,18 @@ async function launchTui() {
|
|
|
102
106
|
// model's context window) so either Codex setup style works.
|
|
103
107
|
const applyClient = (clientKind, scope, model) => {
|
|
104
108
|
if (clientKind === "claude") {
|
|
105
|
-
const r = applyClaude(scope, claudeCopilotReverseEnv(
|
|
109
|
+
const r = applyClaude(scope, claudeCopilotReverseEnv(anthropicBase, "copilot-reverse-local", model, modelLimits[model]));
|
|
106
110
|
writeClientSetup(dataDir(), { ...readClientSetup(dataDir()), claude: true });
|
|
107
111
|
return r;
|
|
108
112
|
}
|
|
109
|
-
const r = applyCodex(scope, { OPENAI_BASE_URL:
|
|
110
|
-
applyCodexToml({ baseUrl:
|
|
113
|
+
const r = applyCodex(scope, { OPENAI_BASE_URL: openaiBase, OPENAI_API_KEY: "copilot-reverse-local", OPENAI_MODEL: model });
|
|
114
|
+
applyCodexToml({ baseUrl: openaiBase, model, contextWindow: modelLimits[model] });
|
|
111
115
|
writeClientSetup(dataDir(), { ...readClientSetup(dataDir()), codex: true });
|
|
112
116
|
return r;
|
|
113
117
|
};
|
|
114
118
|
const setup = { apply: async (clientKind, scope, model) => applyClient(clientKind, scope, model) };
|
|
115
119
|
const onChat = makeOnChat({
|
|
116
|
-
client, workerBaseUrl:
|
|
120
|
+
client, workerBaseUrl: anthropicBase, apiKey: "copilot-reverse-local", model: DEFAULT_MODEL,
|
|
117
121
|
maxInputTokens: DEFAULT_MAX_INPUT_TOKENS, modelLimits,
|
|
118
122
|
listModels: loadModels,
|
|
119
123
|
setupClient: async (c, s, m) => applyClient(c, s, m),
|
|
@@ -130,8 +134,8 @@ async function launchTui() {
|
|
|
130
134
|
loadModels,
|
|
131
135
|
setup,
|
|
132
136
|
info: {
|
|
133
|
-
openai:
|
|
134
|
-
anthropic:
|
|
137
|
+
openai: openaiBase,
|
|
138
|
+
anthropic: anthropicBase,
|
|
135
139
|
supervisorPort: cfg.supervisorPort,
|
|
136
140
|
workerPort: cfg.workerPort,
|
|
137
141
|
dataDir: dataDir(),
|
|
@@ -141,7 +145,7 @@ async function launchTui() {
|
|
|
141
145
|
}));
|
|
142
146
|
}
|
|
143
147
|
const program = new Command();
|
|
144
|
-
program.name("copilot-reverse").description("copilot-reverse: interactive Copilot proxy").version("0.0
|
|
148
|
+
program.name("copilot-reverse").description("copilot-reverse: interactive Copilot proxy").version("0.1.0");
|
|
145
149
|
program.command("login").description("GitHub device-code login").action(() => runDeviceLogin(dataDir()));
|
|
146
150
|
program.action(() => { void launchTui(); });
|
|
147
151
|
program.parseAsync(process.argv);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Live model list from Copilot. Falls back to a curated list if the endpoint is unavailable.
|
|
2
2
|
const MODELS_URL = "https://api.githubcopilot.com/models";
|
|
3
|
-
const
|
|
3
|
+
export const FALLBACK_MODELS = ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4-6", "claude-opus-4-8", "o3-mini"];
|
|
4
4
|
const HEADERS = (token) => ({
|
|
5
5
|
authorization: `Bearer ${token}`,
|
|
6
6
|
"content-type": "application/json",
|
|
@@ -28,9 +28,9 @@ async function getModels(token, fetchFn, timeoutMs) {
|
|
|
28
28
|
export async function fetchCopilotModels(token, fetchFn = fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
29
29
|
const data = await getModels(token, fetchFn, timeoutMs);
|
|
30
30
|
if (!data)
|
|
31
|
-
return
|
|
31
|
+
return FALLBACK_MODELS;
|
|
32
32
|
const ids = [...new Set(data.map((m) => m.id).filter((x) => Boolean(x)))];
|
|
33
|
-
return ids.length ? ids :
|
|
33
|
+
return ids.length ? ids : FALLBACK_MODELS;
|
|
34
34
|
}
|
|
35
35
|
// Map of model id -> its real input/context window, used to size auto-compaction per model and
|
|
36
36
|
// to show the window in the picker. Returns {} on failure/timeout so callers fall back gracefully.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function claudeCodeConfig(e) {
|
|
2
|
-
const base = `http://${e.host}:${e.port}`;
|
|
2
|
+
const base = `http://${e.host}:${e.port}/anthropic`;
|
|
3
3
|
return {
|
|
4
4
|
env: { ANTHROPIC_BASE_URL: base, ANTHROPIC_API_KEY: e.apiKey },
|
|
5
5
|
instructions: `Set these env vars for Claude Code:\n ANTHROPIC_BASE_URL=${base}\n ANTHROPIC_API_KEY=${e.apiKey}`,
|
|
@@ -30,7 +30,7 @@ export function claudeCopilotReverseEnv(base, apiKey, model, contextWindow) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
export function codexConfig(e) {
|
|
33
|
-
const base = `http://${e.host}:${e.port}/
|
|
33
|
+
const base = `http://${e.host}:${e.port}/openai`;
|
|
34
34
|
return {
|
|
35
35
|
env: { OPENAI_BASE_URL: base, OPENAI_API_KEY: e.apiKey },
|
|
36
36
|
instructions: `Set these env vars for Codex / OpenAI clients:\n OPENAI_BASE_URL=${base}\n OPENAI_API_KEY=${e.apiKey}`,
|
|
@@ -39,7 +39,7 @@ export function buildRegistry(ctx, endpoint, opts = {}) {
|
|
|
39
39
|
} });
|
|
40
40
|
reg.add({ name: "/setup-claude", describe: "print Claude Code config", run: async () => claudeCodeConfig(endpoint).instructions.split("\n") });
|
|
41
41
|
reg.add({ name: "/setup-codex", describe: "print Codex/OpenAI config", run: async () => codexConfig(endpoint).instructions.split("\n") });
|
|
42
|
-
reg.add({ name: "/setup-status", describe: "show configured endpoints", run: async () => [`OpenAI: http://${endpoint.host}:${endpoint.port}/
|
|
42
|
+
reg.add({ name: "/setup-status", describe: "show configured endpoints", run: async () => [`OpenAI: http://${endpoint.host}:${endpoint.port}/openai`, `Anthropic: http://${endpoint.host}:${endpoint.port}/anthropic`] });
|
|
43
43
|
reg.add({ name: "/reset-claude", describe: "restore Claude Code config (remove copilot-reverse's keys)", run: async () => opts.resetClient ? opts.resetClient("claude") : ["reset not available"] });
|
|
44
44
|
reg.add({ name: "/reset-codex", describe: "restore Codex/OpenAI config (remove copilot-reverse's keys)", run: async () => opts.resetClient ? opts.resetClient("codex") : ["reset not available"] });
|
|
45
45
|
reg.add({ name: "/login", describe: "sign in to GitHub (device-code)", run: async () => opts.login ? opts.login() : ["login not available"] });
|
|
@@ -5,16 +5,21 @@ import { errorHint } from "./errors.js";
|
|
|
5
5
|
import { CopilotAuthError } from "../providers/copilot/token.js";
|
|
6
6
|
const frame = (event, data) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
7
7
|
export function mountAnthropic(app, router, onMetric) {
|
|
8
|
+
// Model discovery — Anthropic list shape. Claude Desktop / Anthropic-protocol clients GET this
|
|
9
|
+
// before chatting; without it they 404 on the connection test.
|
|
10
|
+
app.get("/anthropic/v1/models", (_req, res) => {
|
|
11
|
+
res.json({ data: router.listModels().map((id) => ({ type: "model", id, display_name: id })), has_more: false });
|
|
12
|
+
});
|
|
8
13
|
// Anthropic clients (Claude Code) call this to size the prompt and decide when to auto-compact.
|
|
9
|
-
app.post("/v1/messages/count_tokens", (req, res) => {
|
|
14
|
+
app.post("/anthropic/v1/messages/count_tokens", (req, res) => {
|
|
10
15
|
res.json({ input_tokens: estimateTokens(anthropicRequestToCanonical(req.body)) });
|
|
11
16
|
});
|
|
12
|
-
app.post("/v1/messages", async (req, res) => {
|
|
17
|
+
app.post("/anthropic/v1/messages", async (req, res) => {
|
|
13
18
|
const start = Date.now();
|
|
14
19
|
const canon = anthropicRequestToCanonical(req.body);
|
|
15
20
|
canon.model = router.resolveModel(canon.model);
|
|
16
21
|
const provider = router.pick(canon.model);
|
|
17
|
-
const metric = (status, error) => onMetric({ endpoint: "/v1/messages", model: canon.model, status, latencyMs: Date.now() - start, error });
|
|
22
|
+
const metric = (status, error) => onMetric({ endpoint: "/anthropic/v1/messages", model: canon.model, status, latencyMs: Date.now() - start, error });
|
|
18
23
|
try {
|
|
19
24
|
if (canon.stream) {
|
|
20
25
|
res.setHeader("content-type", "text/event-stream");
|
|
@@ -3,12 +3,17 @@ import { openaiRequestToCanonical, canonicalToOpenAIResponse, canonicalChunkToOp
|
|
|
3
3
|
import { errorHint } from "./errors.js";
|
|
4
4
|
import { CopilotAuthError } from "../providers/copilot/token.js";
|
|
5
5
|
export function mountOpenAI(app, router, onMetric) {
|
|
6
|
-
|
|
6
|
+
// Model discovery — OpenAI list shape. Clients (LiteLLM-style gateways, "test connection" probes)
|
|
7
|
+
// GET this before chatting; without it they 404 and refuse to connect.
|
|
8
|
+
app.get("/openai/models", (_req, res) => {
|
|
9
|
+
res.json({ object: "list", data: router.listModels().map((id) => ({ id, object: "model", owned_by: "copilot-reverse" })) });
|
|
10
|
+
});
|
|
11
|
+
app.post("/openai/chat/completions", async (req, res) => {
|
|
7
12
|
const start = Date.now();
|
|
8
13
|
const canon = openaiRequestToCanonical(req.body);
|
|
9
14
|
canon.model = router.resolveModel(canon.model);
|
|
10
15
|
const provider = router.pick(canon.model);
|
|
11
|
-
const metric = (status, error) => onMetric({ endpoint: "/
|
|
16
|
+
const metric = (status, error) => onMetric({ endpoint: "/openai/chat/completions", model: canon.model, status, latencyMs: Date.now() - start, error });
|
|
12
17
|
try {
|
|
13
18
|
if (canon.stream) {
|
|
14
19
|
res.setHeader("content-type", "text/event-stream");
|
package/dist/worker/router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bestModelMatch } from "../core/fuzzy.js";
|
|
2
|
+
import { FALLBACK_MODELS } from "../providers/copilot/models.js";
|
|
2
3
|
// M1: single provider. Model name is remapped to the provider's actual id.
|
|
3
4
|
export class Router {
|
|
4
5
|
providers;
|
|
@@ -10,6 +11,9 @@ export class Router {
|
|
|
10
11
|
}
|
|
11
12
|
// The live Copilot model list, used for fuzzy matching (set once fetched at worker startup).
|
|
12
13
|
setAvailableModels(ids) { this.available = ids; }
|
|
14
|
+
// Model ids to advertise from the /models discovery endpoints. Falls back to a curated list
|
|
15
|
+
// until the live fetch resolves, so discovery never returns an empty list.
|
|
16
|
+
listModels() { return this.available.length ? this.available : FALLBACK_MODELS; }
|
|
13
17
|
resolveModel(requested) {
|
|
14
18
|
// Claude Code appends [1m] to signal its 1M context window; Copilot doesn't know that id, so
|
|
15
19
|
// strip it back to the real model before mapping/forwarding.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-reverse",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|