@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 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.5.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 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.
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
 
@@ -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);
@@ -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(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);
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
- catch {
104
- return body;
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 line.
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
- // 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
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
- // --- 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
- });
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 JSON.stringify(parsed);
282
+ return prefixToolNames(parsed);
352
283
  }
353
284
  catch {
354
285
  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.7.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"