@gajae-code/ai 0.18.12 → 0.19.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/CHANGELOG.md +35 -0
- package/dist/types/auth-storage.d.ts +18 -5
- package/dist/types/model-pricing.d.ts +8 -0
- package/dist/types/model-thinking.d.ts +11 -0
- package/dist/types/provider-models/google.d.ts +2 -0
- package/dist/types/providers/amazon-bedrock.d.ts +17 -2
- package/dist/types/providers/anthropic.d.ts +15 -0
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/utils/discovery/antigravity.d.ts +5 -1
- package/dist/types/utils/oauth/glm-zcode.d.ts +0 -30
- package/package.json +3 -3
- package/src/auth-storage.ts +261 -52
- package/src/model-pricing.d.ts +8 -0
- package/src/model-pricing.ts +87 -0
- package/src/model-thinking.d.ts +11 -0
- package/src/model-thinking.ts +60 -5
- package/src/models.json +245 -0
- package/src/models.ts +10 -3
- package/src/provider-models/google.ts +4 -0
- package/src/providers/amazon-bedrock.d.ts +17 -2
- package/src/providers/amazon-bedrock.ts +56 -5
- package/src/providers/anthropic.d.ts +15 -0
- package/src/providers/anthropic.ts +91 -14
- package/src/providers/google-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +6 -6
- package/src/providers/transform-messages.ts +32 -1
- package/src/stream.ts +4 -4
- package/src/utils/discovery/antigravity.ts +69 -26
- package/src/utils/oauth/glm-zcode.d.ts +0 -30
- package/src/utils/oauth/glm-zcode.ts +15 -4
- package/src/utils/oauth/openai-codex.ts +41 -6
- package/src/utils/proxy.ts +35 -6
package/src/auth-storage.ts
CHANGED
|
@@ -1210,7 +1210,7 @@ export function formatOAuthIdentityLabel(
|
|
|
1210
1210
|
return base ? `${base} (${organization})` : organization;
|
|
1211
1211
|
}
|
|
1212
1212
|
|
|
1213
|
-
type OAuthResolutionResult = { apiKey: string; credential: OAuthCredential };
|
|
1213
|
+
type OAuthResolutionResult = { apiKey: string; credential: OAuthCredential; credentialRowId: number };
|
|
1214
1214
|
|
|
1215
1215
|
/**
|
|
1216
1216
|
* Refreshed OAuth access plus identity metadata returned by
|
|
@@ -1525,6 +1525,15 @@ type ConfigApiKeyOwnerFork = {
|
|
|
1525
1525
|
* Reads from storage on reload(), manages round-robin credential selection,
|
|
1526
1526
|
* usage limit tracking, and OAuth token refresh.
|
|
1527
1527
|
*/
|
|
1528
|
+
/** Tier names that denote a model family rather than a plan or quota tier. */
|
|
1529
|
+
const MODEL_FAMILY_TIERS: ReadonlySet<string> = new Set(["opus", "sonnet", "haiku"]);
|
|
1530
|
+
|
|
1531
|
+
/** A recorded usage-limit backoff; `modelId` scopes it to one model or model family. */
|
|
1532
|
+
interface CredentialBackoffEntry {
|
|
1533
|
+
untilMs: number;
|
|
1534
|
+
modelId?: string;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1528
1537
|
export class AuthStorage {
|
|
1529
1538
|
static readonly #defaultBackoffMs = 60_000; // Default backoff when no reset time available
|
|
1530
1539
|
|
|
@@ -1566,7 +1575,7 @@ export class AuthStorage {
|
|
|
1566
1575
|
/** Tracks the last used credential per provider for a session (used for rate-limit switching). */
|
|
1567
1576
|
#sessionLastCredential: Map<string, Map<string, { type: AuthCredential["type"]; index: number }>> = new Map();
|
|
1568
1577
|
/** Maps provider:type -> credentialIndex -> blockedUntilMs for temporary backoff. */
|
|
1569
|
-
#credentialBackoff: Map<string, Map<number,
|
|
1578
|
+
#credentialBackoff: Map<string, Map<number, CredentialBackoffEntry[]>> = new Map();
|
|
1570
1579
|
#usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
1571
1580
|
#rankingStrategyResolver?: (provider: Provider) => CredentialRankingStrategy | undefined;
|
|
1572
1581
|
#usageCache: UsageCache;
|
|
@@ -3050,32 +3059,60 @@ export class AuthStorage {
|
|
|
3050
3059
|
return order;
|
|
3051
3060
|
}
|
|
3052
3061
|
|
|
3053
|
-
/**
|
|
3054
|
-
|
|
3062
|
+
/**
|
|
3063
|
+
* Returns the block expiry for a credential, cleaning up expired entries.
|
|
3064
|
+
* A block recorded for one model only applies to that model; a block without a
|
|
3065
|
+
* model applies to the whole credential. `modelId` is the requested model, so
|
|
3066
|
+
* omitting it asks only about credential-wide blocks.
|
|
3067
|
+
*/
|
|
3068
|
+
#getCredentialBlockedUntil(providerKey: string, credentialIndex: number, modelId?: string): number | undefined {
|
|
3055
3069
|
const backoffMap = this.#credentialBackoff.get(providerKey);
|
|
3056
3070
|
if (!backoffMap) return undefined;
|
|
3057
|
-
const
|
|
3058
|
-
if (!
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3071
|
+
const entries = backoffMap.get(credentialIndex);
|
|
3072
|
+
if (!entries || entries.length === 0) return undefined;
|
|
3073
|
+
const now = Date.now();
|
|
3074
|
+
const live = entries.filter(entry => entry.untilMs > now);
|
|
3075
|
+
if (live.length !== entries.length) {
|
|
3076
|
+
if (live.length > 0) backoffMap.set(credentialIndex, live);
|
|
3077
|
+
else backoffMap.delete(credentialIndex);
|
|
3078
|
+
if (backoffMap.size === 0) this.#credentialBackoff.delete(providerKey);
|
|
3079
|
+
}
|
|
3080
|
+
let blockedUntil: number | undefined;
|
|
3081
|
+
const wanted = modelId?.trim().toLowerCase();
|
|
3082
|
+
// A recorded scope may name a model or a model-family tier, so match a tier
|
|
3083
|
+
// against the requested model id the same way limit scoping does.
|
|
3084
|
+
const entryApplies = (entryModel: string): boolean => {
|
|
3085
|
+
if (wanted === undefined) return false;
|
|
3086
|
+
const scope = entryModel.toLowerCase();
|
|
3087
|
+
if (scope === wanted) return true;
|
|
3088
|
+
return MODEL_FAMILY_TIERS.has(scope) && wanted.includes(scope);
|
|
3089
|
+
};
|
|
3090
|
+
for (const entry of live) {
|
|
3091
|
+
if (entry.modelId !== undefined && !entryApplies(entry.modelId)) continue;
|
|
3092
|
+
if (blockedUntil === undefined || entry.untilMs > blockedUntil) blockedUntil = entry.untilMs;
|
|
3065
3093
|
}
|
|
3066
3094
|
return blockedUntil;
|
|
3067
3095
|
}
|
|
3068
3096
|
|
|
3069
3097
|
/** Checks if a credential is temporarily blocked due to usage limits. */
|
|
3070
|
-
#isCredentialBlocked(providerKey: string, credentialIndex: number): boolean {
|
|
3071
|
-
return this.#getCredentialBlockedUntil(providerKey, credentialIndex) !== undefined;
|
|
3098
|
+
#isCredentialBlocked(providerKey: string, credentialIndex: number, modelId?: string): boolean {
|
|
3099
|
+
return this.#getCredentialBlockedUntil(providerKey, credentialIndex, modelId) !== undefined;
|
|
3072
3100
|
}
|
|
3073
3101
|
|
|
3074
|
-
/** Marks a credential as blocked until the specified time. */
|
|
3075
|
-
#markCredentialBlocked(
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3102
|
+
/** Marks a credential as blocked until the specified time, optionally for one model. */
|
|
3103
|
+
#markCredentialBlocked(
|
|
3104
|
+
providerKey: string,
|
|
3105
|
+
credentialIndex: number,
|
|
3106
|
+
blockedUntilMs: number,
|
|
3107
|
+
modelId?: string,
|
|
3108
|
+
): void {
|
|
3109
|
+
const scoped = modelId?.trim() || undefined;
|
|
3110
|
+
const backoffMap = this.#credentialBackoff.get(providerKey) ?? new Map<number, CredentialBackoffEntry[]>();
|
|
3111
|
+
const entries = backoffMap.get(credentialIndex) ?? [];
|
|
3112
|
+
const existing = entries.find(entry => entry.modelId === scoped);
|
|
3113
|
+
if (existing) existing.untilMs = Math.max(existing.untilMs, blockedUntilMs);
|
|
3114
|
+
else entries.push({ untilMs: blockedUntilMs, modelId: scoped });
|
|
3115
|
+
backoffMap.set(credentialIndex, entries);
|
|
3079
3116
|
this.#credentialBackoff.set(providerKey, backoffMap);
|
|
3080
3117
|
}
|
|
3081
3118
|
|
|
@@ -3366,12 +3403,14 @@ export class AuthStorage {
|
|
|
3366
3403
|
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a runtime API key override is active`,
|
|
3367
3404
|
);
|
|
3368
3405
|
}
|
|
3369
|
-
|
|
3406
|
+
const selected = this.#findCredentialBySelector(provider, selector);
|
|
3407
|
+
const configOverride = this.#configOverrideRegistration(provider, owner);
|
|
3408
|
+
if (configOverride && !(configOverride.envSourced && selected?.credential.type === "api_key")) {
|
|
3370
3409
|
throw new Error(
|
|
3371
3410
|
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a config API key override is active`,
|
|
3372
3411
|
);
|
|
3373
3412
|
}
|
|
3374
|
-
if (!
|
|
3413
|
+
if (!selected) {
|
|
3375
3414
|
throw new Error(`No credential found for ${provider} matching ${this.#formatCredentialSelector(selector)}`);
|
|
3376
3415
|
}
|
|
3377
3416
|
}
|
|
@@ -5123,15 +5162,40 @@ export class AuthStorage {
|
|
|
5123
5162
|
}
|
|
5124
5163
|
|
|
5125
5164
|
/** Returns true if usage indicates rate limit has been reached. */
|
|
5126
|
-
|
|
5127
|
-
|
|
5165
|
+
/**
|
|
5166
|
+
* True when an exhausted limit applies to `modelId`. A limit scoped to one
|
|
5167
|
+
* model (or one tier's model entry) does not exhaust the credential for other
|
|
5168
|
+
* models; a limit with no model scope and not marked shared-blocking applies to
|
|
5169
|
+
* every model.
|
|
5170
|
+
*/
|
|
5171
|
+
#isUsageLimitReached(report: UsageReport, modelId?: string): boolean {
|
|
5172
|
+
return report.limits.some(
|
|
5173
|
+
limit => this.#limitAppliesToModel(limit, modelId) && this.#isUsageLimitExhausted(limit),
|
|
5174
|
+
);
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
/**
|
|
5178
|
+
* Whether a usage limit constrains the requested model. Only model-family
|
|
5179
|
+
* tiers are treated as model scoping: `scope.tier` also carries plans
|
|
5180
|
+
* (Codex/Copilot) and quota tiers (Gemini), which apply to every model.
|
|
5181
|
+
*/
|
|
5182
|
+
#limitAppliesToModel(limit: UsageLimit, modelId?: string): boolean {
|
|
5183
|
+
const wanted = modelId?.trim();
|
|
5184
|
+
// Without a requested model, every limit applies: callers that do not know
|
|
5185
|
+
// the model keep today's conservative "any exhausted limit blocks".
|
|
5186
|
+
if (wanted === undefined) return true;
|
|
5187
|
+
const scopedModel = limit.scope?.modelId?.trim();
|
|
5188
|
+
if (scopedModel) return wanted === scopedModel;
|
|
5189
|
+
const tier = limit.scope?.tier?.trim().toLowerCase();
|
|
5190
|
+
if (tier && MODEL_FAMILY_TIERS.has(tier)) return wanted.toLowerCase().includes(tier);
|
|
5191
|
+
return true;
|
|
5128
5192
|
}
|
|
5129
5193
|
|
|
5130
5194
|
/** Extracts the earliest reset timestamp from exhausted windows (in ms). */
|
|
5131
|
-
#getUsageResetAtMs(report: UsageReport, nowMs: number): number | undefined {
|
|
5195
|
+
#getUsageResetAtMs(report: UsageReport, nowMs: number, modelId?: string): number | undefined {
|
|
5132
5196
|
const candidates: number[] = [];
|
|
5133
5197
|
for (const limit of report.limits) {
|
|
5134
|
-
if (!this.#isUsageLimitExhausted(limit)) continue;
|
|
5198
|
+
if (!this.#limitAppliesToModel(limit, modelId) || !this.#isUsageLimitExhausted(limit)) continue;
|
|
5135
5199
|
const window = limit.window;
|
|
5136
5200
|
if (window?.resetsAt && window.resetsAt > nowMs) {
|
|
5137
5201
|
candidates.push(window.resetsAt);
|
|
@@ -5141,6 +5205,33 @@ export class AuthStorage {
|
|
|
5141
5205
|
return Math.min(...candidates);
|
|
5142
5206
|
}
|
|
5143
5207
|
|
|
5208
|
+
/**
|
|
5209
|
+
* Backoff targets implied by an exhausted usage report: a credential-wide
|
|
5210
|
+
* until for limits that are not model-scoped, plus one entry per exhausted
|
|
5211
|
+
* model-scoped limit. A model-scoped limit must not take the whole credential
|
|
5212
|
+
* out of rotation for other models.
|
|
5213
|
+
*/
|
|
5214
|
+
#exhaustedLimitBlockTargets(
|
|
5215
|
+
report: UsageReport,
|
|
5216
|
+
nowMs: number,
|
|
5217
|
+
): { wideUntil?: number; scoped: Array<{ modelId: string; untilMs: number }> } {
|
|
5218
|
+
let wideUntil: number | undefined;
|
|
5219
|
+
const scoped: Array<{ modelId: string; untilMs: number }> = [];
|
|
5220
|
+
for (const limit of report.limits) {
|
|
5221
|
+
if (!this.#isUsageLimitExhausted(limit)) continue;
|
|
5222
|
+
const resetsAt = limit.window?.resetsAt;
|
|
5223
|
+
if (!resetsAt || resetsAt <= nowMs) continue;
|
|
5224
|
+
const scopeModel = limit.scope?.modelId?.trim();
|
|
5225
|
+
const tier = limit.scope?.tier?.trim().toLowerCase();
|
|
5226
|
+
if (!scopeModel && !(tier && MODEL_FAMILY_TIERS.has(tier))) {
|
|
5227
|
+
wideUntil = wideUntil === undefined ? resetsAt : Math.min(wideUntil, resetsAt);
|
|
5228
|
+
continue;
|
|
5229
|
+
}
|
|
5230
|
+
scoped.push({ modelId: scopeModel ?? tier!, untilMs: resetsAt });
|
|
5231
|
+
}
|
|
5232
|
+
return wideUntil === undefined ? { scoped } : { wideUntil, scoped };
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5144
5235
|
async #getUsageReport(
|
|
5145
5236
|
provider: Provider,
|
|
5146
5237
|
credential: OAuthCredential,
|
|
@@ -5669,7 +5760,15 @@ export class AuthStorage {
|
|
|
5669
5760
|
async markUsageLimitReached(
|
|
5670
5761
|
provider: string,
|
|
5671
5762
|
sessionId: string | undefined,
|
|
5672
|
-
options?: {
|
|
5763
|
+
options?: {
|
|
5764
|
+
retryAfterMs?: number;
|
|
5765
|
+
baseUrl?: string;
|
|
5766
|
+
signal?: AbortSignal;
|
|
5767
|
+
owner?: object;
|
|
5768
|
+
rowId?: number;
|
|
5769
|
+
/** Scope the block to one model; other models keep using the credential. */
|
|
5770
|
+
modelId?: string;
|
|
5771
|
+
},
|
|
5673
5772
|
): Promise<UsageLimitMarkResult> {
|
|
5674
5773
|
provider = resolveOAuthStorageProvider(provider);
|
|
5675
5774
|
const ownerOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
@@ -5690,11 +5789,19 @@ export class AuthStorage {
|
|
|
5690
5789
|
const now = Date.now();
|
|
5691
5790
|
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
|
|
5692
5791
|
|
|
5792
|
+
// Model-scoped exhausted limits only constrain their own model; an
|
|
5793
|
+
// unscoped or shared limit raises the credential-wide backoff.
|
|
5794
|
+
let scopedBlocks: Array<{ modelId: string; untilMs: number }> = [];
|
|
5795
|
+
let reportHasWideLimit = false;
|
|
5693
5796
|
if (initial.credential.type === "oauth" && this.#rankingStrategyResolver?.(provider)) {
|
|
5694
5797
|
const report = await this.#getUsageReport(provider, initial.credential, options);
|
|
5695
|
-
if (report
|
|
5696
|
-
const
|
|
5697
|
-
if (
|
|
5798
|
+
if (report) {
|
|
5799
|
+
const targets = this.#exhaustedLimitBlockTargets(report, Date.now());
|
|
5800
|
+
if (targets.wideUntil !== undefined) {
|
|
5801
|
+
reportHasWideLimit = true;
|
|
5802
|
+
if (targets.wideUntil > blockedUntil) blockedUntil = targets.wideUntil;
|
|
5803
|
+
}
|
|
5804
|
+
scopedBlocks = targets.scoped;
|
|
5698
5805
|
}
|
|
5699
5806
|
}
|
|
5700
5807
|
|
|
@@ -5712,7 +5819,11 @@ export class AuthStorage {
|
|
|
5712
5819
|
candidate.entry.credential.type === credentialKind &&
|
|
5713
5820
|
candidate.entry.id !== failedRowId &&
|
|
5714
5821
|
(!enabledIds || enabledIds.has(candidate.entry.id)) &&
|
|
5715
|
-
!this.#isCredentialBlocked(
|
|
5822
|
+
!this.#isCredentialBlocked(
|
|
5823
|
+
this.#getProviderTypeKey(provider, credentialKind),
|
|
5824
|
+
candidate.index,
|
|
5825
|
+
options?.modelId,
|
|
5826
|
+
),
|
|
5716
5827
|
)
|
|
5717
5828
|
.map(candidate => candidate.entry.id);
|
|
5718
5829
|
};
|
|
@@ -5725,7 +5836,15 @@ export class AuthStorage {
|
|
|
5725
5836
|
};
|
|
5726
5837
|
}
|
|
5727
5838
|
const providerKey = this.#getProviderTypeKey(provider, credentialKind);
|
|
5728
|
-
|
|
5839
|
+
// A report whose only exhausted limits are model-scoped must not take the
|
|
5840
|
+
// whole credential out of rotation; the caller's retry-after backoff applies
|
|
5841
|
+
// to those models instead.
|
|
5842
|
+
if (!(scopedBlocks.length > 0 && !reportHasWideLimit)) {
|
|
5843
|
+
this.#markCredentialBlocked(providerKey, targetIndex, blockedUntil, options?.modelId);
|
|
5844
|
+
}
|
|
5845
|
+
for (const scoped of scopedBlocks) {
|
|
5846
|
+
this.#markCredentialBlocked(providerKey, targetIndex, scoped.untilMs, scoped.modelId);
|
|
5847
|
+
}
|
|
5729
5848
|
return {
|
|
5730
5849
|
state: "marked",
|
|
5731
5850
|
failedRowId,
|
|
@@ -5734,6 +5853,30 @@ export class AuthStorage {
|
|
|
5734
5853
|
};
|
|
5735
5854
|
}
|
|
5736
5855
|
|
|
5856
|
+
/**
|
|
5857
|
+
* Opaque stored row IDs for credentials whose resolved key matches `apiKey`.
|
|
5858
|
+
* Unlike the session's sticky row pointer, these remain tied to the actual key
|
|
5859
|
+
* when concurrent requests share a credential session ID.
|
|
5860
|
+
* A row that cannot be resolved is skipped so an unrelated resolver failure
|
|
5861
|
+
* cannot prevent callers from using already-known credential identity.
|
|
5862
|
+
*/
|
|
5863
|
+
async getCredentialRowIdsMatchingApiKey(provider: string, apiKey: string): Promise<number[]> {
|
|
5864
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
5865
|
+
const rowIds: number[] = [];
|
|
5866
|
+
for (const entry of this.#getStoredCredentials(storageProvider)) {
|
|
5867
|
+
try {
|
|
5868
|
+
if (await this.#credentialMatchesApiKey(storageProvider, entry.credential, apiKey)) rowIds.push(entry.id);
|
|
5869
|
+
} catch (error) {
|
|
5870
|
+
logger.warn("Failed to resolve stored API key while matching credential rows", {
|
|
5871
|
+
provider: storageProvider,
|
|
5872
|
+
credentialId: entry.id,
|
|
5873
|
+
errorType: error instanceof Error ? error.name : typeof error,
|
|
5874
|
+
});
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
return rowIds;
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5737
5880
|
/**
|
|
5738
5881
|
* Mark the stored credential whose key matches `apiKey` usage-limited for
|
|
5739
5882
|
* `retryAfterMs` (or the default backoff). Used when the caller knows
|
|
@@ -5866,7 +6009,11 @@ export class AuthStorage {
|
|
|
5866
6009
|
args.order.map(async idx => {
|
|
5867
6010
|
const selection = args.credentials[idx];
|
|
5868
6011
|
if (!selection) return null;
|
|
5869
|
-
const blockedUntil = this.#getCredentialBlockedUntil(
|
|
6012
|
+
const blockedUntil = this.#getCredentialBlockedUntil(
|
|
6013
|
+
args.providerKey,
|
|
6014
|
+
selection.index,
|
|
6015
|
+
args.options?.modelId,
|
|
6016
|
+
);
|
|
5870
6017
|
if (blockedUntil !== undefined) return { selection, usage: null, usageChecked: false, blockedUntil };
|
|
5871
6018
|
const usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
5872
6019
|
baseUrl: args.options?.baseUrl,
|
|
@@ -5904,13 +6051,29 @@ export class AuthStorage {
|
|
|
5904
6051
|
const { selection, usage, usageChecked } = result;
|
|
5905
6052
|
let { blockedUntil } = result;
|
|
5906
6053
|
let blocked = blockedUntil !== undefined;
|
|
5907
|
-
if (!blocked && usage && this.#isUsageLimitReached(usage)) {
|
|
5908
|
-
const resetAtMs = this.#getUsageResetAtMs(usage, nowMs);
|
|
6054
|
+
if (!blocked && usage && this.#isUsageLimitReached(usage, args.options?.modelId)) {
|
|
6055
|
+
const resetAtMs = this.#getUsageResetAtMs(usage, nowMs, args.options?.modelId);
|
|
5909
6056
|
blockedUntil = resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs;
|
|
5910
|
-
this.#
|
|
6057
|
+
const targets = this.#exhaustedLimitBlockTargets(usage, nowMs);
|
|
6058
|
+
if (targets.wideUntil === undefined && targets.scoped.length > 0) {
|
|
6059
|
+
for (const scoped of targets.scoped) {
|
|
6060
|
+
this.#markCredentialBlocked(args.providerKey, selection.index, scoped.untilMs, scoped.modelId);
|
|
6061
|
+
}
|
|
6062
|
+
} else {
|
|
6063
|
+
this.#markCredentialBlocked(args.providerKey, selection.index, blockedUntil);
|
|
6064
|
+
}
|
|
5911
6065
|
blocked = true;
|
|
5912
6066
|
}
|
|
5913
|
-
|
|
6067
|
+
// Rank on the windows that apply to the requested model so a limit scoped
|
|
6068
|
+
// to another model does not depress this credential's drain metrics.
|
|
6069
|
+
const scopedUsage =
|
|
6070
|
+
usage && args.options?.modelId
|
|
6071
|
+
? {
|
|
6072
|
+
...usage,
|
|
6073
|
+
limits: usage.limits.filter(limit => this.#limitAppliesToModel(limit, args.options?.modelId)),
|
|
6074
|
+
}
|
|
6075
|
+
: usage;
|
|
6076
|
+
const windows = scopedUsage ? strategy.findWindowLimits(scopedUsage) : undefined;
|
|
5914
6077
|
const primary = windows?.primary;
|
|
5915
6078
|
const secondary = windows?.secondary;
|
|
5916
6079
|
const secondaryTarget = secondary ?? primary;
|
|
@@ -6037,7 +6200,8 @@ export class AuthStorage {
|
|
|
6037
6200
|
// (no preference) and sessions whose preferred is blocked still rank, so we pick the account
|
|
6038
6201
|
// with the most headroom proactively and fall back intelligently when rate-limited.
|
|
6039
6202
|
const sessionPreferredIsAvailable =
|
|
6040
|
-
sessionPreferredIndex !== undefined &&
|
|
6203
|
+
sessionPreferredIndex !== undefined &&
|
|
6204
|
+
!this.#isCredentialBlocked(providerKey, sessionPreferredIndex, options?.modelId);
|
|
6041
6205
|
const shouldRank = !selectedCredential && checkUsage && !sessionPreferredIsAvailable;
|
|
6042
6206
|
const candidates = shouldRank
|
|
6043
6207
|
? await this.#rankOAuthSelections({ providerKey, provider, order, credentials, options, strategy: strategy! })
|
|
@@ -6077,7 +6241,7 @@ export class AuthStorage {
|
|
|
6077
6241
|
if (!selectedCredential && sessionPreferredIndex !== undefined) {
|
|
6078
6242
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
6079
6243
|
candidate =>
|
|
6080
|
-
!this.#isCredentialBlocked(providerKey, candidate.selection.index) &&
|
|
6244
|
+
!this.#isCredentialBlocked(providerKey, candidate.selection.index, options?.modelId) &&
|
|
6081
6245
|
candidate.selection.index === sessionPreferredIndex,
|
|
6082
6246
|
);
|
|
6083
6247
|
if (sessionPreferredCandidate > 0) {
|
|
@@ -6143,7 +6307,7 @@ export class AuthStorage {
|
|
|
6143
6307
|
if (resolved) return resolved;
|
|
6144
6308
|
}
|
|
6145
6309
|
|
|
6146
|
-
if (fallback && this.#isCredentialBlocked(providerKey, fallback.selection.index)) {
|
|
6310
|
+
if (fallback && this.#isCredentialBlocked(providerKey, fallback.selection.index, options?.modelId)) {
|
|
6147
6311
|
return this.#tryOAuthCredential(
|
|
6148
6312
|
provider,
|
|
6149
6313
|
fallback.selection,
|
|
@@ -6540,7 +6704,7 @@ export class AuthStorage {
|
|
|
6540
6704
|
): Promise<OAuthResolutionResult | undefined> {
|
|
6541
6705
|
const { checkUsage, allowBlocked, prefetchedUsage = null, usagePrechecked = false } = usageOptions;
|
|
6542
6706
|
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
6543
|
-
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index)) {
|
|
6707
|
+
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index, options?.modelId)) {
|
|
6544
6708
|
return undefined;
|
|
6545
6709
|
}
|
|
6546
6710
|
|
|
@@ -6562,8 +6726,8 @@ export class AuthStorage {
|
|
|
6562
6726
|
});
|
|
6563
6727
|
usageChecked = true;
|
|
6564
6728
|
}
|
|
6565
|
-
if (checkUsage && !allowBlocked && usage && this.#isUsageLimitReached(usage)) {
|
|
6566
|
-
const resetAtMs = this.#getUsageResetAtMs(usage, Date.now());
|
|
6729
|
+
if (checkUsage && !allowBlocked && usage && this.#isUsageLimitReached(usage, options?.modelId)) {
|
|
6730
|
+
const resetAtMs = this.#getUsageResetAtMs(usage, Date.now(), options?.modelId);
|
|
6567
6731
|
this.#markCredentialBlocked(
|
|
6568
6732
|
providerKey,
|
|
6569
6733
|
selection.index,
|
|
@@ -6644,8 +6808,8 @@ export class AuthStorage {
|
|
|
6644
6808
|
});
|
|
6645
6809
|
usageChecked = true;
|
|
6646
6810
|
}
|
|
6647
|
-
if (checkUsage && !allowBlocked && usage && this.#isUsageLimitReached(usage)) {
|
|
6648
|
-
const resetAtMs = this.#getUsageResetAtMs(usage, Date.now());
|
|
6811
|
+
if (checkUsage && !allowBlocked && usage && this.#isUsageLimitReached(usage, options?.modelId)) {
|
|
6812
|
+
const resetAtMs = this.#getUsageResetAtMs(usage, Date.now(), options?.modelId);
|
|
6649
6813
|
this.#markCredentialBlocked(
|
|
6650
6814
|
providerKey,
|
|
6651
6815
|
selection.index,
|
|
@@ -6657,7 +6821,7 @@ export class AuthStorage {
|
|
|
6657
6821
|
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
6658
6822
|
if (!authCredentialEquals(selection.credential, updated)) return undefined;
|
|
6659
6823
|
this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
6660
|
-
return { apiKey: result.apiKey, credential: updated };
|
|
6824
|
+
return { apiKey: result.apiKey, credential: updated, credentialRowId: selection.id };
|
|
6661
6825
|
} catch (error) {
|
|
6662
6826
|
if (options?.signal?.aborted) throw error;
|
|
6663
6827
|
if (isSqliteError(error)) throw error;
|
|
@@ -7000,6 +7164,31 @@ export class AuthStorage {
|
|
|
7000
7164
|
* 10. Fallback resolver (models.yml custom providers, last-resort)
|
|
7001
7165
|
*/
|
|
7002
7166
|
async getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
|
|
7167
|
+
return this.#resolveApiKey(provider, sessionId, options);
|
|
7168
|
+
}
|
|
7169
|
+
|
|
7170
|
+
/**
|
|
7171
|
+
* Resolve an API key and retain the selected stored row identity across token refreshes
|
|
7172
|
+
* and concurrent changes to the session's sticky credential pointer.
|
|
7173
|
+
*/
|
|
7174
|
+
async getApiKeyWithCredentialRowId(
|
|
7175
|
+
provider: string,
|
|
7176
|
+
sessionId?: string,
|
|
7177
|
+
options?: AuthApiKeyOptions,
|
|
7178
|
+
): Promise<{ apiKey: string | undefined; credentialRowId: number | undefined }> {
|
|
7179
|
+
let credentialRowId: number | undefined;
|
|
7180
|
+
const apiKey = await this.#resolveApiKey(provider, sessionId, options, rowId => {
|
|
7181
|
+
credentialRowId = rowId;
|
|
7182
|
+
});
|
|
7183
|
+
return { apiKey, credentialRowId };
|
|
7184
|
+
}
|
|
7185
|
+
|
|
7186
|
+
async #resolveApiKey(
|
|
7187
|
+
provider: string,
|
|
7188
|
+
sessionId?: string,
|
|
7189
|
+
options?: AuthApiKeyOptions,
|
|
7190
|
+
onCredentialRowResolved?: (rowId: number) => void,
|
|
7191
|
+
): Promise<string | undefined> {
|
|
7003
7192
|
provider = resolveOAuthStorageProvider(provider);
|
|
7004
7193
|
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options, sessionId);
|
|
7005
7194
|
|
|
@@ -7021,7 +7210,12 @@ export class AuthStorage {
|
|
|
7021
7210
|
// (validated at login, rotated on 401), while the pointed-to env
|
|
7022
7211
|
// value can go stale with no recovery path — prefer the stored
|
|
7023
7212
|
// credential. Stored OAuth credentials still yield to the override.
|
|
7024
|
-
const storedApiKey = await this.#resolveStoredApiKeyOverEnvConfig(
|
|
7213
|
+
const storedApiKey = await this.#resolveStoredApiKeyOverEnvConfig(
|
|
7214
|
+
provider,
|
|
7215
|
+
selectedCredential,
|
|
7216
|
+
sessionId,
|
|
7217
|
+
onCredentialRowResolved,
|
|
7218
|
+
);
|
|
7025
7219
|
if (storedApiKey) return storedApiKey;
|
|
7026
7220
|
return configKey;
|
|
7027
7221
|
}
|
|
@@ -7036,14 +7230,19 @@ export class AuthStorage {
|
|
|
7036
7230
|
|
|
7037
7231
|
if (selectedCredential?.credential.type === "api_key") {
|
|
7038
7232
|
this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
|
|
7039
|
-
|
|
7233
|
+
const apiKey = await this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
|
|
7234
|
+
if (apiKey) onCredentialRowResolved?.(selectedCredential.id);
|
|
7235
|
+
return apiKey;
|
|
7040
7236
|
}
|
|
7041
7237
|
|
|
7042
7238
|
let oauthAttempted = false;
|
|
7043
7239
|
if (!selectedCredential && this.#getSessionCredential(provider, sessionId)?.type === "oauth") {
|
|
7044
7240
|
const oauthResolved = await this.#resolveOAuthSelection(provider, sessionId, options);
|
|
7045
7241
|
oauthAttempted = true;
|
|
7046
|
-
if (oauthResolved)
|
|
7242
|
+
if (oauthResolved) {
|
|
7243
|
+
onCredentialRowResolved?.(oauthResolved.credentialRowId);
|
|
7244
|
+
return oauthResolved.apiKey;
|
|
7245
|
+
}
|
|
7047
7246
|
}
|
|
7048
7247
|
|
|
7049
7248
|
const attemptedApiKeyIndices = new Set<number>();
|
|
@@ -7055,6 +7254,7 @@ export class AuthStorage {
|
|
|
7055
7254
|
const resolved = await this.#resolveStoredApiKey(provider, apiKeySelection.credential.key);
|
|
7056
7255
|
if (resolved) {
|
|
7057
7256
|
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
7257
|
+
onCredentialRowResolved?.(apiKeySelection.id);
|
|
7058
7258
|
return resolved;
|
|
7059
7259
|
}
|
|
7060
7260
|
}
|
|
@@ -7062,7 +7262,10 @@ export class AuthStorage {
|
|
|
7062
7262
|
|
|
7063
7263
|
if (!oauthAttempted) {
|
|
7064
7264
|
const oauthResolved = await this.#resolveOAuthSelection(provider, sessionId, options);
|
|
7065
|
-
if (oauthResolved)
|
|
7265
|
+
if (oauthResolved) {
|
|
7266
|
+
onCredentialRowResolved?.(oauthResolved.credentialRowId);
|
|
7267
|
+
return oauthResolved.apiKey;
|
|
7268
|
+
}
|
|
7066
7269
|
}
|
|
7067
7270
|
|
|
7068
7271
|
if (!selectedCredential) {
|
|
@@ -7073,6 +7276,7 @@ export class AuthStorage {
|
|
|
7073
7276
|
const resolved = await this.#resolveStoredApiKey(provider, apiKeySelection.credential.key);
|
|
7074
7277
|
if (resolved) {
|
|
7075
7278
|
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
7279
|
+
onCredentialRowResolved?.(apiKeySelection.id);
|
|
7076
7280
|
return resolved;
|
|
7077
7281
|
}
|
|
7078
7282
|
}
|
|
@@ -7091,21 +7295,25 @@ export class AuthStorage {
|
|
|
7091
7295
|
/**
|
|
7092
7296
|
* Resolve a stored api_key credential that outranks an env-sourced config
|
|
7093
7297
|
* override (`apiKeyEnv`). Mirrors the api_key branches of {@link getApiKey}:
|
|
7094
|
-
*
|
|
7095
|
-
* Returns undefined when no
|
|
7096
|
-
* env-sourced override in
|
|
7298
|
+
* a selected credential must resolve or fail closed; unpinned resolution then
|
|
7299
|
+
* tries the round-robin/session pool. Returns undefined when no unpinned
|
|
7300
|
+
* stored api_key credential resolves, leaving the env-sourced override in
|
|
7301
|
+
* effect.
|
|
7097
7302
|
*/
|
|
7098
7303
|
async #resolveStoredApiKeyOverEnvConfig(
|
|
7099
7304
|
provider: string,
|
|
7100
7305
|
selectedCredential: ({ index: number } & StoredCredential) | undefined,
|
|
7101
7306
|
sessionId?: string,
|
|
7307
|
+
onCredentialRowResolved?: (rowId: number) => void,
|
|
7102
7308
|
): Promise<string | undefined> {
|
|
7103
7309
|
if (selectedCredential?.credential.type === "api_key") {
|
|
7104
7310
|
const resolved = await this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
|
|
7105
7311
|
if (resolved) {
|
|
7106
7312
|
this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
|
|
7313
|
+
onCredentialRowResolved?.(selectedCredential.id);
|
|
7107
7314
|
return resolved;
|
|
7108
7315
|
}
|
|
7316
|
+
throw new Error(`Selected credential for ${provider} (id:${selectedCredential.id}) is unavailable`);
|
|
7109
7317
|
}
|
|
7110
7318
|
const attemptedApiKeyIndices = new Set<number>();
|
|
7111
7319
|
for (;;) {
|
|
@@ -7115,6 +7323,7 @@ export class AuthStorage {
|
|
|
7115
7323
|
const resolved = await this.#resolveStoredApiKey(provider, apiKeySelection.credential.key);
|
|
7116
7324
|
if (resolved) {
|
|
7117
7325
|
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
7326
|
+
onCredentialRowResolved?.(apiKeySelection.id);
|
|
7118
7327
|
return resolved;
|
|
7119
7328
|
}
|
|
7120
7329
|
}
|
package/src/model-pricing.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { Api, Model, ModelCost } from "./types";
|
|
2
2
|
export declare function getOpenAIModelCost<TApi extends Api>(model: Model<TApi>, inputTokens: number): ModelCost | undefined;
|
|
3
|
+
export declare function getAnthropicModelCost<TApi extends Api>(model: Model<TApi>, inputTokens: number): ModelCost | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* $/million rate for 1-hour cache writes, or undefined when the model has a single
|
|
6
|
+
* cache-write rate. Cost calculation applies it to `usage.cttl.ephemeral1h` and bills
|
|
7
|
+
* the remaining cache-write tokens at `ModelCost.cacheWrite`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getAnthropicCacheWriteOneHourRate<TApi extends Api>(model: Model<TApi>, inputTokens: number): number | undefined;
|
|
10
|
+
export declare function applyAnthropicModelPricing<TApi extends Api>(model: Model<TApi>): void;
|
|
3
11
|
export declare function applyOpenAIModelPricing<TApi extends Api>(model: Model<TApi>): void;
|
package/src/model-pricing.ts
CHANGED
|
@@ -6,6 +6,17 @@ interface TieredPricing {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const LONG_CONTEXT_THRESHOLD = 272_000;
|
|
9
|
+
// Anthropic Haiku 5.5's direct Messages API charges five times the base rates
|
|
10
|
+
// when prompt input exceeds 100K tokens, including cached input.
|
|
11
|
+
// https://platform.claude.com/docs/en/models/haiku-5-5/overview#pricing
|
|
12
|
+
const ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING: LongContextPricing = {
|
|
13
|
+
threshold: 100_000,
|
|
14
|
+
cost: { input: 0.5, output: 2.5, cacheRead: 0.05, cacheWrite: 0.625 },
|
|
15
|
+
};
|
|
16
|
+
// `ModelCost.cacheWrite` is the 5-minute cache-write rate. Haiku 5.5 charges 1-hour
|
|
17
|
+
// writes at twice the base input rate in each prompt-length tier ($0.20 up to 100K
|
|
18
|
+
// tokens, $1 above), and the provider defaults to `ttl: "1h"`.
|
|
19
|
+
const ANTHROPIC_HAIKU_5_5_CACHE_WRITE_1H = { standard: 0.2, longContext: 1 };
|
|
9
20
|
|
|
10
21
|
const GPT_5_6_SOL_PRICING: TieredPricing = {
|
|
11
22
|
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
@@ -95,6 +106,82 @@ export function getOpenAIModelCost<TApi extends Api>(model: Model<TApi>, inputTo
|
|
|
95
106
|
return inputTokens > pricing.longContextPricing.threshold ? pricing.longContextPricing.cost : pricing.cost;
|
|
96
107
|
}
|
|
97
108
|
|
|
109
|
+
const BEDROCK_HAIKU_5_5_ID = /^(?:(?:au|eu|global|jp|us).)?anthropic.claude-haiku-5-5$/;
|
|
110
|
+
// Bedrock bills Haiku 5.5 prompts above 100K tokens at higher "Long Context" rates.
|
|
111
|
+
// AWS documents the threshold but the per-token rates only on its pricing page,
|
|
112
|
+
// which is not machine-readable; mirror Anthropic's published 5x multiplier on each
|
|
113
|
+
// inference profile's own base rates.
|
|
114
|
+
// https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-haiku-5-5.html#model-card-anthropic-claude-haiku-5-5-pricing
|
|
115
|
+
const BEDROCK_HAIKU_5_5_LONG_CONTEXT_MULTIPLIER = 5;
|
|
116
|
+
|
|
117
|
+
function getAnthropicLongContextPricing<TApi extends Api>(model: Model<TApi>): LongContextPricing | undefined {
|
|
118
|
+
if (model.provider === "anthropic" && model.api === "anthropic-messages" && model.id === "claude-haiku-5-5") {
|
|
119
|
+
return {
|
|
120
|
+
threshold: ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.threshold,
|
|
121
|
+
cost: { ...ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.cost },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
model.provider === "amazon-bedrock" &&
|
|
126
|
+
model.api === "bedrock-converse-stream" &&
|
|
127
|
+
BEDROCK_HAIKU_5_5_ID.test(model.id)
|
|
128
|
+
) {
|
|
129
|
+
const m = BEDROCK_HAIKU_5_5_LONG_CONTEXT_MULTIPLIER;
|
|
130
|
+
const scale = (rate: number): number => Math.round(rate * m * 1e9) / 1e9;
|
|
131
|
+
return {
|
|
132
|
+
threshold: ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.threshold,
|
|
133
|
+
cost: {
|
|
134
|
+
input: scale(model.cost.input),
|
|
135
|
+
output: scale(model.cost.output),
|
|
136
|
+
cacheRead: scale(model.cost.cacheRead),
|
|
137
|
+
cacheWrite: scale(model.cost.cacheWrite),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getAnthropicModelCost<TApi extends Api>(
|
|
145
|
+
model: Model<TApi>,
|
|
146
|
+
inputTokens: number,
|
|
147
|
+
): ModelCost | undefined {
|
|
148
|
+
const pricing = getAnthropicLongContextPricing(model);
|
|
149
|
+
if (!pricing || inputTokens <= pricing.threshold) return undefined;
|
|
150
|
+
return pricing.cost;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* $/million rate for 1-hour cache writes, or undefined when the model has a single
|
|
155
|
+
* cache-write rate. Cost calculation applies it to `usage.cttl.ephemeral1h` and bills
|
|
156
|
+
* the remaining cache-write tokens at `ModelCost.cacheWrite`.
|
|
157
|
+
*/
|
|
158
|
+
export function getAnthropicCacheWriteOneHourRate<TApi extends Api>(
|
|
159
|
+
model: Model<TApi>,
|
|
160
|
+
inputTokens: number,
|
|
161
|
+
): number | undefined {
|
|
162
|
+
const aboveThreshold = inputTokens > ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.threshold;
|
|
163
|
+
if (model.provider === "anthropic" && model.api === "anthropic-messages" && model.id === "claude-haiku-5-5") {
|
|
164
|
+
return aboveThreshold
|
|
165
|
+
? ANTHROPIC_HAIKU_5_5_CACHE_WRITE_1H.longContext
|
|
166
|
+
: ANTHROPIC_HAIKU_5_5_CACHE_WRITE_1H.standard;
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
model.provider === "amazon-bedrock" &&
|
|
170
|
+
model.api === "bedrock-converse-stream" &&
|
|
171
|
+
BEDROCK_HAIKU_5_5_ID.test(model.id)
|
|
172
|
+
) {
|
|
173
|
+
// Same structure as direct: 1-hour writes cost twice the tier's input rate.
|
|
174
|
+
const multiplier = aboveThreshold ? BEDROCK_HAIKU_5_5_LONG_CONTEXT_MULTIPLIER : 1;
|
|
175
|
+
return Math.round(2 * model.cost.input * multiplier * 1e9) / 1e9;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function applyAnthropicModelPricing<TApi extends Api>(model: Model<TApi>): void {
|
|
181
|
+
const pricing = getAnthropicLongContextPricing(model);
|
|
182
|
+
if (pricing) model.longContextPricing = pricing;
|
|
183
|
+
}
|
|
184
|
+
|
|
98
185
|
export function applyOpenAIModelPricing<TApi extends Api>(model: Model<TApi>): void {
|
|
99
186
|
if (model.provider !== "openai" && model.provider !== "openai-codex") {
|
|
100
187
|
return;
|