@cortexkit/anthropic-auth-core 1.2.5 → 1.4.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
@@ -1,6 +1,6 @@
1
1
  # @cortexkit/anthropic-auth-core
2
2
 
3
- Shared Anthropic OAuth, quota, cache, cache-keepalive, fast mode, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
3
+ Shared Anthropic OAuth, quota, routing, cache, cache-keepalive, fast mode, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
4
4
 
5
5
  User-facing packages:
6
6
 
@@ -30,12 +30,16 @@ export type AccountQuotaWindow = {
30
30
  resetsAt?: string;
31
31
  checkedAt: number;
32
32
  };
33
+ export type RoutingMode = 'main-first' | 'fallback-first';
33
34
  export type AccountStorage = {
34
35
  version: 1;
35
36
  main?: {
36
37
  type: 'opencode';
37
38
  provider: 'anthropic';
38
39
  };
40
+ routing?: {
41
+ mode?: RoutingMode;
42
+ };
39
43
  fallbackOn?: number[];
40
44
  refresh?: {
41
45
  enabled?: boolean;
@@ -94,6 +98,8 @@ export declare function acquireRefreshFileLock(options: {
94
98
  ttlMs: number;
95
99
  path?: string;
96
100
  now?: () => number;
101
+ renew?: boolean;
102
+ renewIntervalMs?: number;
97
103
  }): Promise<{
98
104
  release: () => Promise<void>;
99
105
  } | null>;
package/dist/accounts.js CHANGED
@@ -5,6 +5,8 @@ import { dirname, join } from 'node:path';
5
5
  import { ClaudeOAuthRefreshError, refreshClaudeOAuthToken } from "./auth.js";
6
6
  import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from "./constants.js";
7
7
  import { log } from "./logger.js";
8
+ const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
9
+ const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
8
10
  export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
9
11
  export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
10
12
  const DEFAULT_FALLBACK_ON = [401, 403, 429];
@@ -22,7 +24,7 @@ const DEFAULT_MINIMUM_REMAINING = {
22
24
  const DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA = true;
23
25
  const BACKGROUND_TICK_MS = 60_000;
24
26
  const BACKGROUND_TICK_JITTER_MS = 60_000;
25
- const FALLBACK_REFRESH_LOCK_TTL_MS = 2 * 60_000;
27
+ const FALLBACK_REFRESH_LOCK_TTL_MS = 10 * 60_000;
26
28
  const FALLBACK_REFRESH_JOIN_WAIT_MS = 10_000;
27
29
  const FALLBACK_REFRESH_JOIN_POLL_MS = 100;
28
30
  function getConfigDir() {
@@ -114,6 +116,7 @@ function normalizeStorage(value) {
114
116
  return {
115
117
  version: 1,
116
118
  main: { type: 'opencode', provider: 'anthropic' },
119
+ routing: isRecord(value.routing) ? value.routing : undefined,
117
120
  fallbackOn: Array.isArray(value.fallbackOn)
118
121
  ? value.fallbackOn.filter((status) => Number.isInteger(status))
119
122
  : undefined,
@@ -170,6 +173,8 @@ export async function acquireRefreshFileLock(options) {
170
173
  const legacyOwnerPath = join(lockPath, 'owner.json');
171
174
  const ownerId = randomUUID();
172
175
  const now = options.now ?? Date.now;
176
+ let renewTimer = null;
177
+ let released = false;
173
178
  async function readOwner() {
174
179
  try {
175
180
  return JSON.parse(await readFile(lockPath, 'utf8'));
@@ -181,6 +186,9 @@ export async function acquireRefreshFileLock(options) {
181
186
  return JSON.parse(await readFile(legacyOwnerPath, 'utf8'));
182
187
  }
183
188
  }
189
+ async function writeOwner() {
190
+ await writeFile(lockPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600 });
191
+ }
184
192
  async function tryAcquire() {
185
193
  try {
186
194
  await writeFile(lockPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
@@ -193,6 +201,31 @@ export async function acquireRefreshFileLock(options) {
193
201
  throw error;
194
202
  }
195
203
  }
204
+ function scheduleRenewal() {
205
+ if (!options.renew || released)
206
+ return;
207
+ const intervalMs = options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3));
208
+ renewTimer = setRefreshLockRenewalTimeout(() => {
209
+ void (async () => {
210
+ try {
211
+ const owner = await readOwner();
212
+ const currentNow = now();
213
+ if (released ||
214
+ owner?.ownerId !== ownerId ||
215
+ Number(owner?.expiresAt) <= currentNow) {
216
+ return;
217
+ }
218
+ await writeOwner();
219
+ scheduleRenewal();
220
+ }
221
+ catch {
222
+ // If renewal fails, contenders will wait until the last written expiry.
223
+ }
224
+ })();
225
+ }, intervalMs);
226
+ if ('unref' in renewTimer)
227
+ renewTimer.unref();
228
+ }
196
229
  if (!(await tryAcquire())) {
197
230
  try {
198
231
  const owner = await readOwner();
@@ -213,8 +246,14 @@ export async function acquireRefreshFileLock(options) {
213
246
  if (!(await tryAcquire()))
214
247
  return null;
215
248
  }
249
+ scheduleRenewal();
216
250
  return {
217
251
  release: async () => {
252
+ released = true;
253
+ if (renewTimer) {
254
+ clearRefreshLockRenewalTimeout(renewTimer);
255
+ renewTimer = null;
256
+ }
218
257
  try {
219
258
  const owner = await readOwner();
220
259
  if (owner?.ownerId !== ownerId)
@@ -850,6 +889,7 @@ export class FallbackAccountManager {
850
889
  ttlMs: FALLBACK_REFRESH_LOCK_TTL_MS,
851
890
  path: this.configPath,
852
891
  now: this.now,
892
+ renew: true,
853
893
  });
854
894
  if (!fileLock) {
855
895
  log('[refresh] fallback oauth refresh skipped file lock', {
package/dist/cachekeep.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CCH_PATTERN, signRequestBody } from "./cch.js";
1
+ import { signRequestBody } from "./cch.js";
2
2
  import { orderClaudeCodeBody } from "./claude-code.js";
3
3
  export const CLAUDE_CACHE_KEEP_COMMAND_NAME = 'claude-cachekeep';
4
4
  export const CACHE_KEEP_TTL_MS = 60 * 60_000;
@@ -169,8 +169,7 @@ export async function buildCacheKeepPrewarmBody(bodyText) {
169
169
  if (toolChoice?.type === 'tool' || toolChoice?.type === 'any') {
170
170
  delete warm.tool_choice;
171
171
  }
172
- const unsignedBodyText = JSON.stringify(orderClaudeCodeBody(warm)).replace(CCH_PATTERN, 'cch=00000;');
173
- const signedBodyText = await signRequestBody(unsignedBodyText);
172
+ const signedBodyText = await signRequestBody(JSON.stringify(orderClaudeCodeBody(warm)));
174
173
  return { ok: true, bodyText: signedBodyText };
175
174
  }
176
175
  export class CacheKeepManager {
package/dist/cch.d.ts CHANGED
@@ -18,6 +18,8 @@ export declare function extractFirstUserMessageText(messages: Message[]): string
18
18
  * masks to 20 bits, and writes that value into the billing-header placeholder.
19
19
  */
20
20
  export declare function computeCCH(bodyBytes: Uint8Array): Promise<string>;
21
+ export declare function resetBillingHeaderCCH(bodyString: string): string;
22
+ export declare function extractBillingHeaderCCH(bodyString: string): string | null;
21
23
  export declare function signRequestBody(bodyString: string): Promise<string>;
22
24
  /**
23
25
  * Compute a stable 3-character suffix for cc_version.
package/dist/cch.js CHANGED
@@ -2,7 +2,10 @@ import { createHash } from 'node:crypto';
2
2
  import xxhashInit from 'xxhash-wasm';
3
3
  import { CLAUDE_CODE_BUILD_HASH, CLAUDE_CODE_VERSION } from "./constants.js";
4
4
  const CCH_SEED = 0x6e52736ac806831en;
5
+ const CCH_PLACEHOLDER = 'cch=00000;';
5
6
  export const CCH_PATTERN = /\bcch=([0-9a-f]{5});/;
7
+ const BILLING_HEADER_CCH_PATTERN = /("system":\[\{"type":"text","text":"x-anthropic-billing-header: cc_version=[^;"]+; cc_entrypoint=[^;"]+; )cch=([0-9a-f]{5});/;
8
+ const BILLING_HEADER_CCH_PLACEHOLDER_PATTERN = /("system":\[\{"type":"text","text":"x-anthropic-billing-header: cc_version=[^;"]+; cc_entrypoint=[^;"]+; )cch=00000;/;
6
9
  let xxhashPromise = null;
7
10
  let xxhash64Raw = null;
8
11
  async function ensureXxhash() {
@@ -43,11 +46,18 @@ export async function computeCCH(bodyBytes) {
43
46
  const hash = xxhash64Raw?.(bodyBytes, CCH_SEED) ?? 0n;
44
47
  return (hash & 0xfffffn).toString(16).padStart(5, '0');
45
48
  }
49
+ export function resetBillingHeaderCCH(bodyString) {
50
+ return bodyString.replace(BILLING_HEADER_CCH_PATTERN, `$1${CCH_PLACEHOLDER}`);
51
+ }
52
+ export function extractBillingHeaderCCH(bodyString) {
53
+ return BILLING_HEADER_CCH_PATTERN.exec(bodyString)?.[2] ?? null;
54
+ }
46
55
  export async function signRequestBody(bodyString) {
47
- if (!CCH_PATTERN.test(bodyString))
56
+ if (!BILLING_HEADER_CCH_PATTERN.test(bodyString))
48
57
  return bodyString;
49
- const token = await computeCCH(new TextEncoder().encode(bodyString));
50
- return bodyString.replace(CCH_PATTERN, `cch=${token};`);
58
+ const unsignedBodyString = resetBillingHeaderCCH(bodyString);
59
+ const token = await computeCCH(new TextEncoder().encode(unsignedBodyString));
60
+ return unsignedBodyString.replace(BILLING_HEADER_CCH_PLACEHOLDER_PATTERN, `$1cch=${token};`);
51
61
  }
52
62
  /**
53
63
  * Compute a stable 3-character suffix for cc_version.
@@ -13,6 +13,7 @@ export declare function mergeAnthropicBetas(existing: string | null | undefined,
13
13
  export declare function isFastModeSupportedModel(model: unknown): boolean;
14
14
  export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
15
15
  export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
16
+ export declare const PARALLEL_TOOL_CALLS_SYSTEM_PROMPT: string;
16
17
  export declare const CCH_SALT = "59cf53e54c78";
17
18
  export declare const CCH_POSITIONS: number[];
18
19
  export declare const CLAUDE_CODE_VERSION = "2.1.141";
package/dist/constants.js CHANGED
@@ -28,10 +28,17 @@ export function mergeAnthropicBetas(existing, betas) {
28
28
  }
29
29
  export function isFastModeSupportedModel(model) {
30
30
  return (typeof model === 'string' &&
31
- (model.startsWith('claude-opus-4-6') || model.startsWith('claude-opus-4-7')));
31
+ (model.startsWith('claude-opus-4-6') ||
32
+ model.startsWith('claude-opus-4-7') ||
33
+ model.startsWith('claude-opus-4-8')));
32
34
  }
33
35
  export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
34
36
  export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
37
+ export const PARALLEL_TOOL_CALLS_SYSTEM_PROMPT = [
38
+ '<use_parallel_tool_calls>',
39
+ 'For maximum efficiency, whenever you perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially. Prioritize calling tools in parallel whenever possible. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. When running multiple read-only commands like `ls` or `list_dir`, always run all of the commands in parallel. Err on the side of maximizing parallel tool calls rather than running too many tools sequentially.',
40
+ '</use_parallel_tool_calls>',
41
+ ].join('\n');
35
42
  export const CCH_SALT = '59cf53e54c78';
36
43
  export const CCH_POSITIONS = [4, 7, 20];
37
44
  export const CLAUDE_CODE_VERSION = '2.1.141';
package/dist/dump.js CHANGED
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { extractBillingHeaderCCH } from "./cch.js";
5
6
  import { relayLog } from "./logger.js";
6
7
  export const CLAUDE_DUMP_COMMAND_NAME = 'claude-dump';
7
8
  const DUMP_STATUS_TITLE = '## Claude Dump Status';
@@ -9,7 +10,8 @@ const DUMP_ENABLED_TITLE = '## Claude Dump Enabled';
9
10
  const DUMP_DISABLED_TITLE = '## Claude Dump Disabled';
10
11
  const DUMP_USAGE_TITLE = '## Claude Dump Usage';
11
12
  const DUMP_USAGE = 'Usage: `/claude-dump`, `/claude-dump on`, or `/claude-dump off`.';
12
- const DUMP_DIR = join(tmpdir(), 'opencode-anthropic-auth-dumps');
13
+ const DUMP_DIR_ENV = 'OPENCODE_ANTHROPIC_AUTH_DUMP_DIR';
14
+ const DEFAULT_DUMP_DIR = join(tmpdir(), 'opencode-anthropic-auth-dumps');
13
15
  let dumpEnabled = false;
14
16
  let nextDumpId = 0;
15
17
  export function isDumpEnabled() {
@@ -22,7 +24,7 @@ export function resetDumpState() {
22
24
  dumpEnabled = false;
23
25
  }
24
26
  export function getDumpDirectory() {
25
- return DUMP_DIR;
27
+ return process.env[DUMP_DIR_ENV] || DEFAULT_DUMP_DIR;
26
28
  }
27
29
  export function parseDumpCommandAction(argumentsText) {
28
30
  const normalized = argumentsText.trim().split(/\s+/).filter(Boolean);
@@ -40,7 +42,7 @@ export function buildDumpStatusSummary(input) {
40
42
  DUMP_STATUS_TITLE,
41
43
  '',
42
44
  `- Enabled: ${enabled ? 'enabled' : 'disabled'}`,
43
- `- Directory: ${DUMP_DIR}`,
45
+ `- Directory: ${getDumpDirectory()}`,
44
46
  '- Persisted: ~/.config/opencode/anthropic-auth.json',
45
47
  '- Captures: final rewritten Anthropic body plus redacted relay payload metadata',
46
48
  '- Warning: body dumps may contain prompt/session content; turn this off after debugging',
@@ -80,7 +82,7 @@ function hashText(value) {
80
82
  return createHash('sha256').update(value).digest('hex');
81
83
  }
82
84
  function cchToken(bodyText) {
83
- return bodyText.match(/\bcch=([0-9a-f]{5});/)?.[1] ?? null;
85
+ return extractBillingHeaderCCH(bodyText);
84
86
  }
85
87
  function diffSummary(previousBodyText, bodyText) {
86
88
  if (previousBodyText == null)
@@ -183,9 +185,10 @@ export async function dumpRelayRequest(input) {
183
185
  return;
184
186
  nextDumpId += 1;
185
187
  const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(nextDumpId).padStart(5, '0')}-${input.transport}-p${input.protocol}-${input.mode}`;
186
- const prefix = join(DUMP_DIR, id);
188
+ const dumpDir = getDumpDirectory();
189
+ const prefix = join(dumpDir, id);
187
190
  try {
188
- await mkdir(DUMP_DIR, { recursive: true });
191
+ await mkdir(dumpDir, { recursive: true });
189
192
  const metadata = {
190
193
  id,
191
194
  createdAt: new Date().toISOString(),
package/dist/fast.js CHANGED
@@ -32,7 +32,7 @@ export function buildFastModeStatusSummary(input) {
32
32
  `- Enabled: ${enabled ? 'enabled' : 'disabled'}`,
33
33
  '- Persisted: ~/.config/opencode/anthropic-auth.json',
34
34
  '- Scope: adds Anthropic fast mode to supported Opus requests',
35
- '- Supported models: claude-opus-4-6 and claude-opus-4-7',
35
+ '- Supported models: claude-opus-4-6, claude-opus-4-7, and claude-opus-4-8',
36
36
  '- Request changes: adds `speed: "fast"` and the `fast-mode-2026-02-01` beta header',
37
37
  '- Note: fast and standard speeds do not share prompt-cache prefixes',
38
38
  ].join('\n');
package/dist/index.d.ts CHANGED
@@ -11,3 +11,4 @@ export * from './logger.ts';
11
11
  export * from './pkce.ts';
12
12
  export * from './quotas.ts';
13
13
  export * from './relay.ts';
14
+ export * from './routing.ts';
package/dist/index.js CHANGED
@@ -11,3 +11,4 @@ export * from "./logger.js";
11
11
  export * from "./pkce.js";
12
12
  export * from "./quotas.js";
13
13
  export * from "./relay.js";
14
+ export * from "./routing.js";
@@ -0,0 +1,23 @@
1
+ import { type AccountStorage, type RoutingMode } from './accounts.ts';
2
+ export declare const CLAUDE_ROUTING_COMMAND_NAME = "claude-routing";
3
+ export declare const ROUTING_MODES: readonly ["main-first", "fallback-first"];
4
+ export declare const DEFAULT_ROUTING_MODE: RoutingMode;
5
+ type RoutingCommandAction = {
6
+ type: 'status';
7
+ } | {
8
+ type: 'mode';
9
+ mode: RoutingMode;
10
+ } | {
11
+ type: 'usage';
12
+ };
13
+ export declare function getRoutingMode(storage: AccountStorage | null): RoutingMode;
14
+ export declare function setRoutingMode(mode: RoutingMode, path?: string): Promise<AccountStorage>;
15
+ export declare function parseRoutingCommandAction(argumentsText: string): RoutingCommandAction;
16
+ export declare function buildRoutingStatusSummary(input?: {
17
+ mode?: RoutingMode;
18
+ }): string;
19
+ export declare function executeRoutingCommand(input: {
20
+ argumentsText: string;
21
+ mode?: RoutingMode;
22
+ }): string;
23
+ export {};
@@ -0,0 +1,80 @@
1
+ import { getAccountStoragePath, loadAccounts, saveAccounts, } from "./accounts.js";
2
+ export const CLAUDE_ROUTING_COMMAND_NAME = 'claude-routing';
3
+ export const ROUTING_MODES = ['main-first', 'fallback-first'];
4
+ export const DEFAULT_ROUTING_MODE = 'main-first';
5
+ const ROUTING_USAGE = 'Usage: `/claude-routing`, `/claude-routing main-first`, or `/claude-routing fallback-first`.';
6
+ function normalizeRoutingMode(value) {
7
+ return typeof value === 'string' &&
8
+ ROUTING_MODES.includes(value)
9
+ ? value
10
+ : DEFAULT_ROUTING_MODE;
11
+ }
12
+ export function getRoutingMode(storage) {
13
+ return normalizeRoutingMode(storage?.routing?.mode);
14
+ }
15
+ export async function setRoutingMode(mode, path = getAccountStoragePath()) {
16
+ const storage = (await loadAccounts(path)) ?? {
17
+ version: 1,
18
+ main: { type: 'opencode', provider: 'anthropic' },
19
+ accounts: [],
20
+ };
21
+ storage.routing = {
22
+ ...(storage.routing ?? {}),
23
+ mode,
24
+ };
25
+ await saveAccounts(storage, path);
26
+ return storage;
27
+ }
28
+ export function parseRoutingCommandAction(argumentsText) {
29
+ const tokens = argumentsText.trim().toLowerCase().split(/\s+/).filter(Boolean);
30
+ if (tokens.length === 0)
31
+ return { type: 'status' };
32
+ if (tokens.length === 1 && ROUTING_MODES.includes(tokens[0])) {
33
+ return { type: 'mode', mode: tokens[0] };
34
+ }
35
+ if (tokens.length === 2 &&
36
+ tokens[0] === 'mode' &&
37
+ ROUTING_MODES.includes(tokens[1])) {
38
+ return { type: 'mode', mode: tokens[1] };
39
+ }
40
+ return { type: 'usage' };
41
+ }
42
+ function routingDescription(mode) {
43
+ if (mode === 'fallback-first') {
44
+ return 'Try usable fallback accounts before the main account. If no fallback succeeds, try the main account.';
45
+ }
46
+ return 'Try the main account first. Use fallback accounts only when quota policy or fallback errors require it.';
47
+ }
48
+ export function buildRoutingStatusSummary(input) {
49
+ const mode = input?.mode ?? DEFAULT_ROUTING_MODE;
50
+ return [
51
+ '## Claude Routing Status',
52
+ '',
53
+ `- Mode: \`${mode}\``,
54
+ `- Behavior: ${routingDescription(mode)}`,
55
+ '',
56
+ ROUTING_USAGE,
57
+ ].join('\n');
58
+ }
59
+ export function executeRoutingCommand(input) {
60
+ const action = parseRoutingCommandAction(input.argumentsText);
61
+ const mode = input.mode ?? DEFAULT_ROUTING_MODE;
62
+ if (action.type === 'status')
63
+ return buildRoutingStatusSummary({ mode });
64
+ if (action.type === 'mode') {
65
+ return [
66
+ '## Claude Routing Updated',
67
+ '',
68
+ `Mode updated to \`${action.mode}\`.`,
69
+ '',
70
+ ...buildRoutingStatusSummary({ mode: action.mode }).split('\n').slice(2),
71
+ ].join('\n');
72
+ }
73
+ return [
74
+ '## Claude Routing Usage',
75
+ '',
76
+ ROUTING_USAGE,
77
+ '',
78
+ ...buildRoutingStatusSummary({ mode }).split('\n').slice(2),
79
+ ].join('\n');
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.2.5",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",