@cortexkit/anthropic-auth-core 1.21.0 → 1.22.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/dist/index.js CHANGED
@@ -5,13 +5,16 @@ export * from './cachekeep.js';
5
5
  export * from './cachekeep-registry.js';
6
6
  export * from './cch.js';
7
7
  export * from './claude-code.js';
8
+ export * from './claustrum.js';
8
9
  export * from './commands/account.js';
9
10
  export * from './constants.js';
10
11
  export * from './dump.js';
11
12
  export * from './fast.js';
13
+ export * from './json.js';
12
14
  export * from './killswitch.js';
13
15
  export * from './logger.js';
14
16
  export * from './logging.js';
17
+ export * from './mid-conversation-output-config.js';
15
18
  export * from './models.js';
16
19
  export * from './oauth-profile.js';
17
20
  export * from './pkce.js';
@@ -25,3 +28,4 @@ export * from './relay.js';
25
28
  export * from './routing.js';
26
29
  export * from './start.js';
27
30
  export * from './sticky-routing.js';
31
+ export * from './thinking-binding.js';
package/dist/json.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function parseJsonRedacted(text: string): unknown;
package/dist/json.js ADDED
@@ -0,0 +1,10 @@
1
+ export function parseJsonRedacted(text) {
2
+ try {
3
+ return JSON.parse(text);
4
+ }
5
+ catch (error) {
6
+ if (error instanceof SyntaxError)
7
+ throw new SyntaxError('invalid JSON');
8
+ throw error;
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ export declare const MID_CONVERSATION_OUTPUT_CONFIG_BETA = "mid-conversation-output-config-2026-07-01";
2
+ export type AdaptiveEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
3
+ export type MidConversationEffortTransition = {
4
+ /** Number of assistant messages preceding the user boundary for this effort. */
5
+ afterAssistantMessages: number;
6
+ effort: AdaptiveEffort;
7
+ };
8
+ export declare function normalizeAdaptiveEffort(value: unknown): AdaptiveEffort | undefined;
9
+ export declare function usesMidConversationOutputConfig(body: Record<string, unknown>): boolean;
10
+ export declare function applyMidConversationOutputConfig(body: Record<string, unknown>, transitions: readonly MidConversationEffortTransition[]): {
11
+ applied: boolean;
12
+ inserted: number;
13
+ };
@@ -0,0 +1,89 @@
1
+ import { isClaudeFable51Model } from './models.js';
2
+ export const MID_CONVERSATION_OUTPUT_CONFIG_BETA = 'mid-conversation-output-config-2026-07-01';
3
+ function isRecord(value) {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ }
6
+ export function normalizeAdaptiveEffort(value) {
7
+ if (value === 'minimal')
8
+ return 'low';
9
+ if (value === 'low' ||
10
+ value === 'medium' ||
11
+ value === 'high' ||
12
+ value === 'xhigh' ||
13
+ value === 'max') {
14
+ return value;
15
+ }
16
+ return undefined;
17
+ }
18
+ export function usesMidConversationOutputConfig(body) {
19
+ return isClaudeFable51Model(body.model);
20
+ }
21
+ function compactTransitions(transitions) {
22
+ const compacted = [];
23
+ for (const transition of transitions) {
24
+ if (!Number.isSafeInteger(transition.afterAssistantMessages) ||
25
+ transition.afterAssistantMessages < 0 ||
26
+ !normalizeAdaptiveEffort(transition.effort)) {
27
+ continue;
28
+ }
29
+ const normalized = {
30
+ afterAssistantMessages: transition.afterAssistantMessages,
31
+ effort: transition.effort,
32
+ };
33
+ const previous = compacted.at(-1);
34
+ if (previous &&
35
+ previous.afterAssistantMessages === normalized.afterAssistantMessages) {
36
+ previous.effort = normalized.effort;
37
+ continue;
38
+ }
39
+ if (previous?.effort === normalized.effort)
40
+ continue;
41
+ compacted.push(normalized);
42
+ }
43
+ return compacted;
44
+ }
45
+ export function applyMidConversationOutputConfig(body, transitions) {
46
+ if (!usesMidConversationOutputConfig(body) || !Array.isArray(body.messages)) {
47
+ return { applied: false, inserted: 0 };
48
+ }
49
+ const compacted = compactTransitions(transitions);
50
+ const currentOutputConfig = isRecord(body.output_config)
51
+ ? body.output_config
52
+ : {};
53
+ const baseline = compacted[0]?.effort ??
54
+ normalizeAdaptiveEffort(currentOutputConfig.effort) ??
55
+ 'high';
56
+ body.output_config = { ...currentOutputConfig, effort: baseline };
57
+ if (compacted.length < 2)
58
+ return { applied: true, inserted: 0 };
59
+ const messages = body.messages;
60
+ const transitionsByBoundary = new Map();
61
+ for (const transition of compacted.slice(1)) {
62
+ transitionsByBoundary.set(transition.afterAssistantMessages, transition.effort);
63
+ }
64
+ let assistantMessages = 0;
65
+ let activeEffort = baseline;
66
+ let inserted = 0;
67
+ const rewritten = [];
68
+ for (const message of messages) {
69
+ if (isRecord(message) &&
70
+ message.role === 'user' &&
71
+ transitionsByBoundary.has(assistantMessages)) {
72
+ const effort = transitionsByBoundary.get(assistantMessages);
73
+ if (effort && effort !== activeEffort) {
74
+ rewritten.push({
75
+ role: 'system',
76
+ content: [],
77
+ output_config: { effort },
78
+ });
79
+ activeEffort = effort;
80
+ inserted++;
81
+ }
82
+ }
83
+ rewritten.push(message);
84
+ if (isRecord(message) && message.role === 'assistant')
85
+ assistantMessages++;
86
+ }
87
+ body.messages = rewritten;
88
+ return { applied: true, inserted };
89
+ }
package/dist/models.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export declare const CLAUDE_FABLE_5_MODEL_ID = "claude-fable-5";
2
2
  export declare const CLAUDE_MYTHOS_5_MODEL_ID = "claude-mythos-5";
3
+ export declare const CLAUDE_FABLE_5_1_MODEL_ID = "claude-fable-5-1";
4
+ export declare const CLAUDE_MYTHOS_5_1_MODEL_ID = "claude-mythos-5-1";
3
5
  /**
4
6
  * Haiku 4.5 model identifier used by `/claude-prime` to start each OAuth
5
7
  * account's five-hour quota window with a minimal request. Usage is measured
@@ -16,7 +18,8 @@ export declare const CLAUDE_HAIKU_4_5_PRICING: {
16
18
  readonly input: 1;
17
19
  readonly output: 5;
18
20
  };
19
- export declare const CLAUDE_FABLE_MYTHOS_5_MODEL_IDS: readonly ["claude-fable-5", "claude-mythos-5"];
21
+ export declare const CLAUDE_FABLE_MYTHOS_5_1_MODEL_IDS: readonly ["claude-fable-5-1", "claude-mythos-5-1"];
22
+ export declare const CLAUDE_FABLE_MYTHOS_5_MODEL_IDS: readonly ["claude-fable-5", "claude-mythos-5", "claude-fable-5-1", "claude-mythos-5-1"];
20
23
  export type ClaudeFableMythos5ModelId = (typeof CLAUDE_FABLE_MYTHOS_5_MODEL_IDS)[number];
21
24
  export declare const CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING: {
22
25
  readonly type: 'adaptive';
@@ -29,15 +32,38 @@ export declare const CLAUDE_FABLE_MYTHOS_5_PRICING: {
29
32
  readonly cacheWrite5m: 12.5;
30
33
  readonly cacheWrite1h: 20;
31
34
  };
35
+ export declare const CLAUDE_FABLE_MYTHOS_5_1_PRICING: {
36
+ readonly input: 10;
37
+ readonly output: 50;
38
+ readonly cacheRead: 0.25;
39
+ readonly cacheWrite5m: 12.5;
40
+ readonly cacheWrite1h: 20;
41
+ };
32
42
  export declare const CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW = 1000000;
33
43
  export declare const CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS = 128000;
34
44
  export declare const CLAUDE_FABLE_MYTHOS_5_RELEASE_DATE = "2026-06-09";
45
+ export declare const CLAUDE_FABLE_MYTHOS_5_1_RELEASE_DATE = "2026-09-01";
35
46
  export declare const CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS: Record<ClaudeFableMythos5ModelId, {
36
47
  id: ClaudeFableMythos5ModelId;
37
48
  name: string;
38
49
  limited?: boolean;
39
50
  }>;
51
+ export declare const CLAUDE_FABLE_MYTHOS_5_1_MODEL_SPECS: {
52
+ "claude-fable-5-1": {
53
+ id: ClaudeFableMythos5ModelId;
54
+ name: string;
55
+ limited?: boolean;
56
+ };
57
+ "claude-mythos-5-1": {
58
+ id: ClaudeFableMythos5ModelId;
59
+ name: string;
60
+ limited?: boolean;
61
+ };
62
+ };
40
63
  export declare function isClaudeFableOrMythos5Model(model: unknown): boolean;
64
+ export declare function isClaudeFableOrMythos51Model(model: unknown): boolean;
65
+ export declare function isClaudeFable51Model(model: unknown): boolean;
66
+ export declare function getClaudeFableMythos5ReleaseDate(model: unknown): "2026-06-09" | "2026-09-01";
41
67
  export declare const CLAUDE_SONNET_5_MODEL_ID = "claude-sonnet-5";
42
68
  /**
43
69
  * Sonnet 5 enables adaptive thinking by default but ships `display: "omitted"`,
package/dist/models.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export const CLAUDE_FABLE_5_MODEL_ID = 'claude-fable-5';
2
2
  export const CLAUDE_MYTHOS_5_MODEL_ID = 'claude-mythos-5';
3
+ export const CLAUDE_FABLE_5_1_MODEL_ID = 'claude-fable-5-1';
4
+ export const CLAUDE_MYTHOS_5_1_MODEL_ID = 'claude-mythos-5-1';
3
5
  /**
4
6
  * Haiku 4.5 model identifier used by `/claude-prime` to start each OAuth
5
7
  * account's five-hour quota window with a minimal request. Usage is measured
@@ -16,9 +18,14 @@ export const CLAUDE_HAIKU_4_5_PRICING = {
16
18
  input: 1,
17
19
  output: 5,
18
20
  };
21
+ export const CLAUDE_FABLE_MYTHOS_5_1_MODEL_IDS = [
22
+ CLAUDE_FABLE_5_1_MODEL_ID,
23
+ CLAUDE_MYTHOS_5_1_MODEL_ID,
24
+ ];
19
25
  export const CLAUDE_FABLE_MYTHOS_5_MODEL_IDS = [
20
26
  CLAUDE_FABLE_5_MODEL_ID,
21
27
  CLAUDE_MYTHOS_5_MODEL_ID,
28
+ ...CLAUDE_FABLE_MYTHOS_5_1_MODEL_IDS,
22
29
  ];
23
30
  export const CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING = {
24
31
  type: 'adaptive',
@@ -31,9 +38,17 @@ export const CLAUDE_FABLE_MYTHOS_5_PRICING = {
31
38
  cacheWrite5m: 12.5,
32
39
  cacheWrite1h: 20,
33
40
  };
41
+ export const CLAUDE_FABLE_MYTHOS_5_1_PRICING = {
42
+ input: 10,
43
+ output: 50,
44
+ cacheRead: 0.25,
45
+ cacheWrite5m: 12.5,
46
+ cacheWrite1h: 20,
47
+ };
34
48
  export const CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW = 1_000_000;
35
49
  export const CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS = 128_000;
36
50
  export const CLAUDE_FABLE_MYTHOS_5_RELEASE_DATE = '2026-06-09';
51
+ export const CLAUDE_FABLE_MYTHOS_5_1_RELEASE_DATE = '2026-09-01';
37
52
  export const CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS = {
38
53
  [CLAUDE_FABLE_5_MODEL_ID]: {
39
54
  id: CLAUDE_FABLE_5_MODEL_ID,
@@ -44,11 +59,39 @@ export const CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS = {
44
59
  name: 'Claude Mythos 5',
45
60
  limited: true,
46
61
  },
62
+ [CLAUDE_FABLE_5_1_MODEL_ID]: {
63
+ id: CLAUDE_FABLE_5_1_MODEL_ID,
64
+ name: 'Claude Fable 5.1',
65
+ },
66
+ [CLAUDE_MYTHOS_5_1_MODEL_ID]: {
67
+ id: CLAUDE_MYTHOS_5_1_MODEL_ID,
68
+ name: 'Claude Mythos 5.1',
69
+ limited: true,
70
+ },
71
+ };
72
+ export const CLAUDE_FABLE_MYTHOS_5_1_MODEL_SPECS = {
73
+ [CLAUDE_FABLE_5_1_MODEL_ID]: CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS[CLAUDE_FABLE_5_1_MODEL_ID],
74
+ [CLAUDE_MYTHOS_5_1_MODEL_ID]: CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS[CLAUDE_MYTHOS_5_1_MODEL_ID],
47
75
  };
48
76
  export function isClaudeFableOrMythos5Model(model) {
49
77
  return (typeof model === 'string' &&
50
78
  CLAUDE_FABLE_MYTHOS_5_MODEL_IDS.some((id) => model === id || model.startsWith(`${id}-`)));
51
79
  }
80
+ export function isClaudeFableOrMythos51Model(model) {
81
+ return (typeof model === 'string' &&
82
+ CLAUDE_FABLE_MYTHOS_5_1_MODEL_IDS.some((id) => model === id || model.startsWith(`${id}-`)));
83
+ }
84
+ export function isClaudeFable51Model(model) {
85
+ if (typeof model !== 'string')
86
+ return false;
87
+ return (model === CLAUDE_FABLE_5_1_MODEL_ID ||
88
+ model.startsWith(`${CLAUDE_FABLE_5_1_MODEL_ID}-`));
89
+ }
90
+ export function getClaudeFableMythos5ReleaseDate(model) {
91
+ return isClaudeFableOrMythos51Model(model)
92
+ ? CLAUDE_FABLE_MYTHOS_5_1_RELEASE_DATE
93
+ : CLAUDE_FABLE_MYTHOS_5_RELEASE_DATE;
94
+ }
52
95
  export const CLAUDE_SONNET_5_MODEL_ID = 'claude-sonnet-5';
53
96
  /**
54
97
  * Sonnet 5 enables adaptive thinking by default but ships `display: "omitted"`,
@@ -0,0 +1 @@
1
+ export declare function isTransientNetworkError(error: unknown): boolean;
@@ -0,0 +1,27 @@
1
+ const TRANSIENT_NETWORK_ERROR_CODES = new Set([
2
+ 'EAI_AGAIN',
3
+ 'ECONNREFUSED',
4
+ 'ECONNRESET',
5
+ 'EHOSTUNREACH',
6
+ 'ENETUNREACH',
7
+ 'ENOTFOUND',
8
+ 'ETIMEDOUT',
9
+ 'UND_ERR_CONNECT_TIMEOUT',
10
+ ]);
11
+ function errorField(error, field) {
12
+ if (!error || typeof error !== 'object')
13
+ return undefined;
14
+ const value = error[field];
15
+ return typeof value === 'string' ? value : undefined;
16
+ }
17
+ export function isTransientNetworkError(error) {
18
+ const code = errorField(error, 'code');
19
+ if (code && TRANSIENT_NETWORK_ERROR_CODES.has(code))
20
+ return true;
21
+ const message = error instanceof Error
22
+ ? error.message
23
+ : (errorField(error, 'message') ?? String(error));
24
+ if (message.includes('fetch failed'))
25
+ return true;
26
+ return [...TRANSIENT_NETWORK_ERROR_CODES].some((candidate) => message.includes(candidate));
27
+ }
@@ -1,5 +1,5 @@
1
- import { type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
2
- export declare const QUOTA_HEADER_FEED_SCHEMA_VERSION = 2;
1
+ import { type AccountStorage, type OAuthQuotaSnapshot, type QuotaFieldName, type QuotaFieldSource, type QuotaFieldSources } from './accounts.ts';
2
+ export declare const QUOTA_HEADER_FEED_SCHEMA_VERSION = 3;
3
3
  export declare const QUOTA_HEADER_FEED_LEASE_MS = 180000;
4
4
  export type QuotaHeaderFeedIdentity = {
5
5
  identity_source: 'credential_id';
@@ -10,12 +10,25 @@ export type QuotaHeaderFeedIdentity = {
10
10
  } | {
11
11
  identity_source: 'none';
12
12
  };
13
- export type QuotaHeaderFeedEntry = QuotaHeaderFeedIdentity & {
13
+ export type QuotaHeaderFeedProvenance = Partial<Record<QuotaFieldName, QuotaFieldSource>>;
14
+ type QuotaHeaderFeedQuota = Pick<OAuthQuotaSnapshot, 'five_hour' | 'seven_day' | 'bindingWindow' | 'fallbackAdvised' | 'scoped' | 'extraUsage'> & {
15
+ provenance?: QuotaHeaderFeedProvenance;
16
+ };
17
+ type QuotaHeaderFeedMetadata = {
14
18
  schema_version: typeof QUOTA_HEADER_FEED_SCHEMA_VERSION;
15
19
  provider: 'anthropic';
16
20
  configured_account_count: number;
21
+ /** Header observation time; merged poll-owned fields retain their own checkedAt. */
17
22
  observed_at_ms: number;
18
- quota: Pick<OAuthQuotaSnapshot, 'five_hour' | 'seven_day' | 'bindingWindow' | 'fallbackAdvised' | 'scoped' | 'extraUsage'>;
23
+ };
24
+ export type QuotaHeaderFeedEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
25
+ quota: QuotaHeaderFeedQuota;
26
+ };
27
+ export type QuotaHeaderFeedPublishEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
28
+ quota: Omit<QuotaHeaderFeedQuota, 'provenance'> & {
29
+ fieldSources?: QuotaFieldSources;
30
+ };
31
+ accountKey: string;
19
32
  };
