@cortexkit/anthropic-auth-core 1.22.0 → 1.23.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/accounts.d.ts +59 -2
- package/dist/accounts.js +666 -115
- package/dist/auth.js +3 -0
- package/dist/cachekeep.d.ts +18 -1
- package/dist/cachekeep.js +108 -82
- package/dist/cch.d.ts +11 -1
- package/dist/cch.js +48 -3
- package/dist/claude-code.d.ts +7 -1
- package/dist/claude-code.js +18 -3
- package/dist/claustrum-enrollment-registry.d.ts +17 -0
- package/dist/claustrum-enrollment-registry.js +194 -0
- package/dist/claustrum-enrollment.d.ts +70 -0
- package/dist/claustrum-enrollment.js +485 -0
- package/dist/claustrum-scoped-roster.d.ts +23 -0
- package/dist/claustrum-scoped-roster.js +194 -0
- package/dist/claustrum-scoped-runtime.d.ts +28 -0
- package/dist/claustrum-scoped-runtime.js +230 -0
- package/dist/claustrum-scoped.d.ts +39 -0
- package/dist/claustrum-scoped.js +193 -0
- package/dist/claustrum.d.ts +149 -6
- package/dist/claustrum.js +990 -48
- package/dist/commands/account.d.ts +40 -12
- package/dist/commands/account.js +138 -9
- package/dist/constants.d.ts +3 -2
- package/dist/constants.js +3 -2
- package/dist/custom-headers.d.ts +3 -0
- package/dist/custom-headers.js +102 -0
- package/dist/fast.js +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/logger.js +3 -1
- package/dist/model-remap.d.ts +27 -0
- package/dist/model-remap.js +72 -0
- package/dist/models.d.ts +19 -0
- package/dist/models.js +48 -12
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/oauth-profile.js +6 -0
- package/dist/prime.d.ts +11 -3
- package/dist/prime.js +25 -7
- package/dist/quota-header-feed.d.ts +13 -0
- package/dist/quota-header-feed.js +60 -8
- package/dist/quota-manager.d.ts +15 -1
- package/dist/quota-manager.js +73 -22
- package/dist/relay.d.ts +8 -0
- package/dist/relay.js +72 -12
- package/dist/sticky-routing.d.ts +2 -0
- package/dist/sticky-routing.js +19 -1
- package/package.json +3 -2
package/dist/quota-manager.js
CHANGED
|
@@ -51,6 +51,8 @@ export class QuotaManager {
|
|
|
51
51
|
// --- Inflight deduplication ---
|
|
52
52
|
inflightMain = null;
|
|
53
53
|
inflightMainAccountId;
|
|
54
|
+
inflightMainAccessToken;
|
|
55
|
+
inflightMainToken = 0;
|
|
54
56
|
inflightFallbacks = new Map();
|
|
55
57
|
// --- Rate-limiting (scoped per route so a fallback 429 never backs off the
|
|
56
58
|
// main account or vice versa) ---
|
|
@@ -62,6 +64,7 @@ export class QuotaManager {
|
|
|
62
64
|
// --- Config ---
|
|
63
65
|
storage;
|
|
64
66
|
fetchImpl;
|
|
67
|
+
fetchQuotaSnapshot;
|
|
65
68
|
now;
|
|
66
69
|
onMainQuotaFetched;
|
|
67
70
|
onApiError;
|
|
@@ -70,6 +73,7 @@ export class QuotaManager {
|
|
|
70
73
|
constructor(opts) {
|
|
71
74
|
this.storage = opts.storage;
|
|
72
75
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
76
|
+
this.fetchQuotaSnapshot = opts.fetchQuotaSnapshot;
|
|
73
77
|
this.now = opts.now ?? Date.now;
|
|
74
78
|
this.onMainQuotaFetched = opts.onMainQuotaFetched;
|
|
75
79
|
this.onApiError = opts.onApiError;
|
|
@@ -103,6 +107,12 @@ export class QuotaManager {
|
|
|
103
107
|
this.bindFallbackLineage(accountId, account);
|
|
104
108
|
return this.fallbacks.get(accountId) ?? null;
|
|
105
109
|
}
|
|
110
|
+
/** A host transport resolves scoped credentials at dispatch time. */
|
|
111
|
+
canFetchWithoutAccessToken() {
|
|
112
|
+
return Boolean(this.fetchQuotaSnapshot &&
|
|
113
|
+
(this.storage === null ||
|
|
114
|
+
this.storage?.claustrum?.scopedRoster === true));
|
|
115
|
+
}
|
|
106
116
|
getAllFallbacks() {
|
|
107
117
|
return this.fallbacks;
|
|
108
118
|
}
|
|
@@ -113,11 +123,16 @@ export class QuotaManager {
|
|
|
113
123
|
this.mainAccountId = mainAccountId;
|
|
114
124
|
this.main = entry;
|
|
115
125
|
}
|
|
126
|
+
clearMain() {
|
|
127
|
+
this.mainAccountId = undefined;
|
|
128
|
+
this.main = null;
|
|
129
|
+
}
|
|
116
130
|
/** Bind quota state to the account behind the current main credential. */
|
|
117
|
-
setMainQuotaAccountIdentity(accountIdentity, strict = true) {
|
|
131
|
+
setMainQuotaAccountIdentity(accountIdentity, strict = true, force = false) {
|
|
118
132
|
const previousIdentityKnown = this.mainQuotaIdentityKnown;
|
|
119
133
|
const previousAccountIdentity = this.mainAccountId;
|
|
120
|
-
const changed =
|
|
134
|
+
const changed = force ||
|
|
135
|
+
!previousIdentityKnown ||
|
|
121
136
|
this.mainAccountId !== accountIdentity ||
|
|
122
137
|
this.mainQuotaIdentityStrict !== strict;
|
|
123
138
|
this.mainQuotaIdentityKnown = true;
|
|
@@ -126,7 +141,13 @@ export class QuotaManager {
|
|
|
126
141
|
return undefined;
|
|
127
142
|
this.mainGeneration += 1;
|
|
128
143
|
this.mainAccountId = accountIdentity;
|
|
129
|
-
if (
|
|
144
|
+
if (force) {
|
|
145
|
+
this.main = null;
|
|
146
|
+
this.mainLastApiError = undefined;
|
|
147
|
+
this.mainQuotaErrorGeneration += 1;
|
|
148
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, this.now());
|
|
149
|
+
}
|
|
150
|
+
else if (accountIdentity !== undefined &&
|
|
130
151
|
this.main?.quota.accountIdentity !== accountIdentity) {
|
|
131
152
|
this.main = null;
|
|
132
153
|
}
|
|
@@ -195,14 +216,14 @@ export class QuotaManager {
|
|
|
195
216
|
async refreshMain(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
|
|
196
217
|
const mainAccountId = mainAccountIdOrAccessToken;
|
|
197
218
|
const credential = accessToken ?? mainAccountIdOrAccessToken;
|
|
198
|
-
if (!credential)
|
|
219
|
+
if (!credential && !this.canFetchWithoutAccessToken())
|
|
199
220
|
throw new Error('Main OAuth access token is unavailable');
|
|
200
221
|
return (await this.refreshMainWithMetadata(mainAccountId, credential, expectedIdentityGeneration)).quota;
|
|
201
222
|
}
|
|
202
223
|
async refreshMainWithMetadata(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
|
|
203
224
|
const effectiveAccountId = mainAccountIdOrAccessToken;
|
|
204
225
|
const credential = accessToken ?? mainAccountIdOrAccessToken;
|
|
205
|
-
if (!credential)
|
|
226
|
+
if (!credential && !this.canFetchWithoutAccessToken())
|
|
206
227
|
throw new Error('Main OAuth access token is unavailable');
|
|
207
228
|
if (expectedIdentityGeneration !== undefined &&
|
|
208
229
|
(this.mainGeneration !== expectedIdentityGeneration ||
|
|
@@ -215,7 +236,9 @@ export class QuotaManager {
|
|
|
215
236
|
if (this.mainQuotaIdentityKnown)
|
|
216
237
|
this.mainGeneration += 1;
|
|
217
238
|
}
|
|
218
|
-
if (this.inflightMain &&
|
|
239
|
+
if (this.inflightMain &&
|
|
240
|
+
this.inflightMainAccountId === effectiveAccountId &&
|
|
241
|
+
this.inflightMainAccessToken === credential)
|
|
219
242
|
return this.inflightMain;
|
|
220
243
|
// Rate-limit — if API recently 429'd, return stale or throw
|
|
221
244
|
if (this.isBackedOff()) {
|
|
@@ -225,14 +248,19 @@ export class QuotaManager {
|
|
|
225
248
|
throw new Error('Quota API rate-limited — try again later');
|
|
226
249
|
}
|
|
227
250
|
this.inflightMainAccountId = effectiveAccountId;
|
|
251
|
+
this.inflightMainAccessToken = credential;
|
|
228
252
|
const generation = this.mainGeneration;
|
|
229
|
-
|
|
253
|
+
const inflightToken = ++this.inflightMainToken;
|
|
254
|
+
this.inflightMain = this._fetchMain(effectiveAccountId, credential ?? '', generation, inflightToken);
|
|
230
255
|
return this.inflightMain;
|
|
231
256
|
}
|
|
232
257
|
async refreshFallback(accountId, accessToken, account) {
|
|
233
258
|
return (await this.refreshFallbackWithMetadata(accountId, accessToken, account)).quota;
|
|
234
259
|
}
|
|
235
260
|
async refreshFallbackWithMetadata(accountId, accessToken, account) {
|
|
261
|
+
if (!accessToken && !this.canFetchWithoutAccessToken()) {
|
|
262
|
+
throw new Error('Fallback OAuth access token is unavailable');
|
|
263
|
+
}
|
|
236
264
|
this.bindFallbackLineage(accountId, account);
|
|
237
265
|
const inflightKey = QuotaManager.fallbackInflightKey(accountId);
|
|
238
266
|
const inflight = this.inflightFallbacks.get(inflightKey);
|
|
@@ -264,12 +292,13 @@ export class QuotaManager {
|
|
|
264
292
|
for (const account of accounts) {
|
|
265
293
|
if (account.enabled === false)
|
|
266
294
|
continue;
|
|
267
|
-
const accessToken = resolveAccessToken
|
|
268
|
-
(account
|
|
295
|
+
const accessToken = resolveAccessToken !== undefined
|
|
296
|
+
? resolveAccessToken(account)
|
|
297
|
+
: account.access &&
|
|
269
298
|
account.expires !== undefined &&
|
|
270
299
|
account.expires > now
|
|
271
300
|
? account.access
|
|
272
|
-
: undefined
|
|
301
|
+
: undefined;
|
|
273
302
|
if (!accessToken)
|
|
274
303
|
continue;
|
|
275
304
|
this.bindFallbackLineage(account.id, account);
|
|
@@ -575,6 +604,12 @@ export class QuotaManager {
|
|
|
575
604
|
this.fallbackAuthLineages.set(accountId, observedLineage);
|
|
576
605
|
return true;
|
|
577
606
|
}
|
|
607
|
+
isClosed = false;
|
|
608
|
+
close() {
|
|
609
|
+
this.isClosed = true;
|
|
610
|
+
this.inflightFallbacks.clear();
|
|
611
|
+
this.inflightMain = null;
|
|
612
|
+
}
|
|
578
613
|
/**
|
|
579
614
|
* Serialize API calls through a shared gate so only one
|
|
580
615
|
* quota API request runs at a time, with a minimum gap
|
|
@@ -583,6 +618,8 @@ export class QuotaManager {
|
|
|
583
618
|
*/
|
|
584
619
|
_enqueueApiFetch(fn) {
|
|
585
620
|
const gatedFn = async () => {
|
|
621
|
+
if (this.isClosed)
|
|
622
|
+
throw new Error('QuotaManager is closed');
|
|
586
623
|
// Wait until minimum gap since last API call
|
|
587
624
|
const elapsed = this.now() - this.lastApiCallAt;
|
|
588
625
|
if (elapsed < QuotaManager.API_CALL_GAP_MS) {
|
|
@@ -592,6 +629,8 @@ export class QuotaManager {
|
|
|
592
629
|
id.unref();
|
|
593
630
|
});
|
|
594
631
|
}
|
|
632
|
+
if (this.isClosed)
|
|
633
|
+
throw new Error('QuotaManager is closed');
|
|
595
634
|
this.lastApiCallAt = this.now();
|
|
596
635
|
return fn();
|
|
597
636
|
};
|
|
@@ -599,7 +638,7 @@ export class QuotaManager {
|
|
|
599
638
|
this.apiGate = queued.catch(() => { });
|
|
600
639
|
return queued;
|
|
601
640
|
}
|
|
602
|
-
async _fetchMain(mainAccountId, accessToken, generation) {
|
|
641
|
+
async _fetchMain(mainAccountId, accessToken, generation, inflightToken) {
|
|
603
642
|
return this._enqueueApiFetch(async () => {
|
|
604
643
|
try {
|
|
605
644
|
// Re-check backoff inside gate — may have been set by
|
|
@@ -633,11 +672,17 @@ export class QuotaManager {
|
|
|
633
672
|
}
|
|
634
673
|
try {
|
|
635
674
|
const fetchStartedAt = this.now();
|
|
636
|
-
const quota =
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
675
|
+
const quota = this.fetchQuotaSnapshot
|
|
676
|
+
? await this.fetchQuotaSnapshot({
|
|
677
|
+
kind: 'main',
|
|
678
|
+
accountId: mainAccountId,
|
|
679
|
+
accessToken,
|
|
680
|
+
})
|
|
681
|
+
: await fetchOAuthQuotaSnapshot({
|
|
682
|
+
accessToken,
|
|
683
|
+
fetchImpl: this.fetchImpl,
|
|
684
|
+
now: this.now,
|
|
685
|
+
});
|
|
641
686
|
const now = this.now();
|
|
642
687
|
if (this.mainQuotaIdentityKnown &&
|
|
643
688
|
this.mainQuotaIdentityStrict &&
|
|
@@ -686,7 +731,7 @@ export class QuotaManager {
|
|
|
686
731
|
}
|
|
687
732
|
}
|
|
688
733
|
finally {
|
|
689
|
-
if (this.
|
|
734
|
+
if (this.inflightMainToken === inflightToken) {
|
|
690
735
|
this.inflightMain = null;
|
|
691
736
|
this.inflightMainAccountId = undefined;
|
|
692
737
|
}
|
|
@@ -719,11 +764,17 @@ export class QuotaManager {
|
|
|
719
764
|
}
|
|
720
765
|
try {
|
|
721
766
|
const fetchStartedAt = this.now();
|
|
722
|
-
const quota =
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
767
|
+
const quota = this.fetchQuotaSnapshot
|
|
768
|
+
? await this.fetchQuotaSnapshot({
|
|
769
|
+
kind: 'fallback',
|
|
770
|
+
accountId,
|
|
771
|
+
accessToken,
|
|
772
|
+
})
|
|
773
|
+
: await fetchOAuthQuotaSnapshot({
|
|
774
|
+
accessToken,
|
|
775
|
+
fetchImpl: this.fetchImpl,
|
|
776
|
+
now: this.now,
|
|
777
|
+
});
|
|
727
778
|
const now = this.now();
|
|
728
779
|
if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation) {
|
|
729
780
|
return {
|
package/dist/relay.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export type RelayPatch = {
|
|
|
11
11
|
deleteCount: number;
|
|
12
12
|
insert: string;
|
|
13
13
|
};
|
|
14
|
+
export type RelayAttemptAuthorization = {
|
|
15
|
+
headers: Headers;
|
|
16
|
+
/** Called only for a genuine upstream response, never optimistic/transport status. */
|
|
17
|
+
onUpstreamStatus?: (status: number) => void;
|
|
18
|
+
};
|
|
19
|
+
export type AuthorizeRelayAttempt = () => Promise<RelayAttemptAuthorization>;
|
|
14
20
|
export declare function getRelayConfig(storage: AccountStorage | null): RelayConfig | null;
|
|
15
21
|
export declare function generateRelayToken(): string;
|
|
16
22
|
export declare function hashBody(body: string): string;
|
|
@@ -24,6 +30,8 @@ export declare function sendViaRelay(options: {
|
|
|
24
30
|
fallback: () => Promise<Response>;
|
|
25
31
|
affinity?: string | null;
|
|
26
32
|
optimisticResponse?: boolean;
|
|
33
|
+
/** Fresh authorization for every physical relay attempt, including internal retries. */
|
|
34
|
+
authorizeAttempt?: AuthorizeRelayAttempt;
|
|
27
35
|
/**
|
|
28
36
|
* Observes genuine upstream headers once per delivered response. WebSocket
|
|
29
37
|
* retries before downstream bytes replace staged headers, so the callback
|
package/dist/relay.js
CHANGED
|
@@ -2,6 +2,28 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
3
|
import { dumpRelayRequest } from './dump.js';
|
|
4
4
|
import { relayLog } from './logger.js';
|
|
5
|
+
class RelayAuthorizationError extends Error {
|
|
6
|
+
failure;
|
|
7
|
+
constructor(failure) {
|
|
8
|
+
super('Relay request authorization failed');
|
|
9
|
+
this.failure = failure;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function authorizeRelayPayload(payload, authorize) {
|
|
13
|
+
if (!authorize)
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
const attempt = await authorize();
|
|
17
|
+
payload.upstream = {
|
|
18
|
+
...payload.upstream,
|
|
19
|
+
headers: jsonHeaders(attempt.headers),
|
|
20
|
+
};
|
|
21
|
+
return attempt;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
throw new RelayAuthorizationError(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
5
27
|
class RelayStateMismatchError extends Error {
|
|
6
28
|
}
|
|
7
29
|
class RelayWebSocketConnectionResetError extends Error {
|
|
@@ -229,15 +251,29 @@ function updateLocalRelayState(affinity, bodyText, nextHash, revision) {
|
|
|
229
251
|
cleanupRelaySessionCaches(now);
|
|
230
252
|
}
|
|
231
253
|
async function sendRelayHttp(options) {
|
|
232
|
-
const { config, payload, bodyText, fallback, signal } = options;
|
|
254
|
+
const { config, payload, bodyText, fallback, signal, authorizeAttempt } = options;
|
|
233
255
|
let actualPayload = payload;
|
|
256
|
+
let authorization = authorizeAttempt
|
|
257
|
+
? await authorizeRelayPayload(actualPayload, authorizeAttempt)
|
|
258
|
+
: undefined;
|
|
234
259
|
let response = await postRelay(config, actualPayload, signal);
|
|
235
260
|
if (response.status === 409 && actualPayload.mode === 'patch') {
|
|
236
261
|
relayLog(`state mismatch from relay session=${shortAffinity(actualPayload.affinity)}; retrying full_sync`);
|
|
237
262
|
await response.body?.cancel().catch(() => { });
|
|
238
263
|
actualPayload = createFullSyncPayload(actualPayload, bodyText);
|
|
264
|
+
authorization = authorizeAttempt
|
|
265
|
+
? await authorizeRelayPayload(actualPayload, authorizeAttempt)
|
|
266
|
+
: undefined;
|
|
239
267
|
response = await postRelay(config, actualPayload, signal);
|
|
240
268
|
}
|
|
269
|
+
// Old workers already forward Anthropic request IDs. Without upstream
|
|
270
|
+
// provenance a relay's own 401 must never invalidate an OAuth credential.
|
|
271
|
+
if (/^req_[A-Za-z0-9_-]+$/.test(response.headers.get('request-id') ?? '')) {
|
|
272
|
+
try {
|
|
273
|
+
authorization?.onUpstreamStatus?.(response.status);
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
}
|
|
241
277
|
if (!response.ok && response.status >= 500 && config.fallbackToDirect) {
|
|
242
278
|
relayLog(`relay returned ${response.status}; falling back direct session=${shortAffinity(actualPayload.affinity)}`);
|
|
243
279
|
await response.body?.cancel().catch(() => { });
|
|
@@ -325,12 +361,12 @@ class PersistentRelaySession {
|
|
|
325
361
|
this.serverState = undefined;
|
|
326
362
|
this.connecting = undefined;
|
|
327
363
|
}
|
|
328
|
-
send(payload, bodyText, optimisticResponse = false, onResponseHeaders) {
|
|
364
|
+
send(payload, bodyText, optimisticResponse = false, onResponseHeaders, authorizeAttempt) {
|
|
329
365
|
this.touch();
|
|
330
366
|
const enqueuedAt = perfNowMs();
|
|
331
367
|
const start = this.queue
|
|
332
368
|
.catch(() => { })
|
|
333
|
-
.then(() => this.startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders));
|
|
369
|
+
.then(() => this.startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders, authorizeAttempt));
|
|
334
370
|
const result = start.then(async ({ response, getPayload }) => ({
|
|
335
371
|
response: await response,
|
|
336
372
|
payload: getPayload(),
|
|
@@ -341,7 +377,7 @@ class PersistentRelaySession {
|
|
|
341
377
|
this.queue = start.then(({ done }) => done.catch(() => { }), () => { });
|
|
342
378
|
return result;
|
|
343
379
|
}
|
|
344
|
-
async startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders) {
|
|
380
|
+
async startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders, authorizeAttempt) {
|
|
345
381
|
const connectStart = perfNowMs();
|
|
346
382
|
await this.ensureConnected();
|
|
347
383
|
const connectedAt = perfNowMs();
|
|
@@ -364,11 +400,14 @@ class PersistentRelaySession {
|
|
|
364
400
|
requestPayload.patch = createRelayPatch(localState.body, bodyText);
|
|
365
401
|
}
|
|
366
402
|
relayLog(`perf websocket send_start session=${shortAffinity(this.affinity)} request=${requestPayload.id} mode=${requestPayload.mode} queueMs=${formatMs(connectedAt - enqueuedAt)} connectMs=${formatMs(connectedAt - connectStart)} bodyBytes=${bodyText.length} relayBytes=${JSON.stringify(requestPayload).length}`);
|
|
403
|
+
const authorization = authorizeAttempt
|
|
404
|
+
? await authorizeRelayPayload(requestPayload, authorizeAttempt)
|
|
405
|
+
: undefined;
|
|
367
406
|
let activePayload = requestPayload;
|
|
368
|
-
const first = this.sendPayload(requestPayload, bodyText, optimisticResponse, onResponseHeaders);
|
|
407
|
+
const first = this.sendPayload(requestPayload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, authorization);
|
|
369
408
|
void first.done.catch(() => { });
|
|
370
409
|
let activeDone = first.done;
|
|
371
|
-
const response = first.response.catch((error) => {
|
|
410
|
+
const response = first.response.catch(async (error) => {
|
|
372
411
|
if (!(error instanceof RelayStateMismatchError) ||
|
|
373
412
|
requestPayload.mode !== 'patch') {
|
|
374
413
|
throw error;
|
|
@@ -377,8 +416,11 @@ class PersistentRelaySession {
|
|
|
377
416
|
fullSync.protocol = 2;
|
|
378
417
|
fullSync.id = createRequestId();
|
|
379
418
|
fullSync.revision = (this.serverState?.revision ?? 0) + 1;
|
|
419
|
+
const retryAuthorization = authorizeAttempt
|
|
420
|
+
? await authorizeRelayPayload(fullSync, authorizeAttempt)
|
|
421
|
+
: undefined;
|
|
380
422
|
activePayload = fullSync;
|
|
381
|
-
const retry = this.sendPayload(fullSync, bodyText, optimisticResponse, onResponseHeaders);
|
|
423
|
+
const retry = this.sendPayload(fullSync, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, retryAuthorization);
|
|
382
424
|
void retry.done.catch(() => { });
|
|
383
425
|
activeDone = retry.done;
|
|
384
426
|
return retry.response;
|
|
@@ -459,7 +501,7 @@ class PersistentRelaySession {
|
|
|
459
501
|
});
|
|
460
502
|
await this.connecting;
|
|
461
503
|
}
|
|
462
|
-
sendPayload(payload, bodyText, optimisticResponse, onResponseHeaders) {
|
|
504
|
+
sendPayload(payload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, authorization) {
|
|
463
505
|
const socket = this.socket;
|
|
464
506
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
465
507
|
throw new Error('relay websocket is not connected');
|
|
@@ -489,6 +531,8 @@ class PersistentRelaySession {
|
|
|
489
531
|
retryAttempts: 0,
|
|
490
532
|
retryingBeforeResponse: false,
|
|
491
533
|
onResponseHeaders,
|
|
534
|
+
authorizeAttempt,
|
|
535
|
+
authorization,
|
|
492
536
|
stagedResponseHeaders: undefined,
|
|
493
537
|
responseHeadersDelivered: false,
|
|
494
538
|
};
|
|
@@ -541,6 +585,12 @@ class PersistentRelaySession {
|
|
|
541
585
|
retryPayload.protocol = 2;
|
|
542
586
|
retryPayload.id = createRequestId();
|
|
543
587
|
retryPayload.revision = (this.serverState?.revision ?? 0) + 1;
|
|
588
|
+
const authorization = pending.authorizeAttempt
|
|
589
|
+
? await authorizeRelayPayload(retryPayload, pending.authorizeAttempt)
|
|
590
|
+
: undefined;
|
|
591
|
+
if (this.pending !== pending || pending.streamDone)
|
|
592
|
+
return;
|
|
593
|
+
pending.authorization = authorization;
|
|
544
594
|
pending.payload = retryPayload;
|
|
545
595
|
pending.accepted = false;
|
|
546
596
|
pending.acceptedAt = undefined;
|
|
@@ -560,7 +610,7 @@ class PersistentRelaySession {
|
|
|
560
610
|
catch (error) {
|
|
561
611
|
pending.retryingBeforeResponse = false;
|
|
562
612
|
if (this.pending === pending)
|
|
563
|
-
this.failPending(error);
|
|
613
|
+
this.failPending(error instanceof RelayAuthorizationError ? error.failure : error);
|
|
564
614
|
}
|
|
565
615
|
})();
|
|
566
616
|
return true;
|
|
@@ -631,8 +681,12 @@ class PersistentRelaySession {
|
|
|
631
681
|
return;
|
|
632
682
|
const responseStartedAt = perfNowMs();
|
|
633
683
|
pending.responseStartedAt = responseStartedAt;
|
|
684
|
+
try {
|
|
685
|
+
pending.authorization?.onUpstreamStatus?.(message.status);
|
|
686
|
+
}
|
|
687
|
+
catch { }
|
|
634
688
|
relayLog(`perf websocket response_start session=${shortAffinity(this.affinity)} request=${pending.payload.id} sentMs=${formatMs(responseStartedAt - pending.sentAt)} upstreamMs=${pending.acceptedAt == null ? 'unknown' : formatMs(responseStartedAt - pending.acceptedAt)} status=${message.status}`);
|
|
635
|
-
clearTimeout(pending.timeout);
|
|
689
|
+
this.timers.clearTimeout(pending.timeout);
|
|
636
690
|
if (message.headers)
|
|
637
691
|
pending.stagedResponseHeaders = new Headers(message.headers);
|
|
638
692
|
this.resolvePendingResponse(pending, message.status, message.statusText, message.headers);
|
|
@@ -752,7 +806,7 @@ function getPersistentRelaySession(config, affinity, timers) {
|
|
|
752
806
|
return session;
|
|
753
807
|
}
|
|
754
808
|
export async function sendViaRelay(options) {
|
|
755
|
-
const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, dumpTag, onDumpCreated, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
|
|
809
|
+
const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, authorizeAttempt, onResponseHeaders, dumpTag, onDumpCreated, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
|
|
756
810
|
if (!config || !isRelayableAnthropicRequest(input, body))
|
|
757
811
|
return fallback();
|
|
758
812
|
const affinity = explicitAffinity ||
|
|
@@ -797,9 +851,11 @@ export async function sendViaRelay(options) {
|
|
|
797
851
|
setTimeout: setTimeoutImpl,
|
|
798
852
|
clearTimeout: clearTimeoutImpl,
|
|
799
853
|
});
|
|
800
|
-
result = await session.send(payload, bodyText, optimisticResponse === true, onResponseHeaders);
|
|
854
|
+
result = await session.send(payload, bodyText, optimisticResponse === true, onResponseHeaders, authorizeAttempt);
|
|
801
855
|
}
|
|
802
856
|
catch (error) {
|
|
857
|
+
if (error instanceof RelayAuthorizationError)
|
|
858
|
+
throw error;
|
|
803
859
|
relayLog(`websocket relay failed session=${shortAffinity(affinity)}; trying http relay: ${error instanceof Error ? error.message : String(error)}`);
|
|
804
860
|
result = await sendRelayHttp({
|
|
805
861
|
config,
|
|
@@ -807,6 +863,7 @@ export async function sendViaRelay(options) {
|
|
|
807
863
|
bodyText,
|
|
808
864
|
fallback,
|
|
809
865
|
signal: init?.signal,
|
|
866
|
+
authorizeAttempt,
|
|
810
867
|
});
|
|
811
868
|
}
|
|
812
869
|
}
|
|
@@ -817,6 +874,7 @@ export async function sendViaRelay(options) {
|
|
|
817
874
|
bodyText,
|
|
818
875
|
fallback,
|
|
819
876
|
signal: init?.signal,
|
|
877
|
+
authorizeAttempt,
|
|
820
878
|
});
|
|
821
879
|
}
|
|
822
880
|
relayPerfLog('send', {
|
|
@@ -873,6 +931,8 @@ export async function sendViaRelay(options) {
|
|
|
873
931
|
return result.response;
|
|
874
932
|
}
|
|
875
933
|
catch (error) {
|
|
934
|
+
if (error instanceof RelayAuthorizationError)
|
|
935
|
+
throw error.failure;
|
|
876
936
|
if (!config.fallbackToDirect) {
|
|
877
937
|
relayLog(`relay failed session=${shortAffinity(affinity)} and fallbackToDirect=false: ${error instanceof Error ? error.message : String(error)}`);
|
|
878
938
|
throw error;
|
package/dist/sticky-routing.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type StickyRouteCandidate = {
|
|
|
23
23
|
export type StickyRouteAssignment = {
|
|
24
24
|
accountId: string;
|
|
25
25
|
family: StickyRouteFamily;
|
|
26
|
+
affinityModelId?: string;
|
|
26
27
|
assignedAt: number;
|
|
27
28
|
lastSeenAt: number;
|
|
28
29
|
initialInputBytes: number;
|
|
@@ -99,6 +100,7 @@ export declare class StickySessionRouter {
|
|
|
99
100
|
sessionId: string;
|
|
100
101
|
family: StickyRouteFamily;
|
|
101
102
|
modelId?: string;
|
|
103
|
+
affinityModelId?: string;
|
|
102
104
|
candidates: readonly StickyRouteCandidate[];
|
|
103
105
|
retainAccountIds: ReadonlySet<string>;
|
|
104
106
|
storage: AccountStorage | null;
|
package/dist/sticky-routing.js
CHANGED
|
@@ -91,9 +91,16 @@ function normalizeAssignment(value) {
|
|
|
91
91
|
quotaCheckedAt === undefined) {
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
+
if (Object.hasOwn(value, 'affinityModelId') &&
|
|
95
|
+
typeof value.affinityModelId !== 'string') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
94
98
|
return {
|
|
95
99
|
accountId: value.accountId,
|
|
96
100
|
family: value.family,
|
|
101
|
+
affinityModelId: typeof value.affinityModelId === 'string'
|
|
102
|
+
? value.affinityModelId
|
|
103
|
+
: undefined,
|
|
97
104
|
assignedAt,
|
|
98
105
|
lastSeenAt,
|
|
99
106
|
initialInputBytes: Math.max(0, initialInputBytes),
|
|
@@ -429,9 +436,14 @@ export class StickySessionRouter {
|
|
|
429
436
|
return null;
|
|
430
437
|
await this.refreshStateIfChanged();
|
|
431
438
|
const key = sessionKey(input.sessionId);
|
|
439
|
+
const affinityModelId = input.affinityModelId ?? input.modelId;
|
|
440
|
+
const matchesAffinity = (assignment) => affinityModelId !== undefined
|
|
441
|
+
? assignment.affinityModelId === affinityModelId
|
|
442
|
+
: assignment.family === input.family;
|
|
432
443
|
const cached = this.state.assignments[key];
|
|
433
444
|
if (cached &&
|
|
434
445
|
this.assignmentIsActive(cached) &&
|
|
446
|
+
matchesAffinity(cached) &&
|
|
435
447
|
input.retainAccountIds.has(cached.accountId) &&
|
|
436
448
|
!input.excludeAccountIds?.has(cached.accountId)) {
|
|
437
449
|
if (this.now() - cached.lastSeenAt >= STICKY_ROUTING_TOUCH_INTERVAL_MS) {
|
|
@@ -451,6 +463,7 @@ export class StickySessionRouter {
|
|
|
451
463
|
this.prune(state);
|
|
452
464
|
const current = state.assignments[key];
|
|
453
465
|
if (current &&
|
|
466
|
+
matchesAffinity(current) &&
|
|
454
467
|
input.retainAccountIds.has(current.accountId) &&
|
|
455
468
|
!input.excludeAccountIds?.has(current.accountId)) {
|
|
456
469
|
current.lastSeenAt = this.now();
|
|
@@ -462,8 +475,12 @@ export class StickySessionRouter {
|
|
|
462
475
|
migrated: false,
|
|
463
476
|
};
|
|
464
477
|
}
|
|
478
|
+
const affinityChanged = Boolean(current && !matchesAffinity(current));
|
|
479
|
+
if (affinityChanged) {
|
|
480
|
+
delete state.assignments[key];
|
|
481
|
+
}
|
|
465
482
|
const candidates = input.candidates.filter((candidate) => !input.excludeAccountIds?.has(candidate.accountId));
|
|
466
|
-
const preferred = input.preferredAccountId
|
|
483
|
+
const preferred = !affinityChanged && input.preferredAccountId
|
|
467
484
|
? candidates.find((candidate) => candidate.accountId === input.preferredAccountId &&
|
|
468
485
|
stickyRouteCandidateWeight({
|
|
469
486
|
candidate,
|
|
@@ -486,6 +503,7 @@ export class StickySessionRouter {
|
|
|
486
503
|
const assignment = {
|
|
487
504
|
accountId: selected.accountId,
|
|
488
505
|
family: input.family,
|
|
506
|
+
affinityModelId,
|
|
489
507
|
assignedAt: now,
|
|
490
508
|
lastSeenAt: now,
|
|
491
509
|
initialInputBytes: Math.max(1, input.inputBytes),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/anthropic-auth-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"prepublishOnly": "bun run build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@cortexkit/
|
|
31
|
+
"@cortexkit/claustrum-client": "0.3.0",
|
|
32
|
+
"@cortexkit/subc-client": "^0.11.1",
|
|
32
33
|
"xxhash-wasm": "^1.1.0"
|
|
33
34
|
}
|
|
34
35
|
}
|