copillm 0.4.4 → 0.4.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/dist/config/packageInfo.js +1 -1
- package/dist/models/anthropicDefaults.js +12 -3
- package/dist/models/claudeModelId.js +64 -0
- package/dist/server/anthropicModelsResponse.js +7 -1
- package/dist/server/routes/proxyForward.js +9 -1
- package/dist/server/upstream/streaming.js +7 -0
- package/dist/translation/openaiAnthropic.js +11 -1
- package/package.json +3 -1
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { accountModelsCacheReadPath, modelsCacheReadPath } from "../config/home.js";
|
|
4
4
|
import { assertValidAccountId } from "../config/accountId.js";
|
|
5
|
+
import { toAnthropicSurfaceModelId } from "./claudeModelId.js";
|
|
5
6
|
export const ANTHROPIC_FAMILIES = ["opus", "sonnet", "haiku"];
|
|
6
7
|
const SUFFIX_BLOCKLIST = [
|
|
7
8
|
"-high",
|
|
@@ -27,12 +28,20 @@ export function computeAnthropicDefaults(modelIds) {
|
|
|
27
28
|
byFamily[family].push(id);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
31
|
+
// The picked ids feed the ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL env
|
|
32
|
+
// vars Claude Code reads, so they must be in Claude Code's dash-separated
|
|
33
|
+
// surface form (e.g. `claude-sonnet-4-6`). A dotted upstream id like
|
|
34
|
+
// `claude-sonnet-4.6` would canonicalise to the deprecated `claude-sonnet-4-0`
|
|
35
|
+
// inside Claude Code (see src/models/claudeModelId.ts).
|
|
30
36
|
return {
|
|
31
|
-
opus: pickPlainLatest(byFamily.opus),
|
|
32
|
-
sonnet: pickPlainLatest(byFamily.sonnet),
|
|
33
|
-
haiku: pickPlainLatest(byFamily.haiku)
|
|
37
|
+
opus: toSurfaceModelId(pickPlainLatest(byFamily.opus)),
|
|
38
|
+
sonnet: toSurfaceModelId(pickPlainLatest(byFamily.sonnet)),
|
|
39
|
+
haiku: toSurfaceModelId(pickPlainLatest(byFamily.haiku))
|
|
34
40
|
};
|
|
35
41
|
}
|
|
42
|
+
function toSurfaceModelId(modelId) {
|
|
43
|
+
return modelId === null ? null : toAnthropicSurfaceModelId(modelId);
|
|
44
|
+
}
|
|
36
45
|
export function readModelIdsFromCache(accountId) {
|
|
37
46
|
let file;
|
|
38
47
|
if (accountId === undefined) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bidirectional mapping between the **upstream Copilot** Claude model id form
|
|
3
|
+
* and the form Claude Code's model registry expects.
|
|
4
|
+
*
|
|
5
|
+
* GitHub Copilot's catalog names Claude models with a dotted minor version:
|
|
6
|
+
*
|
|
7
|
+
* claude-sonnet-4.6 claude-opus-4.8 claude-haiku-4.5
|
|
8
|
+
*
|
|
9
|
+
* Claude Code, however, keys its internal model registry on a dash-separated
|
|
10
|
+
* form (`claude-sonnet-4-6`) and carries a legacy canonicaliser that maps any
|
|
11
|
+
* unrecognised `claude-sonnet-4…` / `claude-opus-4…` string to the original,
|
|
12
|
+
* now-deprecated `claude-sonnet-4-0` / `claude-opus-4-0` (extracted from the
|
|
13
|
+
* Claude Code binary):
|
|
14
|
+
*
|
|
15
|
+
* if (e.includes("claude-sonnet-4-6")) return "claude-sonnet-4-6";
|
|
16
|
+
* if (e.includes("claude-sonnet-4-5")) return "claude-sonnet-4-5";
|
|
17
|
+
* if (/claude-sonnet-4(?!-\d(?!\d))/.test(e)) return "claude-sonnet-4-0"; // deprecated
|
|
18
|
+
* …same shape for opus…
|
|
19
|
+
*
|
|
20
|
+
* A dotted id like `claude-sonnet-4.6` slips past the specific `includes`
|
|
21
|
+
* checks (they look for a dash) and is caught by the loose regex, so Claude
|
|
22
|
+
* Code believes it is running the deprecated "Sonnet 4" — it injects
|
|
23
|
+
* `You are powered by the model named Sonnet 4` into the system prompt and
|
|
24
|
+
* shows a retirement warning. The dash-separated `claude-sonnet-4-6` is
|
|
25
|
+
* matched by the specific branch first and is not deprecated.
|
|
26
|
+
*
|
|
27
|
+
* So copillm advertises / exports the **dash** form to Claude Code, and
|
|
28
|
+
* rewrites it back to the **dotted** upstream form before forwarding a request
|
|
29
|
+
* to Copilot (which only accepts the dotted id — a dashed id returns upstream
|
|
30
|
+
* 400 `model_not_supported`).
|
|
31
|
+
*
|
|
32
|
+
* Both transforms are scoped to `claude-` ids and only touch the trailing
|
|
33
|
+
* `<major>.<minor>` / `<major>-<minor>` version segment, so they are exact
|
|
34
|
+
* inverses for every Copilot Claude id (each has a single trailing dotted
|
|
35
|
+
* version). Non-claude ids (gpt, gemini — the only ids with mid-string dots
|
|
36
|
+
* such as `gpt-5.3-codex`) are returned unchanged.
|
|
37
|
+
*/
|
|
38
|
+
const CLAUDE_ID_PREFIX = "claude-";
|
|
39
|
+
function isClaudeId(modelId) {
|
|
40
|
+
return modelId.toLowerCase().startsWith(CLAUDE_ID_PREFIX);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Upstream (dotted) -> Claude Code surface (dashed).
|
|
44
|
+
* `claude-sonnet-4.6` -> `claude-sonnet-4-6`. No-op for non-claude ids and for
|
|
45
|
+
* ids without a trailing dotted version.
|
|
46
|
+
*/
|
|
47
|
+
export function toAnthropicSurfaceModelId(modelId) {
|
|
48
|
+
if (!isClaudeId(modelId)) {
|
|
49
|
+
return modelId;
|
|
50
|
+
}
|
|
51
|
+
return modelId.replace(/(\d)\.(\d+)$/, "$1-$2");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Claude Code surface (dashed) -> upstream (dotted).
|
|
55
|
+
* `claude-sonnet-4-6` -> `claude-sonnet-4.6`. No-op for non-claude ids and for
|
|
56
|
+
* ids whose trailing segment is not a `<digit>-<digits>` version (e.g. an
|
|
57
|
+
* already-dotted id passes through unchanged).
|
|
58
|
+
*/
|
|
59
|
+
export function toUpstreamModelId(modelId) {
|
|
60
|
+
if (!isClaudeId(modelId)) {
|
|
61
|
+
return modelId;
|
|
62
|
+
}
|
|
63
|
+
return modelId.replace(/(\d)-(\d+)$/, "$1.$2");
|
|
64
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toAnthropicSurfaceModelId } from "../models/claudeModelId.js";
|
|
1
2
|
/**
|
|
2
3
|
* Claude Code's only client-side marker for a 1M-context-budget model is a
|
|
3
4
|
* literal `[1m]` suffix on the model id (matched in its binary via
|
|
@@ -57,13 +58,18 @@ export function buildAnthropicModelsResponse(models) {
|
|
|
57
58
|
* picker — a label that would imply 1M-class behaviour Claude Code would
|
|
58
59
|
* never deliver for that vendor.
|
|
59
60
|
*
|
|
61
|
+
* The base id is first mapped to Claude Code's dash-separated surface form
|
|
62
|
+
* (`claude-sonnet-4.6` -> `claude-sonnet-4-6`) via `toAnthropicSurfaceModelId`
|
|
63
|
+
* so the advertised id is not mistaken for the deprecated `claude-sonnet-4-0`
|
|
64
|
+
* (see src/models/claudeModelId.ts).
|
|
65
|
+
*
|
|
60
66
|
* Models already carrying the suffix are left alone. Models below the 1M
|
|
61
67
|
* threshold get no alias regardless of name — Claude Code has no
|
|
62
68
|
* client-side marker for the 200K-1M intermediate range, and over-claiming
|
|
63
69
|
* would set the wrong autocompact trigger.
|
|
64
70
|
*/
|
|
65
71
|
export function applyOneMillionAlias(model) {
|
|
66
|
-
const baseId = typeof model.id === "string" ? model.id : "";
|
|
72
|
+
const baseId = toAnthropicSurfaceModelId(typeof model.id === "string" ? model.id : "");
|
|
67
73
|
if (baseId.endsWith(ONE_M_ALIAS_SUFFIX)) {
|
|
68
74
|
return baseId;
|
|
69
75
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveModelId } from "../../models/discovery.js";
|
|
2
|
+
import { toUpstreamModelId } from "../../models/claudeModelId.js";
|
|
2
3
|
import { CopilotTokenManagerError } from "../../auth/copilotToken.js";
|
|
3
4
|
import { anthropicToOpenAI, ProtocolTranslationError } from "../../translation/openaiAnthropic.js";
|
|
4
5
|
import { writeAnthropicPrelude } from "../../translation/streamingOpenAIToAnthropic.js";
|
|
@@ -52,7 +53,14 @@ export async function handleProxyForward(input) {
|
|
|
52
53
|
});
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
// Claude Code talks to copillm in its dash-separated surface form
|
|
57
|
+
// (`claude-sonnet-4-6`); upstream Copilot only accepts the dotted id
|
|
58
|
+
// (`claude-sonnet-4.6`). Rewrite back before forwarding. A local model
|
|
59
|
+
// selection already resolves to the dotted catalog id, so only the
|
|
60
|
+
// unfiltered anthropic path needs the explicit conversion.
|
|
61
|
+
const upstreamModelId = resolvedModel?.id ??
|
|
62
|
+
(requestedModel && route.kind === "anthropic" ? toUpstreamModelId(requestedModel) : null);
|
|
63
|
+
const upstreamBody = upstreamModelId ? rewriteRequestedModel(translatedBody, upstreamModelId) : translatedBody;
|
|
56
64
|
const upstreamPath = route.kind === "codex_responses" ? "/responses" : "/chat/completions";
|
|
57
65
|
const isAnthropicStreaming = route.anthroShape && isStreamingRequestBody(translatedBody);
|
|
58
66
|
let prelude = null;
|
|
@@ -101,6 +101,13 @@ export async function forwardResponse(upstream, anthroShape, res, diagnostics) {
|
|
|
101
101
|
}
|
|
102
102
|
throw error;
|
|
103
103
|
}
|
|
104
|
+
// Echo back the model id the client requested (Claude Code's dash-separated
|
|
105
|
+
// surface id) rather than the dotted id upstream returns, so Claude Code
|
|
106
|
+
// does not re-canonicalise the response model to the deprecated
|
|
107
|
+
// claude-…-4-0. Streaming already does this via the prelude fallbackModel.
|
|
108
|
+
if (diagnostics.requestedModel && payload && typeof payload === "object") {
|
|
109
|
+
payload.model = diagnostics.requestedModel;
|
|
110
|
+
}
|
|
104
111
|
}
|
|
105
112
|
safeSendJson(res, 200, payload);
|
|
106
113
|
}
|
|
@@ -153,7 +153,7 @@ function translateSystemToText(system) {
|
|
|
153
153
|
return joinTextBlocks(system, "Anthropic system prompt");
|
|
154
154
|
}
|
|
155
155
|
function translateAnthropicMessage(message) {
|
|
156
|
-
if (message.role !== "assistant" && message.role !== "user") {
|
|
156
|
+
if (message.role !== "assistant" && message.role !== "user" && message.role !== "system") {
|
|
157
157
|
throw new ProtocolTranslationError("unsupported_message_role", `Unsupported Anthropic message role: ${String(message.role)}.`);
|
|
158
158
|
}
|
|
159
159
|
if (typeof message.content === "string") {
|
|
@@ -162,6 +162,16 @@ function translateAnthropicMessage(message) {
|
|
|
162
162
|
if (!Array.isArray(message.content)) {
|
|
163
163
|
throw new ProtocolTranslationError("invalid_message_content", "Anthropic message content must be a string or array.");
|
|
164
164
|
}
|
|
165
|
+
if (message.role === "system") {
|
|
166
|
+
// Claude Code's "mid-conversation system message" feature (a beta it turns on
|
|
167
|
+
// for correctly-recognised current models, e.g. Opus 4.8) places role:"system"
|
|
168
|
+
// entries inside `messages`, not just the top-level `system` field. Anthropic's
|
|
169
|
+
// own API only accepts system at the top level, but copillm translates to
|
|
170
|
+
// OpenAI chat/completions, which supports system-role messages natively — so
|
|
171
|
+
// map it through instead of 400ing. A 400 here surfaces to the user as a hard
|
|
172
|
+
// API error rather than triggering Claude Code's <system-reminder> fallback.
|
|
173
|
+
return [{ role: "system", content: joinTextBlocks(message.content, "Anthropic system message") }];
|
|
174
|
+
}
|
|
165
175
|
return message.role === "assistant"
|
|
166
176
|
? translateAssistantMessageBlocks(message.content)
|
|
167
177
|
: translateUserMessageBlocks(message.content);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copillm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Local Copilot proxy CLI (OpenAI/Anthropic-compatible)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"dev:start": "npm run build && node dist/cli.js --dev start",
|
|
30
30
|
"dev:stop": "npm run build && node dist/cli.js --dev stop",
|
|
31
31
|
"dev:status": "node dist/cli.js --dev status",
|
|
32
|
+
"dev:link": "node scripts/dev-link.mjs",
|
|
33
|
+
"dev:unlink": "node scripts/dev-unlink.mjs",
|
|
32
34
|
"lint": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit && eslint src",
|
|
33
35
|
"lint:boundaries": "eslint src",
|
|
34
36
|
"test": "vitest run",
|