20
33
  export declare function getDefaultQuotaHeaderFeedDirectory(): string;
21
34
  export declare function configuredAnthropicOAuthAccountCount(input: {
@@ -32,9 +45,8 @@ export declare class QuotaHeaderFeedRegistry {
32
45
  leaseMs?: number;
33
46
  instanceId?: string;
34
47
  });
35
- publish(entry: QuotaHeaderFeedEntry & {
36
- accountKey: string;
37
- }): Promise<void>;
48
+ publish(entry: QuotaHeaderFeedPublishEntry): Promise<void>;
38
49
  list(): Promise<QuotaHeaderFeedEntry[]>;
39
50
  dispose(): Promise<void>;
40
51
  }
52
+ export {};
@@ -2,8 +2,8 @@ import { randomUUID } from 'node:crypto';
2
2
  import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { isOAuthAccount, } from './accounts.js';
6
- export const QUOTA_HEADER_FEED_SCHEMA_VERSION = 2;
5
+ import { isOAuthAccount, QUOTA_FIELD_NAMES, } from './accounts.js';
6
+ export const QUOTA_HEADER_FEED_SCHEMA_VERSION = 3;
7
7
  export const QUOTA_HEADER_FEED_LEASE_MS = 180_000;
8
8
  export function getDefaultQuotaHeaderFeedDirectory() {
9
9
  return (process.env.OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR ??
@@ -129,6 +129,20 @@ function projectExtraUsage(value) {
129
129
  exhausted: candidate.exhausted,
130
130
  };
131
131
  }
132
+ function projectQuotaProvenance(value, presentFields) {
133
+ if (!value || typeof value !== 'object')
134
+ return undefined;
135
+ const candidate = value;
136
+ const provenance = {};
137
+ for (const field of QUOTA_FIELD_NAMES) {
138
+ if (!presentFields.has(field))
139
+ continue;
140
+ const source = candidate[field];
141
+ if (source === 'poll' || source === 'headers')
142
+ provenance[field] = source;
143
+ }
144
+ return Object.keys(provenance).length > 0 ? provenance : undefined;
145
+ }
132
146
  function projectQuota(quota) {
133
147
  const fiveHour = projectQuotaWindow(quota.five_hour);
134
148
  const sevenDay = projectQuotaWindow(quota.seven_day);
@@ -138,6 +152,22 @@ function projectQuota(quota) {
138
152
  .filter((entry) => entry != null)
139
153
  : undefined;
140
154
  const extraUsage = projectExtraUsage(quota.extraUsage);
155
+ const presentFields = new Set();
156
+ if (fiveHour)
157
+ presentFields.add('five_hour');
158
+ if (sevenDay)
159
+ presentFields.add('seven_day');
160
+ if (typeof quota.bindingWindow === 'string') {
161
+ presentFields.add('bindingWindow');
162
+ }
163
+ if (typeof quota.fallbackAdvised === 'boolean') {
164
+ presentFields.add('fallbackAdvised');
165
+ }
166
+ if (scoped)
167
+ presentFields.add('scoped');
168
+ if (extraUsage)
169
+ presentFields.add('extraUsage');
170
+ const provenance = projectQuotaProvenance(quota.fieldSources, presentFields);
141
171
  return {
142
172
  ...(fiveHour && { five_hour: fiveHour }),
143
173
  ...(sevenDay && { seven_day: sevenDay }),
@@ -149,6 +179,7 @@ function projectQuota(quota) {
149
179
  }),
150
180
  ...(scoped && { scoped }),
151
181
  ...(extraUsage && { extraUsage }),
182
+ ...(provenance && { provenance }),
152
183
  };
153
184
  }
154
185
  export class QuotaHeaderFeedRegistry {
@@ -1,3 +1,4 @@
1
+ import { QUOTA_FIELD_NAMES, quotaFieldSource } from './accounts.js';
1
2
  const PREFIX = 'anthropic-ratelimit-unified-';
2
3
  const WINDOW_KEYS = {
3
4
  '5h': 'five_hour',
@@ -31,38 +32,90 @@ function normalizeWindow(headers, suffix, checkedAt) {
31
32
  };
32
33
  }
33
34
  export function normalizeQuotaHeaders(headers, now = Date.now()) {
35
+ const fieldSources = {};
36
+ const fallbackHeader = headers.get(`${PREFIX}fallback`);
34
37
  const snapshot = {
35
- fallbackAdvised: headers.get(`${PREFIX}fallback`) === 'available',
38
+ fallbackAdvised: fallbackHeader === 'available',
36
39
  source: 'headers',
37
40
  checkedAt: now,
38
41
  };
39
42
  for (const [suffix, key] of Object.entries(WINDOW_KEYS)) {
40
43
  const window = normalizeWindow(headers, suffix, now);
41
- if (window)
44
+ if (window) {
42
45
  snapshot[key] = window;
46
+ fieldSources[key] = 'headers';
47
+ }
43
48
  }
44
49
  const representativeClaim = headers.get(`${PREFIX}representative-claim`);
45
50
  if (representativeClaim) {
46
51
  snapshot.bindingWindow = representativeClaim;
47
52
  snapshot.bindingWindowSource = 'headers';
53
+ fieldSources.bindingWindow = 'headers';
48
54
  }
49
- return snapshot;
55
+ if (fallbackHeader !== null)
56
+ fieldSources.fallbackAdvised = 'headers';
57
+ return {
58
+ ...snapshot,
59
+ fieldSources,
60
+ };
50
61
  }
51
62
  export function mergeHeaderQuotaSnapshot(existing, incoming) {
52
- const { scoped: existingScoped, extraUsage: existingExtraUsage, ...existingWithoutPollOwnedFields } = existing ?? {};
53
- const { scoped: _incomingScoped, extraUsage: _incomingExtraUsage, ...incomingWithoutPollOwnedFields } = incoming;
63
+ const { scoped: existingScoped, extraUsage: existingExtraUsage, fieldSources: existingFieldSources, ...existingWithoutPollOwnedFields } = existing ?? {};
64
+ const { scoped: _incomingScoped, extraUsage: _incomingExtraUsage, fieldSources: incomingFieldSources, ...incomingWithoutPollOwnedFields } = incoming;
65
+ const fieldSources = {};
66
+ for (const field of QUOTA_FIELD_NAMES) {
67
+ const value = field === 'scoped'
68
+ ? existingScoped
69
+ : field === 'extraUsage'
70
+ ? existingExtraUsage
71
+ : field === 'fallbackAdvised'
72
+ ? incomingFieldSources?.fallbackAdvised !== undefined
73
+ ? incoming.fallbackAdvised
74
+ : (existing?.fallbackAdvised ?? incoming.fallbackAdvised)
75
+ : field === 'bindingWindow'
76
+ ? existing?.bindingWindowSource === 'poll'
77
+ ? existing?.bindingWindow
78
+ : (incoming.bindingWindow ?? existing?.bindingWindow)
79
+ : (incoming[field] ?? existing?.[field]);
80
+ if (value === undefined)
81
+ continue;
82
+ const source = field === 'fallbackAdvised'
83
+ ? (incomingFieldSources?.fallbackAdvised ??
84
+ (existing?.fallbackAdvised !== undefined
85
+ ? existingFieldSources
86
+ ? existingFieldSources.fallbackAdvised
87
+ : quotaFieldSource(existing, field)
88
+ : undefined))
89
+ : (field === 'scoped' || field === 'extraUsage') && value !== undefined
90
+ ? 'poll'
91
+ : field === 'bindingWindow' &&
92
+ existing?.bindingWindowSource === 'poll' &&
93
+ existing.bindingWindow !== undefined
94
+ ? 'poll'
95
+ : incoming[field] !== undefined
96
+ ? (incomingFieldSources?.[field] ??
97
+ quotaFieldSource(incoming, field))
98
+ : (existingFieldSources?.[field] ??
99
+ quotaFieldSource(existing, field));
100
+ if (source)
101
+ fieldSources[field] = source;
102
+ }
54
103
  return {
55
104
  ...existingWithoutPollOwnedFields,
56
105
  ...incomingWithoutPollOwnedFields,
57
106
  ...(Array.isArray(existingScoped) && { scoped: existingScoped }),
58
107
  ...(existingExtraUsage != null && { extraUsage: existingExtraUsage }),
108
+ fallbackAdvised: incomingFieldSources?.fallbackAdvised !== undefined
109
+ ? incoming.fallbackAdvised
110
+ : (existing?.fallbackAdvised ?? incoming.fallbackAdvised),
59
111
  bindingWindow: existing?.bindingWindowSource === 'poll'
60
112
  ? existing.bindingWindow
61
113
  : (incoming.bindingWindow ?? existing?.bindingWindow),
62
114
  bindingWindowSource: existing?.bindingWindowSource === 'poll'
63
115
  ? 'poll'
64
116
  : (incoming.bindingWindowSource ?? existing?.bindingWindowSource),
65
- // Source tracks 5h/7d freshness; preserved scoped and credit fields remain poll-owned.
117
+ ...(Object.keys(fieldSources).length > 0 && { fieldSources }),
118
+ // Top-level source remains headers for compatibility; fieldSources records ownership after partial harvests.
66
119
  source: 'headers',
67
120
  };
68
121
  }
@@ -23,6 +23,8 @@ export type QuotaManagerOptions = {
23
23
  now?: () => number;
24
24
  onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number, quotaErrorGeneration: number, quotaErrorClearedAt: number | undefined) => void;
25
25
  onApiError?: (error: AccountOperationError, quotaErrorGeneration: number) => void;
26
+ onFallbackQuotaFetched?: (accountId: string, quota: OAuthQuotaSnapshot, checkedAt: number, fetchStartedAt: number) => void;
27
+ onFallbackApiError?: (accountId: string, error: AccountOperationError) => void;
26
28
  };
27
29
  export declare class QuotaManager {
28
30
  private main;
@@ -47,6 +49,8 @@ export declare class QuotaManager {
47
49
  private readonly now;
48
50
  private readonly onMainQuotaFetched;
49
51
  private readonly onApiError;
52
+ private readonly onFallbackQuotaFetched;
53
+ private readonly onFallbackApiError;
50
54
  constructor(opts: QuotaManagerOptions);
51
55
  /**
52
56
  * Cached main quota entry, scoped to the account behind the main credential.
@@ -69,7 +73,7 @@ export declare class QuotaManager {
69
73
  refreshMainWithMetadata(mainAccountIdOrAccessToken: string | undefined, accessToken?: string, expectedIdentityGeneration?: number): Promise<QuotaRefreshResult>;
70
74
  refreshFallback(accountId: string, accessToken: string, account: Pick<OAuthAccount, 'authLineageId'> | undefined): Promise<OAuthQuotaSnapshot>;
71
75
  refreshFallbackWithMetadata(accountId: string, accessToken: string, account: Pick<OAuthAccount, 'authLineageId'> | undefined): Promise<QuotaRefreshResult>;
72
- refreshAllFallbacks(accounts: OAuthAccount[]): Promise<void>;
76
+ refreshAllFallbacks(accounts: OAuthAccount[], resolveAccessToken?: (account: OAuthAccount) => string | undefined): Promise<void>;
73
77
  /**
74
78
  * Fire-and-forget refresh. Does not await, swallows errors.
75
79
  */
@@ -65,12 +65,16 @@ export class QuotaManager {
65
65
  now;
66
66
  onMainQuotaFetched;
67
67
  onApiError;
68
+ onFallbackQuotaFetched;
69
+ onFallbackApiError;
68
70
  constructor(opts) {
69
71
  this.storage = opts.storage;
70
72
  this.fetchImpl = opts.fetchImpl ?? fetch;
71
73
  this.now = opts.now ?? Date.now;
72
74
  this.onMainQuotaFetched = opts.onMainQuotaFetched;
73
75
  this.onApiError = opts.onApiError;
76
+ this.onFallbackQuotaFetched = opts.onFallbackQuotaFetched;
77
+ this.onFallbackApiError = opts.onFallbackApiError;
74
78
  this.seedMainFromStorage(opts.storage, opts.storage?.mainAccountId);
75
79
  this.seedMainBackoffFromStorage(opts.storage);
76
80
  }
@@ -255,19 +259,25 @@ export class QuotaManager {
255
259
  });
256
260
  return promise;
257
261
  }
258
- async refreshAllFallbacks(accounts) {
262
+ async refreshAllFallbacks(accounts, resolveAccessToken) {
259
263
  const now = this.now();
260
264
  for (const account of accounts) {
261
265
  if (account.enabled === false)
262
266
  continue;
263
- if (!account.access)
267
+ const accessToken = resolveAccessToken?.(account) ??
268
+ (account.access &&
269
+ account.expires !== undefined &&
270
+ account.expires > now
271
+ ? account.access
272
+ : undefined);
273
+ if (!accessToken)
264
274
  continue;
265
275
  this.bindFallbackLineage(account.id, account);
266
276
  const cached = this.getFallback(account.id);
267
277
  if (cached && now < cached.refreshAfter)
268
278
  continue;
269
279
  try {
270
- await this.refreshFallback(account.id, account.access, account);
280
+ await this.refreshFallback(account.id, accessToken, account);
271
281
  }
272
282
  catch {
273
283
  // Best-effort — keep stale cache entry if fetch fails
@@ -462,6 +472,18 @@ export class QuotaManager {
462
472
  continue;
463
473
  if (account.enabled === false)
464
474
  continue;
475
+ const persistedError = account.lastQuotaRefreshError;
476
+ const persistedClearAt = quotaSnapshotCheckedAt(account.quota);
477
+ const currentError = this.fallbackApiErrors.get(account.id);
478
+ if (persistedError &&
479
+ quotaBackoffActive(persistedError, this.now()) &&
480
+ persistedClearAt <= (persistedError.checkedAt ?? 0) &&
481
+ (!currentError || persistedError.checkedAt >= currentError.checkedAt)) {
482
+ this.fallbackApiErrors.set(account.id, persistedError);
483
+ }
484
+ else if (currentError && persistedClearAt >= currentError.checkedAt) {
485
+ this.fallbackApiErrors.delete(account.id);
486
+ }
465
487
  if (!account.quota)
466
488
  continue;
467
489
  const checkedAt = quotaSnapshotCheckedAt(account.quota);
@@ -696,6 +718,7 @@ export class QuotaManager {
696
718
  throw new Error('Quota refresh is already in progress');
697
719
  }
698
720
  try {
721
+ const fetchStartedAt = this.now();
699
722
  const quota = await fetchOAuthQuotaSnapshot({
700
723
  accessToken,
701
724
  fetchImpl: this.fetchImpl,
@@ -715,6 +738,7 @@ export class QuotaManager {
715
738
  checkedAt: completedQuota.checkedAt ?? now,
716
739
  }, account);
717
740
  this.fallbackApiErrors.delete(accountId);
741
+ this.onFallbackQuotaFetched?.(accountId, completedQuota, now, fetchStartedAt);
718
742
  return { quota: completedQuota, fetched: true };
719
743
  }
720
744
  finally {
@@ -769,11 +793,13 @@ export class QuotaManager {
769
793
  if (QuotaManager.isAuthError(error))
770
794
  return;
771
795
  const previous = this.fallbackApiErrors.get(accountId);
772
- this.fallbackApiErrors.set(accountId, buildQuotaOperationError({
796
+ const quotaError = buildQuotaOperationError({
773
797
  error,
774
798
  now: this.now(),
775
799
  previous,
776
800
  accountIdentity: accountId,
777
- }));
801
+ });
802
+ this.fallbackApiErrors.set(accountId, quotaError);
803
+ this.onFallbackApiError?.(accountId, quotaError);
778
804
  }
779
805
  }
@@ -0,0 +1,7 @@
1
+ import type { AccountStorage } from './accounts.ts';
2
+ export declare const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
3
+ export type ThinkingPrefixMismatchBehavior = 'account-default' | 'error' | 'drop_block';
4
+ export declare function getThinkingPrefixMismatchBehavior(storage: Pick<AccountStorage, 'thinkingBinding'> | null | undefined): ThinkingPrefixMismatchBehavior;
5
+ export declare function hasReplayableThinkingBlocks(body: Record<string, unknown>): boolean;
6
+ export declare function applyThinkingBindingControls(body: Record<string, unknown>, behavior?: ThinkingPrefixMismatchBehavior): boolean;
7
+ export declare function hasThinkingBindingControls(body: Record<string, unknown>): boolean;