@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 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.5.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 then **split** and only the billing header and identity line are left in the system prompt. The remainder is moved into a user message to bypass system prompt checks.
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
 
@@ -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 OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
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 OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.';
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];
@@ -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(body: string): string;
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 line.
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, OPENCODE_IDENTITY, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from './constants';
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(body) {
80
- try {
81
- const parsed = JSON.parse(body);
82
- if (parsed.tools && Array.isArray(parsed.tools)) {
83
- parsed.tools = parsed.tools.map((tool) => ({
84
- ...tool,
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
- catch {
104
- return body;
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 line.
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
- // Remove the identity line (may be its own paragraph or part of one)
213
- if (paragraph.includes(OPENCODE_IDENTITY)) {
214
- // If the paragraph is JUST the identity, drop it entirely
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
- // --- Relocate non-core system entries to user messages ---
296
- // Anthropic's API validates system[] content for OAuth requests.
297
- // Third-party system prompts trigger a 400 rejection when they
298
- // appear in `system[]`. Keep only the identity block in `system[]`
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 JSON.stringify(parsed);
278
+ return prefixToolNames(parsed);
352
279
  }
353
280
  catch {
354
281
  return body;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ex-machina/opencode-anthropic-auth",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"