@cortexkit/anthropic-auth-core 1.0.0 → 1.1.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, relay, dump, and request-signing helpers used by CortexKit's OpenCode and Pi integrations.
3
+ Shared Anthropic OAuth, quota, cache, 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
 
@@ -52,6 +52,9 @@ export type AccountStorage = {
52
52
  dump?: {
53
53
  enabled?: boolean;
54
54
  };
55
+ claudeFast?: {
56
+ enabled?: boolean;
57
+ };
55
58
  relay?: {
56
59
  enabled?: boolean;
57
60
  url?: string;
@@ -80,6 +83,8 @@ export declare function setCache1hPersistentEnabled(enabled: boolean, mode?: Cac
80
83
  export declare function setCache1hPersistentMode(mode: Cache1hMode, path?: string): Promise<AccountStorage>;
81
84
  export declare function isDumpPersistentlyEnabled(storage: AccountStorage | null): boolean;
82
85
  export declare function setDumpPersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
86
+ export declare function isFastModePersistentlyEnabled(storage: AccountStorage | null): boolean;
87
+ export declare function setFastModePersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
83
88
  export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
84
89
  export declare function getQuotaCheckIntervalMs(storage: AccountStorage | null): number;
85
90
  export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
package/dist/accounts.js CHANGED
@@ -2,7 +2,8 @@ import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
- import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, CLIENT_ID, DEFAULT_CACHE_1H_MODE, TOKEN_URL, } from "./constants.js";
5
+ import { refreshClaudeOAuthToken } from "./auth.js";
6
+ import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from "./constants.js";
6
7
  export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
7
8
  export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
8
9
  const DEFAULT_FALLBACK_ON = [401, 403, 429];
@@ -102,6 +103,7 @@ function normalizeStorage(value) {
102
103
  quota: isRecord(value.quota) ? value.quota : undefined,
103
104
  claudeCache: isRecord(value.claudeCache) ? value.claudeCache : undefined,
104
105
  dump: isRecord(value.dump) ? value.dump : undefined,
106
+ claudeFast: isRecord(value.claudeFast) ? value.claudeFast : undefined,
105
107
  relay: isRecord(value.relay) ? value.relay : undefined,
106
108
  accounts: value.accounts
107
109
  .map(normalizeAccount)
@@ -199,6 +201,22 @@ export async function setDumpPersistentEnabled(enabled, path = getAccountStorage
199
201
  await saveAccounts(storage, path);
200
202
  return storage;
201
203
  }
204
+ export function isFastModePersistentlyEnabled(storage) {
205
+ return storage?.claudeFast?.enabled === true;
206
+ }
207
+ export async function setFastModePersistentEnabled(enabled, path = getAccountStoragePath()) {
208
+ const storage = (await loadAccounts(path)) ?? {
209
+ version: 1,
210
+ main: { type: 'opencode', provider: 'anthropic' },
211
+ accounts: [],
212
+ };
213
+ storage.claudeFast = {
214
+ ...(storage.claudeFast ?? {}),
215
+ enabled,
216
+ };
217
+ await saveAccounts(storage, path);
218
+ return storage;
219
+ }
202
220
  function getFallbackStatuses(storage) {
203
221
  return storage?.fallbackOn?.length ? storage.fallbackOn : DEFAULT_FALLBACK_ON;
204
222
  }
@@ -545,29 +563,16 @@ export class FallbackAccountManager {
545
563
  }
546
564
  const sourceAccount = latestAccount ?? account;
547
565
  const refreshToken = sourceAccount.refresh;
548
- const response = await this.fetchImpl(TOKEN_URL, {
549
- method: 'POST',
550
- headers: {
551
- 'Content-Type': 'application/json',
552
- Accept: 'application/json, text/plain, */*',
553
- 'User-Agent': 'axios/1.13.6',
554
- },
555
- body: JSON.stringify({
556
- grant_type: 'refresh_token',
557
- refresh_token: refreshToken,
558
- client_id: CLIENT_ID,
559
- }),
566
+ const refreshed = await refreshClaudeOAuthToken({
567
+ refreshToken,
568
+ fetchImpl: this.fetchImpl,
569
+ now: this.now,
560
570
  });
561
- if (!response.ok) {
562
- const body = await response.text().catch(() => '');
563
- throw new Error(`Fallback token refresh failed: ${response.status} — ${body}`);
564
- }
565
- const json = (await response.json());
566
- const refreshedAt = this.now();
567
- sourceAccount.access = json.access_token;
568
- sourceAccount.refresh = json.refresh_token ?? refreshToken;
569
- sourceAccount.expires = refreshedAt + json.expires_in * 1000;
570
- sourceAccount.lastRefreshedAt = refreshedAt;
571
+ sourceAccount.access = refreshed.access;
572
+ sourceAccount.refresh = refreshed.refresh;
573
+ sourceAccount.expires = refreshed.expires;
574
+ sourceAccount.lastRefreshedAt =
575
+ refreshed.expires - refreshed.expiresIn * 1000;
571
576
  sourceAccount.lastRefreshError = undefined;
572
577
  updateStoredAccount(storage, sourceAccount);
573
578
  return sourceAccount;
package/dist/auth.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ export declare class ClaudeOAuthRefreshError extends Error {
2
+ readonly status: number;
3
+ readonly body: string;
4
+ constructor(status: number, body: string);
5
+ }
6
+ export type ClaudeOAuthRefreshResult = {
7
+ access: string;
8
+ refresh: string;
9
+ expires: number;
10
+ expiresIn: number;
11
+ };
12
+ export declare function refreshClaudeOAuthToken(input: {
13
+ refreshToken: string;
14
+ fetchImpl?: typeof fetch;
15
+ now?: () => number;
16
+ }): Promise<ClaudeOAuthRefreshResult>;
1
17
  export type AuthorizationResult = {
2
18
  url: string;
3
19
  redirectUri: string;
package/dist/auth.js CHANGED
@@ -1,5 +1,42 @@
1
1
  import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from "./constants.js";
2
2
  import { generatePKCE } from "./pkce.js";
3
+ export class ClaudeOAuthRefreshError extends Error {
4
+ status;
5
+ body;
6
+ constructor(status, body) {
7
+ super(`Claude OAuth refresh failed: ${status} — ${body}`);
8
+ this.status = status;
9
+ this.body = body;
10
+ this.name = 'ClaudeOAuthRefreshError';
11
+ }
12
+ }
13
+ export async function refreshClaudeOAuthToken(input) {
14
+ const fetchImpl = input.fetchImpl ?? fetch;
15
+ const response = await fetchImpl(TOKEN_URL, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ Accept: 'application/json',
20
+ },
21
+ body: JSON.stringify({
22
+ grant_type: 'refresh_token',
23
+ refresh_token: input.refreshToken,
24
+ client_id: CLIENT_ID,
25
+ }),
26
+ });
27
+ if (!response.ok) {
28
+ const body = await response.text().catch(() => '');
29
+ throw new ClaudeOAuthRefreshError(response.status, body);
30
+ }
31
+ const json = (await response.json());
32
+ const refreshedAt = input.now?.() ?? Date.now();
33
+ return {
34
+ access: json.access_token,
35
+ refresh: json.refresh_token ?? input.refreshToken,
36
+ expires: refreshedAt + json.expires_in * 1000,
37
+ expiresIn: json.expires_in,
38
+ };
39
+ }
3
40
  function generateState() {
4
41
  return crypto.randomUUID().replace(/-/g, '');
5
42
  }
package/dist/cch.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import xxhashInit from 'xxhash-wasm';
3
- import { CLAUDE_CODE_VERSION } from "./constants.js";
3
+ import { CLAUDE_CODE_BUILD_HASH, CLAUDE_CODE_VERSION } from "./constants.js";
4
4
  const CCH_SEED = 0x6e52736ac806831en;
5
5
  export const CCH_PATTERN = /\bcch=([0-9a-f]{5});/;
6
6
  let xxhashPromise = null;
@@ -58,6 +58,8 @@ export async function signRequestBody(bodyString) {
58
58
  * busting prompt-cache prefixes on every turn.
59
59
  */
60
60
  export function computeVersionSuffix(version = CLAUDE_CODE_VERSION, date = new Date()) {
61
+ if (version === CLAUDE_CODE_VERSION)
62
+ return CLAUDE_CODE_BUILD_HASH;
61
63
  const dayStamp = date.toISOString().slice(0, 10);
62
64
  return createHash('sha256')
63
65
  .update(`${dayStamp}${version}`)
@@ -0,0 +1,19 @@
1
+ export type ClaudeCodeIdentity = {
2
+ deviceId: string;
3
+ accountUuid?: string;
4
+ sessionId: string;
5
+ };
6
+ export declare function getClaudeCodeIdentity(seed: string): ClaudeCodeIdentity;
7
+ export declare function resolveClaudeCodeIdentity(accessToken: string, model?: string): Promise<ClaudeCodeIdentity>;
8
+ export declare function buildClaudeCodeMetadataUserId(identity: ClaudeCodeIdentity): string | null;
9
+ export declare function applyClaudeCodeMetadata(body: Record<string, unknown>, identity: ClaudeCodeIdentity): boolean;
10
+ export declare const CLAUDE_CODE_FULL_AGENT_BETAS: readonly ["claude-code-20250219", "oauth-2025-04-20", "interleaved-thinking-2025-05-14", "context-management-2025-06-27", "prompt-caching-scope-2026-01-05", "advisor-tool-2026-03-01", "advanced-tool-use-2025-11-20", "context-1m-2025-08-07", "effort-2025-11-24", "extended-cache-ttl-2025-04-11", "cache-diagnosis-2026-04-07"];
11
+ export declare function selectClaudeCodeBetas(body?: Record<string, unknown> | null, extraBetas?: string[]): string;
12
+ export declare function applyClaudeCodeHeaders(headers: Headers, accessToken: string, options?: {
13
+ body?: Record<string, unknown> | null;
14
+ identity?: ClaudeCodeIdentity;
15
+ extraBetas?: string[];
16
+ }): Headers;
17
+ export declare function orderClaudeCodeBody<T extends Record<string, unknown>>(body: T): T;
18
+ export declare function claudeCodeBuildHash(): string;
19
+ export declare function claudeCodeEntrypoint(): string;
@@ -0,0 +1,258 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
+ import { CLAUDE_CODE_BUILD_HASH, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_STAINLESS_PACKAGE_VERSION, CLAUDE_CODE_STAINLESS_RUNTIME_VERSION, CLAUDE_CODE_VERSION, FAST_MODE_BETA, USER_AGENT, } from "./constants.js";
3
+ const IDENTITY_CACHE_LIMIT = 1_000;
4
+ const identityCache = new Map();
5
+ function setBounded(map, key, value) {
6
+ if (!map.has(key) && map.size >= IDENTITY_CACHE_LIMIT) {
7
+ const oldest = map.keys().next().value;
8
+ if (oldest !== undefined)
9
+ map.delete(oldest);
10
+ }
11
+ map.set(key, value);
12
+ }
13
+ export function getClaudeCodeIdentity(seed) {
14
+ const cacheKey = seed || 'anonymous';
15
+ const cached = identityCache.get(cacheKey);
16
+ if (cached)
17
+ return cached;
18
+ const identity = {
19
+ deviceId: randomBytes(32).toString('hex'),
20
+ sessionId: randomUUID(),
21
+ };
22
+ setBounded(identityCache, cacheKey, identity);
23
+ return identity;
24
+ }
25
+ const bootstrapFetches = new Map();
26
+ async function fetchClaudeCodeAccountUuid(accessToken, model) {
27
+ if (!accessToken.startsWith('sk-ant-oat'))
28
+ return null;
29
+ const controller = new AbortController();
30
+ const timeout = setTimeout(() => controller.abort(), 5_000);
31
+ try {
32
+ const url = new URL('https://api.anthropic.com/api/claude_cli/bootstrap');
33
+ url.searchParams.set('entrypoint', CLAUDE_CODE_ENTRYPOINT);
34
+ if (model)
35
+ url.searchParams.set('model', model);
36
+ const response = await fetch(url, {
37
+ method: 'GET',
38
+ headers: {
39
+ accept: 'application/json, text/plain, */*',
40
+ authorization: `Bearer ${accessToken}`,
41
+ 'content-type': 'application/json',
42
+ 'anthropic-beta': 'oauth-2025-04-20',
43
+ 'user-agent': `claude-code/${CLAUDE_CODE_VERSION}`,
44
+ },
45
+ signal: controller.signal,
46
+ });
47
+ if (!response.ok)
48
+ return null;
49
+ const data = (await response.json().catch(() => null));
50
+ const accountUuid = data?.oauth_account?.account_uuid;
51
+ return typeof accountUuid === 'string' && accountUuid ? accountUuid : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ finally {
57
+ clearTimeout(timeout);
58
+ }
59
+ }
60
+ export async function resolveClaudeCodeIdentity(accessToken, model) {
61
+ const identity = getClaudeCodeIdentity(accessToken);
62
+ if (!accessToken.startsWith('sk-ant-oat'))
63
+ return identity;
64
+ let fetchPromise = bootstrapFetches.get(accessToken);
65
+ if (!fetchPromise) {
66
+ fetchPromise = fetchClaudeCodeAccountUuid(accessToken, model);
67
+ bootstrapFetches.set(accessToken, fetchPromise);
68
+ }
69
+ const accountUuid = await fetchPromise;
70
+ if (!accountUuid) {
71
+ bootstrapFetches.delete(accessToken);
72
+ return identity;
73
+ }
74
+ const accountCacheKey = `account:${accountUuid}`;
75
+ const accountIdentity = identityCache.get(accountCacheKey);
76
+ if (accountIdentity) {
77
+ setBounded(identityCache, accessToken, accountIdentity);
78
+ return accountIdentity;
79
+ }
80
+ identity.accountUuid = accountUuid;
81
+ setBounded(identityCache, accountCacheKey, identity);
82
+ return identity;
83
+ }
84
+ function isRecord(value) {
85
+ return value != null && typeof value === 'object' && !Array.isArray(value);
86
+ }
87
+ export function buildClaudeCodeMetadataUserId(identity) {
88
+ if (!identity.accountUuid)
89
+ return null;
90
+ return JSON.stringify({
91
+ device_id: identity.deviceId,
92
+ account_uuid: identity.accountUuid,
93
+ session_id: identity.sessionId,
94
+ });
95
+ }
96
+ export function applyClaudeCodeMetadata(body, identity) {
97
+ const userId = buildClaudeCodeMetadataUserId(identity);
98
+ if (!userId) {
99
+ if (isRecord(body.metadata))
100
+ delete body.metadata.user_id;
101
+ return false;
102
+ }
103
+ if (!isRecord(body.metadata))
104
+ body.metadata = {};
105
+ const metadata = body.metadata;
106
+ if (isRecord(metadata))
107
+ metadata.user_id = userId;
108
+ return true;
109
+ }
110
+ export const CLAUDE_CODE_FULL_AGENT_BETAS = [
111
+ 'claude-code-20250219',
112
+ 'oauth-2025-04-20',
113
+ 'interleaved-thinking-2025-05-14',
114
+ 'context-management-2025-06-27',
115
+ 'prompt-caching-scope-2026-01-05',
116
+ 'advisor-tool-2026-03-01',
117
+ 'advanced-tool-use-2025-11-20',
118
+ 'context-1m-2025-08-07',
119
+ 'effort-2025-11-24',
120
+ 'extended-cache-ttl-2025-04-11',
121
+ 'cache-diagnosis-2026-04-07',
122
+ ];
123
+ const CLAUDE_CODE_STRUCTURED_OUTPUT_BETAS = [
124
+ 'oauth-2025-04-20',
125
+ 'interleaved-thinking-2025-05-14',
126
+ 'context-management-2025-06-27',
127
+ 'prompt-caching-scope-2026-01-05',
128
+ 'advisor-tool-2026-03-01',
129
+ 'structured-outputs-2025-12-15',
130
+ 'cache-diagnosis-2026-04-07',
131
+ ];
132
+ const CLAUDE_CODE_BASE_BETAS = [
133
+ 'oauth-2025-04-20',
134
+ 'interleaved-thinking-2025-05-14',
135
+ 'context-management-2025-06-27',
136
+ 'prompt-caching-scope-2026-01-05',
137
+ 'advisor-tool-2026-03-01',
138
+ 'cache-diagnosis-2026-04-07',
139
+ ];
140
+ function hasStructuredOutput(body) {
141
+ const outputConfig = body.output_config;
142
+ const format = outputConfig?.format;
143
+ return format?.type === 'json_schema';
144
+ }
145
+ function hasFullAgentShape(body) {
146
+ return (Array.isArray(body.tools) &&
147
+ body.tools.length > 0 &&
148
+ Array.isArray(body.system) &&
149
+ isRecord(body.thinking) &&
150
+ isRecord(body.context_management) &&
151
+ isRecord(body.output_config) &&
152
+ isRecord(body.diagnostics));
153
+ }
154
+ export function selectClaudeCodeBetas(body, extraBetas = []) {
155
+ const selected = body
156
+ ? hasFullAgentShape(body)
157
+ ? [...CLAUDE_CODE_FULL_AGENT_BETAS]
158
+ : hasStructuredOutput(body)
159
+ ? [...CLAUDE_CODE_STRUCTURED_OUTPUT_BETAS]
160
+ : [...CLAUDE_CODE_BASE_BETAS]
161
+ : [...CLAUDE_CODE_BASE_BETAS];
162
+ if (body?.speed === 'fast')
163
+ selected.push(FAST_MODE_BETA);
164
+ for (const beta of extraBetas) {
165
+ const trimmed = beta.trim();
166
+ if (trimmed)
167
+ selected.push(trimmed);
168
+ }
169
+ return [...new Set(selected)].join(',');
170
+ }
171
+ function stainlessOS() {
172
+ switch (process.platform) {
173
+ case 'darwin':
174
+ return 'MacOS';
175
+ case 'win32':
176
+ return 'Windows';
177
+ case 'linux':
178
+ return 'Linux';
179
+ case 'freebsd':
180
+ return 'FreeBSD';
181
+ default:
182
+ return 'Unknown';
183
+ }
184
+ }
185
+ function stainlessArch() {
186
+ switch (process.arch) {
187
+ case 'arm64':
188
+ return 'arm64';
189
+ case 'x64':
190
+ return 'x64';
191
+ case 'ia32':
192
+ return 'x32';
193
+ default:
194
+ return process.arch;
195
+ }
196
+ }
197
+ export function applyClaudeCodeHeaders(headers, accessToken, options = {}) {
198
+ const identity = options.identity ?? getClaudeCodeIdentity(accessToken);
199
+ const incomingBetas = (headers.get('anthropic-beta') ?? '')
200
+ .split(',')
201
+ .map((beta) => beta.trim())
202
+ .filter(Boolean);
203
+ const extraBetas = [...incomingBetas, ...(options.extraBetas ?? [])];
204
+ headers.set('accept', 'application/json');
205
+ headers.set('authorization', `Bearer ${accessToken}`);
206
+ headers.set('content-type', 'application/json');
207
+ headers.set('user-agent', USER_AGENT);
208
+ headers.set('anthropic-beta', selectClaudeCodeBetas(options.body, extraBetas));
209
+ headers.set('anthropic-dangerous-direct-browser-access', 'true');
210
+ headers.set('anthropic-version', '2023-06-01');
211
+ headers.set('x-app', 'cli');
212
+ headers.set('x-client-request-id', randomUUID());
213
+ headers.set('x-claude-code-session-id', identity.sessionId);
214
+ headers.set('x-stainless-arch', stainlessArch());
215
+ headers.set('x-stainless-lang', 'js');
216
+ headers.set('x-stainless-os', stainlessOS());
217
+ headers.set('x-stainless-package-version', CLAUDE_CODE_STAINLESS_PACKAGE_VERSION);
218
+ headers.set('x-stainless-retry-count', '0');
219
+ headers.set('x-stainless-runtime', 'node');
220
+ headers.set('x-stainless-runtime-version', CLAUDE_CODE_STAINLESS_RUNTIME_VERSION);
221
+ headers.set('x-stainless-timeout', '600');
222
+ headers.delete('x-api-key');
223
+ return headers;
224
+ }
225
+ const BODY_FIELD_ORDER = [
226
+ 'model',
227
+ 'messages',
228
+ 'system',
229
+ 'tools',
230
+ 'tool_choice',
231
+ 'metadata',
232
+ 'max_tokens',
233
+ 'temperature',
234
+ 'thinking',
235
+ 'context_management',
236
+ 'output_config',
237
+ 'diagnostics',
238
+ 'stream',
239
+ 'speed',
240
+ ];
241
+ export function orderClaudeCodeBody(body) {
242
+ const ordered = {};
243
+ for (const key of BODY_FIELD_ORDER) {
244
+ if (Object.hasOwn(body, key))
245
+ ordered[key] = body[key];
246
+ }
247
+ for (const [key, value] of Object.entries(body)) {
248
+ if (!Object.hasOwn(ordered, key))
249
+ ordered[key] = value;
250
+ }
251
+ return ordered;
252
+ }
253
+ export function claudeCodeBuildHash() {
254
+ return CLAUDE_CODE_BUILD_HASH;
255
+ }
256
+ export function claudeCodeEntrypoint() {
257
+ return CLAUDE_CODE_ENTRYPOINT;
258
+ }
@@ -8,13 +8,19 @@ 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 FAST_MODE_BETA = "fast-mode-2026-02-01";
12
+ export declare function mergeAnthropicBetas(existing: string | null | undefined, betas: string[]): string;
13
+ export declare function isFastModeSupportedModel(model: unknown): boolean;
11
14
  export declare const OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
12
15
  export declare const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
13
16
  export declare const CCH_SALT = "59cf53e54c78";
14
17
  export declare const CCH_POSITIONS: number[];
15
- export declare const CLAUDE_CODE_VERSION = "2.1.87";
18
+ export declare const CLAUDE_CODE_VERSION = "2.1.141";
19
+ export declare const CLAUDE_CODE_BUILD_HASH = "67b";
16
20
  export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
17
- export declare const USER_AGENT = "claude-cli/2.1.87 (external, cli)";
21
+ export declare const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = "0.94.0";
22
+ export declare const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = "v24.3.0";
23
+ export declare const USER_AGENT = "claude-cli/2.1.141 (external, sdk-cli)";
18
24
  export declare const CACHE_1H_MODES: readonly ["explicit", "automatic", "hybrid"];
19
25
  export type Cache1hMode = (typeof CACHE_1H_MODES)[number];
20
26
  export declare const DEFAULT_CACHE_1H_MODE: Cache1hMode;
package/dist/constants.js CHANGED
@@ -18,13 +18,28 @@ export const REQUIRED_BETAS = [
18
18
  'oauth-2025-04-20',
19
19
  'interleaved-thinking-2025-05-14',
20
20
  ];
21
+ export const FAST_MODE_BETA = 'fast-mode-2026-02-01';
22
+ export function mergeAnthropicBetas(existing, betas) {
23
+ const incoming = (existing ?? '')
24
+ .split(',')
25
+ .map((beta) => beta.trim())
26
+ .filter(Boolean);
27
+ return [...new Set([...incoming, ...betas])].join(',');
28
+ }
29
+ export function isFastModeSupportedModel(model) {
30
+ return (typeof model === 'string' &&
31
+ (model.startsWith('claude-opus-4-6') || model.startsWith('claude-opus-4-7')));
32
+ }
21
33
  export const OPENCODE_IDENTITY_PREFIX = 'You are OpenCode';
22
34
  export const CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
23
35
  export const CCH_SALT = '59cf53e54c78';
24
36
  export const CCH_POSITIONS = [4, 7, 20];
25
- export const CLAUDE_CODE_VERSION = '2.1.87';
37
+ export const CLAUDE_CODE_VERSION = '2.1.141';
38
+ export const CLAUDE_CODE_BUILD_HASH = '67b';
26
39
  export const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
27
- export const USER_AGENT = 'claude-cli/2.1.87 (external, cli)';
40
+ export const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = '0.94.0';
41
+ export const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = 'v24.3.0';
42
+ export const USER_AGENT = 'claude-cli/2.1.141 (external, sdk-cli)';
28
43
  export const CACHE_1H_MODES = ['explicit', 'automatic', 'hybrid'];
29
44
  export const DEFAULT_CACHE_1H_MODE = 'explicit';
30
45
  /**
package/dist/fast.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export declare const CLAUDE_FAST_COMMAND_NAME = "claude-fast";
2
+ export type FastModeCommandAction = {
3
+ type: 'status';
4
+ } | {
5
+ type: 'enable';
6
+ } | {
7
+ type: 'disable';
8
+ } | {
9
+ type: 'usage';
10
+ };
11
+ export declare function isFastModeEnabled(): boolean;
12
+ export declare function setFastModeEnabled(enabled: boolean): void;
13
+ export declare function resetFastModeState(): void;
14
+ export declare function parseFastModeCommandAction(argumentsText: string): FastModeCommandAction;
15
+ export declare function buildFastModeStatusSummary(input?: {
16
+ enabled?: boolean;
17
+ }): string;
18
+ export declare function executeFastModeCommand(input: {
19
+ argumentsText: string;
20
+ enabled?: boolean;
21
+ }): string;
package/dist/fast.js ADDED
@@ -0,0 +1,66 @@
1
+ export const CLAUDE_FAST_COMMAND_NAME = 'claude-fast';
2
+ const FAST_STATUS_TITLE = '## Claude Fast Mode Status';
3
+ const FAST_ENABLED_TITLE = '## Claude Fast Mode Enabled';
4
+ const FAST_DISABLED_TITLE = '## Claude Fast Mode Disabled';
5
+ const FAST_USAGE_TITLE = '## Claude Fast Mode Usage';
6
+ const FAST_USAGE = 'Usage: `/claude-fast`, `/claude-fast on`, or `/claude-fast off`.';
7
+ let fastModeEnabled = false;
8
+ export function isFastModeEnabled() {
9
+ return fastModeEnabled;
10
+ }
11
+ export function setFastModeEnabled(enabled) {
12
+ fastModeEnabled = enabled;
13
+ }
14
+ export function resetFastModeState() {
15
+ fastModeEnabled = false;
16
+ }
17
+ export function parseFastModeCommandAction(argumentsText) {
18
+ const normalized = argumentsText.trim().split(/\s+/).filter(Boolean);
19
+ if (normalized.length === 0)
20
+ return { type: 'status' };
21
+ if (normalized.length === 1 && normalized[0] === 'on')
22
+ return { type: 'enable' };
23
+ if (normalized.length === 1 && normalized[0] === 'off')
24
+ return { type: 'disable' };
25
+ return { type: 'usage' };
26
+ }
27
+ export function buildFastModeStatusSummary(input) {
28
+ const enabled = input?.enabled ?? fastModeEnabled;
29
+ return [
30
+ FAST_STATUS_TITLE,
31
+ '',
32
+ `- Enabled: ${enabled ? 'enabled' : 'disabled'}`,
33
+ '- Persisted: ~/.config/opencode/anthropic-auth.json',
34
+ '- Scope: adds Anthropic fast mode to supported Opus requests',
35
+ '- Supported models: claude-opus-4-6 and claude-opus-4-7',
36
+ '- Request changes: adds `speed: "fast"` and the `fast-mode-2026-02-01` beta header',
37
+ '- Note: fast and standard speeds do not share prompt-cache prefixes',
38
+ ].join('\n');
39
+ }
40
+ export function executeFastModeCommand(input) {
41
+ const action = parseFastModeCommandAction(input.argumentsText);
42
+ const enabled = input.enabled ?? fastModeEnabled;
43
+ if (action.type === 'status')
44
+ return buildFastModeStatusSummary({ enabled });
45
+ if (action.type === 'enable') {
46
+ return [
47
+ FAST_ENABLED_TITLE,
48
+ '',
49
+ buildFastModeStatusSummary({ enabled: true }),
50
+ ].join('\n');
51
+ }
52
+ if (action.type === 'disable') {
53
+ return [
54
+ FAST_DISABLED_TITLE,
55
+ '',
56
+ buildFastModeStatusSummary({ enabled: false }),
57
+ ].join('\n');
58
+ }
59
+ return [
60
+ FAST_USAGE_TITLE,
61
+ '',
62
+ FAST_USAGE,
63
+ '',
64
+ buildFastModeStatusSummary({ enabled }),
65
+ ].join('\n');
66
+ }
package/dist/index.d.ts CHANGED
@@ -2,8 +2,10 @@ export * from './accounts.ts';
2
2
  export * from './auth.ts';
3
3
  export * from './cache1h.ts';
4
4
  export * from './cch.ts';
5
+ export * from './claude-code.ts';
5
6
  export * from './constants.ts';
6
7
  export * from './dump.ts';
8
+ export * from './fast.ts';
7
9
  export * from './logger.ts';
8
10
  export * from './pkce.ts';
9
11
  export * from './quotas.ts';
package/dist/index.js CHANGED
@@ -2,8 +2,10 @@ export * from "./accounts.js";
2
2
  export * from "./auth.js";
3
3
  export * from "./cache1h.js";
4
4
  export * from "./cch.js";
5
+ export * from "./claude-code.js";
5
6
  export * from "./constants.js";
6
7
  export * from "./dump.js";
8
+ export * from "./fast.js";
7
9
  export * from "./logger.js";
8
10
  export * from "./pkce.js";
9
11
  export * from "./quotas.js";
package/dist/relay.d.ts CHANGED
@@ -22,6 +22,7 @@ export declare function sendViaRelay(options: {
22
22
  headers: Headers;
23
23
  body: RequestInit['body'] | null | undefined;
24
24
  fallback: () => Promise<Response>;
25
+ affinity?: string | null;
25
26
  optimisticResponse?: boolean;
26
27
  }): Promise<Response>;
27
- export declare const WORKER_SCRIPT = "\nasync function hashBody(body) {\n const bytes = new TextEncoder().encode(body)\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return 'sha256:' + hex\n}\n\nfunction applyPatch(base, patch) {\n return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)\n}\n\nfunction applyPatchSet(base, patch) {\n if (!Array.isArray(patch)) return applyPatch(base, patch)\n if (patch.length === 0) return base\n\n let result = ''\n let cursor = 0\n for (const item of patch) {\n if (item.start < cursor) return { error: 'overlapping patch', status: 400 }\n result += base.slice(cursor, item.start)\n result += item.insert\n cursor = item.start + item.deleteCount\n }\n return result + base.slice(cursor)\n}\n\nasync function readState(env, affinity) {\n const raw = await env.RELAY_STATE.get('session:' + affinity)\n return raw ? JSON.parse(raw) : null\n}\n\nasync function writeState(env, affinity, state) {\n await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })\n}\n\nasync function resolveBody(env, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n const state = await readState(env, payload.affinity)\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nasync function prepareUpstream(env, payload) {\n if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = await resolveBody(env, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n\n if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n await writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision })\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'relay',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body }\n}\n\nfunction resolveBodyFromState(state, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nfunction deferWorkerTask(task) {\n return new Promise((resolve) => setTimeout(resolve, 0)).then(task)\n}\n\nfunction checkpointWebSocketState(env, payload, body, nextState) {\n return deferWorkerTask(async () => {\n try {\n if ((await hashBody(body)) !== payload.next_hash) {\n throw new Error('hash mismatch')\n }\n await writeState(env, payload.affinity, nextState)\n } catch (error) {\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n event: 'checkpoint_write_failed',\n affinity: String(payload.affinity).slice(0, 12),\n message: error instanceof Error ? error.message : String(error),\n }))\n }\n })\n}\n\nfunction prepareWebSocketUpstream(env, state, payload) {\n if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = resolveBodyFromState(state, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n if (typeof body !== 'string') return { error: 'invalid body', status: 400 }\n\n const nextState = { body, hash: payload.next_hash, revision: payload.revision }\n const checkpoint = checkpointWebSocketState(env, payload, body, nextState)\n const logAccepted = () => console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, state: nextState, checkpoint, logAccepted }\n}\n\nfunction headersToObject(headers) {\n const result = {}\n for (const [key, value] of headers.entries()) result[key] = value\n return result\n}\n\nasync function handleRelayPayload(env, payload) {\n const prepared = await prepareUpstream(env, payload)\n if (prepared.error) return prepared\n const upstream = await fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: prepared.body,\n })\n return { upstream }\n}\n\nasync function handleWebSocket(socket, env, ctx, payload, getState, setState) {\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))\n } catch {\n clearInterval(heartbeat)\n }\n }, 5000)\n try {\n const result = prepareWebSocketUpstream(env, getState(), payload)\n if (result.error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))\n return\n }\n\n setState(result.state)\n socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))\n ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))\n\n const upstreamPromise = fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: result.body,\n })\n ctx?.waitUntil?.(result.checkpoint)\n const upstream = await upstreamPromise\n socket.send(JSON.stringify({\n protocol: 2,\n type: 'response_start',\n id: payload.id,\n status: upstream.status,\n statusText: upstream.statusText,\n headers: headersToObject(upstream.headers),\n }))\n\n const reader = upstream.body?.getReader()\n if (reader) {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n socket.send(value)\n }\n }\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id }))\n } catch (error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))\n } finally {\n clearInterval(heartbeat)\n }\n}\n\nexport default {\n async fetch(request, env, ctx) {\n if (request.headers.get('Upgrade') === 'websocket') {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n const affinity = url.searchParams.get('affinity')\n if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })\n if (!affinity) return new Response('missing affinity', { status: 400 })\n const pair = new WebSocketPair()\n const client = pair[0]\n const server = pair[1]\n server.binaryType = 'arraybuffer'\n let state = null\n let ready = false\n server.accept()\n const loadState = readState(env, affinity).then((loadedState) => {\n state = loadedState\n ready = true\n server.send(JSON.stringify({\n protocol: 2,\n type: 'ready',\n state: state ? { hash: state.hash, revision: state.revision } : null,\n }))\n }).catch((error) => {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))\n server.close(1011, 'state load failed')\n })\n ctx?.waitUntil?.(loadState)\n if (!ctx?.waitUntil) void loadState\n let busy = false\n server.addEventListener('message', (event) => {\n if (!ready) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))\n return\n }\n if (busy) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))\n return\n }\n let payload\n try {\n payload = JSON.parse(event.data)\n } catch {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))\n return\n }\n payload.affinity = affinity\n busy = true\n const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })\n ctx?.waitUntil?.(run)\n if (!ctx?.waitUntil) void run\n })\n return new Response(null, { status: 101, webSocket: client })\n }\n\n if (request.method === 'GET') return new Response('ok')\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {\n return new Response('unauthorized', { status: 401 })\n }\n\n const payload = await request.json()\n const result = await handleRelayPayload(env, payload)\n if (result.error) return Response.json({ error: result.error }, { status: result.status })\n\n const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n },\n}\n";
28
+ export declare const WORKER_SCRIPT = "\nasync function hashBody(body) {\n const bytes = new TextEncoder().encode(body)\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return 'sha256:' + hex\n}\n\nfunction applyPatch(base, patch) {\n return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)\n}\n\nfunction applyPatchSet(base, patch) {\n if (!Array.isArray(patch)) return applyPatch(base, patch)\n if (patch.length === 0) return base\n\n let result = ''\n let cursor = 0\n for (const item of patch) {\n if (item.start < cursor) return { error: 'overlapping patch', status: 400 }\n result += base.slice(cursor, item.start)\n result += item.insert\n cursor = item.start + item.deleteCount\n }\n return result + base.slice(cursor)\n}\n\nasync function readState(env, affinity) {\n const raw = await env.RELAY_STATE.get('session:' + affinity)\n return raw ? JSON.parse(raw) : null\n}\n\nasync function writeState(env, affinity, state) {\n await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })\n}\n\nasync function resolveBody(env, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n const state = await readState(env, payload.affinity)\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nasync function prepareUpstream(env, payload) {\n if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = await resolveBody(env, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n\n if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n await writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision })\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'relay',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body }\n}\n\nfunction resolveBodyFromState(state, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nfunction deferWorkerTask(task) {\n return new Promise((resolve) => setTimeout(resolve, 0)).then(task)\n}\n\nfunction checkpointWebSocketState(env, payload, body, nextState) {\n return deferWorkerTask(async () => {\n try {\n if ((await hashBody(body)) !== payload.next_hash) {\n throw new Error('hash mismatch')\n }\n await writeState(env, payload.affinity, nextState)\n } catch (error) {\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n event: 'checkpoint_write_failed',\n affinity: String(payload.affinity).slice(0, 12),\n message: error instanceof Error ? error.message : String(error),\n }))\n }\n })\n}\n\nasync function prepareWebSocketUpstream(env, state, payload) {\n if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = resolveBodyFromState(state, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n if (typeof body !== 'string') return { error: 'invalid body', status: 400 }\n if ((await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const nextState = { body, hash: payload.next_hash, revision: payload.revision }\n const checkpoint = checkpointWebSocketState(env, payload, body, nextState)\n const logAccepted = () => console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, state: nextState, checkpoint, logAccepted }\n}\n\nfunction headersToObject(headers) {\n const result = {}\n for (const [key, value] of headers.entries()) result[key] = value\n return result\n}\n\nasync function handleRelayPayload(env, payload) {\n const prepared = await prepareUpstream(env, payload)\n if (prepared.error) return prepared\n const upstream = await fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: prepared.body,\n })\n return { upstream }\n}\n\nasync function handleWebSocket(socket, env, ctx, payload, getState, setState) {\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))\n } catch {\n clearInterval(heartbeat)\n }\n }, 5000)\n try {\n const result = await prepareWebSocketUpstream(env, getState(), payload)\n if (result.error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))\n return\n }\n\n setState(result.state)\n socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))\n ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))\n\n const upstreamPromise = fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: result.body,\n })\n ctx?.waitUntil?.(result.checkpoint)\n const upstream = await upstreamPromise\n socket.send(JSON.stringify({\n protocol: 2,\n type: 'response_start',\n id: payload.id,\n status: upstream.status,\n statusText: upstream.statusText,\n headers: headersToObject(upstream.headers),\n }))\n\n const reader = upstream.body?.getReader()\n if (reader) {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n socket.send(value)\n }\n }\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id }))\n } catch (error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))\n } finally {\n clearInterval(heartbeat)\n }\n}\n\nexport default {\n async fetch(request, env, ctx) {\n if (request.headers.get('Upgrade') === 'websocket') {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n const affinity = url.searchParams.get('affinity')\n if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })\n if (!affinity) return new Response('missing affinity', { status: 400 })\n const pair = new WebSocketPair()\n const client = pair[0]\n const server = pair[1]\n server.binaryType = 'arraybuffer'\n let state = null\n let ready = false\n server.accept()\n const loadState = readState(env, affinity).then((loadedState) => {\n state = loadedState\n ready = true\n server.send(JSON.stringify({\n protocol: 2,\n type: 'ready',\n state: state ? { hash: state.hash, revision: state.revision } : null,\n }))\n }).catch((error) => {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))\n server.close(1011, 'state load failed')\n })\n ctx?.waitUntil?.(loadState)\n if (!ctx?.waitUntil) void loadState\n let busy = false\n server.addEventListener('message', (event) => {\n if (!ready) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))\n return\n }\n if (busy) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))\n return\n }\n let payload\n try {\n payload = JSON.parse(event.data)\n } catch {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))\n return\n }\n payload.affinity = affinity\n busy = true\n const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })\n ctx?.waitUntil?.(run)\n if (!ctx?.waitUntil) void run\n })\n return new Response(null, { status: 101, webSocket: client })\n }\n\n if (request.method === 'GET') return new Response('ok')\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {\n return new Response('unauthorized', { status: 401 })\n }\n\n const payload = await request.json()\n const result = await handleRelayPayload(env, payload)\n if (result.error) return Response.json({ error: result.error }, { status: result.status })\n\n const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n },\n}\n";
package/dist/relay.js CHANGED
@@ -107,7 +107,7 @@ function isRelayableAnthropicRequest(input, body) {
107
107
  return false;
108
108
  try {
109
109
  const url = input instanceof Request ? new URL(input.url) : new URL(input.toString());
110
- return url.pathname === '/v1/messages';
110
+ return url.pathname === '/v1/messages' || url.pathname === '/messages';
111
111
  }
112
112
  catch {
113
113
  return false;
@@ -115,8 +115,11 @@ function isRelayableAnthropicRequest(input, body) {
115
115
  }
116
116
  function jsonHeaders(headers) {
117
117
  const result = {};
118
- for (const [key, value] of headers.entries())
118
+ for (const [key, value] of headers.entries()) {
119
+ if (key === 'x-session-affinity' || key === 'x-opencode-session')
120
+ continue;
119
121
  result[key] = value;
122
+ }
120
123
  return result;
121
124
  }
122
125
  async function postRelay(config, payload) {
@@ -363,6 +366,10 @@ class PersistentRelaySession {
363
366
  this.connecting = undefined;
364
367
  }
365
368
  if (this.pending) {
369
+ if (this.pending.retryingBeforeResponse)
370
+ return;
371
+ if (this.retryPendingBeforeResponse(this.pending, 'closed before response'))
372
+ return;
366
373
  const error = new Error('relay websocket closed before response');
367
374
  this.failPending(error);
368
375
  }
@@ -397,6 +404,8 @@ class PersistentRelaySession {
397
404
  rejectDone,
398
405
  sentAt,
399
406
  optimisticResponse,
407
+ retryAttempts: 0,
408
+ retryingBeforeResponse: false,
400
409
  };
401
410
  this.pending = pending;
402
411
  this.resetPendingTimeout(pending);
@@ -415,10 +424,60 @@ class PersistentRelaySession {
415
424
  pending.timeout = setTimeout(() => {
416
425
  if (this.pending !== pending)
417
426
  return;
427
+ if (this.retryPendingBeforeResponse(pending, 'response timed out'))
428
+ return;
418
429
  this.failPending(new Error('relay websocket response timed out'));
419
430
  this.socket?.close();
420
431
  }, 45_000);
421
432
  }
433
+ retryPendingBeforeResponse(pending, reason) {
434
+ if (pending.responseStartedAt != null)
435
+ return false;
436
+ if (pending.streamDone)
437
+ return false;
438
+ if (pending.retryAttempts >= 1)
439
+ return false;
440
+ pending.retryAttempts += 1;
441
+ pending.retryingBeforeResponse = true;
442
+ clearTimeout(pending.timeout);
443
+ const attempt = pending.retryAttempts;
444
+ const originalId = pending.payload.id;
445
+ relayLog(`websocket ${reason}; reconnecting and retrying full_sync session=${shortAffinity(this.affinity)} request=${originalId} retry=${attempt}`);
446
+ this.socket?.close();
447
+ this.socket = undefined;
448
+ this.serverState = undefined;
449
+ this.connecting = undefined;
450
+ void (async () => {
451
+ try {
452
+ await this.ensureConnected();
453
+ if (this.pending !== pending)
454
+ return;
455
+ const retryPayload = createFullSyncPayload(pending.payload, pending.bodyText);
456
+ retryPayload.protocol = 2;
457
+ retryPayload.id = createRequestId();
458
+ retryPayload.revision = (this.serverState?.revision ?? 0) + 1;
459
+ pending.payload = retryPayload;
460
+ pending.accepted = false;
461
+ pending.acceptedAt = undefined;
462
+ pending.responseStartedAt = undefined;
463
+ pending.sentAt = perfNowMs();
464
+ pending.retryingBeforeResponse = false;
465
+ const socket = this.socket;
466
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
467
+ throw new Error('relay websocket is not connected');
468
+ }
469
+ this.resetPendingTimeout(pending);
470
+ relayLog(`perf websocket retry_send session=${shortAffinity(this.affinity)} request=${retryPayload.id} previous=${originalId} mode=full_sync retry=${attempt} bodyBytes=${pending.bodyText.length} relayBytes=${JSON.stringify(retryPayload).length}`);
471
+ socket.send(JSON.stringify(retryPayload));
472
+ }
473
+ catch (error) {
474
+ pending.retryingBeforeResponse = false;
475
+ if (this.pending === pending)
476
+ this.failPending(error);
477
+ }
478
+ })();
479
+ return true;
480
+ }
422
481
  handleBinaryChunk(data) {
423
482
  const pending = this.pending;
424
483
  if (!pending?.responseStarted)
@@ -485,6 +544,10 @@ class PersistentRelaySession {
485
544
  return;
486
545
  }
487
546
  if (message.type === 'error') {
547
+ if (message.status === 409 &&
548
+ this.retryPendingBeforeResponse(pending, message.message || 'state mismatch')) {
549
+ return;
550
+ }
488
551
  const error = message.status === 409
489
552
  ? new RelayStateMismatchError(message.message || 'state mismatch')
490
553
  : new Error(message.message || 'relay websocket error');
@@ -529,10 +592,12 @@ function getPersistentRelaySession(config, affinity) {
529
592
  return session;
530
593
  }
531
594
  export async function sendViaRelay(options) {
532
- const { config, input, init, headers, body, fallback, optimisticResponse } = options;
595
+ const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, } = options;
533
596
  if (!config || !isRelayableAnthropicRequest(input, body))
534
597
  return fallback();
535
- const affinity = headers.get('x-session-affinity') || headers.get('x-opencode-session');
598
+ const affinity = explicitAffinity ||
599
+ headers.get('x-session-affinity') ||
600
+ headers.get('x-opencode-session');
536
601
  if (!affinity) {
537
602
  relayLog('skipping relay: missing x-session-affinity header');
538
603
  return fallback();
@@ -700,7 +765,7 @@ function checkpointWebSocketState(env, payload, body, nextState) {
700
765
  })
701
766
  }
702
767
 
703
- function prepareWebSocketUpstream(env, state, payload) {
768
+ async function prepareWebSocketUpstream(env, state, payload) {
704
769
  if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {
705
770
  return { error: 'invalid payload', status: 400 }
706
771
  }
@@ -708,6 +773,9 @@ function prepareWebSocketUpstream(env, state, payload) {
708
773
  const body = resolveBodyFromState(state, payload)
709
774
  if (body && typeof body === 'object' && 'error' in body) return body
710
775
  if (typeof body !== 'string') return { error: 'invalid body', status: 400 }
776
+ if ((await hashBody(body)) !== payload.next_hash) {
777
+ return { error: 'hash mismatch', status: 409 }
778
+ }
711
779
 
712
780
  const nextState = { body, hash: payload.next_hash, revision: payload.revision }
713
781
  const checkpoint = checkpointWebSocketState(env, payload, body, nextState)
@@ -749,7 +817,7 @@ async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
749
817
  }
750
818
  }, 5000)
751
819
  try {
752
- const result = prepareWebSocketUpstream(env, getState(), payload)
820
+ const result = await prepareWebSocketUpstream(env, getState(), payload)
753
821
  if (result.error) {
754
822
  socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))
755
823
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",