@ex-machina/opencode-anthropic-auth 1.4.1 → 1.5.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 +16 -3
- package/dist/cch.d.ts +24 -0
- package/dist/cch.js +47 -0
- package/dist/constants.d.ts +5 -1
- package/dist/constants.js +5 -1
- package/dist/transform.d.ts +6 -0
- package/dist/transform.js +24 -2
- package/package.json +2 -2
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.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
|
|
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
|
|
package/dist/cch.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type Message = {
|
|
2
|
+
role?: string;
|
|
3
|
+
content?: string | Array<{
|
|
4
|
+
type?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Extract text from the first user message's first text block.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractFirstUserMessageText(messages: Message[]): string;
|
|
12
|
+
/**
|
|
13
|
+
* Compute cch: first 5 hex characters of SHA-256(messageText).
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeCCH(messageText: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Compute the 3-char version suffix from the sampled message characters.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeVersionSuffix(messageText: string, version?: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Build the complete billing header string for insertion into system[0].
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildBillingHeaderValue(messages: Message[], version: string | undefined, entrypoint: string): string;
|
|
24
|
+
export {};
|
package/dist/cch.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { CCH_POSITIONS, CCH_SALT, CLAUDE_CODE_VERSION } from './constants';
|
|
3
|
+
/**
|
|
4
|
+
* Extract text from the first user message's first text block.
|
|
5
|
+
*/
|
|
6
|
+
export function extractFirstUserMessageText(messages) {
|
|
7
|
+
const userMsg = messages.find((message) => message.role === 'user');
|
|
8
|
+
if (!userMsg)
|
|
9
|
+
return '';
|
|
10
|
+
const { content } = userMsg;
|
|
11
|
+
if (typeof content === 'string')
|
|
12
|
+
return content;
|
|
13
|
+
if (Array.isArray(content)) {
|
|
14
|
+
const textBlock = content.find((block) => block.type === 'text');
|
|
15
|
+
if (textBlock?.text)
|
|
16
|
+
return textBlock.text;
|
|
17
|
+
}
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compute cch: first 5 hex characters of SHA-256(messageText).
|
|
22
|
+
*/
|
|
23
|
+
export function computeCCH(messageText) {
|
|
24
|
+
return createHash('sha256').update(messageText).digest('hex').slice(0, 5);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute the 3-char version suffix from the sampled message characters.
|
|
28
|
+
*/
|
|
29
|
+
export function computeVersionSuffix(messageText, version = CLAUDE_CODE_VERSION) {
|
|
30
|
+
const chars = CCH_POSITIONS.map((index) => messageText[index] || '0').join('');
|
|
31
|
+
return createHash('sha256')
|
|
32
|
+
.update(`${CCH_SALT}${chars}${version}`)
|
|
33
|
+
.digest('hex')
|
|
34
|
+
.slice(0, 3);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the complete billing header string for insertion into system[0].
|
|
38
|
+
*/
|
|
39
|
+
export function buildBillingHeaderValue(messages, version = CLAUDE_CODE_VERSION, entrypoint) {
|
|
40
|
+
const text = extractFirstUserMessageText(messages);
|
|
41
|
+
const suffix = computeVersionSuffix(text, version);
|
|
42
|
+
const cch = computeCCH(text);
|
|
43
|
+
return ('x-anthropic-billing-header: ' +
|
|
44
|
+
`cc_version=${version}.${suffix}; ` +
|
|
45
|
+
`cc_entrypoint=${entrypoint}; ` +
|
|
46
|
+
`cch=${cch};`);
|
|
47
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -10,7 +10,11 @@ export declare const TOOL_PREFIX = "mcp_";
|
|
|
10
10
|
export declare const REQUIRED_BETAS: string[];
|
|
11
11
|
export declare const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
|
|
12
12
|
export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const CCH_SALT = "59cf53e54c78";
|
|
14
|
+
export declare const CCH_POSITIONS: number[];
|
|
15
|
+
export declare const CLAUDE_CODE_VERSION = "2.1.87";
|
|
16
|
+
export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
17
|
+
export declare const USER_AGENT = "claude-cli/2.1.87 (external, cli)";
|
|
14
18
|
/**
|
|
15
19
|
* Anchors that identify paragraphs to remove from the system prompt.
|
|
16
20
|
* Any paragraph (text between blank lines) containing one of these
|
package/dist/constants.js
CHANGED
|
@@ -20,7 +20,11 @@ export const REQUIRED_BETAS = [
|
|
|
20
20
|
];
|
|
21
21
|
export const OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.';
|
|
22
22
|
export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
23
|
-
export const
|
|
23
|
+
export const CCH_SALT = '59cf53e54c78';
|
|
24
|
+
export const CCH_POSITIONS = [4, 7, 20];
|
|
25
|
+
export const CLAUDE_CODE_VERSION = '2.1.87';
|
|
26
|
+
export const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
|
|
27
|
+
export const USER_AGENT = 'claude-cli/2.1.87 (external, cli)';
|
|
24
28
|
/**
|
|
25
29
|
* Anchors that identify paragraphs to remove from the system prompt.
|
|
26
30
|
* Any paragraph (text between blank lines) containing one of these
|
package/dist/transform.d.ts
CHANGED
|
@@ -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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
3
|
/**
|
|
3
4
|
* Merge headers from a Request object and/or a RequestInit headers value
|
|
4
5
|
* into a single Headers instance.
|
|
@@ -108,6 +109,15 @@ export function isInsecure() {
|
|
|
108
109
|
const raw = process.env.ANTHROPIC_INSECURE?.trim();
|
|
109
110
|
return raw === '1' || raw === 'true';
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Check if system prompt relocation should be skipped.
|
|
114
|
+
* When enabled, sanitized system blocks stay in system[] instead of
|
|
115
|
+
* being moved to the first user message.
|
|
116
|
+
*/
|
|
117
|
+
export function experimentalKeepSystemPrompt() {
|
|
118
|
+
const raw = process.env.EXPERIMENTAL_KEEP_SYSTEM_PROMPT?.trim();
|
|
119
|
+
return raw === '1' || raw === 'true';
|
|
120
|
+
}
|
|
111
121
|
/**
|
|
112
122
|
* Parse ANTHROPIC_BASE_URL from the environment.
|
|
113
123
|
* Returns a valid HTTP(S) URL or null if unset/invalid.
|
|
@@ -265,6 +275,10 @@ export function prependClaudeCodeIdentity(system) {
|
|
|
265
275
|
export function rewriteRequestBody(body) {
|
|
266
276
|
try {
|
|
267
277
|
const parsed = JSON.parse(body);
|
|
278
|
+
const billingHeader = Array.isArray(parsed.messages) &&
|
|
279
|
+
parsed.messages.some((message) => message.role === 'user')
|
|
280
|
+
? buildBillingHeaderValue(parsed.messages, undefined, CLAUDE_CODE_ENTRYPOINT)
|
|
281
|
+
: null;
|
|
268
282
|
// Sanitize system prompt and prepend Claude Code identity
|
|
269
283
|
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
270
284
|
// --- Relocate non-core system entries to user messages ---
|
|
@@ -272,7 +286,9 @@ export function rewriteRequestBody(body) {
|
|
|
272
286
|
// Third-party system prompts trigger a 400 rejection when they
|
|
273
287
|
// appear in `system[]`. Keep only the identity block in `system[]`
|
|
274
288
|
// and prepend everything else to the first user message.
|
|
275
|
-
if (
|
|
289
|
+
if (!experimentalKeepSystemPrompt() &&
|
|
290
|
+
Array.isArray(parsed.system) &&
|
|
291
|
+
parsed.system.length > 1) {
|
|
276
292
|
const kept = [parsed.system[0]]; // identity block
|
|
277
293
|
const movedTexts = [];
|
|
278
294
|
for (let i = 1; i < parsed.system.length; i++) {
|
|
@@ -295,6 +311,12 @@ export function rewriteRequestBody(body) {
|
|
|
295
311
|
}
|
|
296
312
|
}
|
|
297
313
|
}
|
|
314
|
+
const identityBlock = parsed.system[0];
|
|
315
|
+
if (billingHeader &&
|
|
316
|
+
identityBlock?.type === 'text' &&
|
|
317
|
+
identityBlock.text === CLAUDE_CODE_IDENTITY) {
|
|
318
|
+
identityBlock.text = `${billingHeader}\n\n${CLAUDE_CODE_IDENTITY}`;
|
|
319
|
+
}
|
|
298
320
|
// Prefix tool names
|
|
299
321
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
300
322
|
parsed.tools = parsed.tools.map((tool) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ex-machina/opencode-anthropic-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"extract": "bun scripts/extract-system-prompt.ts",
|
|
19
19
|
"test": "bun test",
|
|
20
20
|
"types": "tsc",
|
|
21
|
-
"format": "biome
|
|
21
|
+
"format": "biome check --write --unsafe",
|
|
22
22
|
"format:check": "biome format .",
|
|
23
23
|
"lint": "biome lint .",
|
|
24
24
|
"change": "changeset",
|