@oh-my-pi/pi-ai 16.3.14 → 16.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +5 -2
- package/dist/types/auth-broker/remote-store.d.ts +1 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +8 -0
- package/dist/types/auth-storage.d.ts +5 -0
- package/dist/types/providers/azure-openai-responses.d.ts +1 -1
- package/dist/types/providers/ollama.d.ts +1 -1
- package/dist/types/providers/openai-chat-server-schema.d.ts +1 -1
- package/dist/types/providers/openai-chat-wire.d.ts +1 -1
- package/dist/types/providers/openai-codex/request-transformer.d.ts +43 -5
- package/dist/types/providers/openai-codex-responses.d.ts +51 -8
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/providers/openai-responses-wire.d.ts +16 -7
- package/dist/types/providers/openai-responses.d.ts +1 -1
- package/dist/types/providers/openai-shared.d.ts +38 -8
- package/dist/types/registry/novita.d.ts +6 -0
- package/dist/types/registry/oauth/device-code.d.ts +25 -0
- package/dist/types/registry/oauth/index.d.ts +1 -25
- package/dist/types/registry/oauth/xai-oauth.d.ts +9 -25
- package/dist/types/registry/registry.d.ts +4 -1
- package/dist/types/registry/xai-oauth.d.ts +0 -1
- package/dist/types/types.d.ts +26 -3
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +60 -5
- package/src/auth-broker/server.ts +1 -0
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/auth-gateway/server.ts +2 -2
- package/src/auth-storage.ts +198 -60
- package/src/error/flags.ts +4 -1
- package/src/error/rate-limit.ts +7 -1
- package/src/providers/amazon-bedrock.ts +1 -0
- package/src/providers/anthropic-messages-server.ts +18 -0
- package/src/providers/azure-openai-responses.ts +3 -3
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +8 -1
- package/src/providers/openai-chat-wire.ts +1 -1
- package/src/providers/openai-codex/request-transformer.ts +94 -41
- package/src/providers/openai-codex-responses.ts +475 -39
- package/src/providers/openai-completions.ts +38 -12
- package/src/providers/openai-responses-server.ts +8 -1
- package/src/providers/openai-responses-wire.ts +16 -7
- package/src/providers/openai-responses.ts +18 -5
- package/src/providers/openai-shared.ts +125 -13
- package/src/registry/novita.ts +22 -0
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +191 -80
- package/src/registry/oauth/device-code.ts +92 -0
- package/src/registry/oauth/index.ts +1 -91
- package/src/registry/oauth/xai-oauth.ts +231 -191
- package/src/registry/registry.ts +2 -0
- package/src/registry/xai-oauth.ts +0 -1
- package/src/stream.ts +7 -1
- package/src/types.ts +29 -3
package/src/auth-storage.ts
CHANGED
|
@@ -134,6 +134,8 @@ export interface StoredCredentialBlock {
|
|
|
134
134
|
blockScope: string;
|
|
135
135
|
/** Epoch milliseconds. */
|
|
136
136
|
blockedUntilMs: number;
|
|
137
|
+
/** Last row update timestamp in epoch milliseconds, when provided by the backing store. */
|
|
138
|
+
updatedAtMs?: number;
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/**
|
|
@@ -320,6 +322,8 @@ export interface AuthCredentialStore {
|
|
|
320
322
|
cleanExpiredCache(): void;
|
|
321
323
|
/** Non-expired block for one (credential, providerKey, scope) key, or undefined. */
|
|
322
324
|
getCredentialBlock?(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
325
|
+
/** Earliest time a shared-store block should be eligible for live-usage reconciliation. */
|
|
326
|
+
getCredentialBlockReconcileAfter?(credentialId: number, providerKey: string, blockScope: string): number | undefined;
|
|
323
327
|
/** Upsert with MAX semantics: keep the later blockedUntilMs on conflict. */
|
|
324
328
|
upsertCredentialBlock?(block: StoredCredentialBlock): void;
|
|
325
329
|
/** Drop every block row for a credential (all providerKeys/scopes). */
|
|
@@ -761,25 +765,84 @@ function isAbortSignalOption(
|
|
|
761
765
|
return typeof value === "object" && value !== null && "aborted" in value && "addEventListener" in value;
|
|
762
766
|
}
|
|
763
767
|
|
|
764
|
-
|
|
765
|
-
|
|
768
|
+
type OpenAICodexPlanRequirement = "none" | "paid" | "pro";
|
|
769
|
+
type OpenAICodexPlanClass = "free" | "paid" | "pro" | "unknown";
|
|
770
|
+
|
|
771
|
+
const GPT_56_PAID_CODEX_MODEL_PATTERN = /^gpt-5\.6-(?:sol|luna)(?:-pro)?$/;
|
|
772
|
+
const OPENAI_CODEX_PRO_PLAN_TOKENS: Record<string, true> = {
|
|
773
|
+
pro: true,
|
|
774
|
+
};
|
|
775
|
+
const OPENAI_CODEX_PAID_PLAN_TOKENS: Record<string, true> = {
|
|
776
|
+
plus: true,
|
|
777
|
+
business: true,
|
|
778
|
+
team: true,
|
|
779
|
+
enterprise: true,
|
|
780
|
+
edu: true,
|
|
781
|
+
education: true,
|
|
782
|
+
teacher: true,
|
|
783
|
+
teachers: true,
|
|
784
|
+
health: true,
|
|
785
|
+
gov: true,
|
|
786
|
+
government: true,
|
|
787
|
+
};
|
|
788
|
+
const OPENAI_CODEX_FREE_PLAN_TOKENS: Record<string, true> = {
|
|
789
|
+
free: true,
|
|
790
|
+
go: true,
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Account tier needed for model-aware Codex OAuth routing.
|
|
795
|
+
*
|
|
796
|
+
* GPT-5.6 Terra (including its local pro-mode alias) remains available on every
|
|
797
|
+
* plan. Sol and Luna pro-mode aliases inherit their base models' paid tier;
|
|
798
|
+
* only Spark currently has a documented Pro-plan preference in Codex.
|
|
799
|
+
*/
|
|
800
|
+
function resolveOpenAICodexPlanRequirement(provider: string, modelId: string | undefined): OpenAICodexPlanRequirement {
|
|
801
|
+
if (provider !== "openai-codex" || typeof modelId !== "string") return "none";
|
|
802
|
+
const separator = modelId.lastIndexOf("/");
|
|
803
|
+
const bareModelId = (separator === -1 ? modelId : modelId.slice(separator + 1)).toLowerCase();
|
|
804
|
+
if (bareModelId.includes("-spark")) return "pro";
|
|
805
|
+
if (bareModelId === "gpt-5.6" || GPT_56_PAID_CODEX_MODEL_PATTERN.test(bareModelId)) return "paid";
|
|
806
|
+
return "none";
|
|
766
807
|
}
|
|
767
808
|
|
|
768
809
|
function getUsagePlanType(report: UsageReport | null): string | undefined {
|
|
769
810
|
const metadata = report?.metadata;
|
|
770
|
-
if (!metadata
|
|
771
|
-
const planType =
|
|
772
|
-
|
|
811
|
+
if (!metadata) return undefined;
|
|
812
|
+
const planType = metadata.planType;
|
|
813
|
+
if (typeof planType !== "string") return undefined;
|
|
814
|
+
const normalized = planType
|
|
815
|
+
.trim()
|
|
816
|
+
.toLowerCase()
|
|
817
|
+
.replace(/[\s-]+/g, "_");
|
|
818
|
+
return normalized.startsWith("chatgpt_") ? normalized.slice("chatgpt_".length) : normalized;
|
|
773
819
|
}
|
|
774
820
|
|
|
775
|
-
function
|
|
821
|
+
function classifyOpenAICodexPlan(report: UsageReport | null): OpenAICodexPlanClass {
|
|
776
822
|
const planType = getUsagePlanType(report);
|
|
777
|
-
if (!planType) return
|
|
778
|
-
|
|
823
|
+
if (!planType) return "unknown";
|
|
824
|
+
// Pro Lite is a paid Codex tier, but does not imply full Pro-only model access.
|
|
825
|
+
if (planType === "prolite" || planType === "pro_lite") return "paid";
|
|
826
|
+
const tokens = planType.split("_");
|
|
827
|
+
if (tokens.some(token => OPENAI_CODEX_PRO_PLAN_TOKENS[token] === true)) return "pro";
|
|
828
|
+
if (tokens.some(token => OPENAI_CODEX_PAID_PLAN_TOKENS[token] === true)) return "paid";
|
|
829
|
+
if (tokens.some(token => OPENAI_CODEX_FREE_PLAN_TOKENS[token] === true)) return "free";
|
|
830
|
+
return "unknown";
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function getOpenAICodexPlanEligibility(
|
|
834
|
+
report: UsageReport | null,
|
|
835
|
+
requirement: OpenAICodexPlanRequirement,
|
|
836
|
+
): boolean | undefined {
|
|
837
|
+
if (requirement === "none") return true;
|
|
838
|
+
const planClass = classifyOpenAICodexPlan(report);
|
|
839
|
+
if (planClass === "unknown") return undefined;
|
|
840
|
+
return requirement === "paid" ? planClass !== "free" : planClass === "pro";
|
|
779
841
|
}
|
|
780
842
|
|
|
781
|
-
function
|
|
782
|
-
|
|
843
|
+
function getOpenAICodexPlanPriority(report: UsageReport | null, requirement: OpenAICodexPlanRequirement): number {
|
|
844
|
+
const eligibility = getOpenAICodexPlanEligibility(report, requirement);
|
|
845
|
+
return eligibility === true ? 0 : eligibility === undefined ? 1 : 2;
|
|
783
846
|
}
|
|
784
847
|
|
|
785
848
|
function compareUsageRankingMetric(left: number, right: number): number {
|
|
@@ -965,6 +1028,8 @@ export class AuthStorage {
|
|
|
965
1028
|
#sessionLastCredential: Map<string, Map<string, { type: AuthCredential["type"]; index: number }>> = new Map();
|
|
966
1029
|
/** Maps provider:type -> credentialIndex -> blockedUntilMs for temporary backoff. */
|
|
967
1030
|
#credentialBackoff: Map<string, Map<number, number>> = new Map();
|
|
1031
|
+
/** Earliest time a freshly-set in-memory block may be cleared by live usage reconciliation. */
|
|
1032
|
+
#credentialBackoffProbeAfter: Map<string, Map<number, number>> = new Map();
|
|
968
1033
|
#usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
969
1034
|
#rankingStrategyResolver?: (provider: Provider) => CredentialRankingStrategy | undefined;
|
|
970
1035
|
#usageCache: UsageCache;
|
|
@@ -1345,6 +1410,9 @@ export class AuthStorage {
|
|
|
1345
1410
|
if (backoffMap.size === 0) {
|
|
1346
1411
|
this.#credentialBackoff.delete(backoffKey);
|
|
1347
1412
|
}
|
|
1413
|
+
const probeAfterMap = this.#credentialBackoffProbeAfter.get(backoffKey);
|
|
1414
|
+
probeAfterMap?.delete(credentialIndex);
|
|
1415
|
+
if (probeAfterMap?.size === 0) this.#credentialBackoffProbeAfter.delete(backoffKey);
|
|
1348
1416
|
return undefined;
|
|
1349
1417
|
}
|
|
1350
1418
|
return blockedUntil;
|
|
@@ -1437,6 +1505,9 @@ export class AuthStorage {
|
|
|
1437
1505
|
const nextBlockedUntil = Math.max(existing, blockedUntilMs);
|
|
1438
1506
|
backoffMap.set(credentialIndex, nextBlockedUntil);
|
|
1439
1507
|
this.#credentialBackoff.set(backoffKey, backoffMap);
|
|
1508
|
+
const probeAfterMap = this.#credentialBackoffProbeAfter.get(backoffKey) ?? new Map<number, number>();
|
|
1509
|
+
probeAfterMap.set(credentialIndex, Math.min(nextBlockedUntil, Date.now() + USAGE_REPORT_TTL_MS));
|
|
1510
|
+
this.#credentialBackoffProbeAfter.set(backoffKey, probeAfterMap);
|
|
1440
1511
|
this.#invalidateUsageReportCache(provider);
|
|
1441
1512
|
|
|
1442
1513
|
const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
|
|
@@ -3186,8 +3257,7 @@ export class AuthStorage {
|
|
|
3186
3257
|
#compareRankedOAuthCandidatePriority(
|
|
3187
3258
|
left: RankedOAuthCandidate,
|
|
3188
3259
|
right: RankedOAuthCandidate,
|
|
3189
|
-
|
|
3190
|
-
modelId: string | undefined,
|
|
3260
|
+
planRequirement: OpenAICodexPlanRequirement,
|
|
3191
3261
|
): number {
|
|
3192
3262
|
if (left.blocked !== right.blocked) return left.blocked ? 1 : -1;
|
|
3193
3263
|
if (left.blocked && right.blocked) {
|
|
@@ -3196,7 +3266,7 @@ export class AuthStorage {
|
|
|
3196
3266
|
if (leftBlockedUntil !== rightBlockedUntil) return leftBlockedUntil - rightBlockedUntil;
|
|
3197
3267
|
return 0;
|
|
3198
3268
|
}
|
|
3199
|
-
if (
|
|
3269
|
+
if (planRequirement !== "none" && left.planPriority !== right.planPriority) {
|
|
3200
3270
|
return left.planPriority - right.planPriority;
|
|
3201
3271
|
}
|
|
3202
3272
|
if (left.hasPriorityBoost !== right.hasPriorityBoost) return left.hasPriorityBoost ? -1 : 1;
|
|
@@ -3214,20 +3284,18 @@ export class AuthStorage {
|
|
|
3214
3284
|
#compareRankedOAuthCandidates(
|
|
3215
3285
|
left: RankedOAuthCandidate,
|
|
3216
3286
|
right: RankedOAuthCandidate,
|
|
3217
|
-
|
|
3218
|
-
modelId: string | undefined,
|
|
3287
|
+
planRequirement: OpenAICodexPlanRequirement,
|
|
3219
3288
|
): number {
|
|
3220
|
-
const priority = this.#compareRankedOAuthCandidatePriority(left, right,
|
|
3289
|
+
const priority = this.#compareRankedOAuthCandidatePriority(left, right, planRequirement);
|
|
3221
3290
|
return priority !== 0 ? priority : left.orderPos - right.orderPos;
|
|
3222
3291
|
}
|
|
3223
3292
|
|
|
3224
3293
|
#orderRankedOAuthCandidates(
|
|
3225
3294
|
candidates: RankedOAuthCandidate[],
|
|
3226
3295
|
sessionId: string | undefined,
|
|
3227
|
-
|
|
3228
|
-
modelId: string | undefined,
|
|
3296
|
+
planRequirement: OpenAICodexPlanRequirement,
|
|
3229
3297
|
): OAuthCandidate[] {
|
|
3230
|
-
candidates.sort((left, right) => this.#compareRankedOAuthCandidates(left, right,
|
|
3298
|
+
candidates.sort((left, right) => this.#compareRankedOAuthCandidates(left, right, planRequirement));
|
|
3231
3299
|
if (!sessionId) {
|
|
3232
3300
|
return candidates.map(candidate => ({
|
|
3233
3301
|
selection: candidate.selection,
|
|
@@ -3252,7 +3320,7 @@ export class AuthStorage {
|
|
|
3252
3320
|
for (const candidate of unblocked) {
|
|
3253
3321
|
if (
|
|
3254
3322
|
candidate !== previous &&
|
|
3255
|
-
this.#compareRankedOAuthCandidatePriority(previous, candidate,
|
|
3323
|
+
this.#compareRankedOAuthCandidatePriority(previous, candidate, planRequirement) !== 0
|
|
3256
3324
|
) {
|
|
3257
3325
|
bucketIndex += 1;
|
|
3258
3326
|
}
|
|
@@ -3297,6 +3365,7 @@ export class AuthStorage {
|
|
|
3297
3365
|
providerKey: string;
|
|
3298
3366
|
provider: string;
|
|
3299
3367
|
order: number[];
|
|
3368
|
+
planRequirement: OpenAICodexPlanRequirement;
|
|
3300
3369
|
credentials: OAuthSelection[];
|
|
3301
3370
|
options?: AuthApiKeyOptions;
|
|
3302
3371
|
sessionId?: string;
|
|
@@ -3316,18 +3385,36 @@ export class AuthStorage {
|
|
|
3316
3385
|
args.order.map(async idx => {
|
|
3317
3386
|
const selection = args.credentials[idx];
|
|
3318
3387
|
if (!selection) return null;
|
|
3319
|
-
|
|
3388
|
+
let blockedUntil = this.#getCredentialBlockedUntil(
|
|
3320
3389
|
args.provider,
|
|
3321
3390
|
args.providerKey,
|
|
3322
3391
|
selection.index,
|
|
3323
3392
|
args.blockScope,
|
|
3324
3393
|
);
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3394
|
+
let usage: UsageReport | null = null;
|
|
3395
|
+
let usageChecked = false;
|
|
3396
|
+
if (blockedUntil !== undefined && args.provider === "openai-codex") {
|
|
3397
|
+
usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
3398
|
+
...args.options,
|
|
3399
|
+
timeoutMs: this.#usageRequestTimeoutMs,
|
|
3400
|
+
});
|
|
3401
|
+
usageChecked = true;
|
|
3402
|
+
blockedUntil = this.#getCredentialBlockedUntil(
|
|
3403
|
+
args.provider,
|
|
3404
|
+
args.providerKey,
|
|
3405
|
+
selection.index,
|
|
3406
|
+
args.blockScope,
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
if (blockedUntil !== undefined) return { selection, usage, usageChecked, blockedUntil };
|
|
3410
|
+
if (!usageChecked) {
|
|
3411
|
+
usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
3412
|
+
...args.options,
|
|
3413
|
+
timeoutMs: this.#usageRequestTimeoutMs,
|
|
3414
|
+
});
|
|
3415
|
+
usageChecked = true;
|
|
3416
|
+
}
|
|
3417
|
+
return { selection, usage, usageChecked, blockedUntil: undefined as number | undefined };
|
|
3331
3418
|
}),
|
|
3332
3419
|
);
|
|
3333
3420
|
const timeoutSignal = Promise.withResolvers<null>();
|
|
@@ -3378,7 +3465,7 @@ export class AuthStorage {
|
|
|
3378
3465
|
blocked,
|
|
3379
3466
|
blockedUntil,
|
|
3380
3467
|
hasPriorityBoost: strategy.hasPriorityBoost?.(primary) ?? false,
|
|
3381
|
-
planPriority: getOpenAICodexPlanPriority(usage),
|
|
3468
|
+
planPriority: getOpenAICodexPlanPriority(usage, args.planRequirement),
|
|
3382
3469
|
secondaryUsed: this.#normalizeUsageFraction(secondaryTarget),
|
|
3383
3470
|
secondaryDrainRate: this.#computeWindowDrainRate(
|
|
3384
3471
|
secondaryTarget,
|
|
@@ -3390,7 +3477,7 @@ export class AuthStorage {
|
|
|
3390
3477
|
orderPos,
|
|
3391
3478
|
});
|
|
3392
3479
|
}
|
|
3393
|
-
return this.#orderRankedOAuthCandidates(ranked, args.sessionId, args.
|
|
3480
|
+
return this.#orderRankedOAuthCandidates(ranked, args.sessionId, args.planRequirement);
|
|
3394
3481
|
}
|
|
3395
3482
|
|
|
3396
3483
|
/**
|
|
@@ -3418,8 +3505,9 @@ export class AuthStorage {
|
|
|
3418
3505
|
const strategy = this.#rankingStrategyResolver?.(provider);
|
|
3419
3506
|
const rankingContext: CredentialRankingContext = { modelId: options?.modelId };
|
|
3420
3507
|
const blockScope = strategy?.blockScope?.(rankingContext);
|
|
3421
|
-
const
|
|
3422
|
-
const
|
|
3508
|
+
const planRequirement = resolveOpenAICodexPlanRequirement(provider, options?.modelId);
|
|
3509
|
+
const hasPlanRequirement = planRequirement !== "none";
|
|
3510
|
+
const checkUsage = strategy !== undefined && (credentials.length > 1 || hasPlanRequirement);
|
|
3423
3511
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
3424
3512
|
const sessionPreferredIndex = sessionCredential?.type === "oauth" ? sessionCredential.index : undefined;
|
|
3425
3513
|
const sessionPreferredCredential =
|
|
@@ -3438,12 +3526,13 @@ export class AuthStorage {
|
|
|
3438
3526
|
sessionPreferredIndex !== undefined &&
|
|
3439
3527
|
sessionPreferredCanRefreshOrUse &&
|
|
3440
3528
|
!this.#isCredentialBlocked(provider, providerKey, sessionPreferredIndex, blockScope);
|
|
3441
|
-
const shouldRank = checkUsage && (!sessionPreferredIsAvailable ||
|
|
3529
|
+
const shouldRank = checkUsage && (!sessionPreferredIsAvailable || hasPlanRequirement);
|
|
3442
3530
|
const rankingOrder = shouldRank && sessionId ? credentials.map((_credential, index) => index) : order;
|
|
3443
3531
|
const candidates = shouldRank
|
|
3444
3532
|
? await this.#rankOAuthSelections({
|
|
3445
3533
|
providerKey,
|
|
3446
3534
|
provider,
|
|
3535
|
+
planRequirement,
|
|
3447
3536
|
order: rankingOrder,
|
|
3448
3537
|
credentials,
|
|
3449
3538
|
options,
|
|
@@ -3457,7 +3546,7 @@ export class AuthStorage {
|
|
|
3457
3546
|
.filter((selection): selection is { credential: OAuthCredential; index: number } => Boolean(selection))
|
|
3458
3547
|
.map(selection => ({ selection, usage: null, usageChecked: false }));
|
|
3459
3548
|
|
|
3460
|
-
if (sessionPreferredIndex !== undefined && !
|
|
3549
|
+
if (sessionPreferredIndex !== undefined && !hasPlanRequirement) {
|
|
3461
3550
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
3462
3551
|
candidate =>
|
|
3463
3552
|
!this.#isCredentialBlocked(provider, providerKey, candidate.selection.index, blockScope) &&
|
|
@@ -3537,10 +3626,12 @@ export class AuthStorage {
|
|
|
3537
3626
|
}),
|
|
3538
3627
|
);
|
|
3539
3628
|
|
|
3540
|
-
//
|
|
3541
|
-
//
|
|
3542
|
-
|
|
3543
|
-
|
|
3629
|
+
// Enforce a tier only when at least one account is confirmed eligible. If
|
|
3630
|
+
// every report is unknown or ineligible, preserve trial/grandfathered access
|
|
3631
|
+
// by allowing the normal candidate fallback to attempt the request.
|
|
3632
|
+
const enforcePlanRequirement =
|
|
3633
|
+
hasPlanRequirement &&
|
|
3634
|
+
candidates.some(candidate => getOpenAICodexPlanEligibility(candidate.usage, planRequirement) === true);
|
|
3544
3635
|
|
|
3545
3636
|
const fallback = candidates[0];
|
|
3546
3637
|
|
|
@@ -3556,7 +3647,8 @@ export class AuthStorage {
|
|
|
3556
3647
|
allowBlocked: false,
|
|
3557
3648
|
prefetchedUsage: candidate.usage,
|
|
3558
3649
|
usagePrechecked: candidate.usageChecked,
|
|
3559
|
-
|
|
3650
|
+
planRequirement,
|
|
3651
|
+
enforcePlanRequirement,
|
|
3560
3652
|
strategy,
|
|
3561
3653
|
rankingContext,
|
|
3562
3654
|
blockScope,
|
|
@@ -3571,7 +3663,8 @@ export class AuthStorage {
|
|
|
3571
3663
|
allowBlocked: true,
|
|
3572
3664
|
prefetchedUsage: fallback.usage,
|
|
3573
3665
|
usagePrechecked: fallback.usageChecked,
|
|
3574
|
-
|
|
3666
|
+
planRequirement,
|
|
3667
|
+
enforcePlanRequirement,
|
|
3575
3668
|
strategy,
|
|
3576
3669
|
rankingContext,
|
|
3577
3670
|
blockScope,
|
|
@@ -3709,7 +3802,8 @@ export class AuthStorage {
|
|
|
3709
3802
|
allowBlocked: boolean;
|
|
3710
3803
|
prefetchedUsage?: UsageReport | null;
|
|
3711
3804
|
usagePrechecked?: boolean;
|
|
3712
|
-
|
|
3805
|
+
planRequirement?: OpenAICodexPlanRequirement;
|
|
3806
|
+
enforcePlanRequirement?: boolean;
|
|
3713
3807
|
strategy?: CredentialRankingStrategy;
|
|
3714
3808
|
rankingContext?: CredentialRankingContext;
|
|
3715
3809
|
blockScope?: string;
|
|
@@ -3722,7 +3816,8 @@ export class AuthStorage {
|
|
|
3722
3816
|
allowBlocked,
|
|
3723
3817
|
prefetchedUsage = null,
|
|
3724
3818
|
usagePrechecked = false,
|
|
3725
|
-
|
|
3819
|
+
planRequirement: providedPlanRequirement,
|
|
3820
|
+
enforcePlanRequirement,
|
|
3726
3821
|
strategy,
|
|
3727
3822
|
rankingContext,
|
|
3728
3823
|
blockScope,
|
|
@@ -3741,12 +3836,13 @@ export class AuthStorage {
|
|
|
3741
3836
|
// refresh / persist / CAS-disable addresses the row by this stable id.
|
|
3742
3837
|
const credentialId = this.#getStoredCredentials(provider)[selection.index]?.id;
|
|
3743
3838
|
|
|
3744
|
-
const
|
|
3745
|
-
const
|
|
3839
|
+
const planRequirement = providedPlanRequirement ?? resolveOpenAICodexPlanRequirement(provider, options?.modelId);
|
|
3840
|
+
const hasPlanRequirement = planRequirement !== "none";
|
|
3841
|
+
const applyPlanFilter = enforcePlanRequirement ?? hasPlanRequirement;
|
|
3746
3842
|
let usage: UsageReport | null = null;
|
|
3747
3843
|
let usageChecked = false;
|
|
3748
3844
|
|
|
3749
|
-
if ((checkUsage && !allowBlocked) ||
|
|
3845
|
+
if ((checkUsage && !allowBlocked) || hasPlanRequirement) {
|
|
3750
3846
|
if (usagePrechecked) {
|
|
3751
3847
|
usage = prefetchedUsage;
|
|
3752
3848
|
usageChecked = true;
|
|
@@ -3757,7 +3853,7 @@ export class AuthStorage {
|
|
|
3757
3853
|
});
|
|
3758
3854
|
usageChecked = true;
|
|
3759
3855
|
}
|
|
3760
|
-
if (
|
|
3856
|
+
if (applyPlanFilter && getOpenAICodexPlanEligibility(usage, planRequirement) !== true) {
|
|
3761
3857
|
return undefined;
|
|
3762
3858
|
}
|
|
3763
3859
|
if (checkUsage && !allowBlocked && usage && strategy && rankingContext) {
|
|
@@ -3825,7 +3921,7 @@ export class AuthStorage {
|
|
|
3825
3921
|
} else {
|
|
3826
3922
|
this.#replaceCredentialAt(provider, selection.index, updated);
|
|
3827
3923
|
}
|
|
3828
|
-
if ((checkUsage && !allowBlocked) ||
|
|
3924
|
+
if ((checkUsage && !allowBlocked) || hasPlanRequirement) {
|
|
3829
3925
|
const sameAccount = selection.credential.accountId === updated.accountId;
|
|
3830
3926
|
if (!usageChecked || !sameAccount) {
|
|
3831
3927
|
usage = await this.#getUsageReport(provider, updated, {
|
|
@@ -3834,7 +3930,7 @@ export class AuthStorage {
|
|
|
3834
3930
|
});
|
|
3835
3931
|
usageChecked = true;
|
|
3836
3932
|
}
|
|
3837
|
-
if (
|
|
3933
|
+
if (applyPlanFilter && getOpenAICodexPlanEligibility(usage, planRequirement) !== true) {
|
|
3838
3934
|
return undefined;
|
|
3839
3935
|
}
|
|
3840
3936
|
if (checkUsage && !allowBlocked && usage && strategy && rankingContext) {
|
|
@@ -4367,23 +4463,24 @@ export class AuthStorage {
|
|
|
4367
4463
|
backoffMap.delete(index);
|
|
4368
4464
|
if (backoffMap.size === 0) this.#credentialBackoff.delete(key);
|
|
4369
4465
|
}
|
|
4466
|
+
for (const [key, probeAfterMap] of this.#credentialBackoffProbeAfter) {
|
|
4467
|
+
if (key !== providerKey && !key.startsWith(scopedPrefix)) continue;
|
|
4468
|
+
probeAfterMap.delete(index);
|
|
4469
|
+
if (probeAfterMap.size === 0) this.#credentialBackoffProbeAfter.delete(key);
|
|
4470
|
+
}
|
|
4370
4471
|
}
|
|
4371
4472
|
|
|
4372
4473
|
/**
|
|
4373
4474
|
* Self-heal a stale Codex usage-limit block: when a fresh live usage report
|
|
4374
|
-
*
|
|
4375
|
-
* in-memory `openai-codex:oauth` blocks so
|
|
4376
|
-
*
|
|
4377
|
-
* scope, so clearing every block for the credential id is exact.
|
|
4475
|
+
* says the account is allowed and below every reported limit, drop the
|
|
4476
|
+
* persisted and in-memory `openai-codex:oauth` blocks so credential selection
|
|
4477
|
+
* can re-include recovered seats before a stale block naturally expires.
|
|
4378
4478
|
*/
|
|
4379
4479
|
#isHealthyCodexUsageReport(report: UsageReport): boolean {
|
|
4480
|
+
if (report.provider !== "openai-codex") return false;
|
|
4380
4481
|
const metadata = report.metadata;
|
|
4381
|
-
return
|
|
4382
|
-
|
|
4383
|
-
metadata?.allowed === true &&
|
|
4384
|
-
metadata.limitReached === false &&
|
|
4385
|
-
!this.#isUsageLimitReached(report.limits)
|
|
4386
|
-
);
|
|
4482
|
+
if (metadata?.allowed !== true || metadata.limitReached !== false) return false;
|
|
4483
|
+
return !this.#isUsageLimitReached(report.limits);
|
|
4387
4484
|
}
|
|
4388
4485
|
|
|
4389
4486
|
#reconcileCodexUsageBlockForCredential(provider: Provider, credentialId: number, report: UsageReport): void {
|
|
@@ -4396,6 +4493,19 @@ export class AuthStorage {
|
|
|
4396
4493
|
const blockScope = this.#rankingStrategyResolver?.(provider)?.blockScope?.({});
|
|
4397
4494
|
const blockedUntilMs = this.#getCredentialBlockedUntil(provider, providerKey, credentialIndex, blockScope);
|
|
4398
4495
|
if (blockedUntilMs === undefined) return;
|
|
4496
|
+
// `/usage` can lag the request path that just returned 429. Fresh local or
|
|
4497
|
+
// broker-sourced blocks get one usage-cache window before healthy reports may
|
|
4498
|
+
// clear them.
|
|
4499
|
+
const nowMs = Date.now();
|
|
4500
|
+
const scopedBackoffKey = this.#toScopedBackoffKey(providerKey, blockScope);
|
|
4501
|
+
const globalProbeAfterMs = this.#credentialBackoffProbeAfter.get(providerKey)?.get(credentialIndex) ?? 0;
|
|
4502
|
+
const scopedProbeAfterMs = this.#credentialBackoffProbeAfter.get(scopedBackoffKey)?.get(credentialIndex) ?? 0;
|
|
4503
|
+
const getStoreReconcileAfter = this.#store.getCredentialBlockReconcileAfter?.bind(this.#store);
|
|
4504
|
+
const storeGlobalProbeAfterMs = getStoreReconcileAfter?.(credentialId, providerKey, "") ?? 0;
|
|
4505
|
+
const storeScopedProbeAfterMs = getStoreReconcileAfter?.(credentialId, providerKey, blockScope ?? "") ?? 0;
|
|
4506
|
+
if (Math.max(globalProbeAfterMs, scopedProbeAfterMs, storeGlobalProbeAfterMs, storeScopedProbeAfterMs) > nowMs) {
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
4399
4509
|
this.#clearCredentialBlocks(provider, credentialId);
|
|
4400
4510
|
logger.info("Cleared stale Codex usage-limit block after healthy live usage report", {
|
|
4401
4511
|
credentialId,
|
|
@@ -4899,6 +5009,7 @@ type CredentialBlockRow = {
|
|
|
4899
5009
|
provider_key: string;
|
|
4900
5010
|
block_scope: string;
|
|
4901
5011
|
blocked_until_ms: number;
|
|
5012
|
+
updated_at: number;
|
|
4902
5013
|
};
|
|
4903
5014
|
|
|
4904
5015
|
type SerializedCredentialRecord = {
|
|
@@ -5114,6 +5225,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5114
5225
|
#upsertCredentialBlockStmt: Statement;
|
|
5115
5226
|
#deleteCredentialBlocksStmt: Statement;
|
|
5116
5227
|
#deleteExpiredCredentialBlocksStmt: Statement;
|
|
5228
|
+
#credentialBlockReconcileAfter: Map<string, number> = new Map();
|
|
5117
5229
|
#insertUsageHistoryStmt: Statement;
|
|
5118
5230
|
#insertUsageCostStmt: Statement;
|
|
5119
5231
|
#listUsageCostsStmt: Statement;
|
|
@@ -5160,10 +5272,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5160
5272
|
);
|
|
5161
5273
|
this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
|
|
5162
5274
|
this.#getCredentialBlockStmt = this.#db.prepare(
|
|
5163
|
-
"SELECT blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
5275
|
+
"SELECT blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
|
|
5164
5276
|
);
|
|
5165
5277
|
this.#listCredentialBlocksByCredentialStmt = this.#db.prepare(
|
|
5166
|
-
"SELECT credential_id, provider_key, block_scope, blocked_until_ms FROM auth_credential_blocks WHERE credential_id = ? AND blocked_until_ms > ? ORDER BY provider_key ASC, block_scope ASC",
|
|
5278
|
+
"SELECT credential_id, provider_key, block_scope, blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND blocked_until_ms > ? ORDER BY provider_key ASC, block_scope ASC",
|
|
5167
5279
|
);
|
|
5168
5280
|
this.#upsertCredentialBlockStmt = this.#db.prepare(
|
|
5169
5281
|
`INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
|
|
@@ -5777,11 +5889,26 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5777
5889
|
const nowMs = Date.now();
|
|
5778
5890
|
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5779
5891
|
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
5780
|
-
| { blocked_until_ms?: number }
|
|
5892
|
+
| { blocked_until_ms?: number; updated_at?: number }
|
|
5781
5893
|
| undefined;
|
|
5782
5894
|
return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
|
|
5783
5895
|
}
|
|
5784
5896
|
|
|
5897
|
+
getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
|
|
5898
|
+
const nowMs = Date.now();
|
|
5899
|
+
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5900
|
+
const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
|
|
5901
|
+
| { blocked_until_ms?: number; updated_at?: number }
|
|
5902
|
+
| undefined;
|
|
5903
|
+
if (typeof row?.blocked_until_ms !== "number") return undefined;
|
|
5904
|
+
const memoryReconcileAfter =
|
|
5905
|
+
this.#credentialBlockReconcileAfter.get(`${credentialId}\0${providerKey}\0${blockScope}`) ?? 0;
|
|
5906
|
+
const persistedReconcileAfter =
|
|
5907
|
+
typeof row.updated_at === "number" ? row.updated_at * 1000 + USAGE_REPORT_TTL_MS : 0;
|
|
5908
|
+
const reconcileAfter = Math.max(memoryReconcileAfter, persistedReconcileAfter);
|
|
5909
|
+
return reconcileAfter > nowMs ? Math.min(row.blocked_until_ms, reconcileAfter) : undefined;
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5785
5912
|
upsertCredentialBlock(block: StoredCredentialBlock): void {
|
|
5786
5913
|
this.#upsertCredentialBlockStmt.run(
|
|
5787
5914
|
block.credentialId,
|
|
@@ -5789,14 +5916,24 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5789
5916
|
block.blockScope,
|
|
5790
5917
|
block.blockedUntilMs,
|
|
5791
5918
|
);
|
|
5919
|
+
this.#credentialBlockReconcileAfter.set(
|
|
5920
|
+
`${block.credentialId}\0${block.providerKey}\0${block.blockScope}`,
|
|
5921
|
+
Math.min(block.blockedUntilMs, Date.now() + USAGE_REPORT_TTL_MS),
|
|
5922
|
+
);
|
|
5792
5923
|
}
|
|
5793
5924
|
|
|
5794
5925
|
deleteCredentialBlocks(credentialId: number): void {
|
|
5795
5926
|
this.#deleteCredentialBlocksStmt.run(credentialId);
|
|
5927
|
+
for (const key of this.#credentialBlockReconcileAfter.keys()) {
|
|
5928
|
+
if (key.startsWith(`${credentialId}\0`)) this.#credentialBlockReconcileAfter.delete(key);
|
|
5929
|
+
}
|
|
5796
5930
|
}
|
|
5797
5931
|
|
|
5798
5932
|
cleanExpiredCredentialBlocks(nowMs: number): void {
|
|
5799
5933
|
this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
|
|
5934
|
+
for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
|
|
5935
|
+
if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
|
|
5936
|
+
}
|
|
5800
5937
|
}
|
|
5801
5938
|
|
|
5802
5939
|
listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
|
|
@@ -5815,6 +5952,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5815
5952
|
providerKey: row.provider_key,
|
|
5816
5953
|
blockScope: row.block_scope,
|
|
5817
5954
|
blockedUntilMs: row.blocked_until_ms,
|
|
5955
|
+
updatedAtMs: row.updated_at * 1000,
|
|
5818
5956
|
});
|
|
5819
5957
|
}
|
|
5820
5958
|
}
|
package/src/error/flags.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isUnexpectedSocketCloseMessage } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Api, AssistantMessage } from "../types";
|
|
3
|
+
import { AwsCredentialsError } from "./aws";
|
|
3
4
|
import {
|
|
4
5
|
AnthropicConnectionError,
|
|
5
6
|
AnthropicConnectionTimeoutError,
|
|
@@ -346,7 +347,9 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
if (link instanceof
|
|
350
|
+
if (link instanceof AwsCredentialsError) {
|
|
351
|
+
kinds |= Flag.AuthFailed;
|
|
352
|
+
} else if (link instanceof AnthropicConnectionTimeoutError) {
|
|
350
353
|
kinds |= Flag.Timeout | Flag.Transient;
|
|
351
354
|
} else if (link instanceof AnthropicConnectionError) {
|
|
352
355
|
kinds |= Flag.Transient;
|
package/src/error/rate-limit.ts
CHANGED
|
@@ -67,6 +67,12 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
|
67
67
|
lower.includes("exhausted") ||
|
|
68
68
|
lower.includes("quota") ||
|
|
69
69
|
lower.includes("usage limit") ||
|
|
70
|
+
// xAI SuperGrok: HTTP 403 "run out of credits" / spending-limit is an
|
|
71
|
+
// account-local cap — rotate, don't treat as auth failure.
|
|
72
|
+
lower.includes("run out of credits") ||
|
|
73
|
+
lower.includes("out of credits") ||
|
|
74
|
+
lower.includes("spending-limit") ||
|
|
75
|
+
lower.includes("spending limit") ||
|
|
70
76
|
INSUFFICIENT_BALANCE_PATTERN.test(errorMessage)
|
|
71
77
|
) {
|
|
72
78
|
return "QUOTA_EXHAUSTED";
|
|
@@ -100,7 +106,7 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
100
106
|
|
|
101
107
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
102
108
|
const USAGE_LIMIT_PATTERN =
|
|
103
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)/i;
|
|
109
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)|run out of credits|out of credits|spending[- _]?limit|personal-team-blocked/i;
|
|
104
110
|
|
|
105
111
|
/**
|
|
106
112
|
* HTTP status codes that, absent richer body classification, represent an
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
1
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { type } from "arktype";
|
|
3
4
|
import { captureRequestHeaders, resolvePromptCacheKey } from "../auth-gateway/http";
|
|
@@ -291,6 +292,19 @@ function deriveCacheRetention(data: {
|
|
|
291
292
|
return strongest;
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Inbound `output_config.effort` wire literal → catalog `Effort` (1:1).
|
|
297
|
+
* Values outside this table (none exist in the schema today) are ignored
|
|
298
|
+
* rather than guessed at.
|
|
299
|
+
*/
|
|
300
|
+
const REASONING_EFFORT_BY_WIRE: Partial<Record<string, Effort>> = {
|
|
301
|
+
low: Effort.Low,
|
|
302
|
+
medium: Effort.Medium,
|
|
303
|
+
high: Effort.High,
|
|
304
|
+
xhigh: Effort.XHigh,
|
|
305
|
+
max: Effort.Max,
|
|
306
|
+
};
|
|
307
|
+
|
|
294
308
|
export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
|
|
295
309
|
const data = anthropicMessagesRequestSchema(body);
|
|
296
310
|
if (data instanceof type.errors) {
|
|
@@ -351,6 +365,10 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
|
|
|
351
365
|
if (data.output_config?.task_budget) {
|
|
352
366
|
options.taskBudget = data.output_config.task_budget;
|
|
353
367
|
}
|
|
368
|
+
if (data.output_config?.effort) {
|
|
369
|
+
const mapped = REASONING_EFFORT_BY_WIRE[data.output_config.effort];
|
|
370
|
+
if (mapped !== undefined) options.reasoning = mapped;
|
|
371
|
+
}
|
|
354
372
|
const cacheRetention = deriveCacheRetention(data);
|
|
355
373
|
if (cacheRetention !== undefined) options.cacheRetention = cacheRetention;
|
|
356
374
|
// Anthropic clients commonly send `metadata: { user_id }`; forward verbatim
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
applyResponsesReasoningParams,
|
|
35
35
|
buildResponsesInput,
|
|
36
36
|
createInitialResponsesAssistantMessage,
|
|
37
|
-
|
|
37
|
+
getOpenAIPromptCacheKey,
|
|
38
38
|
isOpenAIResponsesProgressEvent,
|
|
39
39
|
parseAzureDeploymentNameMap,
|
|
40
40
|
processResponsesStream,
|
|
@@ -56,7 +56,7 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?:
|
|
|
56
56
|
|
|
57
57
|
// Azure OpenAI Responses-specific options
|
|
58
58
|
export interface AzureOpenAIResponsesOptions extends StreamOptions {
|
|
59
|
-
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
59
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
60
60
|
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
61
61
|
azureApiVersion?: string;
|
|
62
62
|
azureResourceName?: string;
|
|
@@ -348,7 +348,7 @@ function buildParams(
|
|
|
348
348
|
model: deploymentName,
|
|
349
349
|
input: messages,
|
|
350
350
|
stream: true,
|
|
351
|
-
prompt_cache_key:
|
|
351
|
+
prompt_cache_key: getOpenAIPromptCacheKey(options),
|
|
352
352
|
// Encrypted reasoning replay (applyResponsesReasoningParams) requires
|
|
353
353
|
// stateless responses, matching the openai provider.
|
|
354
354
|
store: false,
|
package/src/providers/ollama.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { transformMessages } from "./transform-messages";
|
|
|
35
35
|
import { joinTextWithImagePlaceholder, partitionVisionContent } from "./vision-guard";
|
|
36
36
|
|
|
37
37
|
export interface OllamaChatOptions extends StreamOptions {
|
|
38
|
-
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
38
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
39
39
|
disableReasoning?: boolean;
|
|
40
40
|
toolChoice?: ToolChoice;
|
|
41
41
|
}
|