@ex-machina/opencode-anthropic-auth 1.6.0 → 1.7.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 +7 -6
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/transform.d.ts +2 -8
- package/dist/transform.js +33 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
An [OpenCode](https://github.com/anomalyco/opencode) plugin that provides Anthropic OAuth authentication, enabling Claude Pro/Max users to use their subscription directly with OpenCode.
|
|
4
4
|
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> If you are seeing issues, please try to `rm -rf ~/.config/opencode` and check your `opencode.json` config to make sure you're on the latest version.
|
|
7
|
+
>
|
|
8
|
+
> Try this FIRST before making an Issue. Thanks!
|
|
9
|
+
|
|
5
10
|
## Usage
|
|
6
11
|
|
|
7
12
|
Add the plugin to your OpenCode configuration:
|
|
@@ -21,7 +26,7 @@ Add the plugin to your OpenCode configuration:
|
|
|
21
26
|
|
|
22
27
|
```json
|
|
23
28
|
{
|
|
24
|
-
"plugin": ["@ex-machina/opencode-anthropic-auth@1.
|
|
29
|
+
"plugin": ["@ex-machina/opencode-anthropic-auth@1.6.1"]
|
|
25
30
|
}
|
|
26
31
|
```
|
|
27
32
|
|
|
@@ -41,7 +46,6 @@ The plugin supports the following environment variables:
|
|
|
41
46
|
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
42
47
|
| `ANTHROPIC_BASE_URL` | Override the API endpoint URL (e.g. for proxying). Must be a valid HTTP(S) URL. |
|
|
43
48
|
| `ANTHROPIC_INSECURE` | Set to `1` or `true` to skip TLS certificate verification. Only effective when `ANTHROPIC_BASE_URL` is also set. |
|
|
44
|
-
| `EXPERIMENTAL_KEEP_SYSTEM_PROMPT` | Set to `1` or `true` to keep the sanitized system prompt in the `system[]` field instead of relocating it to a user message. See [System Prompt Sanitization](#system-prompt-sanitization). |
|
|
45
49
|
|
|
46
50
|
## How It Works
|
|
47
51
|
|
|
@@ -62,10 +66,7 @@ The Anthropic API for Max subscriptions has specific requirements for the system
|
|
|
62
66
|
2. **Paragraph removal by anchor** — Any paragraph containing a known URL anchor (e.g. `github.com/anomalyco/opencode`, `opencode.ai/docs`) is removed entirely. This is resilient to upstream rewording — as long as the anchor URL appears somewhere in the paragraph, the removal works regardless of surrounding text changes.
|
|
63
67
|
3. **Inline text replacements** — Short branded strings inside paragraphs we want to keep are replaced (e.g. "OpenCode" → "the assistant" in the professional objectivity section).
|
|
64
68
|
|
|
65
|
-
Everything else in the system prompt is preserved: tone/style guidance, task management instructions, tool usage policy, environment info, skills, user/project instructions, and file paths containing "opencode". The system prompt is
|
|
66
|
-
|
|
67
|
-
> [!NOTE]
|
|
68
|
-
> Set `EXPERIMENTAL_KEEP_SYSTEM_PROMPT=1` to skip the relocation step. The sanitized system prompt will remain in `system[]` in its entirety. This may cause API rejections for OAuth-authenticated requests.
|
|
69
|
+
Everything else in the system prompt is preserved: tone/style guidance, task management instructions, tool usage policy, environment info, skills, user/project instructions, and file paths containing "opencode". The sanitized system prompt is structured as three blocks in `system[]`: the billing header, the Claude Code identity line, and the remaining system content.
|
|
69
70
|
|
|
70
71
|
## Development
|
|
71
72
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
|
8
8
|
export declare const OAUTH_SCOPES: string[];
|
|
9
9
|
export declare const TOOL_PREFIX = "mcp_";
|
|
10
10
|
export declare const REQUIRED_BETAS: string[];
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
|
|
12
12
|
export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
13
13
|
export declare const CCH_SALT = "59cf53e54c78";
|
|
14
14
|
export declare const CCH_POSITIONS: number[];
|
package/dist/constants.js
CHANGED
|
@@ -18,7 +18,7 @@ export const REQUIRED_BETAS = [
|
|
|
18
18
|
'oauth-2025-04-20',
|
|
19
19
|
'interleaved-thinking-2025-05-14',
|
|
20
20
|
];
|
|
21
|
-
export const
|
|
21
|
+
export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
|
|
22
22
|
export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
23
23
|
export const CCH_SALT = '59cf53e54c78';
|
|
24
24
|
export const CCH_POSITIONS = [4, 7, 20];
|
package/dist/transform.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare function setOAuthHeaders(headers: Headers, accessToken: string):
|
|
|
17
17
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
18
18
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
19
19
|
*/
|
|
20
|
-
export declare function prefixToolNames(
|
|
20
|
+
export declare function prefixToolNames(parsed: Record<string, unknown>): string;
|
|
21
21
|
/**
|
|
22
22
|
* Strip TOOL_PREFIX from tool names in streaming response text.
|
|
23
23
|
*/
|
|
@@ -27,12 +27,6 @@ export declare function stripToolPrefix(text: string): string;
|
|
|
27
27
|
* Only effective when ANTHROPIC_BASE_URL is also set.
|
|
28
28
|
*/
|
|
29
29
|
export declare function isInsecure(): boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Check if system prompt relocation should be skipped.
|
|
32
|
-
* When enabled, sanitized system blocks stay in system[] instead of
|
|
33
|
-
* being moved to the first user message.
|
|
34
|
-
*/
|
|
35
|
-
export declare function experimentalKeepSystemPrompt(): boolean;
|
|
36
30
|
/**
|
|
37
31
|
* Rewrite the request URL to add ?beta=true for /v1/messages requests.
|
|
38
32
|
* When ANTHROPIC_BASE_URL is set, overrides the origin (protocol + host)
|
|
@@ -46,7 +40,7 @@ export declare function rewriteUrl(input: FetchInput): {
|
|
|
46
40
|
/**
|
|
47
41
|
* Sanitize OpenCode-branded strings from the system prompt text.
|
|
48
42
|
*
|
|
49
|
-
* 1. Removes the OPENCODE_IDENTITY
|
|
43
|
+
* 1. Removes the OPENCODE_IDENTITY paragraph.
|
|
50
44
|
* 2. Removes any paragraph (text between blank lines) that contains
|
|
51
45
|
* one of the PARAGRAPH_REMOVAL_ANCHORS — typically URLs that
|
|
52
46
|
* identify OpenCode-specific content.
|
package/dist/transform.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildBillingHeaderValue } from './cch';
|
|
2
|
-
import { CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY,
|
|
2
|
+
import { CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from './constants';
|
|
3
3
|
/**
|
|
4
4
|
* Prefix a tool name with TOOL_PREFIX and uppercase the first character.
|
|
5
5
|
* Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
|
|
@@ -12,6 +12,10 @@ function prefixName(name) {
|
|
|
12
12
|
* Reverse prefixName: strip TOOL_PREFIX and restore the original leading case.
|
|
13
13
|
*/
|
|
14
14
|
function unprefixName(name) {
|
|
15
|
+
// StructuredOutput is still used as StructuredOutput
|
|
16
|
+
if (name === 'StructuredOutput') {
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
15
19
|
return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
|
|
16
20
|
}
|
|
17
21
|
/**
|
|
@@ -76,33 +80,27 @@ export function setOAuthHeaders(headers, accessToken) {
|
|
|
76
80
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
77
81
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
78
82
|
*/
|
|
79
|
-
export function prefixToolNames(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
name: tool.name ? prefixName(tool.name) : tool.name,
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
88
|
-
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
89
|
-
parsed.messages = parsed.messages.map((msg) => {
|
|
90
|
-
if (msg.content && Array.isArray(msg.content)) {
|
|
91
|
-
msg.content = msg.content.map((block) => {
|
|
92
|
-
if (block.type === 'tool_use' && block.name) {
|
|
93
|
-
return { ...block, name: prefixName(block.name) };
|
|
94
|
-
}
|
|
95
|
-
return block;
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
return msg;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return JSON.stringify(parsed);
|
|
83
|
+
export function prefixToolNames(parsed) {
|
|
84
|
+
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
85
|
+
parsed.tools = parsed.tools.map((tool) => ({
|
|
86
|
+
...tool,
|
|
87
|
+
name: tool.name ? prefixName(tool.name) : tool.name,
|
|
88
|
+
}));
|
|
102
89
|
}
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
91
|
+
parsed.messages = parsed.messages.map((msg) => {
|
|
92
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
93
|
+
msg.content = msg.content.map((block) => {
|
|
94
|
+
if (block.type === 'tool_use' && block.name) {
|
|
95
|
+
return { ...block, name: prefixName(block.name) };
|
|
96
|
+
}
|
|
97
|
+
return block;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return msg;
|
|
101
|
+
});
|
|
105
102
|
}
|
|
103
|
+
return JSON.stringify(parsed);
|
|
106
104
|
}
|
|
107
105
|
/**
|
|
108
106
|
* Strip TOOL_PREFIX from tool names in streaming response text.
|
|
@@ -120,15 +118,6 @@ export function isInsecure() {
|
|
|
120
118
|
const raw = process.env.ANTHROPIC_INSECURE?.trim();
|
|
121
119
|
return raw === '1' || raw === 'true';
|
|
122
120
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Check if system prompt relocation should be skipped.
|
|
125
|
-
* When enabled, sanitized system blocks stay in system[] instead of
|
|
126
|
-
* being moved to the first user message.
|
|
127
|
-
*/
|
|
128
|
-
export function experimentalKeepSystemPrompt() {
|
|
129
|
-
const raw = process.env.EXPERIMENTAL_KEEP_SYSTEM_PROMPT?.trim();
|
|
130
|
-
return raw === '1' || raw === 'true';
|
|
131
|
-
}
|
|
132
121
|
/**
|
|
133
122
|
* Parse ANTHROPIC_BASE_URL from the environment.
|
|
134
123
|
* Returns a valid HTTP(S) URL or null if unset/invalid.
|
|
@@ -192,7 +181,7 @@ export function rewriteUrl(input) {
|
|
|
192
181
|
/**
|
|
193
182
|
* Sanitize OpenCode-branded strings from the system prompt text.
|
|
194
183
|
*
|
|
195
|
-
* 1. Removes the OPENCODE_IDENTITY
|
|
184
|
+
* 1. Removes the OPENCODE_IDENTITY paragraph.
|
|
196
185
|
* 2. Removes any paragraph (text between blank lines) that contains
|
|
197
186
|
* one of the PARAGRAPH_REMOVAL_ANCHORS — typically URLs that
|
|
198
187
|
* identify OpenCode-specific content.
|
|
@@ -204,17 +193,12 @@ export function rewriteUrl(input) {
|
|
|
204
193
|
* somewhere in the paragraph, the removal works.
|
|
205
194
|
*/
|
|
206
195
|
export function sanitizeSystemText(text) {
|
|
207
|
-
if (!text.includes(OPENCODE_IDENTITY))
|
|
208
|
-
return text;
|
|
209
196
|
// Split into paragraphs (separated by one or more blank lines)
|
|
210
197
|
const paragraphs = text.split(/\n\n+/);
|
|
211
198
|
const filtered = paragraphs.filter((paragraph) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (paragraph.trim() === OPENCODE_IDENTITY)
|
|
216
|
-
return false;
|
|
217
|
-
// Otherwise it's mixed — we'll handle inline below
|
|
199
|
+
if (paragraph.includes(OPENCODE_IDENTITY_PREFIX)) {
|
|
200
|
+
// If the paragraph contains the identity, drop it entirely
|
|
201
|
+
return false;
|
|
218
202
|
}
|
|
219
203
|
// Remove paragraphs containing any removal anchor
|
|
220
204
|
for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
|
|
@@ -224,8 +208,6 @@ export function sanitizeSystemText(text) {
|
|
|
224
208
|
return true;
|
|
225
209
|
});
|
|
226
210
|
let result = filtered.join('\n\n');
|
|
227
|
-
// Remove the identity line if it was part of a larger paragraph
|
|
228
|
-
result = result.replace(OPENCODE_IDENTITY, '').replace(/\n{3,}/g, '\n\n');
|
|
229
211
|
// Apply inline text replacements
|
|
230
212
|
for (const rule of TEXT_REPLACEMENTS) {
|
|
231
213
|
result = result.replace(rule.match, rule.replacement);
|
|
@@ -292,63 +274,12 @@ export function rewriteRequestBody(body) {
|
|
|
292
274
|
: null;
|
|
293
275
|
// Sanitize system prompt and prepend Claude Code identity
|
|
294
276
|
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// and prepend everything else to the first user message.
|
|
300
|
-
if (!experimentalKeepSystemPrompt() &&
|
|
301
|
-
Array.isArray(parsed.system) &&
|
|
302
|
-
parsed.system.length > 1) {
|
|
303
|
-
const kept = [parsed.system[0]]; // identity block
|
|
304
|
-
const movedTexts = [];
|
|
305
|
-
for (let i = 1; i < parsed.system.length; i++) {
|
|
306
|
-
const entry = parsed.system[i];
|
|
307
|
-
const txt = typeof entry === 'string' ? entry : (entry?.text ?? '');
|
|
308
|
-
if (txt.length > 0)
|
|
309
|
-
movedTexts.push(txt);
|
|
310
|
-
}
|
|
311
|
-
if (movedTexts.length > 0 && Array.isArray(parsed.messages)) {
|
|
312
|
-
const firstUser = parsed.messages.find((m) => m.role === 'user');
|
|
313
|
-
if (firstUser) {
|
|
314
|
-
parsed.system = kept;
|
|
315
|
-
const prefix = movedTexts.join('\n\n');
|
|
316
|
-
if (typeof firstUser.content === 'string') {
|
|
317
|
-
firstUser.content = `${prefix}\n\n${firstUser.content}`;
|
|
318
|
-
}
|
|
319
|
-
else if (Array.isArray(firstUser.content)) {
|
|
320
|
-
firstUser.content.unshift({ type: 'text', text: prefix });
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
const identityBlock = parsed.system[0];
|
|
326
|
-
if (billingHeader &&
|
|
327
|
-
identityBlock?.type === 'text' &&
|
|
328
|
-
identityBlock.text === CLAUDE_CODE_IDENTITY) {
|
|
329
|
-
identityBlock.text = `${billingHeader}\n\n${CLAUDE_CODE_IDENTITY}`;
|
|
330
|
-
}
|
|
331
|
-
// Prefix tool names
|
|
332
|
-
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
333
|
-
parsed.tools = parsed.tools.map((tool) => ({
|
|
334
|
-
...tool,
|
|
335
|
-
name: tool.name ? prefixName(tool.name) : tool.name,
|
|
336
|
-
}));
|
|
337
|
-
}
|
|
338
|
-
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
339
|
-
parsed.messages = parsed.messages.map((msg) => {
|
|
340
|
-
if (msg.content && Array.isArray(msg.content)) {
|
|
341
|
-
msg.content = msg.content.map((block) => {
|
|
342
|
-
if (block.type === 'tool_use' && block.name) {
|
|
343
|
-
return { ...block, name: prefixName(block.name) };
|
|
344
|
-
}
|
|
345
|
-
return block;
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
return msg;
|
|
349
|
-
});
|
|
277
|
+
// Prepend the billing header as a separate system block so the
|
|
278
|
+
// final layout is: [billing header, identity, ...rest]
|
|
279
|
+
if (billingHeader && Array.isArray(parsed.system)) {
|
|
280
|
+
parsed.system.unshift({ type: 'text', text: billingHeader });
|
|
350
281
|
}
|
|
351
|
-
return
|
|
282
|
+
return prefixToolNames(parsed);
|
|
352
283
|
}
|
|
353
284
|
catch {
|
|
354
285
|
return body;
|