@gajae-code/ai 0.17.1 → 0.17.4
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 +117 -0
- package/dist/types/auth-gateway/server.d.ts +23 -1
- package/dist/types/auth-storage.d.ts +12 -1
- package/dist/types/model-thinking.d.ts +10 -6
- package/dist/types/provider-models/openai-compat.d.ts +2 -2
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/cursor.d.ts +10 -0
- package/dist/types/providers/openai-completions.d.ts +9 -1
- package/dist/types/types.d.ts +16 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +10 -0
- package/dist/types/utils/fallback-transport.d.ts +4 -0
- package/dist/types/utils/h2-fetch.d.ts +8 -2
- package/dist/types/utils/stream-repetition-guard.d.ts +107 -0
- package/dist/types/utils/tool-call-healing.d.ts +4 -0
- package/dist/types/utils/tool-fence-strip.d.ts +27 -0
- package/package.json +3 -3
- package/src/auth-gateway/server.ts +48 -9
- package/src/auth-storage.ts +185 -48
- package/src/model-manager.ts +11 -8
- package/src/model-pricing.ts +22 -0
- package/src/model-thinking.d.ts +10 -6
- package/src/model-thinking.ts +93 -11
- package/src/models.json +241 -15
- package/src/provider-models/openai-compat.ts +27 -19
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +10 -2
- package/src/providers/cursor.d.ts +10 -0
- package/src/providers/cursor.ts +176 -31
- package/src/providers/openai-completions.d.ts +9 -1
- package/src/providers/openai-completions.ts +379 -128
- package/src/providers/openai-opencodex-responses.ts +15 -5
- package/src/stream.ts +24 -1
- package/src/types.d.ts +16 -0
- package/src/types.ts +17 -0
- package/src/utils/discovery/openai-compatible.ts +16 -2
- package/src/utils/fallback-transport.d.ts +4 -0
- package/src/utils/fallback-transport.ts +11 -0
- package/src/utils/h2-fetch.ts +70 -7
- package/src/utils/http-inspector.ts +4 -2
- package/src/utils/idle-iterator.ts +109 -96
- package/src/utils/json-parse.ts +12 -4
- package/src/utils/stream-repetition-guard.d.ts +107 -0
- package/src/utils/stream-repetition-guard.ts +290 -0
- package/src/utils/tool-call-healing.d.ts +4 -0
- package/src/utils/tool-call-healing.ts +4 -0
- package/src/utils/tool-fence-strip.d.ts +27 -0
- package/src/utils/tool-fence-strip.ts +64 -0
package/src/auth-storage.ts
CHANGED
|
@@ -481,6 +481,10 @@ export interface AuthCredentialStore {
|
|
|
481
481
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
|
|
482
482
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
483
483
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
484
|
+
/** Atomically claim a cross-process usage poll lease for a bounded interval. */
|
|
485
|
+
tryAcquireUsageFetchLease?(key: string, owner: string, nowMs: number, leaseMs: number): boolean | undefined;
|
|
486
|
+
/** Release a usage poll lease owned by this process. */
|
|
487
|
+
releaseUsageFetchLease?(key: string, owner: string): void;
|
|
484
488
|
/** Atomically allocate a durable sequence for broker restart epochs. */
|
|
485
489
|
allocateMonotonicSequence(key: string, expiresAtSec: number): number;
|
|
486
490
|
deleteCachePrefix?(prefix: string): void;
|
|
@@ -929,6 +933,21 @@ const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>(
|
|
|
929
933
|
]);
|
|
930
934
|
|
|
931
935
|
const USAGE_CACHE_PREFIX = "usage_cache:";
|
|
936
|
+
function hashUsageDiagnosticValue(value: unknown): string | undefined {
|
|
937
|
+
if (typeof value !== "string" || value.length === 0) return undefined;
|
|
938
|
+
return Bun.hash(value).toString(16);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function redactUsageDiagnosticUrl(value: string | undefined): string | undefined {
|
|
942
|
+
if (!value) return undefined;
|
|
943
|
+
try {
|
|
944
|
+
const parsed = new URL(value);
|
|
945
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
946
|
+
} catch {
|
|
947
|
+
return "[invalid-url]";
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
932
951
|
// 5 min stale tolerance. Anthropic / OpenAI rate-limit /usage hard at the IP
|
|
933
952
|
// level so we can't fetch all N credentials every cycle; with a long cache
|
|
934
953
|
// each credential's last-known value sticks visible while peers retry. UI
|
|
@@ -945,6 +964,9 @@ const USAGE_FAILURE_BACKOFF_MS = 10_000;
|
|
|
945
964
|
// Bumped from 3s — Anthropic model usage retries up to 3 times with exponential backoff
|
|
946
965
|
// (~3.5s total worst case); a tight per-request budget aborts retries mid-cycle.
|
|
947
966
|
const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
967
|
+
/** Grace period after the configured provider timeout before peers retry a poll. */
|
|
968
|
+
const USAGE_FETCH_LEASE_GRACE_MS = 5_000;
|
|
969
|
+
const USAGE_FETCH_WAIT_POLL_MS = 25;
|
|
948
970
|
const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
|
|
949
971
|
/** Maximum provider ownership window; expiry recovers a crashed process without indefinite blocking. */
|
|
950
972
|
const OAUTH_REFRESH_LEASE_MS = DEFAULT_OAUTH_REFRESH_TIMEOUT_MS + 5_000;
|
|
@@ -1407,6 +1429,7 @@ export class AuthStorage {
|
|
|
1407
1429
|
#usageReportsInFlight: Map<string, Promise<UsageReport[] | null>> = new Map();
|
|
1408
1430
|
#usageFetch: typeof fetch;
|
|
1409
1431
|
#usageRequestTimeoutMs: number;
|
|
1432
|
+
#usageFetchLeaseMs: number;
|
|
1410
1433
|
#credentialRankingMode: CredentialRankingMode = "balanced";
|
|
1411
1434
|
#usageLogger?: UsageLogger;
|
|
1412
1435
|
#fallbackResolver?: (provider: string) => string | undefined;
|
|
@@ -1459,6 +1482,13 @@ export class AuthStorage {
|
|
|
1459
1482
|
this.#usageCache = new AuthStorageUsageCache(this.#store);
|
|
1460
1483
|
this.#usageFetch = options.usageFetch ?? fetch;
|
|
1461
1484
|
this.#usageRequestTimeoutMs = options.usageRequestTimeoutMs ?? DEFAULT_USAGE_REQUEST_TIMEOUT_MS;
|
|
1485
|
+
const usageTimeoutForLease =
|
|
1486
|
+
typeof this.#usageRequestTimeoutMs === "number" &&
|
|
1487
|
+
Number.isFinite(this.#usageRequestTimeoutMs) &&
|
|
1488
|
+
this.#usageRequestTimeoutMs > 0
|
|
1489
|
+
? this.#usageRequestTimeoutMs
|
|
1490
|
+
: DEFAULT_USAGE_REQUEST_TIMEOUT_MS;
|
|
1491
|
+
this.#usageFetchLeaseMs = usageTimeoutForLease + USAGE_FETCH_LEASE_GRACE_MS;
|
|
1462
1492
|
this.#credentialRankingMode = options.credentialRankingMode ?? "balanced";
|
|
1463
1493
|
this.#refreshOAuthCredentialOverride = options.refreshOAuthCredential;
|
|
1464
1494
|
this.#fetchUsageReportsOverride = options.fetchUsageReports;
|
|
@@ -2009,6 +2039,7 @@ export class AuthStorage {
|
|
|
2009
2039
|
this.#usageRequestInFlight.clear();
|
|
2010
2040
|
this.#usageReportsInFlight.clear();
|
|
2011
2041
|
this.#usageCache.deletePrefix?.(`report:${storageProvider}:`);
|
|
2042
|
+
this.#usageCache.deletePrefix?.("reports:");
|
|
2012
2043
|
for (const [scopeId, selectors] of this.#sessionCredentialSelectors) {
|
|
2013
2044
|
const selector = selectors.get(storageProvider);
|
|
2014
2045
|
const selected = selector
|
|
@@ -2422,6 +2453,7 @@ export class AuthStorage {
|
|
|
2422
2453
|
this.#data.set(provider, credentials);
|
|
2423
2454
|
}
|
|
2424
2455
|
if (identityOrderChanged) this.#resetProviderAssignments(storageProvider);
|
|
2456
|
+
if (!tokenRotationOnly) this.#invalidateUsageCacheForProvider(storageProvider);
|
|
2425
2457
|
if (tokenRotationOnly) this.#bumpGeneration("oauth-token-rotation");
|
|
2426
2458
|
else this.#bumpGeneration("credentials", provider);
|
|
2427
2459
|
}
|
|
@@ -3111,6 +3143,7 @@ export class AuthStorage {
|
|
|
3111
3143
|
this.#usageRequestInFlight.clear();
|
|
3112
3144
|
this.#usageReportsInFlight.clear();
|
|
3113
3145
|
this.#usageCache.deletePrefix?.(`report:${provider}:`);
|
|
3146
|
+
this.#usageCache.deletePrefix?.("reports:");
|
|
3114
3147
|
}
|
|
3115
3148
|
|
|
3116
3149
|
/**
|
|
@@ -4037,6 +4070,8 @@ export class AuthStorage {
|
|
|
4037
4070
|
logDetails: boolean = true,
|
|
4038
4071
|
): Promise<UsageReport | null> {
|
|
4039
4072
|
const cacheKey = this.#buildUsageReportCacheKey(request);
|
|
4073
|
+
const provider = resolveOAuthStorageProvider(request.provider);
|
|
4074
|
+
const credentialGeneration = this.#getProviderGeneration(provider);
|
|
4040
4075
|
const now = Date.now();
|
|
4041
4076
|
const cached = this.#usageCache.get<UsageReport | null>(cacheKey);
|
|
4042
4077
|
// Fresh cache hit: return whatever's there (success or null fallback).
|
|
@@ -4049,6 +4084,7 @@ export class AuthStorage {
|
|
|
4049
4084
|
|
|
4050
4085
|
const promise = (async () => {
|
|
4051
4086
|
const report = await this.#fetchUsageUncached(request, timeoutMs, logDetails);
|
|
4087
|
+
if (this.#getProviderGeneration(provider) !== credentialGeneration) return null;
|
|
4052
4088
|
const ttlJitter = USAGE_REPORT_TTL_MS * (Math.random() * 0.5 - 0.25);
|
|
4053
4089
|
if (report !== null) {
|
|
4054
4090
|
// Success: stagger per-credential cache expiry so all accounts don't
|
|
@@ -4080,6 +4116,51 @@ export class AuthStorage {
|
|
|
4080
4116
|
return promise;
|
|
4081
4117
|
}
|
|
4082
4118
|
|
|
4119
|
+
#captureUsageProviderGenerations(requests: ReadonlyArray<UsageRequestDescriptor>): Map<string, number> {
|
|
4120
|
+
const generations = new Map<string, number>();
|
|
4121
|
+
for (const request of requests) {
|
|
4122
|
+
const provider = resolveOAuthStorageProvider(request.provider);
|
|
4123
|
+
if (!generations.has(provider)) generations.set(provider, this.#getProviderGeneration(provider));
|
|
4124
|
+
}
|
|
4125
|
+
return generations;
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
#usageProviderGenerationsMatch(generations: ReadonlyMap<string, number>): boolean {
|
|
4129
|
+
for (const [provider, generation] of generations) {
|
|
4130
|
+
if (this.#getProviderGeneration(provider) !== generation) return false;
|
|
4131
|
+
}
|
|
4132
|
+
return true;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
#readAggregateUsageCache(cacheKey: string): UsageReport[] | undefined {
|
|
4136
|
+
const cached = this.#usageCache.get<UsageReport[]>(cacheKey);
|
|
4137
|
+
if (!cached || cached.expiresAt <= Date.now() || !Array.isArray(cached.value)) return undefined;
|
|
4138
|
+
return cached.value;
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
#tryAcquireUsageFetchLease(cacheKey: string): boolean | undefined {
|
|
4142
|
+
const claim = this.#store.tryAcquireUsageFetchLease;
|
|
4143
|
+
if (!claim) return undefined;
|
|
4144
|
+
return claim.call(this.#store, cacheKey, this.#oauthRefreshLeaseOwner, Date.now(), this.#usageFetchLeaseMs);
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
async #waitForAggregateUsagePoll(
|
|
4148
|
+
cacheKey: string,
|
|
4149
|
+
): Promise<{ cached: UsageReport[] | undefined; leaseAcquired: boolean }> {
|
|
4150
|
+
const deadline = Date.now() + this.#usageFetchLeaseMs;
|
|
4151
|
+
while (Date.now() < deadline) {
|
|
4152
|
+
const cached = this.#readAggregateUsageCache(cacheKey);
|
|
4153
|
+
if (cached !== undefined) return { cached, leaseAcquired: false };
|
|
4154
|
+
if (this.#tryAcquireUsageFetchLease(cacheKey) === true) return { cached: undefined, leaseAcquired: true };
|
|
4155
|
+
await Bun.sleep(USAGE_FETCH_WAIT_POLL_MS);
|
|
4156
|
+
}
|
|
4157
|
+
return { cached: this.#readAggregateUsageCache(cacheKey), leaseAcquired: false };
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
#releaseUsageFetchLease(cacheKey: string): void {
|
|
4161
|
+
this.#store.releaseUsageFetchLease?.(cacheKey, this.#oauthRefreshLeaseOwner);
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4083
4164
|
#collectUsageRequests(options?: {
|
|
4084
4165
|
provider?: Provider;
|
|
4085
4166
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
@@ -4297,10 +4378,15 @@ export class AuthStorage {
|
|
|
4297
4378
|
async fetchUsageReports(options?: {
|
|
4298
4379
|
provider?: Provider;
|
|
4299
4380
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
4300
|
-
/** Caller
|
|
4381
|
+
/** Caller’s cancel signal; only rejects this caller, never the shared upstream fetch. */
|
|
4301
4382
|
signal?: AbortSignal;
|
|
4302
4383
|
/** Disable provider/account/error logging for secret-safe control surfaces. */
|
|
4303
4384
|
logDetails?: boolean;
|
|
4385
|
+
/**
|
|
4386
|
+
* Enable identity correlation diagnostics. Values are one-way hashed and
|
|
4387
|
+
* URLs are reduced to their origin; the default logs provider/type/count only.
|
|
4388
|
+
*/
|
|
4389
|
+
logIdentity?: boolean;
|
|
4304
4390
|
}): Promise<UsageReport[] | null> {
|
|
4305
4391
|
// Caller override > store-level hook > local per-credential fan-out.
|
|
4306
4392
|
// `RemoteAuthCredentialStore` implements the store hook so a gateway
|
|
@@ -4340,63 +4426,87 @@ export class AuthStorage {
|
|
|
4340
4426
|
const requests = this.#collectUsageRequests(options);
|
|
4341
4427
|
if (requests.length === 0) return [];
|
|
4342
4428
|
|
|
4343
|
-
if (options?.logDetails !== false) {
|
|
4344
|
-
this.#usageLogger?.debug("Usage fetch requested", {
|
|
4345
|
-
providers: [...new Set(requests.map(request => request.provider))].sort(),
|
|
4346
|
-
});
|
|
4347
|
-
}
|
|
4348
|
-
|
|
4349
4429
|
// Per-credential caching with jitter lives in #fetchUsageCached, so we
|
|
4350
|
-
//
|
|
4351
|
-
//
|
|
4352
|
-
//
|
|
4353
|
-
// aggregate cache freezes whichever set landed first).
|
|
4430
|
+
// also retain the completed aggregate in the durable store. The aggregate
|
|
4431
|
+
// cache is what lets another process reuse this poll instead of starting a
|
|
4432
|
+
// second provider fan-out while the first process is still collecting rows.
|
|
4354
4433
|
const cacheKey = this.#buildUsageReportsCacheKey(requests);
|
|
4355
|
-
|
|
4434
|
+
const providerGenerations = this.#captureUsageProviderGenerations(requests);
|
|
4356
4435
|
const inFlight = this.#usageReportsInFlight.get(cacheKey);
|
|
4357
4436
|
if (inFlight) return raceUsageWithSignal(inFlight, options?.signal);
|
|
4358
4437
|
|
|
4359
4438
|
const promise = (async () => {
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4439
|
+
let leaseOwned = false;
|
|
4440
|
+
try {
|
|
4441
|
+
const initialLeaseClaim = this.#tryAcquireUsageFetchLease(cacheKey);
|
|
4442
|
+
const leaseSupported = initialLeaseClaim !== undefined;
|
|
4443
|
+
if (leaseSupported) {
|
|
4444
|
+
leaseOwned = initialLeaseClaim === true;
|
|
4445
|
+
if (!leaseOwned) {
|
|
4446
|
+
const shared = await this.#waitForAggregateUsagePoll(cacheKey);
|
|
4447
|
+
if (shared.cached !== undefined) return shared.cached;
|
|
4448
|
+
leaseOwned = shared.leaseAcquired || this.#tryAcquireUsageFetchLease(cacheKey) === true;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
if (options?.logDetails !== false) {
|
|
4453
|
+
this.#usageLogger?.debug("Usage fetch requested", {
|
|
4454
|
+
providers: [...new Set(requests.map(request => request.provider))].sort(),
|
|
4455
|
+
credentialTypes: [...new Set(requests.map(request => request.credential.type))].sort(),
|
|
4456
|
+
credentials: requests.length,
|
|
4368
4457
|
});
|
|
4458
|
+
if (options?.logIdentity === true) {
|
|
4459
|
+
for (const request of requests) {
|
|
4460
|
+
this.#usageLogger?.debug("Usage fetch queued", {
|
|
4461
|
+
provider: request.provider,
|
|
4462
|
+
credentialType: request.credential.type,
|
|
4463
|
+
baseUrl: redactUsageDiagnosticUrl(request.baseUrl),
|
|
4464
|
+
accountId: hashUsageDiagnosticValue(request.credential.accountId),
|
|
4465
|
+
email: hashUsageDiagnosticValue(request.credential.email),
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4369
4469
|
}
|
|
4370
|
-
}
|
|
4371
4470
|
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4471
|
+
const results = await Promise.all(
|
|
4472
|
+
requests.map(request =>
|
|
4473
|
+
this.#fetchUsageCached(request, this.#usageRequestTimeoutMs, options?.logDetails !== false),
|
|
4474
|
+
),
|
|
4475
|
+
);
|
|
4476
|
+
const reports = results.filter((report): report is UsageReport => report !== null);
|
|
4477
|
+
const resolved = this.#dedupeUsageReports(reports);
|
|
4478
|
+
if (this.#usageProviderGenerationsMatch(providerGenerations)) {
|
|
4479
|
+
// This is a lease-scoped handoff for concurrent processes, not a
|
|
4480
|
+
// long-lived aggregate cache: per-credential jitter remains active
|
|
4481
|
+
// on the next poll.
|
|
4482
|
+
this.#usageCache.set(cacheKey, { value: resolved, expiresAt: Date.now() + this.#usageFetchLeaseMs });
|
|
4483
|
+
}
|
|
4484
|
+
if (options?.logDetails !== false) {
|
|
4485
|
+
this.#usageLogger?.debug("Usage fetch resolved", {
|
|
4486
|
+
reports:
|
|
4487
|
+
options?.logIdentity === true
|
|
4488
|
+
? resolved.map(report => ({
|
|
4489
|
+
provider: report.provider,
|
|
4490
|
+
limits: report.limits.length,
|
|
4491
|
+
account: hashUsageDiagnosticValue(
|
|
4492
|
+
this.#getUsageReportMetadataValue(report, "email") ??
|
|
4493
|
+
this.#getUsageReportMetadataValue(report, "accountId") ??
|
|
4494
|
+
this.#getUsageReportMetadataValue(report, "account") ??
|
|
4495
|
+
this.#getUsageReportMetadataValue(report, "user") ??
|
|
4496
|
+
this.#getUsageReportMetadataValue(report, "username") ??
|
|
4497
|
+
this.#getUsageReportScopeAccountId(report),
|
|
4498
|
+
),
|
|
4499
|
+
}))
|
|
4500
|
+
: {
|
|
4501
|
+
count: resolved.length,
|
|
4502
|
+
providers: [...new Set(resolved.map(report => report.provider))].sort(),
|
|
4503
|
+
},
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
return resolved;
|
|
4507
|
+
} finally {
|
|
4508
|
+
if (leaseOwned) this.#releaseUsageFetchLease(cacheKey);
|
|
4398
4509
|
}
|
|
4399
|
-
return resolved;
|
|
4400
4510
|
})().finally(() => {
|
|
4401
4511
|
if (this.#usageReportsInFlight.get(cacheKey) === promise) {
|
|
4402
4512
|
this.#usageReportsInFlight.delete(cacheKey);
|
|
@@ -6669,6 +6779,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6669
6779
|
#getCacheStmt: Statement;
|
|
6670
6780
|
#getCacheIncludingExpiredStmt: Statement;
|
|
6671
6781
|
#upsertCacheStmt: Statement;
|
|
6782
|
+
#claimUsageFetchLeaseStmt: Statement;
|
|
6783
|
+
#releaseUsageFetchLeaseStmt: Statement;
|
|
6672
6784
|
#deleteCachePrefixStmt: Statement;
|
|
6673
6785
|
#deleteExpiredCacheStmt: Statement;
|
|
6674
6786
|
#closed = false;
|
|
@@ -6718,6 +6830,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6718
6830
|
this.#upsertCacheStmt = this.#db.prepare(
|
|
6719
6831
|
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
|
|
6720
6832
|
);
|
|
6833
|
+
this.#claimUsageFetchLeaseStmt = this.#db.prepare(
|
|
6834
|
+
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at WHERE cache.expires_at <= ?",
|
|
6835
|
+
);
|
|
6836
|
+
this.#releaseUsageFetchLeaseStmt = this.#db.prepare("DELETE FROM cache WHERE key = ? AND value = ?");
|
|
6721
6837
|
this.#deleteCachePrefixStmt = this.#db.prepare("DELETE FROM cache WHERE substr(key, 1, ?) = ?");
|
|
6722
6838
|
this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
|
|
6723
6839
|
}
|
|
@@ -7431,6 +7547,27 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7431
7547
|
}
|
|
7432
7548
|
}
|
|
7433
7549
|
|
|
7550
|
+
tryAcquireUsageFetchLease(key: string, owner: string, nowMs: number, leaseMs: number): boolean | undefined {
|
|
7551
|
+
try {
|
|
7552
|
+
const nowSec = Math.floor(nowMs / 1000);
|
|
7553
|
+
const expiresAtSec = Math.ceil((nowMs + leaseMs) / 1000);
|
|
7554
|
+
const result = this.#claimUsageFetchLeaseStmt.run(`usage_fetch_lease:${key}`, owner, expiresAtSec, nowSec) as {
|
|
7555
|
+
changes: number;
|
|
7556
|
+
};
|
|
7557
|
+
return result.changes === 1;
|
|
7558
|
+
} catch {
|
|
7559
|
+
return undefined;
|
|
7560
|
+
}
|
|
7561
|
+
}
|
|
7562
|
+
|
|
7563
|
+
releaseUsageFetchLease(key: string, owner: string): void {
|
|
7564
|
+
try {
|
|
7565
|
+
this.#releaseUsageFetchLeaseStmt.run(`usage_fetch_lease:${key}`, owner);
|
|
7566
|
+
} catch {
|
|
7567
|
+
// Ignore cache lease cleanup failures; the bounded expiry recovers it.
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7570
|
+
|
|
7434
7571
|
allocateMonotonicSequence(key: string, expiresAtSec: number): number {
|
|
7435
7572
|
const allocate = this.#db.transaction(() => {
|
|
7436
7573
|
const row = this.#getCacheIncludingExpiredStmt.get(key) as { value?: string } | undefined;
|
package/src/model-manager.ts
CHANGED
|
@@ -553,15 +553,18 @@ function fingerprintStatic<TApi extends Api>(models: readonly Model<TApi>[]): st
|
|
|
553
553
|
|
|
554
554
|
function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamicModel: Model<TApi>): Model<TApi> {
|
|
555
555
|
const supportsImage = existingModel.input.includes("image") || dynamicModel.input.includes("image");
|
|
556
|
-
// Before
|
|
556
|
+
// Before these exact OpenCode models were curated, ID-only discovery cached the
|
|
557
557
|
// non-reasoning Completions defaults. A fresh authoritative cache can skip
|
|
558
558
|
// discovery after an upgrade, so recover its limits from reviewed static
|
|
559
|
-
//
|
|
559
|
+
// metadata here. Do not reinterpret individual numeric limits or
|
|
560
560
|
// apply this correction to reviewed discovery rows or other model IDs.
|
|
561
|
-
const
|
|
562
|
-
existingModel.provider === "opencode-go" &&
|
|
563
|
-
|
|
564
|
-
|
|
561
|
+
const hasPreReviewOpenCodeLimits =
|
|
562
|
+
((existingModel.provider === "opencode-go" &&
|
|
563
|
+
existingModel.id === "muse-spark-1.3-contributor" &&
|
|
564
|
+
existingModel.api === "openai-responses") ||
|
|
565
|
+
((existingModel.provider === "opencode-go" || existingModel.provider === "opencode-zen") &&
|
|
566
|
+
existingModel.id === "union-alpha" &&
|
|
567
|
+
existingModel.api === "anthropic-messages")) &&
|
|
565
568
|
existingModel.reasoning &&
|
|
566
569
|
dynamicModel.api === "openai-completions" &&
|
|
567
570
|
!dynamicModel.reasoning &&
|
|
@@ -594,10 +597,10 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
594
597
|
cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead),
|
|
595
598
|
cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite),
|
|
596
599
|
},
|
|
597
|
-
contextWindow:
|
|
600
|
+
contextWindow: hasPreReviewOpenCodeLimits
|
|
598
601
|
? existingModel.contextWindow
|
|
599
602
|
: preferDiscoveryLimit(dynamicModel.contextWindow, existingModel.contextWindow),
|
|
600
|
-
maxTokens:
|
|
603
|
+
maxTokens: hasPreReviewOpenCodeLimits
|
|
601
604
|
? existingModel.maxTokens
|
|
602
605
|
: preferDiscoveryLimit(dynamicModel.maxTokens, existingModel.maxTokens),
|
|
603
606
|
headers: dynamicModel.headers ? { ...existingModel.headers, ...dynamicModel.headers } : existingModel.headers,
|
package/src/model-pricing.ts
CHANGED
|
@@ -25,9 +25,31 @@ const GPT_6_ASTRA_PRICING: TieredPricing = {
|
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
// GPT-6 Sol: $2/$10 standard, cache read $0.20, cache write $2.50; inputs past
|
|
29
|
+
// 272K apply 2x to input/cache and 1.5x to output.
|
|
30
|
+
const GPT_6_SOL_PRICING: TieredPricing = {
|
|
31
|
+
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
|
32
|
+
longContextPricing: {
|
|
33
|
+
threshold: LONG_CONTEXT_THRESHOLD,
|
|
34
|
+
cost: { input: 4, output: 15, cacheRead: 0.4, cacheWrite: 5 },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// GPT-6 Luna: $0.10/$0.50 standard, cache read $0.01, cache write $0.125;
|
|
39
|
+
// inputs past 272K apply 2x to input/cache and 1.5x to output.
|
|
40
|
+
const GPT_6_LUNA_PRICING: TieredPricing = {
|
|
41
|
+
cost: { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 },
|
|
42
|
+
longContextPricing: {
|
|
43
|
+
threshold: LONG_CONTEXT_THRESHOLD,
|
|
44
|
+
cost: { input: 0.2, output: 0.75, cacheRead: 0.02, cacheWrite: 0.25 },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
28
48
|
// OpenAI Standard pricing: https://developers.openai.com/api/docs/pricing
|
|
29
49
|
const OPENAI_GPT_5_6_PRICING: ReadonlyMap<string, TieredPricing> = new Map([
|
|
30
50
|
["gpt-6-astra", GPT_6_ASTRA_PRICING],
|
|
51
|
+
["gpt-6-sol", GPT_6_SOL_PRICING],
|
|
52
|
+
["gpt-6-luna", GPT_6_LUNA_PRICING],
|
|
31
53
|
["gpt-5.6", GPT_5_6_SOL_PRICING],
|
|
32
54
|
["gpt-5.6-sol", GPT_5_6_SOL_PRICING],
|
|
33
55
|
[
|
package/src/model-thinking.d.ts
CHANGED
|
@@ -28,14 +28,18 @@ export declare function enrichModelThinking<TApi extends Api>(model: ApiModel<TA
|
|
|
28
28
|
* canonical rules, replacing any existing `thinking`.
|
|
29
29
|
*/
|
|
30
30
|
export declare function refreshModelThinking<TApi extends Api>(model: ApiModel<TApi>): ApiModel<TApi>;
|
|
31
|
+
/**
|
|
32
|
+
* Native MiniMax thinking semantics, scoped to first-party regional routes.
|
|
33
|
+
* M3 supports adaptive/disabled; M2.x always thinks, even when disabled is sent.
|
|
34
|
+
* https://platform.minimax.io/docs/api-reference/text-openai-api#thinking-control
|
|
35
|
+
* https://platform.minimax.io/docs/api-reference/text-anthropic-api#thinking-control
|
|
36
|
+
*/
|
|
37
|
+
export declare function getMiniMaxThinkingMode(model: ApiModel<Api>, resolvedBaseUrl?: string): "toggle" | "always-on" | undefined;
|
|
31
38
|
/**
|
|
32
39
|
* Returns whether the configured transport has an audited user-facing reasoning control.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* endpoints must opt in with `compat.supportsReasoningEffort: true`; providers using a
|
|
37
|
-
* non-OpenAI request shape must also declare `compat.thinkingFormat`. Bundled providers
|
|
38
|
-
* remain governed by their catalog and compatibility metadata.
|
|
40
|
+
* Custom OpenAI-compatible endpoints must opt in with supportsReasoningEffort and,
|
|
41
|
+
* for non-OpenAI request shapes, thinkingFormat. Native MiniMax switches are
|
|
42
|
+
* separate from reasoning_effort, which those endpoints do not support.
|
|
39
43
|
*/
|
|
40
44
|
export declare function modelSupportsReasoningControl<TApi extends Api>(model: ApiModel<TApi>, resolvedBaseUrl?: string): boolean;
|
|
41
45
|
/**
|
package/src/model-thinking.ts
CHANGED
|
@@ -66,6 +66,17 @@ const GPT_5_6_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effo
|
|
|
66
66
|
const GPT_5_5_DEFAULT_EFFORT = Effort.XHigh;
|
|
67
67
|
const KIMI_K3_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max];
|
|
68
68
|
const DEEPSEEK_V4_FLASH_0731_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max];
|
|
69
|
+
const ALIBABA_GLM_53_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max];
|
|
70
|
+
const ALIBABA_DEEPSEEK_V4_PINNED_IDS = new Set([
|
|
71
|
+
"deepseek-v4-flash-0731",
|
|
72
|
+
"deepseek-v4-pro-0813",
|
|
73
|
+
"deepseek-v4.1-flash",
|
|
74
|
+
]);
|
|
75
|
+
const ALIBABA_DEEPSEEK_V4_PINNED_NAMES: Record<string, string> = {
|
|
76
|
+
"deepseek-v4-flash-0731": "DeepSeek V4 Flash 0731",
|
|
77
|
+
"deepseek-v4-pro-0813": "DeepSeek V4 Pro 0813",
|
|
78
|
+
"deepseek-v4.1-flash": "DeepSeek V4.1 Flash",
|
|
79
|
+
};
|
|
69
80
|
const GROK_4_5_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
|
|
70
81
|
const GROK_4_6_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
|
71
82
|
const GROK_4_20_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
@@ -181,7 +192,7 @@ export function enrichModelThinking<TApi extends Api>(model: ApiModel<TApi>): Ap
|
|
|
181
192
|
if (cached !== undefined) {
|
|
182
193
|
return cached as ApiModel<TApi>;
|
|
183
194
|
}
|
|
184
|
-
const normalizedThinking = normalizeThinkingConfig(model.thinking);
|
|
195
|
+
const normalizedThinking = getMiniMaxThinkingMode(model) ? undefined : normalizeThinkingConfig(model.thinking);
|
|
185
196
|
let result: ApiModel<TApi>;
|
|
186
197
|
if (isGroqCompoundReasoningUnsupported(model)) {
|
|
187
198
|
result =
|
|
@@ -228,20 +239,61 @@ export function refreshModelThinking<TApi extends Api>(model: ApiModel<TApi>): A
|
|
|
228
239
|
return { ...model, thinking: inferModelThinking(model) };
|
|
229
240
|
}
|
|
230
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Native MiniMax thinking semantics, scoped to first-party regional routes.
|
|
244
|
+
* M3 supports adaptive/disabled; M2.x always thinks, even when disabled is sent.
|
|
245
|
+
* https://platform.minimax.io/docs/api-reference/text-openai-api#thinking-control
|
|
246
|
+
* https://platform.minimax.io/docs/api-reference/text-anthropic-api#thinking-control
|
|
247
|
+
*/
|
|
248
|
+
export function getMiniMaxThinkingMode(
|
|
249
|
+
model: ApiModel<Api>,
|
|
250
|
+
resolvedBaseUrl?: string,
|
|
251
|
+
): "toggle" | "always-on" | undefined {
|
|
252
|
+
const isDirectRoute =
|
|
253
|
+
(model.api === "anthropic-messages" && (model.provider === "minimax" || model.provider === "minimax-cn")) ||
|
|
254
|
+
(model.api === "openai-completions" &&
|
|
255
|
+
(model.provider === "minimax-code" || model.provider === "minimax-code-cn"));
|
|
256
|
+
if (!isDirectRoute) return undefined;
|
|
257
|
+
// Provider identity survives baseUrl overrides. Only the normalized native
|
|
258
|
+
// endpoint, not a provider label or a matching hostname suffix, proves this contract.
|
|
259
|
+
try {
|
|
260
|
+
const endpoint = new URL(resolvedBaseUrl ?? model.baseUrl);
|
|
261
|
+
const host = model.provider.endsWith("-cn") ? "api.minimaxi.com" : "api.minimax.io";
|
|
262
|
+
const path = endpoint.pathname.replace(/\/+$/, "");
|
|
263
|
+
const validPath =
|
|
264
|
+
model.api === "anthropic-messages" ? path === "/anthropic" || path === "/anthropic/v1" : path === "/v1";
|
|
265
|
+
if (
|
|
266
|
+
endpoint.origin !== `https://${host}` ||
|
|
267
|
+
!validPath ||
|
|
268
|
+
endpoint.username ||
|
|
269
|
+
endpoint.password ||
|
|
270
|
+
endpoint.search ||
|
|
271
|
+
endpoint.hash
|
|
272
|
+
)
|
|
273
|
+
return undefined;
|
|
274
|
+
} catch {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
if (model.id === "MiniMax-M3" || model.id === "MiniMax-M3[1m]") return "toggle";
|
|
278
|
+
if (/^MiniMax-M2(?:[.\-[]|$)/.test(model.id)) return "always-on";
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
231
282
|
/**
|
|
232
283
|
* Returns whether the configured transport has an audited user-facing reasoning control.
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* endpoints must opt in with `compat.supportsReasoningEffort: true`; providers using a
|
|
237
|
-
* non-OpenAI request shape must also declare `compat.thinkingFormat`. Bundled providers
|
|
238
|
-
* remain governed by their catalog and compatibility metadata.
|
|
284
|
+
* Custom OpenAI-compatible endpoints must opt in with supportsReasoningEffort and,
|
|
285
|
+
* for non-OpenAI request shapes, thinkingFormat. Native MiniMax switches are
|
|
286
|
+
* separate from reasoning_effort, which those endpoints do not support.
|
|
239
287
|
*/
|
|
240
288
|
export function modelSupportsReasoningControl<TApi extends Api>(
|
|
241
289
|
model: ApiModel<TApi>,
|
|
242
290
|
resolvedBaseUrl?: string,
|
|
243
291
|
): boolean {
|
|
244
292
|
if (!model.reasoning) return false;
|
|
293
|
+
// MiniMax's native thinking switch is not OpenAI reasoning_effort. M2.x
|
|
294
|
+
// always thinks; M3 supports adaptive/disabled, but no effort or budget.
|
|
295
|
+
const miniMaxMode = getMiniMaxThinkingMode(model, resolvedBaseUrl);
|
|
296
|
+
if (miniMaxMode) return miniMaxMode === "toggle";
|
|
245
297
|
if (model.api === "openai-completions") {
|
|
246
298
|
const completionsModel = model as ApiModel<"openai-completions">;
|
|
247
299
|
const explicitSupport = completionsModel.compat?.supportsReasoningEffort;
|
|
@@ -279,9 +331,13 @@ export function applyGeneratedModelPolicies(models: ApiModel<Api>[]): void {
|
|
|
279
331
|
if (source.provider === "xai" && (source.id === "grok-4.5" || source.id === "grok-4.6")) {
|
|
280
332
|
source.reasoning = true;
|
|
281
333
|
}
|
|
282
|
-
if (source.provider === "alibaba-token-plan" && source.id
|
|
334
|
+
if (source.provider === "alibaba-token-plan" && ALIBABA_DEEPSEEK_V4_PINNED_IDS.has(source.id)) {
|
|
283
335
|
source.reasoning = true;
|
|
284
|
-
source.name =
|
|
336
|
+
source.name = ALIBABA_DEEPSEEK_V4_PINNED_NAMES[source.id] ?? source.name;
|
|
337
|
+
}
|
|
338
|
+
if (source.provider === "alibaba-token-plan" && source.id === "glm-5.3") {
|
|
339
|
+
source.reasoning = true;
|
|
340
|
+
source.name = "GLM-5.3";
|
|
285
341
|
}
|
|
286
342
|
if (source.id.split("/").at(-1)?.toLowerCase() === "muse-spark-1.2") {
|
|
287
343
|
source.reasoning = true;
|
|
@@ -628,7 +684,7 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
|
|
|
628
684
|
levels: [Effort.Low, Effort.High, Effort.Max],
|
|
629
685
|
};
|
|
630
686
|
}
|
|
631
|
-
if (model.provider === "alibaba-token-plan" && model.id
|
|
687
|
+
if (model.provider === "alibaba-token-plan" && ALIBABA_DEEPSEEK_V4_PINNED_IDS.has(model.id)) {
|
|
632
688
|
model.contextWindow = 1_000_000;
|
|
633
689
|
model.maxTokens = 384_000;
|
|
634
690
|
model.compat = {
|
|
@@ -639,6 +695,24 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
|
|
|
639
695
|
requiresReasoningContentForToolCalls: true,
|
|
640
696
|
};
|
|
641
697
|
}
|
|
698
|
+
if (model.provider === "alibaba-token-plan" && model.id === "glm-5.3") {
|
|
699
|
+
model.contextWindow = 1_000_000;
|
|
700
|
+
model.maxTokens = 131_072;
|
|
701
|
+
model.compat = {
|
|
702
|
+
...(model.compat ?? {}),
|
|
703
|
+
supportsDeveloperRole: false,
|
|
704
|
+
supportsReasoningEffort: true,
|
|
705
|
+
reasoningContentField: "reasoning_content",
|
|
706
|
+
requiresReasoningContentForToolCalls: true,
|
|
707
|
+
};
|
|
708
|
+
model.thinking = {
|
|
709
|
+
mode: "effort",
|
|
710
|
+
minLevel: Effort.Low,
|
|
711
|
+
maxLevel: Effort.Max,
|
|
712
|
+
defaultLevel: Effort.Max,
|
|
713
|
+
levels: [Effort.Low, Effort.High, Effort.Max],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
642
716
|
if (model.provider === "xai" && (model.id === "grok-4.5" || model.id === "grok-4.6")) {
|
|
643
717
|
model.maxTokens = Math.min(model.maxTokens, 64_000);
|
|
644
718
|
}
|
|
@@ -777,6 +851,11 @@ function inferDefaultEffort<TApi extends Api>(model: ApiModel<TApi>, parsedModel
|
|
|
777
851
|
}
|
|
778
852
|
|
|
779
853
|
function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingConfig {
|
|
854
|
+
if (getMiniMaxThinkingMode(model) === "toggle") {
|
|
855
|
+
// The existing persisted effort value represents the single enabled state;
|
|
856
|
+
// UI consumers label it "on", and transports send no effort or budget.
|
|
857
|
+
return { mode: "effort", minLevel: Effort.High, maxLevel: Effort.High };
|
|
858
|
+
}
|
|
780
859
|
const parsedModel = parseKnownModel(model.id);
|
|
781
860
|
const efforts = inferSupportedEfforts(parsedModel, model);
|
|
782
861
|
const minLevel = efforts[0];
|
|
@@ -852,9 +931,12 @@ function inferSupportedEfforts<TApi extends Api>(parsedModel: ParsedModel, model
|
|
|
852
931
|
if (model.provider === "kimi-code" && model.id === "k3") {
|
|
853
932
|
return KIMI_K3_EFFORTS;
|
|
854
933
|
}
|
|
855
|
-
if (model.provider === "alibaba-token-plan" && model.id
|
|
934
|
+
if (model.provider === "alibaba-token-plan" && ALIBABA_DEEPSEEK_V4_PINNED_IDS.has(model.id)) {
|
|
856
935
|
return DEEPSEEK_V4_FLASH_0731_EFFORTS;
|
|
857
936
|
}
|
|
937
|
+
if (model.provider === "alibaba-token-plan" && model.id === "glm-5.3") {
|
|
938
|
+
return ALIBABA_GLM_53_EFFORTS;
|
|
939
|
+
}
|
|
858
940
|
switch (parsedModel.family) {
|
|
859
941
|
case "openai":
|
|
860
942
|
return inferOpenAISupportedEfforts(parsedModel);
|