opencode-anthropic-fix 0.0.49 → 0.1.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 +14 -1
- package/index.mjs +163 -71
- package/lib/accounts.mjs +4 -0
- package/lib/oauth.mjs +4 -0
- package/lib/storage.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The [original plugin](https://github.com/anomalyco/opencode-anthropic-auth) prov
|
|
|
44
44
|
- **Configurable strategies** — sticky, round-robin, or hybrid account selection
|
|
45
45
|
- **Claude Code signature emulation** — full HTTP header, system prompt, beta flag, and metadata mimicry derived from Claude Code's open source code. System prompt cache scoping follows the real CC's three-path `splitSysPromptPrefix()` architecture (boundary/global/org) with exact marker detection and scope-aware `cache_control` generation
|
|
46
46
|
- **OAuth endpoint fingerprint parity** — matches the real CLI's bundled axios 1.13.6 HTTP client signature (`Accept`, `User-Agent`, `Content-Type`) on all OAuth token endpoint calls, required since 2026-03-21 server-side enforcement
|
|
47
|
-
- **Billing header fingerprint parity** — `cc_version` suffix uses the real CLI's 3-char fingerprint hash (SHA-256 of salt + first user message chars + version), `cch`
|
|
47
|
+
- **Billing header fingerprint parity** — `cc_version` suffix uses the real CLI's 3-char fingerprint hash (SHA-256 of salt + first user message chars + version), `cch=00000` is a static placeholder (xxHash64 attestation was removed in Claude Code v2.1.97), system prompt is sanitized to match Claude Code's pattern validation, and `X-Claude-Code-Session-Id` header is sent on all requests
|
|
48
48
|
- **Adaptive thinking for Opus/Sonnet 4.6** — automatically normalizes thinking to `{type: "adaptive"}` for supported models, with `effort-2025-11-24` beta
|
|
49
49
|
- **Upstream-aligned auto betas** — 13+ always-on betas matching Claude Code 2.1.92 (`redact-thinking-2026-02-12` available as opt-in to preserve thinking block visibility)
|
|
50
50
|
- **1M context limit override** — patches `model.limit.context` so OpenCode compacts at the right threshold while `models.dev` catches up
|
|
@@ -700,6 +700,19 @@ Make sure `~/.local/bin` is on your PATH:
|
|
|
700
700
|
export PATH="$HOME/.local/bin:$PATH"
|
|
701
701
|
```
|
|
702
702
|
|
|
703
|
+
## Credits & Acknowledgments
|
|
704
|
+
|
|
705
|
+
### Reverse Engineering & Research
|
|
706
|
+
|
|
707
|
+
- **[CCH Attestation Reverse Engineering](https://a10k.co/b/reverse-engineering-claude-code-cch.html)** — Deep dive into the xxHash64 algorithm and Bun binary attestation mechanism
|
|
708
|
+
- **[OpenClaw Billing Proxy](https://github.com/zacdcook/openclaw-billing-proxy)** — Early exploration of billing headers and trigger phrase discovery
|
|
709
|
+
- **[Free Code CCH Implementation](https://github.com/paoloanzn/free-code/pull/9)** — Reference implementation of xxHash64 in JavaScript
|
|
710
|
+
- **[rmk40](https://github.com/rmk40/opencode-anthropic-auth)** — Co-author on fingerprint extraction work and multi-account architecture
|
|
711
|
+
|
|
712
|
+
### Original Work
|
|
713
|
+
|
|
714
|
+
- [AnomalyCo](https://github.com/anomalyco/opencode-anthropic-auth) — Original OpenCode Anthropic auth plugin
|
|
715
|
+
|
|
703
716
|
## License
|
|
704
717
|
|
|
705
718
|
This project is licensed under the [GNU General Public License v3.0](LICENSE) (GPLv3).
|
package/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { stdin, stdout } from "node:process";
|
|
|
3
3
|
import { randomBytes, randomUUID, createHash as createHashCrypto } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, resolve, basename } from "node:path";
|
|
6
|
+
// xxhash-wasm import removed: CCH attestation was removed in CC v2.1.97
|
|
6
7
|
import { AccountManager } from "./lib/accounts.mjs";
|
|
7
8
|
import { authorize as oauthAuthorize, exchange as oauthExchange, refreshToken } from "./lib/oauth.mjs";
|
|
8
9
|
import { loadConfig, loadConfigFresh, saveConfig, CLIENT_ID, getConfigDir } from "./lib/config.mjs";
|
|
@@ -448,6 +449,8 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
448
449
|
acc.access = credentials.access;
|
|
449
450
|
acc.expires = credentials.expires;
|
|
450
451
|
if (credentials.email) acc.email = credentials.email;
|
|
452
|
+
if (credentials.accountUuid) acc.accountUuid = credentials.accountUuid;
|
|
453
|
+
if (credentials.organizationUuid) acc.organizationUuid = credentials.organizationUuid;
|
|
451
454
|
acc.enabled = true;
|
|
452
455
|
acc.consecutiveFailures = 0;
|
|
453
456
|
acc.lastFailureTime = null;
|
|
@@ -469,6 +472,8 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
469
472
|
stored.accounts.push({
|
|
470
473
|
id: `${now}:${credentials.refresh.slice(0, 12)}`,
|
|
471
474
|
email: credentials.email,
|
|
475
|
+
accountUuid: credentials.accountUuid,
|
|
476
|
+
organizationUuid: credentials.organizationUuid,
|
|
472
477
|
refreshToken: credentials.refresh,
|
|
473
478
|
access: credentials.access,
|
|
474
479
|
expires: credentials.expires,
|
|
@@ -481,8 +486,25 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
481
486
|
lastFailureTime: null,
|
|
482
487
|
stats: createDefaultStats(now),
|
|
483
488
|
});
|
|
484
|
-
|
|
489
|
+
// If accountUuid wasn't in the token exchange response, fetch from profile API
|
|
485
490
|
const newAccount = stored.accounts[stored.accounts.length - 1];
|
|
491
|
+
if (!newAccount.accountUuid && newAccount.access) {
|
|
492
|
+
try {
|
|
493
|
+
const profileResp = await globalThis.fetch("https://api.anthropic.com/api/oauth/profile", {
|
|
494
|
+
method: "GET",
|
|
495
|
+
headers: { Authorization: `Bearer ${newAccount.access}`, "Content-Type": "application/json" },
|
|
496
|
+
signal: AbortSignal.timeout(10_000),
|
|
497
|
+
});
|
|
498
|
+
if (profileResp.ok) {
|
|
499
|
+
const profile = await profileResp.json();
|
|
500
|
+
if (profile.account?.uuid) newAccount.accountUuid = profile.account.uuid;
|
|
501
|
+
if (profile.organization?.uuid) newAccount.organizationUuid = profile.organization.uuid;
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
/* Best-effort — don't fail account creation */
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
await saveAccounts(stored);
|
|
486
508
|
await persistOpenCodeAuth(newAccount.refreshToken, newAccount.access, newAccount.expires);
|
|
487
509
|
await reloadAccountManagerFromDisk();
|
|
488
510
|
pendingSlashOAuth.delete(sessionID);
|
|
@@ -2793,12 +2815,16 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2793
2815
|
_adaptiveOverride,
|
|
2794
2816
|
_tokenEconomy,
|
|
2795
2817
|
);
|
|
2818
|
+
// v2.1.97: cch=00000 is now static (xxHash64 attestation removed).
|
|
2819
|
+
// Send body as-is without cch replacement.
|
|
2820
|
+
const finalBody = body;
|
|
2821
|
+
|
|
2796
2822
|
// Execute the request
|
|
2797
2823
|
let response;
|
|
2798
2824
|
try {
|
|
2799
2825
|
response = await fetch(requestInput, {
|
|
2800
2826
|
...requestInit,
|
|
2801
|
-
body,
|
|
2827
|
+
body: finalBody,
|
|
2802
2828
|
headers: requestHeaders,
|
|
2803
2829
|
// Disable keepalive when a previous ECONNRESET/EPIPE was detected
|
|
2804
2830
|
// to force a fresh TCP connection and avoid stale socket reuse.
|
|
@@ -3225,11 +3251,14 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
3225
3251
|
}
|
|
3226
3252
|
}
|
|
3227
3253
|
|
|
3228
|
-
// Auto-disable extended cache TTL if the API
|
|
3254
|
+
// Auto-disable extended cache TTL ONLY if the API explicitly says TTL is
|
|
3255
|
+
// not supported. Do NOT disable on TTL ordering errors (which are fixable).
|
|
3229
3256
|
if (
|
|
3230
3257
|
response.status === 400 &&
|
|
3231
3258
|
errorBody &&
|
|
3232
|
-
|
|
3259
|
+
errorBody.includes("cache_control") &&
|
|
3260
|
+
!errorBody.includes("must not come after") &&
|
|
3261
|
+
!errorBody.includes("maximum of")
|
|
3233
3262
|
) {
|
|
3234
3263
|
if (config.cache_policy && config.cache_policy.ttl_supported !== false) {
|
|
3235
3264
|
config.cache_policy.ttl_supported = false;
|
|
@@ -4929,17 +4958,22 @@ process.once("beforeExit", _beforeExitHandler);
|
|
|
4929
4958
|
// Request building helpers (extracted from original fetch interceptor)
|
|
4930
4959
|
// ---------------------------------------------------------------------------
|
|
4931
4960
|
|
|
4932
|
-
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
4961
|
+
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.97";
|
|
4933
4962
|
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
4934
|
-
const CLAUDE_CODE_BUILD_TIME = "2026-04-
|
|
4963
|
+
const CLAUDE_CODE_BUILD_TIME = "2026-04-08T20:46:46Z";
|
|
4935
4964
|
|
|
4936
|
-
// The @anthropic-ai/sdk version bundled with Claude Code v2.1.
|
|
4965
|
+
// The @anthropic-ai/sdk version bundled with Claude Code v2.1.97.
|
|
4937
4966
|
// This is distinct from the CLI version and goes in X-Stainless-Package-Version.
|
|
4938
|
-
// Verified by extracting VERSION="0.208.0" from the bundled cli.js of all versions .80-.
|
|
4967
|
+
// Verified by extracting VERSION="0.208.0" from the bundled cli.js of all versions .80-.97.
|
|
4939
4968
|
const ANTHROPIC_SDK_VERSION = "0.208.0";
|
|
4940
4969
|
|
|
4941
4970
|
// Map of CLI version → bundled SDK version (update when CLI version changes)
|
|
4942
4971
|
const CLI_TO_SDK_VERSION = new Map([
|
|
4972
|
+
["2.1.97", "0.208.0"],
|
|
4973
|
+
["2.1.96", "0.208.0"],
|
|
4974
|
+
["2.1.95", "0.208.0"],
|
|
4975
|
+
["2.1.94", "0.208.0"],
|
|
4976
|
+
["2.1.93", "0.208.0"],
|
|
4943
4977
|
["2.1.92", "0.208.0"],
|
|
4944
4978
|
["2.1.91", "0.208.0"],
|
|
4945
4979
|
["2.1.90", "0.208.0"],
|
|
@@ -4966,6 +5000,12 @@ function getSdkVersion(cliVersion) {
|
|
|
4966
5000
|
const BILLING_HASH_SALT = "59cf53e54c78";
|
|
4967
5001
|
const BILLING_HASH_INDICES = [4, 7, 20];
|
|
4968
5002
|
|
|
5003
|
+
// cch attestation: REMOVED in v2.1.97.
|
|
5004
|
+
// Previously (v2.1.96), CC computed xxHash64 of the serialized body and replaced
|
|
5005
|
+
// "cch=00000" with the 20-bit masked hash. In v2.1.97, the xxHash64 computation
|
|
5006
|
+
// was completely removed — "cch=00000" is now sent as a static placeholder.
|
|
5007
|
+
// Sending a computed cch value would now be detected as non-genuine.
|
|
5008
|
+
|
|
4969
5009
|
/**
|
|
4970
5010
|
* Compute the billing cache hash (cch) matching Claude Code's NP1() function.
|
|
4971
5011
|
* SHA256(salt + chars_at_indices[4,7,20]_from_first_user_msg + version).slice(0,3)
|
|
@@ -5192,24 +5232,23 @@ const BEDROCK_UNSUPPORTED_BETAS = new Set([
|
|
|
5192
5232
|
"interleaved-thinking-2025-05-14",
|
|
5193
5233
|
"context-1m-2025-08-07",
|
|
5194
5234
|
"tool-search-tool-2025-10-19",
|
|
5195
|
-
"code-execution-2025-08-25",
|
|
5196
|
-
"files-api-2025-04-14",
|
|
5197
|
-
"fine-grained-tool-streaming-2025-05-14",
|
|
5198
5235
|
]);
|
|
5199
5236
|
const EXPERIMENTAL_BETA_FLAGS = new Set([
|
|
5200
5237
|
"adaptive-thinking-2026-01-28",
|
|
5201
5238
|
"advanced-tool-use-2025-11-20",
|
|
5239
|
+
"advisor-tool-2026-03-01",
|
|
5202
5240
|
"afk-mode-2026-01-31",
|
|
5203
5241
|
"code-execution-2025-08-25",
|
|
5242
|
+
"compact-2026-01-12",
|
|
5204
5243
|
"context-1m-2025-08-07",
|
|
5205
5244
|
"context-management-2025-06-27",
|
|
5206
5245
|
"fast-mode-2026-02-01",
|
|
5207
5246
|
"files-api-2025-04-14",
|
|
5208
|
-
"fine-grained-tool-streaming-2025-05-14",
|
|
5209
5247
|
"interleaved-thinking-2025-05-14",
|
|
5210
5248
|
"prompt-caching-scope-2026-01-05",
|
|
5211
5249
|
"redact-thinking-2026-02-12",
|
|
5212
5250
|
"structured-outputs-2025-12-15",
|
|
5251
|
+
"task-budgets-2026-03-13",
|
|
5213
5252
|
"tool-search-tool-2025-10-19",
|
|
5214
5253
|
"web-search-2025-03-05",
|
|
5215
5254
|
]);
|
|
@@ -5321,7 +5360,8 @@ function isNonInteractiveMode() {
|
|
|
5321
5360
|
|
|
5322
5361
|
/**
|
|
5323
5362
|
* Build the extended User-Agent for API calls.
|
|
5324
|
-
*
|
|
5363
|
+
* Real CC v96 sends "claude-cli/{version} (external, {entrypoint})" — confirmed via
|
|
5364
|
+
* proxy capture of real CC on Windows/Node.js.
|
|
5325
5365
|
* @param {string} version
|
|
5326
5366
|
* @returns {string}
|
|
5327
5367
|
*/
|
|
@@ -5439,12 +5479,17 @@ function isAdaptiveThinkingModel(model) {
|
|
|
5439
5479
|
|
|
5440
5480
|
/**
|
|
5441
5481
|
* Check if a model is eligible for 1M context (can receive context-1m beta).
|
|
5442
|
-
*
|
|
5482
|
+
* Real CC v2.1.97 U01(): claude-sonnet-4* || opus-4-6 are eligible.
|
|
5483
|
+
* Also matches explicit "1m" in the name (e.g. "claude-opus-4-6[1m]").
|
|
5443
5484
|
* @param {string} model
|
|
5444
5485
|
* @returns {boolean}
|
|
5445
5486
|
*/
|
|
5446
5487
|
function isEligibleFor1MContext(model) {
|
|
5447
|
-
|
|
5488
|
+
if (!model) return false;
|
|
5489
|
+
// Explicit 1m suffix/tag in model name
|
|
5490
|
+
if (/(^|[-_ ])1m($|[-_ ])|context[-_]?1m|\[1m\]/i.test(model)) return true;
|
|
5491
|
+
// CC v2.1.97 U01: claude-sonnet-4* (any Sonnet 4.x) or opus-4-6
|
|
5492
|
+
return /claude-sonnet-4|sonnet[._-]4/i.test(model) || isOpus46Model(model);
|
|
5448
5493
|
}
|
|
5449
5494
|
|
|
5450
5495
|
/**
|
|
@@ -5477,11 +5522,20 @@ function supportsWebSearch(model) {
|
|
|
5477
5522
|
|
|
5478
5523
|
/**
|
|
5479
5524
|
* @param {URL | null} requestUrl
|
|
5480
|
-
* @returns {"anthropic" | "bedrock" | "vertex" | "foundry"}
|
|
5525
|
+
* @returns {"anthropic" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "mantle"}
|
|
5481
5526
|
*/
|
|
5482
5527
|
function detectProvider(requestUrl) {
|
|
5528
|
+
// Match Claude Code provider precedence first (env-driven), then URL fallback.
|
|
5529
|
+
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_BEDROCK)) return "bedrock";
|
|
5530
|
+
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_FOUNDRY)) return "foundry";
|
|
5531
|
+
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_ANTHROPIC_AWS)) return "anthropicAws";
|
|
5532
|
+
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_MANTLE)) return "mantle";
|
|
5533
|
+
if (isTruthyEnv(process.env.CLAUDE_CODE_USE_VERTEX)) return "vertex";
|
|
5534
|
+
|
|
5483
5535
|
if (!requestUrl) return "anthropic";
|
|
5484
5536
|
const host = requestUrl.hostname.toLowerCase();
|
|
5537
|
+
if (host.includes("mantle")) return "mantle";
|
|
5538
|
+
if (host.includes("anthropicaws")) return "anthropicAws";
|
|
5485
5539
|
if (host.includes("bedrock") || host.includes("amazonaws.com")) return "bedrock";
|
|
5486
5540
|
if (host.includes("aiplatform") || host.includes("vertex")) return "vertex";
|
|
5487
5541
|
if (host.includes("foundry") || host.includes("azure")) return "foundry";
|
|
@@ -5596,20 +5650,21 @@ function buildRequestMetadata(input) {
|
|
|
5596
5650
|
|
|
5597
5651
|
/**
|
|
5598
5652
|
* Build the billing header block for Claude Code system prompt injection.
|
|
5599
|
-
* Claude Code v2.1.
|
|
5600
|
-
* cch is a static "00000" placeholder
|
|
5653
|
+
* Claude Code v2.1.97: cc_version includes 3-char fingerprint hash (not model ID).
|
|
5654
|
+
* cch is a static "00000" placeholder (xxHash64 attestation removed in v2.1.97).
|
|
5601
5655
|
*
|
|
5602
5656
|
* Real CC (system.ts:78): version = `${MACRO.VERSION}.${fingerprint}`
|
|
5603
|
-
* Real CC (system.ts:82): cch =
|
|
5657
|
+
* Real CC (system.ts:82): cch = ' cch=00000;' (static, no longer computed)
|
|
5604
5658
|
*
|
|
5605
|
-
* @param {string} version - CLI version (e.g., "2.1.
|
|
5659
|
+
* @param {string} version - CLI version (e.g., "2.1.97")
|
|
5606
5660
|
* @param {string} [firstUserMessage] - First user message text for fingerprint computation
|
|
5607
|
-
* @param {string} [provider] - API provider ("anthropic" | "bedrock" | "vertex" | "foundry")
|
|
5661
|
+
* @param {string} [provider] - API provider ("anthropic" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "mantle")
|
|
5608
5662
|
* @returns {string}
|
|
5609
5663
|
*/
|
|
5610
5664
|
function buildAnthropicBillingHeader(version, firstUserMessage, provider) {
|
|
5611
5665
|
if (isFalsyEnv(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) return "";
|
|
5612
|
-
|
|
5666
|
+
// Real CC sends cc_entrypoint=cli (confirmed via proxy capture).
|
|
5667
|
+
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
|
|
5613
5668
|
// Fix #1: cc_version suffix is the 3-char fingerprint hash, NOT the model ID.
|
|
5614
5669
|
// computeBillingCacheHash() computes SHA256(salt + msg[4]+msg[7]+msg[20] + version)[:3]
|
|
5615
5670
|
// which matches computeFingerprint() in the real CC source (utils/fingerprint.ts).
|
|
@@ -5617,29 +5672,44 @@ function buildAnthropicBillingHeader(version, firstUserMessage, provider) {
|
|
|
5617
5672
|
// the hash from "000" chars (indices 4,7,20 all missing → fallback "0").
|
|
5618
5673
|
const fingerprint = computeBillingCacheHash(firstUserMessage || "", version);
|
|
5619
5674
|
const ccVersion = `${version}.${fingerprint}`;
|
|
5620
|
-
//
|
|
5621
|
-
//
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
let header = `x-anthropic-billing-header: cc_version=${ccVersion}; cc_entrypoint=${entrypoint};${cchPart}`;
|
|
5675
|
+
// cch: v2.1.97 sends static "cch=00000" — xxHash64 attestation was removed.
|
|
5676
|
+
// The server uses the PRESENCE of cch=00000 as a CC identification signal.
|
|
5677
|
+
const cchDisabled = provider === "bedrock" || provider === "anthropicAws" || provider === "mantle";
|
|
5678
|
+
const cchPart = cchDisabled ? "" : " cch=00000;";
|
|
5679
|
+
// Build workload part (upstream concatenates directly, no regex replace)
|
|
5680
|
+
let workloadPart = "";
|
|
5627
5681
|
const workload = process.env.CLAUDE_CODE_WORKLOAD;
|
|
5628
5682
|
if (workload) {
|
|
5629
5683
|
// QA fix M5: sanitize workload value to prevent header injection
|
|
5630
5684
|
const safeWorkload = workload.replace(/[;\s\r\n]/g, "_");
|
|
5631
|
-
|
|
5685
|
+
workloadPart = ` cc_workload=${safeWorkload};`;
|
|
5632
5686
|
}
|
|
5633
|
-
return header;
|
|
5687
|
+
return `x-anthropic-billing-header: cc_version=${ccVersion}; cc_entrypoint=${entrypoint};${cchPart}${workloadPart}`;
|
|
5634
5688
|
}
|
|
5635
5689
|
|
|
5636
5690
|
/**
|
|
5637
5691
|
* @param {string} text
|
|
5638
5692
|
* @returns {string}
|
|
5639
5693
|
*/
|
|
5694
|
+
// Max system prompt length that passes CC billing validation.
|
|
5695
|
+
// The server pattern-matches the system prompt against the real CC prompt.
|
|
5696
|
+
// Opencode's customizations after ~5800 chars diverge and trigger extra usage billing.
|
|
5697
|
+
const MAX_SAFE_SYSTEM_TEXT_LENGTH = 5000;
|
|
5698
|
+
|
|
5640
5699
|
function sanitizeSystemText(text) {
|
|
5641
5700
|
// QA fix M4: use word boundaries to avoid mangling URLs and code identifiers
|
|
5642
|
-
|
|
5701
|
+
let sanitized = text.replace(/\bOpenCode\b/g, "Claude Code").replace(/\bopencode\b/gi, "Claude");
|
|
5702
|
+
// Strip non-CC custom prefixes before the standard CC prompt.
|
|
5703
|
+
const ccStandardStart = sanitized.indexOf("You are an interactive");
|
|
5704
|
+
if (ccStandardStart > 0) {
|
|
5705
|
+
sanitized = sanitized.slice(ccStandardStart);
|
|
5706
|
+
}
|
|
5707
|
+
// Truncate to safe length — opencode customizations beyond this point
|
|
5708
|
+
// diverge from real CC and trigger extra usage billing detection.
|
|
5709
|
+
if (sanitized.length > MAX_SAFE_SYSTEM_TEXT_LENGTH) {
|
|
5710
|
+
sanitized = sanitized.slice(0, MAX_SAFE_SYSTEM_TEXT_LENGTH);
|
|
5711
|
+
}
|
|
5712
|
+
return sanitized;
|
|
5643
5713
|
}
|
|
5644
5714
|
|
|
5645
5715
|
/**
|
|
@@ -5904,9 +5974,9 @@ function splitSysPromptPrefix(blocks, attributionHeader, identityString, useBoun
|
|
|
5904
5974
|
if (attributionHeader) result.push({ text: attributionHeader, cacheScope: null });
|
|
5905
5975
|
// Identity: cacheScope null in boundary mode (real CC behavior)
|
|
5906
5976
|
result.push({ text: identityString, cacheScope: null });
|
|
5907
|
-
const staticJoined = staticBlocks.join("\n
|
|
5977
|
+
const staticJoined = staticBlocks.join("\n");
|
|
5908
5978
|
if (staticJoined) result.push({ text: staticJoined, cacheScope: "global" });
|
|
5909
|
-
const dynamicJoined = dynamicBlocks.join("\n
|
|
5979
|
+
const dynamicJoined = dynamicBlocks.join("\n");
|
|
5910
5980
|
if (dynamicJoined) result.push({ text: dynamicJoined, cacheScope: null });
|
|
5911
5981
|
return result;
|
|
5912
5982
|
}
|
|
@@ -5921,7 +5991,7 @@ function splitSysPromptPrefix(blocks, attributionHeader, identityString, useBoun
|
|
|
5921
5991
|
const result = [];
|
|
5922
5992
|
if (attributionHeader) result.push({ text: attributionHeader, cacheScope: null });
|
|
5923
5993
|
result.push({ text: identityString, cacheScope: "org" });
|
|
5924
|
-
const restJoined = rest.join("\n
|
|
5994
|
+
const restJoined = rest.join("\n");
|
|
5925
5995
|
if (restJoined) result.push({ text: restJoined, cacheScope: "org" });
|
|
5926
5996
|
return result;
|
|
5927
5997
|
}
|
|
@@ -5987,7 +6057,7 @@ function buildSystemPromptBlocks(system, signature) {
|
|
|
5987
6057
|
* @param {string} incomingBeta
|
|
5988
6058
|
* @param {boolean} signatureEnabled
|
|
5989
6059
|
* @param {string} model
|
|
5990
|
-
* @param {"anthropic" | "bedrock" | "vertex" | "foundry"} provider
|
|
6060
|
+
* @param {"anthropic" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "mantle"} provider
|
|
5991
6061
|
* @param {string[]} [customBetas]
|
|
5992
6062
|
* @param {import('./lib/config.mjs').AccountSelectionStrategy} [strategy]
|
|
5993
6063
|
* @param {string} [requestPath]
|
|
@@ -6044,7 +6114,7 @@ function buildAnthropicBetaHeader(
|
|
|
6044
6114
|
// Tool search: use provider-aware header.
|
|
6045
6115
|
// 1P/Foundry u2192 advanced-tool-use-2025-11-20 (enables broader tool capabilities)
|
|
6046
6116
|
// Vertex/Bedrock u2192 tool-search-tool-2025-10-19 (3P-compatible subset)
|
|
6047
|
-
if (provider === "vertex" || provider === "bedrock") {
|
|
6117
|
+
if (provider === "vertex" || provider === "bedrock" || provider === "mantle") {
|
|
6048
6118
|
betas.push("tool-search-tool-2025-10-19");
|
|
6049
6119
|
} else {
|
|
6050
6120
|
betas.push(ADVANCED_TOOL_USE_BETA_FLAG); // "advanced-tool-use-2025-11-20"
|
|
@@ -6339,7 +6409,9 @@ function buildRequestHeaders(
|
|
|
6339
6409
|
requestHeaders.set("x-stainless-arch", getStainlessArch(process.arch));
|
|
6340
6410
|
requestHeaders.set("x-stainless-lang", "js");
|
|
6341
6411
|
requestHeaders.set("x-stainless-os", getStainlessOs(process.platform));
|
|
6342
|
-
|
|
6412
|
+
// Real CC sends 0.81.0 (confirmed via proxy capture), not the internal 0.208.0.
|
|
6413
|
+
requestHeaders.set("x-stainless-package-version", "0.81.0");
|
|
6414
|
+
// Real CC on Windows/Node reports "node" — confirmed via proxy capture.
|
|
6343
6415
|
requestHeaders.set("x-stainless-runtime", "node");
|
|
6344
6416
|
requestHeaders.set("x-stainless-runtime-version", process.version);
|
|
6345
6417
|
const incomingRetryCount = requestHeaders.get("x-stainless-retry-count");
|
|
@@ -6347,21 +6419,10 @@ function buildRequestHeaders(
|
|
|
6347
6419
|
"x-stainless-retry-count",
|
|
6348
6420
|
incomingRetryCount && !isFalsyEnv(incomingRetryCount) ? incomingRetryCount : "0",
|
|
6349
6421
|
);
|
|
6350
|
-
// x-stainless-timeout:
|
|
6351
|
-
|
|
6352
|
-
//
|
|
6353
|
-
|
|
6354
|
-
try {
|
|
6355
|
-
const parsed = JSON.parse(requestBody);
|
|
6356
|
-
// stream defaults to true in Claude Code; only explicitly false means non-streaming
|
|
6357
|
-
if (parsed.stream === false) {
|
|
6358
|
-
requestHeaders.set("x-stainless-timeout", "600");
|
|
6359
|
-
}
|
|
6360
|
-
// Streaming requests: omit x-stainless-timeout (real SDK behavior)
|
|
6361
|
-
} catch {
|
|
6362
|
-
// Non-JSON body or parse error — omit header (safe default)
|
|
6363
|
-
}
|
|
6364
|
-
}
|
|
6422
|
+
// x-stainless-timeout: real CC sends 600 on ALL requests (confirmed via proxy capture).
|
|
6423
|
+
requestHeaders.set("x-stainless-timeout", "600");
|
|
6424
|
+
// anthropic-dangerous-direct-browser-access: real CC sends this on all requests.
|
|
6425
|
+
requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
|
|
6365
6426
|
const stainlessHelpers = buildStainlessHelperHeader(tools, messages);
|
|
6366
6427
|
if (stainlessHelpers) {
|
|
6367
6428
|
requestHeaders.set("x-stainless-helper", stainlessHelpers);
|
|
@@ -6383,10 +6444,11 @@ function buildRequestHeaders(
|
|
|
6383
6444
|
requestHeaders.set("x-anthropic-additional-protection", "true");
|
|
6384
6445
|
}
|
|
6385
6446
|
|
|
6386
|
-
//
|
|
6387
|
-
requestHeaders.set("x-client-request-id", randomUUID());
|
|
6447
|
+
// x-client-request-id: NOT sent by real CC (confirmed via proxy capture). Removed.
|
|
6388
6448
|
}
|
|
6389
6449
|
requestHeaders.delete("x-api-key");
|
|
6450
|
+
// x-session-affinity: set by opencode SDK but NOT in real CC. Strip it.
|
|
6451
|
+
requestHeaders.delete("x-session-affinity");
|
|
6390
6452
|
|
|
6391
6453
|
return requestHeaders;
|
|
6392
6454
|
}
|
|
@@ -6545,17 +6607,33 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6545
6607
|
signature.cachePolicy?.ttl_supported !== false &&
|
|
6546
6608
|
!isTitleGen
|
|
6547
6609
|
) {
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6610
|
+
// Strip ALL incoming cache_control from tools and messages to prevent
|
|
6611
|
+
// TTL ordering violations (host SDK may set ttl=5m which conflicts with
|
|
6612
|
+
// our system prompt ttl=1h). Then add our own 1h to the last user message
|
|
6613
|
+
// (matching real CC behavior seen in proxy capture).
|
|
6614
|
+
const ccTtl = signature.cachePolicy?.ttl || "1h";
|
|
6615
|
+
if (Array.isArray(parsed.tools)) {
|
|
6616
|
+
for (const tool of parsed.tools) {
|
|
6617
|
+
if (tool.cache_control) delete tool.cache_control;
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
if (Array.isArray(parsed.messages)) {
|
|
6551
6621
|
for (const msg of parsed.messages) {
|
|
6552
|
-
if (
|
|
6553
|
-
|
|
6554
|
-
|
|
6622
|
+
if (Array.isArray(msg.content)) {
|
|
6623
|
+
for (const block of msg.content) {
|
|
6624
|
+
if (block.cache_control) delete block.cache_control;
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
// Add cache_control to last user message (real CC does this)
|
|
6629
|
+
for (let i = parsed.messages.length - 1; i >= 0; i--) {
|
|
6630
|
+
const msg = parsed.messages[i];
|
|
6631
|
+
if (msg.role !== "user" || !Array.isArray(msg.content) || msg.content.length === 0) continue;
|
|
6555
6632
|
const lastBlock = msg.content[msg.content.length - 1];
|
|
6556
6633
|
if (lastBlock && typeof lastBlock === "object") {
|
|
6557
|
-
lastBlock.cache_control =
|
|
6634
|
+
lastBlock.cache_control = { type: "ephemeral", ttl: ccTtl };
|
|
6558
6635
|
}
|
|
6636
|
+
break;
|
|
6559
6637
|
}
|
|
6560
6638
|
}
|
|
6561
6639
|
}
|
|
@@ -6595,9 +6673,10 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
|
|
|
6595
6673
|
}
|
|
6596
6674
|
}
|
|
6597
6675
|
|
|
6598
|
-
// Fast mode: inject speed parameter for
|
|
6676
|
+
// Fast mode: inject speed parameter for Opus 4.6 only (v2.1.97 restriction).
|
|
6677
|
+
// Real CC v2.1.97 xJ() checks: model.includes("opus-4-6") — Sonnet is NOT eligible.
|
|
6599
6678
|
const fastModeEnabled = signature.fastMode && !isFalsyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_FAST_MODE);
|
|
6600
|
-
if (fastModeEnabled && parsed.model &&
|
|
6679
|
+
if (fastModeEnabled && parsed.model && isOpus46Model(parsed.model)) {
|
|
6601
6680
|
parsed.speed = "fast";
|
|
6602
6681
|
}
|
|
6603
6682
|
|
|
@@ -6662,13 +6741,19 @@ function transformRequestUrl(input) {
|
|
|
6662
6741
|
requestUrl = null;
|
|
6663
6742
|
}
|
|
6664
6743
|
|
|
6665
|
-
if (
|
|
6666
|
-
requestUrl
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6744
|
+
if (requestUrl && !requestUrl.searchParams.has("beta")) {
|
|
6745
|
+
const p = requestUrl.pathname;
|
|
6746
|
+
// SDK may send to /messages (base URL includes /v1) or /v1/messages (base URL is root)
|
|
6747
|
+
const isMessages =
|
|
6748
|
+
p === "/v1/messages" || p === "/messages" || p === "/v1/messages/count_tokens" || p === "/messages/count_tokens";
|
|
6749
|
+
if (isMessages) {
|
|
6750
|
+
// Normalize path to /v1/messages (required by API and proxies)
|
|
6751
|
+
if (!p.startsWith("/v1/")) {
|
|
6752
|
+
requestUrl.pathname = "/v1" + p;
|
|
6753
|
+
}
|
|
6754
|
+
requestUrl.searchParams.set("beta", "true");
|
|
6755
|
+
requestInput = input instanceof Request ? new Request(requestUrl.toString(), input) : requestUrl;
|
|
6756
|
+
}
|
|
6672
6757
|
}
|
|
6673
6758
|
|
|
6674
6759
|
return { requestInput, requestUrl };
|
|
@@ -7149,6 +7234,13 @@ async function refreshAccountToken(account, client, source = "foreground", { onT
|
|
|
7149
7234
|
if (json.refresh_token) {
|
|
7150
7235
|
account.refreshToken = json.refresh_token;
|
|
7151
7236
|
}
|
|
7237
|
+
// Extract account UUID from token refresh response if present
|
|
7238
|
+
if (json.account?.uuid) {
|
|
7239
|
+
account.accountUuid = json.account.uuid;
|
|
7240
|
+
}
|
|
7241
|
+
if (json.organization?.uuid) {
|
|
7242
|
+
account.organizationUuid = json.organization.uuid;
|
|
7243
|
+
}
|
|
7152
7244
|
markTokenStateUpdated(account);
|
|
7153
7245
|
|
|
7154
7246
|
// Persist new tokens to disk BEFORE releasing the cross-process lock.
|
package/lib/accounts.mjs
CHANGED
|
@@ -107,6 +107,8 @@ export class AccountManager {
|
|
|
107
107
|
id: acc.id || `${acc.addedAt}:${hashTokenFragment(acc.refreshToken)}`,
|
|
108
108
|
index,
|
|
109
109
|
email: acc.email,
|
|
110
|
+
accountUuid: acc.accountUuid,
|
|
111
|
+
organizationUuid: acc.organizationUuid,
|
|
110
112
|
refreshToken: acc.refreshToken,
|
|
111
113
|
access: acc.access,
|
|
112
114
|
expires: acc.expires,
|
|
@@ -736,6 +738,8 @@ export class AccountManager {
|
|
|
736
738
|
return {
|
|
737
739
|
id: acc.id,
|
|
738
740
|
email: acc.email,
|
|
741
|
+
accountUuid: acc.accountUuid,
|
|
742
|
+
organizationUuid: acc.organizationUuid,
|
|
739
743
|
refreshToken: freshestAuth.refreshToken,
|
|
740
744
|
access: freshestAuth.access,
|
|
741
745
|
expires: freshestAuth.expires,
|
package/lib/oauth.mjs
CHANGED
|
@@ -292,6 +292,10 @@ export async function exchange(code, verifier) {
|
|
|
292
292
|
access: json.access_token,
|
|
293
293
|
expires: Date.now() + json.expires_in * 1000,
|
|
294
294
|
email: json.account?.email_address || undefined,
|
|
295
|
+
// Real CC extracts account UUID from token response (oauth/client.ts:253)
|
|
296
|
+
// and uses it in metadata.user_id.account_uuid for billing correlation.
|
|
297
|
+
accountUuid: json.account?.uuid || undefined,
|
|
298
|
+
organizationUuid: json.organization?.uuid || undefined,
|
|
295
299
|
};
|
|
296
300
|
}
|
|
297
301
|
|
package/lib/storage.mjs
CHANGED
|
@@ -163,6 +163,8 @@ function validateAccount(raw, now) {
|
|
|
163
163
|
return {
|
|
164
164
|
id,
|
|
165
165
|
email: typeof acc.email === "string" ? acc.email : undefined,
|
|
166
|
+
accountUuid: typeof acc.accountUuid === "string" ? acc.accountUuid : undefined,
|
|
167
|
+
organizationUuid: typeof acc.organizationUuid === "string" ? acc.organizationUuid : undefined,
|
|
166
168
|
refreshToken: acc.refreshToken,
|
|
167
169
|
access: typeof acc.access === "string" ? acc.access : undefined,
|
|
168
170
|
expires: typeof acc.expires === "number" && Number.isFinite(acc.expires) ? acc.expires : undefined,
|