@ex-machina/opencode-anthropic-auth 1.5.0 → 1.6.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
@@ -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.4.0"]
24
+ "plugin": ["@ex-machina/opencode-anthropic-auth@1.5.1"]
25
25
  }
26
26
  ```
27
27
 
@@ -33,6 +33,16 @@ The plugin provides three authentication options:
33
33
  - **Create an API Key** - OAuth flow via `console.anthropic.com` that creates an API key on your behalf.
34
34
  - **Manually enter API Key** - Standard API key entry for users who already have one.
35
35
 
36
+ ## Configuration
37
+
38
+ The plugin supports the following environment variables:
39
+
40
+ | Variable | Description |
41
+ |-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
42
+ | `ANTHROPIC_BASE_URL` | Override the API endpoint URL (e.g. for proxying). Must be a valid HTTP(S) URL. |
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
+
36
46
  ## How It Works
37
47
 
38
48
  For Claude Pro/Max authentication, the plugin:
@@ -46,13 +56,16 @@ For Claude Pro/Max authentication, the plugin:
46
56
 
47
57
  ### System Prompt Sanitization
48
58
 
49
- The Anthropic API for Max subscriptions requires the system prompt to identify as Claude Code. The plugin rewrites the system prompt on each request using an **anchor-based** approach that minimizes what gets changed:
59
+ The Anthropic API for Max subscriptions has specific requirements for the system prompt to identify as Claude Code. The plugin rewrites the system prompt on each request using an **anchor-based** approach that minimizes what gets changed:
50
60
 
51
61
  1. **Identity swap** — The OpenCode identity line is removed and replaced with the Claude Code identity.
52
62
  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.
53
63
  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).
54
64
 
55
- 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".
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.
56
69
 
57
70
  ## Development
58
71
 
@@ -27,6 +27,12 @@ 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;
30
36
  /**
31
37
  * Rewrite the request URL to add ?beta=true for /v1/messages requests.
32
38
  * When ANTHROPIC_BASE_URL is set, overrides the origin (protocol + host)
package/dist/transform.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { buildBillingHeaderValue } from './cch';
2
2
  import { CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from './constants';
3
+ /**
4
+ * Prefix a tool name with TOOL_PREFIX and uppercase the first character.
5
+ * Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
6
+ * lowercase names (mcp_bash, mcp_read) are flagged as non-Claude-Code clients.
7
+ */
8
+ function prefixName(name) {
9
+ return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
10
+ }
11
+ /**
12
+ * Reverse prefixName: strip TOOL_PREFIX and restore the original leading case.
13
+ */
14
+ function unprefixName(name) {
15
+ return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
16
+ }
3
17
  /**
4
18
  * Merge headers from a Request object and/or a RequestInit headers value
5
19
  * into a single Headers instance.
@@ -68,7 +82,7 @@ export function prefixToolNames(body) {
68
82
  if (parsed.tools && Array.isArray(parsed.tools)) {
69
83
  parsed.tools = parsed.tools.map((tool) => ({
70
84
  ...tool,
71
- name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name,
85
+ name: tool.name ? prefixName(tool.name) : tool.name,
72
86
  }));
73
87
  }
74
88
  if (parsed.messages && Array.isArray(parsed.messages)) {
@@ -76,10 +90,7 @@ export function prefixToolNames(body) {
76
90
  if (msg.content && Array.isArray(msg.content)) {
77
91
  msg.content = msg.content.map((block) => {
78
92
  if (block.type === 'tool_use' && block.name) {
79
- return {
80
- ...block,
81
- name: `${TOOL_PREFIX}${block.name}`,
82
- };
93
+ return { ...block, name: prefixName(block.name) };
83
94
  }
84
95
  return block;
85
96
  });
@@ -97,7 +108,7 @@ export function prefixToolNames(body) {
97
108
  * Strip TOOL_PREFIX from tool names in streaming response text.
98
109
  */
99
110
  export function stripToolPrefix(text) {
100
- return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
111
+ return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, (_match, name) => `"name": "${unprefixName(name)}"`);
101
112
  }
102
113
  /**
103
114
  * Check if TLS verification should be skipped for custom API endpoints.
@@ -109,6 +120,15 @@ export function isInsecure() {
109
120
  const raw = process.env.ANTHROPIC_INSECURE?.trim();
110
121
  return raw === '1' || raw === 'true';
111
122
  }
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
+ }
112
132
  /**
113
133
  * Parse ANTHROPIC_BASE_URL from the environment.
114
134
  * Returns a valid HTTP(S) URL or null if unset/invalid.
@@ -277,7 +297,9 @@ export function rewriteRequestBody(body) {
277
297
  // Third-party system prompts trigger a 400 rejection when they
278
298
  // appear in `system[]`. Keep only the identity block in `system[]`
279
299
  // and prepend everything else to the first user message.
280
- if (Array.isArray(parsed.system) && parsed.system.length > 1) {
300
+ if (!experimentalKeepSystemPrompt() &&
301
+ Array.isArray(parsed.system) &&
302
+ parsed.system.length > 1) {
281
303
  const kept = [parsed.system[0]]; // identity block
282
304
  const movedTexts = [];
283
305
  for (let i = 1; i < parsed.system.length; i++) {
@@ -310,7 +332,7 @@ export function rewriteRequestBody(body) {
310
332
  if (parsed.tools && Array.isArray(parsed.tools)) {
311
333
  parsed.tools = parsed.tools.map((tool) => ({
312
334
  ...tool,
313
- name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name,
335
+ name: tool.name ? prefixName(tool.name) : tool.name,
314
336
  }));
315
337
  }
316
338
  if (parsed.messages && Array.isArray(parsed.messages)) {
@@ -318,7 +340,7 @@ export function rewriteRequestBody(body) {
318
340
  if (msg.content && Array.isArray(msg.content)) {
319
341
  msg.content = msg.content.map((block) => {
320
342
  if (block.type === 'tool_use' && block.name) {
321
- return { ...block, name: `${TOOL_PREFIX}${block.name}` };
343
+ return { ...block, name: prefixName(block.name) };
322
344
  }
323
345
  return block;
324
346
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ex-machina/opencode-anthropic-auth",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"