@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 +1 -1
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +41 -1
- package/dist/cachekeep.js +2 -3
- package/dist/cch.d.ts +2 -0
- package/dist/cch.js +13 -3
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +8 -1
- package/dist/dump.js +9 -6
- package/dist/fast.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/routing.d.ts +23 -0
- package/dist/routing.js +80 -0
- package/package.json +1 -1
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
|
|
package/dist/accounts.d.ts
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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
|
|
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 (!
|
|
56
|
+
if (!BILLING_HEADER_CCH_PATTERN.test(bodyString))
|
|
48
57
|
return bodyString;
|
|
49
|
-
const
|
|
50
|
-
|
|
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.
|
package/dist/constants.d.ts
CHANGED
|
@@ -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') ||
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
|
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
|
|
188
|
+
const dumpDir = getDumpDirectory();
|
|
189
|
+
const prefix = join(dumpDir, id);
|
|
187
190
|
try {
|
|
188
|
-
await mkdir(
|
|
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-
|
|
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
package/dist/index.js
CHANGED
|
@@ -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 {};
|
package/dist/routing.js
ADDED
|
@@ -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
|
+
}
|