opencode-anthropic-fix 0.0.23 → 0.0.24
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/index.mjs +7 -2
- package/lib/oauth.mjs +78 -19
- package/lib/oauth.test.mjs +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ The [original plugin](https://github.com/anomalyco/opencode-anthropic-auth) prov
|
|
|
43
43
|
- **Standalone CLI** — manage accounts without opening OpenCode
|
|
44
44
|
- **Configurable strategies** — sticky, round-robin, or hybrid account selection
|
|
45
45
|
- **Claude Code signature emulation** — full HTTP header, system prompt, beta flag, and metadata mimicry derived from Claude Code's open source code
|
|
46
|
+
- **OAuth endpoint fingerprint parity** — sends Claude Code-style `User-Agent` on `/v1/oauth/token` and `/v1/oauth/revoke` to match current OAuth validation
|
|
46
47
|
- **Effort-based thinking for Opus 4.6** — maps `budgetTokens` to effort levels (`low`/`medium`/`high`) and includes `effort-2025-11-24`
|
|
47
48
|
- **1M context limit override** — patches `model.limit.context` so OpenCode compacts at the right threshold while `models.dev` catches up
|
|
48
49
|
- **Runtime config + custom betas** — `/anthropic set`, `/anthropic config`, and `/anthropic betas` slash commands for live feature toggling without restarting OpenCode
|
package/index.mjs
CHANGED
|
@@ -103,7 +103,7 @@ async function promptManageAccounts(accountManager) {
|
|
|
103
103
|
// Request building helpers (extracted from original fetch interceptor)
|
|
104
104
|
// ---------------------------------------------------------------------------
|
|
105
105
|
|
|
106
|
-
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
106
|
+
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.79";
|
|
107
107
|
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
108
108
|
const CLAUDE_CODE_BETA_FLAG = "claude-code-20250219";
|
|
109
109
|
const EFFORT_BETA_FLAG = "effort-2025-11-24";
|
|
@@ -1866,7 +1866,12 @@ export async function AnthropicAuthPlugin({ client }) {
|
|
|
1866
1866
|
|
|
1867
1867
|
const credentials = await exchange(code, pending.verifier);
|
|
1868
1868
|
if (credentials.type === "failed") {
|
|
1869
|
-
return {
|
|
1869
|
+
return {
|
|
1870
|
+
ok: false,
|
|
1871
|
+
message: credentials.details
|
|
1872
|
+
? `Token exchange failed (${credentials.details}).`
|
|
1873
|
+
: "Token exchange failed. The code may be invalid or expired.",
|
|
1874
|
+
};
|
|
1870
1875
|
}
|
|
1871
1876
|
|
|
1872
1877
|
const stored = (await loadAccounts()) || { version: 1, accounts: [], activeIndex: 0 };
|
package/lib/oauth.mjs
CHANGED
|
@@ -10,6 +10,8 @@ const OAUTH_MAX_HOST = "claude.ai";
|
|
|
10
10
|
const OAUTH_REDIRECT_URI = `https://${OAUTH_CONSOLE_HOST}/oauth/code/callback`;
|
|
11
11
|
const OAUTH_TOKEN_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/token`;
|
|
12
12
|
const OAUTH_REVOKE_URL = `https://${OAUTH_CONSOLE_HOST}/v1/oauth/revoke`;
|
|
13
|
+
const OAUTH_CLI_VERSION_PROFILE = "2.1.79";
|
|
14
|
+
const OAUTH_USER_AGENT = `claude-cli/${OAUTH_CLI_VERSION_PROFILE} (external, cli)`;
|
|
13
15
|
|
|
14
16
|
function base64url(input) {
|
|
15
17
|
return Buffer.from(input).toString("base64url");
|
|
@@ -48,28 +50,79 @@ export async function authorize(mode) {
|
|
|
48
50
|
* Exchange an authorization code for tokens.
|
|
49
51
|
* @param {string} code
|
|
50
52
|
* @param {string} verifier
|
|
51
|
-
* @returns {Promise<
|
|
53
|
+
* @returns {Promise<
|
|
54
|
+
* | { type: "success", refresh: string, access: string, expires: number, email?: string }
|
|
55
|
+
* | { type: "failed", status?: number, code?: string, reason?: string, details?: string }
|
|
56
|
+
* >}
|
|
52
57
|
*/
|
|
53
58
|
export async function exchange(code, verifier) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
const fail = (status, rawText = "") => {
|
|
60
|
+
let errorCode;
|
|
61
|
+
let reason;
|
|
62
|
+
|
|
63
|
+
if (rawText) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(rawText);
|
|
66
|
+
if (typeof parsed.error === "string" && parsed.error) {
|
|
67
|
+
errorCode = parsed.error;
|
|
68
|
+
}
|
|
69
|
+
if (typeof parsed.error_description === "string" && parsed.error_description) {
|
|
70
|
+
reason = parsed.error_description;
|
|
71
|
+
} else if (typeof parsed.message === "string" && parsed.message) {
|
|
72
|
+
reason = parsed.message;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
reason = rawText.trim() || undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const detailsParts = [];
|
|
80
|
+
if (typeof status === "number") detailsParts.push(`HTTP ${status}`);
|
|
81
|
+
if (errorCode) detailsParts.push(errorCode);
|
|
82
|
+
if (reason) detailsParts.push(reason);
|
|
83
|
+
|
|
70
84
|
return {
|
|
71
85
|
type: "failed",
|
|
86
|
+
...(typeof status === "number" ? { status } : {}),
|
|
87
|
+
...(errorCode ? { code: errorCode } : {}),
|
|
88
|
+
...(reason ? { reason } : {}),
|
|
89
|
+
...(detailsParts.length ? { details: detailsParts.join(" · ") } : {}),
|
|
72
90
|
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const splits = code.split("#");
|
|
94
|
+
let result;
|
|
95
|
+
try {
|
|
96
|
+
result = await fetch(OAUTH_TOKEN_URL, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"User-Agent": OAUTH_USER_AGENT,
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
code: splits[0],
|
|
104
|
+
state: splits[1],
|
|
105
|
+
grant_type: "authorization_code",
|
|
106
|
+
client_id: CLIENT_ID,
|
|
107
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
108
|
+
code_verifier: verifier,
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return fail(undefined, err instanceof Error ? err.message : String(err));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
const raw =
|
|
117
|
+
typeof result.text === "function"
|
|
118
|
+
? await result
|
|
119
|
+
.text()
|
|
120
|
+
.then((value) => (typeof value === "string" ? value : ""))
|
|
121
|
+
.catch(() => "")
|
|
122
|
+
: "";
|
|
123
|
+
return fail(result.status, raw);
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
const json = await result.json();
|
|
74
127
|
return {
|
|
75
128
|
type: "success",
|
|
@@ -94,7 +147,10 @@ export async function revoke(refreshToken) {
|
|
|
94
147
|
try {
|
|
95
148
|
const resp = await fetch(OAUTH_REVOKE_URL, {
|
|
96
149
|
method: "POST",
|
|
97
|
-
headers: {
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"User-Agent": OAUTH_USER_AGENT,
|
|
153
|
+
},
|
|
98
154
|
body: JSON.stringify({
|
|
99
155
|
token: refreshToken,
|
|
100
156
|
token_type_hint: "refresh_token",
|
|
@@ -118,7 +174,10 @@ export async function revoke(refreshToken) {
|
|
|
118
174
|
export async function refreshToken(refreshTokenValue, options = {}) {
|
|
119
175
|
const resp = await fetch(OAUTH_TOKEN_URL, {
|
|
120
176
|
method: "POST",
|
|
121
|
-
headers: {
|
|
177
|
+
headers: {
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
"User-Agent": OAUTH_USER_AGENT,
|
|
180
|
+
},
|
|
122
181
|
body: JSON.stringify({
|
|
123
182
|
grant_type: "refresh_token",
|
|
124
183
|
client_id: CLIENT_ID,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { exchange, refreshToken, revoke } from "./oauth.mjs";
|
|
3
|
+
|
|
4
|
+
const mockFetch = vi.fn();
|
|
5
|
+
|
|
6
|
+
describe("oauth headers", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.unstubAllGlobals();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("sends Claude Code user-agent on token exchange", async () => {
|
|
17
|
+
mockFetch.mockResolvedValueOnce({
|
|
18
|
+
ok: false,
|
|
19
|
+
status: 400,
|
|
20
|
+
text: async () => "",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await exchange("code#state", "verifier");
|
|
24
|
+
|
|
25
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
26
|
+
expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("sends Claude Code user-agent on token refresh", async () => {
|
|
30
|
+
mockFetch.mockResolvedValueOnce({
|
|
31
|
+
ok: true,
|
|
32
|
+
json: async () => ({ access_token: "a", refresh_token: "r", expires_in: 3600 }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await refreshToken("refresh-token");
|
|
36
|
+
|
|
37
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
38
|
+
expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("sends Claude Code user-agent on token revoke", async () => {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
43
|
+
|
|
44
|
+
await revoke("refresh-token");
|
|
45
|
+
|
|
46
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
47
|
+
expect(init.headers["User-Agent"]).toBe("claude-cli/2.1.79 (external, cli)");
|
|
48
|
+
});
|
|
49
|
+
});
|