@ex-machina/opencode-anthropic-auth 1.6.0 → 1.6.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/README.md +2 -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 +29 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Add the plugin to your OpenCode configuration:
|
|
|
21
21
|
|
|
22
22
|
```json
|
|
23
23
|
{
|
|
24
|
-
"plugin": ["@ex-machina/opencode-anthropic-auth@1.
|
|
24
|
+
"plugin": ["@ex-machina/opencode-anthropic-auth@1.6.0"]
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -41,7 +41,6 @@ The plugin supports the following environment variables:
|
|
|
41
41
|
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
42
42
|
| `ANTHROPIC_BASE_URL` | Override the API endpoint URL (e.g. for proxying). Must be a valid HTTP(S) URL. |
|
|
43
43
|
| `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
44
|
|
|
46
45
|
## How It Works
|
|
47
46
|
|
|
@@ -62,10 +61,7 @@ The Anthropic API for Max subscriptions has specific requirements for the system
|
|
|
62
61
|
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
62
|
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
63
|
|
|
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.
|
|
64
|
+
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
65
|
|
|
70
66
|
## Development
|
|
71
67
|
|
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);
|
|
@@ -76,33 +76,27 @@ export function setOAuthHeaders(headers, accessToken) {
|
|
|
76
76
|
* Add TOOL_PREFIX to tool names in the request body.
|
|
77
77
|
* Prefixes both tool definitions and tool_use blocks in messages.
|
|
78
78
|
*/
|
|
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);
|
|
79
|
+
export function prefixToolNames(parsed) {
|
|
80
|
+
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
81
|
+
parsed.tools = parsed.tools.map((tool) => ({
|
|
82
|
+
...tool,
|
|
83
|
+
name: tool.name ? prefixName(tool.name) : tool.name,
|
|
84
|
+
}));
|
|
102
85
|
}
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
87
|
+
parsed.messages = parsed.messages.map((msg) => {
|
|
88
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
89
|
+
msg.content = msg.content.map((block) => {
|
|
90
|
+
if (block.type === 'tool_use' && block.name) {
|
|
91
|
+
return { ...block, name: prefixName(block.name) };
|
|
92
|
+
}
|
|
93
|
+
return block;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return msg;
|
|
97
|
+
});
|
|
105
98
|
}
|
|
99
|
+
return JSON.stringify(parsed);
|
|
106
100
|
}
|
|
107
101
|
/**
|
|
108
102
|
* Strip TOOL_PREFIX from tool names in streaming response text.
|
|
@@ -120,15 +114,6 @@ export function isInsecure() {
|
|
|
120
114
|
const raw = process.env.ANTHROPIC_INSECURE?.trim();
|
|
121
115
|
return raw === '1' || raw === 'true';
|
|
122
116
|
}
|
|
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
117
|
/**
|
|
133
118
|
* Parse ANTHROPIC_BASE_URL from the environment.
|
|
134
119
|
* Returns a valid HTTP(S) URL or null if unset/invalid.
|
|
@@ -192,7 +177,7 @@ export function rewriteUrl(input) {
|
|
|
192
177
|
/**
|
|
193
178
|
* Sanitize OpenCode-branded strings from the system prompt text.
|
|
194
179
|
*
|
|
195
|
-
* 1. Removes the OPENCODE_IDENTITY
|
|
180
|
+
* 1. Removes the OPENCODE_IDENTITY paragraph.
|
|
196
181
|
* 2. Removes any paragraph (text between blank lines) that contains
|
|
197
182
|
* one of the PARAGRAPH_REMOVAL_ANCHORS — typically URLs that
|
|
198
183
|
* identify OpenCode-specific content.
|
|
@@ -204,17 +189,12 @@ export function rewriteUrl(input) {
|
|
|
204
189
|
* somewhere in the paragraph, the removal works.
|
|
205
190
|
*/
|
|
206
191
|
export function sanitizeSystemText(text) {
|
|
207
|
-
if (!text.includes(OPENCODE_IDENTITY))
|
|
208
|
-
return text;
|
|
209
192
|
// Split into paragraphs (separated by one or more blank lines)
|
|
210
193
|
const paragraphs = text.split(/\n\n+/);
|
|
211
194
|
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
|
|
195
|
+
if (paragraph.includes(OPENCODE_IDENTITY_PREFIX)) {
|
|
196
|
+
// If the paragraph contains the identity, drop it entirely
|
|
197
|
+
return false;
|
|
218
198
|
}
|
|
219
199
|
// Remove paragraphs containing any removal anchor
|
|
220
200
|
for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
|
|
@@ -224,8 +204,6 @@ export function sanitizeSystemText(text) {
|
|
|
224
204
|
return true;
|
|
225
205
|
});
|
|
226
206
|
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
207
|
// Apply inline text replacements
|
|
230
208
|
for (const rule of TEXT_REPLACEMENTS) {
|
|
231
209
|
result = result.replace(rule.match, rule.replacement);
|
|
@@ -292,63 +270,12 @@ export function rewriteRequestBody(body) {
|
|
|
292
270
|
: null;
|
|
293
271
|
// Sanitize system prompt and prepend Claude Code identity
|
|
294
272
|
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
|
-
});
|
|
273
|
+
// Prepend the billing header as a separate system block so the
|
|
274
|
+
// final layout is: [billing header, identity, ...rest]
|
|
275
|
+
if (billingHeader && Array.isArray(parsed.system)) {
|
|
276
|
+
parsed.system.unshift({ type: 'text', text: billingHeader });
|
|
350
277
|
}
|
|
351
|
-
return
|
|
278
|
+
return prefixToolNames(parsed);
|
|
352
279
|
}
|
|
353
280
|
catch {
|
|
354
281
|
return body;
|