@oh-my-pi/pi-ai 17.3.2 → 17.3.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 +13 -0
- package/dist/types/auth-broker/client.d.ts +8 -1
- package/dist/types/auth-storage.d.ts +3 -4
- package/dist/types/error/flags.d.ts +1 -0
- package/dist/types/error/provider.d.ts +2 -0
- package/package.json +5 -5
- package/src/auth-broker/client.ts +24 -7
- package/src/auth-broker/remote-store.ts +48 -3
- package/src/auth-broker/server.ts +1 -1
- package/src/auth-storage.ts +146 -47
- package/src/error/flags.ts +11 -1
- package/src/error/provider.ts +6 -5
- package/src/providers/google-gemini-cli.ts +32 -18
- package/src/registry/alibaba-token-plan.ts +6 -2
- package/src/registry/oauth/github-copilot.ts +1 -22
- package/src/usage/alibaba-token-plan.ts +89 -34
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.4] - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `omp usage invalidate` to discard stale OAuth and API-key usage snapshots, then force a cache-bypassing, per-provider serialized refresh with a broker request budget sized for the full unfiltered account batch, so upgraded subscriptions do not silently retain pre-change quota data.
|
|
10
|
+
- Fixed quota reporting and Cookie capture guidance for China (Beijing) Alibaba Token Plan credentials ([#8509](https://github.com/can1357/oh-my-pi/issues/8509)).
|
|
11
|
+
|
|
12
|
+
## [17.3.3] - 2026-08-14
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Distinguished Gemini thought-only `STOP` responses from empty transports, avoiding repeated identical reasoning requests and duplicate Antigravity endpoint streams while surfacing the missing final output for session-level recovery.
|
|
17
|
+
|
|
5
18
|
## [17.3.2] - 2026-08-13
|
|
6
19
|
|
|
7
20
|
### Fixed
|
|
@@ -66,7 +66,14 @@ export declare class AuthBrokerClient {
|
|
|
66
66
|
openSnapshotStream(opts?: {
|
|
67
67
|
signal?: AbortSignal;
|
|
68
68
|
}): AsyncGenerator<SnapshotStreamEvent>;
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Fetch aggregate broker usage with a timeout sized for serialized
|
|
71
|
+
* same-provider account probes.
|
|
72
|
+
*/
|
|
73
|
+
fetchUsage(options?: {
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
maxAccountsPerProvider?: number;
|
|
76
|
+
}): Promise<UsageResponse>;
|
|
70
77
|
/** Recorded usage-limit snapshots from the broker host, oldest first. */
|
|
71
78
|
fetchUsageHistory(query?: {
|
|
72
79
|
sinceMs?: number;
|
|
@@ -1111,10 +1111,9 @@ export declare class AuthStorage {
|
|
|
1111
1111
|
signal?: AbortSignal;
|
|
1112
1112
|
}): Promise<ResetCreditRedeemOutcome>;
|
|
1113
1113
|
/**
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
* store are invalidated.
|
|
1114
|
+
* Discard cached usage reports before a user-requested refresh. The next
|
|
1115
|
+
* read probes upstream serially per provider; a failure reports no fresh
|
|
1116
|
+
* usage instead of replaying an invalidated last-good snapshot.
|
|
1118
1117
|
*/
|
|
1119
1118
|
invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void>;
|
|
1120
1119
|
invalidateCredentialMatching(provider: string, apiKey: string, options?: InvalidateCredentialMatchingOptions): Promise<boolean>;
|
|
@@ -8,6 +8,7 @@ export declare const Flag: {
|
|
|
8
8
|
readonly StaleResponsesItem: 1048576;
|
|
9
9
|
readonly MalformedFunctionCall: 2097152;
|
|
10
10
|
readonly ProviderFinishError: 4194304;
|
|
11
|
+
readonly EmptyResponse: 8192;
|
|
11
12
|
readonly ContentBlocked: 32768;
|
|
12
13
|
/** Account-scoped provider policy denial that may succeed with another credential. */
|
|
13
14
|
readonly AccountPolicy: 16384;
|
|
@@ -7,6 +7,8 @@ export type ProviderResponseErrorKind =
|
|
|
7
7
|
| "output"
|
|
8
8
|
/** Response body was empty/missing when content was required. */
|
|
9
9
|
| "empty-body"
|
|
10
|
+
/** Response completed without actionable output (for example, thoughts only). */
|
|
11
|
+
| "empty-output"
|
|
10
12
|
/** Malformed wire envelope (unexpected message ordering / shape). */
|
|
11
13
|
| "envelope"
|
|
12
14
|
/** Content was blocked by a provider safety filter. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.4",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/omptype": "17.3.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
43
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
44
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
41
|
+
"@oh-my-pi/omptype": "17.3.4",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "17.3.4",
|
|
43
|
+
"@oh-my-pi/pi-utils": "17.3.4",
|
|
44
|
+
"@oh-my-pi/pi-wire": "17.3.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@bufbuild/protoc-gen-es": "^2.12.1",
|
|
@@ -249,12 +249,23 @@ export class AuthBrokerClient {
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Fetch aggregate broker usage with a timeout sized for serialized
|
|
254
|
+
* same-provider account probes.
|
|
255
|
+
*/
|
|
256
|
+
fetchUsage(options: { signal?: AbortSignal; maxAccountsPerProvider?: number } = {}): Promise<UsageResponse> {
|
|
257
|
+
const requestedAccountCount = options.maxAccountsPerProvider;
|
|
258
|
+
const accountCount =
|
|
259
|
+
typeof requestedAccountCount === "number" && Number.isFinite(requestedAccountCount)
|
|
260
|
+
? Math.max(1, Math.floor(requestedAccountCount))
|
|
261
|
+
: 1;
|
|
262
|
+
const perAccountTimeoutMs = Math.max(DEFAULT_TIMEOUT_MS, this.#timeoutMs);
|
|
263
|
+
const timeoutMs = perAccountTimeoutMs * (accountCount + 1);
|
|
264
|
+
return this.#request<UsageResponse>("GET", "/v1/usage", {
|
|
265
|
+
schema: "usageResponseSchema",
|
|
266
|
+
signal: options.signal,
|
|
267
|
+
timeoutMs,
|
|
268
|
+
});
|
|
258
269
|
}
|
|
259
270
|
|
|
260
271
|
/** Recorded usage-limit snapshots from the broker host, oldest first. */
|
|
@@ -369,7 +380,13 @@ export class AuthBrokerClient {
|
|
|
369
380
|
async #request<t>(
|
|
370
381
|
method: "GET" | "POST" | "DELETE",
|
|
371
382
|
path: string,
|
|
372
|
-
opts: {
|
|
383
|
+
opts: {
|
|
384
|
+
schema: AuthBrokerResponseSchemaName;
|
|
385
|
+
auth?: boolean;
|
|
386
|
+
body?: unknown;
|
|
387
|
+
signal?: AbortSignal;
|
|
388
|
+
timeoutMs?: number;
|
|
389
|
+
},
|
|
373
390
|
): Promise<t> {
|
|
374
391
|
const response = await this.#fetchRaw(method, path, opts);
|
|
375
392
|
const text = await response.text();
|
|
@@ -253,6 +253,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
253
253
|
#usageInflight?: Promise<UsageReport[] | null>;
|
|
254
254
|
#credentialBlockReconcileAfter: Map<string, number> = new Map();
|
|
255
255
|
#usageCacheEpoch = 0;
|
|
256
|
+
/** Raw broker credentials retained to size aggregate usage requests before account-pool filtering. */
|
|
257
|
+
#brokerUsageProviderByCredentialId = new Map<number, Provider>();
|
|
258
|
+
#brokerUsageAccountCounts = new Map<Provider, number>();
|
|
256
259
|
/** Per-snapshot lookup of oauth credentials by provider; rebuilt when `#snapshot` is replaced. */
|
|
257
260
|
#usageFilterLookup?: { snapshot: SnapshotResponse; byProvider: Map<Provider, OAuthCredential[]> };
|
|
258
261
|
/** Memoized `#filterUsageReports` output, keyed on (input identity, lookup identity). */
|
|
@@ -296,6 +299,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
296
299
|
|
|
297
300
|
#applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void {
|
|
298
301
|
const nowMs = Date.now();
|
|
302
|
+
this.#replaceBrokerUsageAccounts(snapshot.credentials);
|
|
299
303
|
const previousCredentials = this.#snapshot.credentials;
|
|
300
304
|
const credentials = snapshot.credentials
|
|
301
305
|
.filter(entry => isCredentialInAccountPool(entry, this.#accountPool))
|
|
@@ -427,8 +431,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
427
431
|
generation: number,
|
|
428
432
|
serverNowMs: number,
|
|
429
433
|
): void {
|
|
434
|
+
this.#upsertBrokerUsageAccount(entry);
|
|
430
435
|
if (!isCredentialInAccountPool(entry, this.#accountPool)) {
|
|
431
|
-
this.#removeStreamCredential(entry.id, refresher, generation, serverNowMs);
|
|
436
|
+
this.#removeStreamCredential(entry.id, refresher, generation, serverNowMs, { retainBrokerUsageAccount: true });
|
|
432
437
|
return;
|
|
433
438
|
}
|
|
434
439
|
const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
|
|
@@ -446,7 +451,14 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
446
451
|
this.#snapshotReceivedAt = Date.now();
|
|
447
452
|
}
|
|
448
453
|
|
|
449
|
-
#removeStreamCredential(
|
|
454
|
+
#removeStreamCredential(
|
|
455
|
+
id: number,
|
|
456
|
+
refresher: RefresherSchedule,
|
|
457
|
+
generation: number,
|
|
458
|
+
serverNowMs: number,
|
|
459
|
+
options?: { retainBrokerUsageAccount?: boolean },
|
|
460
|
+
): void {
|
|
461
|
+
if (!options?.retainBrokerUsageAccount) this.#removeBrokerUsageAccount(id);
|
|
450
462
|
const removed = this.#snapshot.credentials.find(entry => entry.id === id);
|
|
451
463
|
if (removed?.blocks && removed.blocks.length > 0) this.#invalidateUsageCache();
|
|
452
464
|
const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id);
|
|
@@ -1066,6 +1078,39 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1066
1078
|
});
|
|
1067
1079
|
}
|
|
1068
1080
|
|
|
1081
|
+
#replaceBrokerUsageAccounts(entries: readonly SnapshotEntry[]): void {
|
|
1082
|
+
this.#brokerUsageProviderByCredentialId.clear();
|
|
1083
|
+
this.#brokerUsageAccountCounts.clear();
|
|
1084
|
+
for (const entry of entries) this.#upsertBrokerUsageAccount(entry);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
#upsertBrokerUsageAccount(entry: Pick<SnapshotEntry, "id" | "provider">): void {
|
|
1088
|
+
const previous = this.#brokerUsageProviderByCredentialId.get(entry.id);
|
|
1089
|
+
if (previous === entry.provider) return;
|
|
1090
|
+
if (previous !== undefined) {
|
|
1091
|
+
const count = this.#brokerUsageAccountCounts.get(previous) ?? 0;
|
|
1092
|
+
if (count <= 1) this.#brokerUsageAccountCounts.delete(previous);
|
|
1093
|
+
else this.#brokerUsageAccountCounts.set(previous, count - 1);
|
|
1094
|
+
}
|
|
1095
|
+
this.#brokerUsageProviderByCredentialId.set(entry.id, entry.provider);
|
|
1096
|
+
this.#brokerUsageAccountCounts.set(entry.provider, (this.#brokerUsageAccountCounts.get(entry.provider) ?? 0) + 1);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
#removeBrokerUsageAccount(id: number): void {
|
|
1100
|
+
const provider = this.#brokerUsageProviderByCredentialId.get(id);
|
|
1101
|
+
if (provider === undefined) return;
|
|
1102
|
+
this.#brokerUsageProviderByCredentialId.delete(id);
|
|
1103
|
+
const count = this.#brokerUsageAccountCounts.get(provider) ?? 0;
|
|
1104
|
+
if (count <= 1) this.#brokerUsageAccountCounts.delete(provider);
|
|
1105
|
+
else this.#brokerUsageAccountCounts.set(provider, count - 1);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
#maxBrokerUsageAccounts(): number {
|
|
1109
|
+
let maximum = 1;
|
|
1110
|
+
for (const count of this.#brokerUsageAccountCounts.values()) maximum = Math.max(maximum, count);
|
|
1111
|
+
return maximum;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1069
1114
|
#loadUsageReports(): Promise<UsageReport[] | null> {
|
|
1070
1115
|
const cached = this.#usageCache;
|
|
1071
1116
|
if (cached && Date.now() - cached.fetchedAt < USAGE_CACHE_TTL_MS) {
|
|
@@ -1074,7 +1119,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
1074
1119
|
if (this.#usageInflight) return this.#usageInflight;
|
|
1075
1120
|
const epoch = this.#usageCacheEpoch;
|
|
1076
1121
|
const inflight = this.#client
|
|
1077
|
-
.fetchUsage()
|
|
1122
|
+
.fetchUsage({ maxAccountsPerProvider: this.#maxBrokerUsageAccounts() })
|
|
1078
1123
|
.then(body => {
|
|
1079
1124
|
if (epoch !== this.#usageCacheEpoch) return this.#loadUsageReports();
|
|
1080
1125
|
this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
|
|
@@ -741,7 +741,7 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
741
741
|
}
|
|
742
742
|
if (req.method === "POST" && pathname === "/v1/usage/stale") {
|
|
743
743
|
try {
|
|
744
|
-
opts.storage.invalidateUsageCache?.();
|
|
744
|
+
await opts.storage.invalidateUsageCache?.();
|
|
745
745
|
logger.info("auth-broker usage cache invalidated", { peer });
|
|
746
746
|
return json(200, { ok: true });
|
|
747
747
|
} catch (error) {
|
package/src/auth-storage.ts
CHANGED
|
@@ -677,6 +677,7 @@ const DEFAULT_USAGE_PROVIDER_MAP = new Map<Provider, UsageProvider>(
|
|
|
677
677
|
);
|
|
678
678
|
|
|
679
679
|
const USAGE_CACHE_PREFIX = "usage_cache:";
|
|
680
|
+
const USAGE_FORCE_REFRESH_CACHE_PREFIX = "force-refresh:";
|
|
680
681
|
const USAGE_HEADER_INGEST_INTERVAL_MS = 60_000;
|
|
681
682
|
const USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000;
|
|
682
683
|
/**
|
|
@@ -686,6 +687,12 @@ const USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000;
|
|
|
686
687
|
* on the next poll.
|
|
687
688
|
*/
|
|
688
689
|
const USAGE_FAILURE_BACKOFF_MS = 10_000;
|
|
690
|
+
/**
|
|
691
|
+
* A manual invalidation persists across the next CLI process and serializes
|
|
692
|
+
* same-provider probes, avoiding a cold-account burst against IP-limited
|
|
693
|
+
* upstream usage endpoints.
|
|
694
|
+
*/
|
|
695
|
+
const USAGE_FORCE_REFRESH_TTL_MS = 5 * 60_000;
|
|
689
696
|
// Bumped from 3s — Claude usage retries up to 3 times with exponential backoff
|
|
690
697
|
// (~3.5s total worst case); a tight per-request budget aborts retries mid-cycle.
|
|
691
698
|
const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
@@ -782,6 +789,7 @@ interface UsageCache {
|
|
|
782
789
|
get<T>(key: string): UsageCacheEntry<T> | undefined;
|
|
783
790
|
getStale<T>(key: string): UsageCacheEntry<T> | undefined;
|
|
784
791
|
set<T>(key: string, entry: UsageCacheEntry<T>): void;
|
|
792
|
+
deletePrefix(prefix: string): boolean;
|
|
785
793
|
cleanup?(): void;
|
|
786
794
|
}
|
|
787
795
|
|
|
@@ -791,6 +799,11 @@ type UsageRequestDescriptor = {
|
|
|
791
799
|
baseUrl?: string;
|
|
792
800
|
};
|
|
793
801
|
|
|
802
|
+
type ForcedUsageRefresh = {
|
|
803
|
+
all: boolean;
|
|
804
|
+
providers: Set<Provider>;
|
|
805
|
+
};
|
|
806
|
+
|
|
794
807
|
type AuthApiKeyOptions = {
|
|
795
808
|
baseUrl?: string;
|
|
796
809
|
modelId?: string;
|
|
@@ -1185,6 +1198,12 @@ class AuthStorageUsageCache implements UsageCache {
|
|
|
1185
1198
|
this.store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
|
|
1186
1199
|
}
|
|
1187
1200
|
|
|
1201
|
+
deletePrefix(prefix: string): boolean {
|
|
1202
|
+
if (!this.store.deleteCachePrefix) return false;
|
|
1203
|
+
this.store.deleteCachePrefix(`${USAGE_CACHE_PREFIX}${prefix}`);
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1188
1207
|
cleanup(): void {
|
|
1189
1208
|
this.store.cleanExpiredCache();
|
|
1190
1209
|
}
|
|
@@ -2955,20 +2974,63 @@ export class AuthStorage {
|
|
|
2955
2974
|
return baseUrl?.trim().replace(/\/+$/, "") ?? "";
|
|
2956
2975
|
}
|
|
2957
2976
|
|
|
2977
|
+
#usageCacheProviderKey(provider: Provider): string {
|
|
2978
|
+
const versionOverride = USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES[provider];
|
|
2979
|
+
return versionOverride === undefined ? provider : `${versionOverride}:${provider}`;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
#usageForceRefreshCacheKey(provider?: Provider): string {
|
|
2983
|
+
return provider
|
|
2984
|
+
? `${USAGE_FORCE_REFRESH_CACHE_PREFIX}provider:${provider}`
|
|
2985
|
+
: `${USAGE_FORCE_REFRESH_CACHE_PREFIX}all`;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
#markUsageForceRefresh(provider?: Provider): void {
|
|
2989
|
+
this.#usageCache.set(this.#usageForceRefreshCacheKey(provider), {
|
|
2990
|
+
value: true,
|
|
2991
|
+
expiresAt: Date.now() + USAGE_FORCE_REFRESH_TTL_MS,
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
#hasUsageForceRefresh(provider?: Provider): boolean {
|
|
2996
|
+
const key = this.#usageForceRefreshCacheKey(provider);
|
|
2997
|
+
const entry = this.#usageCache.get<boolean>(key);
|
|
2998
|
+
if (entry?.value !== true) return false;
|
|
2999
|
+
if (entry.expiresAt > Date.now()) return true;
|
|
3000
|
+
this.#usageCache.set(key, { value: null, expiresAt: 0 });
|
|
3001
|
+
return false;
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
#usageForceRefresh(requests: readonly UsageRequestDescriptor[]): ForcedUsageRefresh {
|
|
3005
|
+
const all = this.#hasUsageForceRefresh();
|
|
3006
|
+
const providers = new Set<Provider>();
|
|
3007
|
+
for (const request of requests) providers.add(request.provider);
|
|
3008
|
+
if (!all) {
|
|
3009
|
+
for (const provider of providers) {
|
|
3010
|
+
if (!this.#hasUsageForceRefresh(provider)) providers.delete(provider);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
return { all, providers };
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
#clearUsageForceRefresh(refresh: ForcedUsageRefresh): void {
|
|
3017
|
+
if (refresh.all) this.#usageCache.set(this.#usageForceRefreshCacheKey(), { value: null, expiresAt: 0 });
|
|
3018
|
+
for (const provider of refresh.providers) {
|
|
3019
|
+
this.#usageCache.set(this.#usageForceRefreshCacheKey(provider), { value: null, expiresAt: 0 });
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
2958
3023
|
#buildUsageReportCacheKey(request: UsageRequestDescriptor): string {
|
|
2959
3024
|
const baseUrl = this.#normalizeUsageBaseUrl(request.baseUrl) || "default";
|
|
2960
3025
|
const identity = this.#buildUsageCacheIdentity(request.credential);
|
|
2961
|
-
const
|
|
2962
|
-
const providerKey = versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
|
|
3026
|
+
const providerKey = this.#usageCacheProviderKey(request.provider);
|
|
2963
3027
|
return `report:${providerKey}:${baseUrl}:${identity}`;
|
|
2964
3028
|
}
|
|
2965
3029
|
|
|
2966
3030
|
#buildUsageReportsCacheKey(requests: ReadonlyArray<UsageRequestDescriptor>): string {
|
|
2967
3031
|
const snapshot = requests
|
|
2968
3032
|
.map(request => {
|
|
2969
|
-
const
|
|
2970
|
-
const providerKey =
|
|
2971
|
-
versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
|
|
3033
|
+
const providerKey = this.#usageCacheProviderKey(request.provider);
|
|
2972
3034
|
return `${providerKey}:${this.#normalizeUsageBaseUrl(request.baseUrl) || "default"}:${this.#buildUsageCacheIdentity(request.credential)}`;
|
|
2973
3035
|
})
|
|
2974
3036
|
.sort()
|
|
@@ -3210,19 +3272,24 @@ export class AuthStorage {
|
|
|
3210
3272
|
}
|
|
3211
3273
|
}
|
|
3212
3274
|
|
|
3213
|
-
async #fetchUsageCached(
|
|
3275
|
+
async #fetchUsageCached(
|
|
3276
|
+
request: UsageRequestDescriptor,
|
|
3277
|
+
options: { timeoutMs?: number; forceRefresh?: boolean } = {},
|
|
3278
|
+
): Promise<UsageReport | null> {
|
|
3279
|
+
const timeoutMs = options.timeoutMs;
|
|
3280
|
+
const forceRefresh = options.forceRefresh ?? false;
|
|
3214
3281
|
const cacheKey = this.#buildUsageReportCacheKey(request);
|
|
3215
3282
|
const now = Date.now();
|
|
3216
|
-
const cached = this.#usageCache.get<UsageReport | null>(cacheKey);
|
|
3283
|
+
const cached = forceRefresh ? undefined : this.#usageCache.get<UsageReport | null>(cacheKey);
|
|
3217
3284
|
// Fresh cache hit: return whatever's there (success or null fallback).
|
|
3218
3285
|
if (cached && cached.expiresAt > now) {
|
|
3219
3286
|
return cached.value;
|
|
3220
3287
|
}
|
|
3221
3288
|
|
|
3222
|
-
const inFlight = this.#usageRequestInFlight.get(cacheKey);
|
|
3223
|
-
if (inFlight) return inFlight;
|
|
3224
|
-
|
|
3225
3289
|
const usageCacheEpoch = this.#usageCacheEpoch;
|
|
3290
|
+
const inFlightKey = `${cacheKey}\0${usageCacheEpoch}`;
|
|
3291
|
+
const inFlight = this.#usageRequestInFlight.get(inFlightKey);
|
|
3292
|
+
if (inFlight) return inFlight;
|
|
3226
3293
|
const promise = (async () => {
|
|
3227
3294
|
const report = await this.#fetchUsageUncached(request, timeoutMs);
|
|
3228
3295
|
if (usageCacheEpoch !== this.#usageCacheEpoch) return report;
|
|
@@ -3242,7 +3309,8 @@ export class AuthStorage {
|
|
|
3242
3309
|
// re-hit the endpoint on every poll. Most providers serve the last good
|
|
3243
3310
|
// value through transient failures. Session-cookie providers can opt out
|
|
3244
3311
|
// so an expired login does not display stale quota indefinitely.
|
|
3245
|
-
const retainLastGood =
|
|
3312
|
+
const retainLastGood =
|
|
3313
|
+
!forceRefresh && this.#usageProviderResolver?.(request.provider)?.retainLastGoodOnFailure !== false;
|
|
3246
3314
|
const lastGood = retainLastGood
|
|
3247
3315
|
? (this.#usageCache.getStale<UsageReport | null>(cacheKey)?.value ?? null)
|
|
3248
3316
|
: null;
|
|
@@ -3251,10 +3319,10 @@ export class AuthStorage {
|
|
|
3251
3319
|
this.#usageCache.set(cacheKey, { value: lastGood, expiresAt: coolDown });
|
|
3252
3320
|
return lastGood;
|
|
3253
3321
|
})().finally(() => {
|
|
3254
|
-
this.#usageRequestInFlight.delete(
|
|
3322
|
+
this.#usageRequestInFlight.delete(inFlightKey);
|
|
3255
3323
|
});
|
|
3256
3324
|
|
|
3257
|
-
this.#usageRequestInFlight.set(
|
|
3325
|
+
this.#usageRequestInFlight.set(inFlightKey, promise);
|
|
3258
3326
|
return promise;
|
|
3259
3327
|
}
|
|
3260
3328
|
|
|
@@ -3730,10 +3798,9 @@ export class AuthStorage {
|
|
|
3730
3798
|
if (!resolvedApiKey) return null;
|
|
3731
3799
|
usageCredential.apiKey = resolvedApiKey;
|
|
3732
3800
|
}
|
|
3733
|
-
return this.#fetchUsageCached(
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
);
|
|
3801
|
+
return this.#fetchUsageCached(this.#buildUsageRequest(provider, usageCredential, options?.baseUrl), {
|
|
3802
|
+
timeoutMs: options?.timeoutMs ?? this.#usageRequestTimeoutMs,
|
|
3803
|
+
});
|
|
3737
3804
|
}
|
|
3738
3805
|
|
|
3739
3806
|
/**
|
|
@@ -3927,6 +3994,37 @@ export class AuthStorage {
|
|
|
3927
3994
|
return true;
|
|
3928
3995
|
}
|
|
3929
3996
|
|
|
3997
|
+
/**
|
|
3998
|
+
* Fetch every requested report, keeping normal polls parallel while a
|
|
3999
|
+
* manually invalidated provider probes accounts one at a time.
|
|
4000
|
+
*/
|
|
4001
|
+
#fetchUsageRequests(
|
|
4002
|
+
requests: readonly UsageRequestDescriptor[],
|
|
4003
|
+
serializedProviders: ReadonlySet<Provider>,
|
|
4004
|
+
): Promise<Array<UsageReport | null>> {
|
|
4005
|
+
const tails = new Map<Provider, Promise<void>>();
|
|
4006
|
+
return Promise.all(
|
|
4007
|
+
requests.map(request => {
|
|
4008
|
+
const forceRefresh = serializedProviders.has(request.provider);
|
|
4009
|
+
if (!forceRefresh) {
|
|
4010
|
+
return this.#fetchUsageCached(request, { timeoutMs: this.#usageRequestTimeoutMs });
|
|
4011
|
+
}
|
|
4012
|
+
const tail = tails.get(request.provider) ?? Promise.resolve();
|
|
4013
|
+
const current = tail.then(() =>
|
|
4014
|
+
this.#fetchUsageCached(request, { timeoutMs: this.#usageRequestTimeoutMs, forceRefresh: true }),
|
|
4015
|
+
);
|
|
4016
|
+
tails.set(
|
|
4017
|
+
request.provider,
|
|
4018
|
+
current.then(
|
|
4019
|
+
() => undefined,
|
|
4020
|
+
() => undefined,
|
|
4021
|
+
),
|
|
4022
|
+
);
|
|
4023
|
+
return current;
|
|
4024
|
+
}),
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
|
|
3930
4028
|
async fetchUsageReports(options?: {
|
|
3931
4029
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
3932
4030
|
/** Caller's cancel signal; only rejects this caller, never the shared upstream fetch. */
|
|
@@ -3945,15 +4043,15 @@ export class AuthStorage {
|
|
|
3945
4043
|
// dispatch + credential selection) coalesce into one upstream call.
|
|
3946
4044
|
// Each caller's `signal` only cancels THAT caller's await; the
|
|
3947
4045
|
// shared upstream fetch runs to completion so peers aren't punished.
|
|
3948
|
-
const
|
|
3949
|
-
let shared = this.#usageReportsInFlight.get(
|
|
4046
|
+
const overrideKey = `__override__\0${this.#usageCacheEpoch}`;
|
|
4047
|
+
let shared = this.#usageReportsInFlight.get(overrideKey);
|
|
3950
4048
|
if (!shared) {
|
|
3951
4049
|
// Don't forward the caller signal into the shared fetch — first caller's
|
|
3952
4050
|
// abort would otherwise cancel the upstream for every peer.
|
|
3953
4051
|
shared = override().finally(() => {
|
|
3954
|
-
this.#usageReportsInFlight.delete(
|
|
4052
|
+
this.#usageReportsInFlight.delete(overrideKey);
|
|
3955
4053
|
});
|
|
3956
|
-
this.#usageReportsInFlight.set(
|
|
4054
|
+
this.#usageReportsInFlight.set(overrideKey, shared);
|
|
3957
4055
|
}
|
|
3958
4056
|
const reports = await raceUsageWithSignal(shared, options?.signal);
|
|
3959
4057
|
if (shouldReconcileStoreHookReports && reports) this.#reconcileCodexUsageBlocksFromReports(reports);
|
|
@@ -3973,7 +4071,8 @@ export class AuthStorage {
|
|
|
3973
4071
|
// a single decorrelation snapshot for 30s, defeating the jitter (some
|
|
3974
4072
|
// accounts can be missing from one fetch and present in the next; the
|
|
3975
4073
|
// aggregate cache freezes whichever set landed first).
|
|
3976
|
-
const
|
|
4074
|
+
const forcedRefresh = this.#usageForceRefresh(requests);
|
|
4075
|
+
const cacheKey = `${this.#buildUsageReportsCacheKey(requests)}\0${this.#usageCacheEpoch}`;
|
|
3977
4076
|
|
|
3978
4077
|
const inFlight = this.#usageReportsInFlight.get(cacheKey);
|
|
3979
4078
|
if (inFlight) return inFlight;
|
|
@@ -3989,9 +4088,7 @@ export class AuthStorage {
|
|
|
3989
4088
|
});
|
|
3990
4089
|
}
|
|
3991
4090
|
|
|
3992
|
-
const results = await
|
|
3993
|
-
requests.map(request => this.#fetchUsageCached(request, this.#usageRequestTimeoutMs)),
|
|
3994
|
-
);
|
|
4091
|
+
const results = await this.#fetchUsageRequests(requests, forcedRefresh.providers);
|
|
3995
4092
|
const reports = results.filter((report): report is UsageReport => report !== null);
|
|
3996
4093
|
const deduped = this.#dedupeUsageReports(reports);
|
|
3997
4094
|
// no outer cache write — see comment above.
|
|
@@ -4012,6 +4109,7 @@ export class AuthStorage {
|
|
|
4012
4109
|
};
|
|
4013
4110
|
}),
|
|
4014
4111
|
});
|
|
4112
|
+
this.#clearUsageForceRefresh(forcedRefresh);
|
|
4015
4113
|
return resolved;
|
|
4016
4114
|
})().finally(() => {
|
|
4017
4115
|
this.#usageReportsInFlight.delete(cacheKey);
|
|
@@ -5729,31 +5827,32 @@ export class AuthStorage {
|
|
|
5729
5827
|
}
|
|
5730
5828
|
|
|
5731
5829
|
/**
|
|
5732
|
-
*
|
|
5733
|
-
*
|
|
5734
|
-
* provider
|
|
5735
|
-
* store are invalidated.
|
|
5830
|
+
* Drop report snapshots for a user-requested refresh so a failed probe
|
|
5831
|
+
* cannot replay the pre-invalidation last-good value. The persisted marker
|
|
5832
|
+
* makes the next same-provider refresh serial rather than a cold fan-out.
|
|
5736
5833
|
*/
|
|
5737
|
-
async
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
const cacheKey = this.#buildUsageReportCacheKey(
|
|
5748
|
-
this.#buildUsageRequestForOauth(entry.provider, entry.credential),
|
|
5749
|
-
);
|
|
5750
|
-
const existing = this.#usageCache.getStale<UsageReport | null>(cacheKey);
|
|
5751
|
-
this.#usageCache.set(cacheKey, { value: existing?.value ?? null, expiresAt: expired });
|
|
5752
|
-
}
|
|
5753
|
-
} catch (err) {
|
|
5754
|
-
logger.debug("Failed to list auth credentials for complete usage cache invalidation", { err });
|
|
5834
|
+
async #clearUsageReportCache(provider?: string): Promise<void> {
|
|
5835
|
+
this.#usageCacheEpoch += 1;
|
|
5836
|
+
const prefix = provider ? `report:${this.#usageCacheProviderKey(provider)}:` : "report:";
|
|
5837
|
+
if (!this.#usageCache.deletePrefix(prefix)) {
|
|
5838
|
+
// Third-party stores may not support prefix deletion. Clear every active
|
|
5839
|
+
// request key instead, including API-key and environment credentials.
|
|
5840
|
+
const requests = await this.#collectUsageRequests();
|
|
5841
|
+
for (const request of requests) {
|
|
5842
|
+
if (provider && request.provider !== provider) continue;
|
|
5843
|
+
this.#usageCache.set(this.#buildUsageReportCacheKey(request), { value: null, expiresAt: 0 });
|
|
5755
5844
|
}
|
|
5756
5845
|
}
|
|
5846
|
+
if (!this.#fetchUsageReportsOverride && !this.#store.fetchUsageReports) this.#markUsageForceRefresh(provider);
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
/**
|
|
5850
|
+
* Discard cached usage reports before a user-requested refresh. The next
|
|
5851
|
+
* read probes upstream serially per provider; a failure reports no fresh
|
|
5852
|
+
* usage instead of replaying an invalidated last-good snapshot.
|
|
5853
|
+
*/
|
|
5854
|
+
async invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void> {
|
|
5855
|
+
await this.#clearUsageReportCache(provider);
|
|
5757
5856
|
|
|
5758
5857
|
if (this.#store.invalidateUsageCache) {
|
|
5759
5858
|
await this.#store.invalidateUsageCache(signal).catch(err => {
|
package/src/error/flags.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const Flag = {
|
|
|
24
24
|
StaleResponsesItem: 0x0010_0000,
|
|
25
25
|
MalformedFunctionCall: 0x0020_0000,
|
|
26
26
|
ProviderFinishError: 0x0040_0000,
|
|
27
|
+
EmptyResponse: 0x0000_2000,
|
|
27
28
|
ContentBlocked: 0x0000_8000,
|
|
28
29
|
/** Account-scoped provider policy denial that may succeed with another credential. */
|
|
29
30
|
AccountPolicy: 0x0000_4000,
|
|
@@ -50,6 +51,7 @@ const KIND_MASK =
|
|
|
50
51
|
Flag.StaleResponsesItem |
|
|
51
52
|
Flag.MalformedFunctionCall |
|
|
52
53
|
Flag.ProviderFinishError |
|
|
54
|
+
Flag.EmptyResponse |
|
|
53
55
|
Flag.ContentBlocked |
|
|
54
56
|
Flag.AccountPolicy |
|
|
55
57
|
Flag.ContextOverflow |
|
|
@@ -62,7 +64,12 @@ const KIND_MASK =
|
|
|
62
64
|
Flag.OAuthExpiry;
|
|
63
65
|
|
|
64
66
|
const RETRIABLE_KINDS =
|
|
65
|
-
Flag.Transient |
|
|
67
|
+
Flag.Transient |
|
|
68
|
+
Flag.UsageLimit |
|
|
69
|
+
Flag.ThinkingLoop |
|
|
70
|
+
Flag.StaleResponsesItem |
|
|
71
|
+
Flag.ProviderFinishError |
|
|
72
|
+
Flag.EmptyResponse;
|
|
66
73
|
|
|
67
74
|
const OVERFLOW_PATTERNS = [
|
|
68
75
|
/prompt is too long/i, // Anthropic
|
|
@@ -104,6 +111,7 @@ const AUTH_FAILURE_PATTERN =
|
|
|
104
111
|
/\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i;
|
|
105
112
|
const MALFORMED_FUNCTION_CALL_PATTERN = /\bmalformed.?function.?call\b/i;
|
|
106
113
|
const PROVIDER_FINISH_ERROR_PATTERN = /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i;
|
|
114
|
+
const EMPTY_RESPONSE_PATTERN = /\bthought-only response without final output\b/i;
|
|
107
115
|
const CONTENT_FILTER_PATTERN = /\b(?:incomplete:\s*)?content_filter\b/i;
|
|
108
116
|
const ACCOUNT_POLICY_PATTERN = /\bcyber_policy\b|trusted access for cyber/i;
|
|
109
117
|
const STALE_RESPONSE_ITEM_PATTERNS = [/\bItem with id ['"][^'"]+['"] not found\.?/i, /previous[ _]?response/i] as const;
|
|
@@ -197,6 +205,7 @@ const ERROR_KIND_LABELS: readonly [Flag, string][] = [
|
|
|
197
205
|
[Flag.StaleResponsesItem, "stale-responses-item"],
|
|
198
206
|
[Flag.MalformedFunctionCall, "malformed-function-call"],
|
|
199
207
|
[Flag.ProviderFinishError, "provider-finish-error"],
|
|
208
|
+
[Flag.EmptyResponse, "empty-response"],
|
|
200
209
|
[Flag.ContentBlocked, "content-blocked"],
|
|
201
210
|
[Flag.AccountPolicy, "account-policy"],
|
|
202
211
|
[Flag.ContextOverflow, "context-overflow"],
|
|
@@ -340,6 +349,7 @@ function classifyText(errorMessage: string | undefined, errorStatus: number | un
|
|
|
340
349
|
if (matchesOverflowText(errorMessage)) kinds |= Flag.ContextOverflow;
|
|
341
350
|
if (isMalformedFunctionCallText(errorMessage)) kinds |= Flag.MalformedFunctionCall;
|
|
342
351
|
if (isProviderFinishErrorText(errorMessage)) kinds |= Flag.ProviderFinishError;
|
|
352
|
+
if (EMPTY_RESPONSE_PATTERN.test(errorMessage)) kinds |= Flag.EmptyResponse | Flag.Transient;
|
|
343
353
|
if (isContentBlockedText(errorMessage)) kinds |= Flag.ContentBlocked;
|
|
344
354
|
if (ACCOUNT_POLICY_PATTERN.test(errorMessage)) kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
345
355
|
if (isAuthFailureText(errorMessage)) kinds |= Flag.AuthFailed;
|
package/src/error/provider.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ProviderResponseErrorKind =
|
|
|
9
9
|
| "output"
|
|
10
10
|
/** Response body was empty/missing when content was required. */
|
|
11
11
|
| "empty-body"
|
|
12
|
+
/** Response completed without actionable output (for example, thoughts only). */
|
|
13
|
+
| "empty-output"
|
|
12
14
|
/** Malformed wire envelope (unexpected message ordering / shape). */
|
|
13
15
|
| "envelope"
|
|
14
16
|
/** Content was blocked by a provider safety filter. */
|
|
@@ -38,11 +40,10 @@ export class ProviderResponseError extends Error {
|
|
|
38
40
|
this.kind = options.kind ?? "output";
|
|
39
41
|
// A safety filter block is terminal and intentionally non-retryable.
|
|
40
42
|
if (this.kind === "content-blocked") attach(this, create(Flag.ContentBlocked));
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// output was already emitted.
|
|
43
|
+
// A logically empty completed output needs a session-level reminder that
|
|
44
|
+
// asks for the missing final answer. Empty bodies and incomplete streams
|
|
45
|
+
// stay on the generic transient retry/model-fallback path.
|
|
46
|
+
else if (this.kind === "empty-output") attach(this, create(Flag.Transient, Flag.EmptyResponse));
|
|
46
47
|
else if (this.kind === "incomplete-stream" || this.kind === "empty-body") attach(this, create(Flag.Transient));
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -622,11 +622,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
622
622
|
const isFlashLeakModel = model.id.includes("flash");
|
|
623
623
|
|
|
624
624
|
let started = false;
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
// hidden thought part also flips it (via `ensureStarted`); a thinking-only
|
|
628
|
-
// STOP must still fail over to the alternate Antigravity endpoint (#8480).
|
|
629
|
-
let emittedVisibleContent = false;
|
|
625
|
+
// Once any stream event starts, the endpoint is committed downstream.
|
|
626
|
+
// Failover remains safe only while `started` is false.
|
|
630
627
|
let sawFinishReason = false;
|
|
631
628
|
let lastResponseId: string | undefined;
|
|
632
629
|
const ensureStarted = () => {
|
|
@@ -705,7 +702,6 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
705
702
|
|
|
706
703
|
const emitVisibleText = (delta: string, thoughtSignature?: string): void => {
|
|
707
704
|
if (!delta) return;
|
|
708
|
-
emittedVisibleContent = true;
|
|
709
705
|
const block = startTextBlock();
|
|
710
706
|
block.text += delta;
|
|
711
707
|
block.textSignature = retainThoughtSignature(block.textSignature, thoughtSignature);
|
|
@@ -864,7 +860,6 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
864
860
|
};
|
|
865
861
|
|
|
866
862
|
output.content.push(toolCall);
|
|
867
|
-
emittedVisibleContent = true;
|
|
868
863
|
ensureStarted();
|
|
869
864
|
pushToolCallEvents(toolCall, blockIndex(), output, stream);
|
|
870
865
|
}
|
|
@@ -932,13 +927,17 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
932
927
|
};
|
|
933
928
|
|
|
934
929
|
let receivedContent = false;
|
|
930
|
+
const hasThinkingOutput = () =>
|
|
931
|
+
output.content.some(
|
|
932
|
+
block =>
|
|
933
|
+
block.type === "thinking" && (block.thinking.trim().length > 0 || Boolean(block.thinkingSignature)),
|
|
934
|
+
);
|
|
935
935
|
|
|
936
936
|
for (let i = 0; i < endpoints.length; i++) {
|
|
937
937
|
const endpoint = endpoints[i];
|
|
938
938
|
const isLastEndpoint = i === endpoints.length - 1;
|
|
939
939
|
try {
|
|
940
940
|
started = false;
|
|
941
|
-
emittedVisibleContent = false;
|
|
942
941
|
resetOutput();
|
|
943
942
|
|
|
944
943
|
// Per attempt: arm a pre-response (TTFT) timer, cleared the instant
|
|
@@ -1022,17 +1021,26 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1022
1021
|
}
|
|
1023
1022
|
|
|
1024
1023
|
const streamed = await streamResponse(currentResponse);
|
|
1025
|
-
//
|
|
1026
|
-
// endpoint
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1024
|
+
// Eventless silence may fail over to the alternate Antigravity
|
|
1025
|
+
// endpoint. Once thinking has streamed, the endpoint is already
|
|
1026
|
+
// committed downstream; Advisor mode may accept that silence,
|
|
1027
|
+
// while normal sessions surface it to final-output recovery.
|
|
1028
|
+
const thoughtOnly = hasThinkingOutput();
|
|
1029
1029
|
const acceptedSilence =
|
|
1030
|
-
options?.acceptEmptyResponse === true &&
|
|
1030
|
+
options?.acceptEmptyResponse === true &&
|
|
1031
|
+
!streamed.strippedPlanningLeak &&
|
|
1032
|
+
(isLastEndpoint || thoughtOnly);
|
|
1031
1033
|
if (output.stopReason !== "stop" || streamed.meaningful || acceptedSilence) {
|
|
1032
1034
|
receivedContent = streamed.meaningful || acceptedSilence;
|
|
1033
1035
|
break;
|
|
1034
1036
|
}
|
|
1035
1037
|
|
|
1038
|
+
// A thought-only STOP is a complete provider response, not a
|
|
1039
|
+
// transiently empty transport. Replaying the identical request
|
|
1040
|
+
// burns another full reasoning pass; let session recovery add
|
|
1041
|
+
// an explicit final-output reminder instead.
|
|
1042
|
+
if (thoughtOnly) break;
|
|
1043
|
+
|
|
1036
1044
|
if (emptyAttempt < MAX_EMPTY_STREAM_RETRIES) {
|
|
1037
1045
|
resetOutput();
|
|
1038
1046
|
}
|
|
@@ -1046,10 +1054,16 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1046
1054
|
}
|
|
1047
1055
|
|
|
1048
1056
|
if (!receivedContent) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1057
|
+
const thoughtOnly = hasThinkingOutput();
|
|
1058
|
+
throw new AIError.ProviderResponseError(
|
|
1059
|
+
thoughtOnly
|
|
1060
|
+
? "Cloud Code Assist API returned a thought-only response without final output"
|
|
1061
|
+
: "Cloud Code Assist API returned an empty response",
|
|
1062
|
+
{
|
|
1063
|
+
provider: model.provider,
|
|
1064
|
+
kind: thoughtOnly ? "empty-output" : "empty-body",
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
1055
1069
|
if (options?.signal?.aborted) {
|
|
@@ -1081,7 +1095,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1081
1095
|
const status = extractHttpStatusFromError(error);
|
|
1082
1096
|
if (
|
|
1083
1097
|
!isLastEndpoint &&
|
|
1084
|
-
!
|
|
1098
|
+
!started &&
|
|
1085
1099
|
(AIError.isTransientStatus(status) ||
|
|
1086
1100
|
(status === undefined &&
|
|
1087
1101
|
!(error instanceof AIError.ProviderResponseError && error.kind === "output") &&
|
|
@@ -86,9 +86,13 @@ export async function loginAlibabaTokenPlan(options: OAuthController): Promise<s
|
|
|
86
86
|
fetch: options.fetch,
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
const cookieRequestHost =
|
|
90
|
+
baseUrl === ALIBABA_TOKEN_PLAN_CN_BASE_URL ? "bailian-cs.console.aliyun.com" : "cs-data.qwencloud.com";
|
|
89
91
|
const rawCookie = await options.onPrompt({
|
|
90
92
|
message:
|
|
91
|
-
|
|
93
|
+
baseUrl === ALIBABA_TOKEN_PLAN_CN_BASE_URL
|
|
94
|
+
? "Optional quota reporting: open browser DevTools → Network, reload the Token Plan page, filter for api.json, and select the bailian-cs.console.aliyun.com/data/api.json request whose api query ends in /tokenplan/personal/api/v2/usage. Copy Request Headers → Cookie, then paste the complete name=value; ... value here, or press Enter to skip."
|
|
95
|
+
: "Optional quota reporting: open browser DevTools → Network, reload the Token Plan page, filter for api.json, and select the cs-data.qwencloud.com/data/api.json request whose api query ends in /tokenplan/personal/api/v2/usage. Copy Request Headers → Cookie, then paste the complete name=value; ... value here, or press Enter to skip.",
|
|
92
96
|
placeholder: "name=value; name=value; ...",
|
|
93
97
|
allowEmpty: true,
|
|
94
98
|
});
|
|
@@ -107,7 +111,7 @@ export async function loginAlibabaTokenPlan(options: OAuthController): Promise<s
|
|
|
107
111
|
})
|
|
108
112
|
) {
|
|
109
113
|
throw new AIError.ConfigurationError(
|
|
110
|
-
|
|
114
|
+
`Invalid QwenCloud Cookie header. Copy the complete Cookie request header from the ${cookieRequestHost} usage request, not a single cookie value.`,
|
|
111
115
|
);
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -5,10 +5,10 @@ import { scheduler } from "node:timers/promises";
|
|
|
5
5
|
import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
|
|
6
6
|
import {
|
|
7
7
|
COPILOT_API_HEADERS,
|
|
8
|
+
discoverGitHubCopilotApiEndpoint,
|
|
8
9
|
getGitHubCopilotBaseUrl,
|
|
9
10
|
isPublicGitHubHost,
|
|
10
11
|
normalizeDomain,
|
|
11
|
-
normalizeGitHubCopilotApiEndpoint,
|
|
12
12
|
normalizeGitHubCopilotEnterpriseDomain,
|
|
13
13
|
OPENCODE_HEADERS,
|
|
14
14
|
} from "@oh-my-pi/pi-catalog/wire/github-copilot";
|
|
@@ -226,27 +226,6 @@ export function refreshGitHubCopilotToken(
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
async function discoverGitHubCopilotApiEndpoint(token: string, fetchImpl: FetchImpl): Promise<string | undefined> {
|
|
230
|
-
try {
|
|
231
|
-
const data = await fetchJson(
|
|
232
|
-
"https://api.github.com/copilot_internal/user",
|
|
233
|
-
{
|
|
234
|
-
headers: {
|
|
235
|
-
Accept: "application/json",
|
|
236
|
-
Authorization: `token ${token}`,
|
|
237
|
-
...OPENCODE_HEADERS,
|
|
238
|
-
},
|
|
239
|
-
},
|
|
240
|
-
fetchImpl,
|
|
241
|
-
);
|
|
242
|
-
if (!data || typeof data !== "object") return undefined;
|
|
243
|
-
const endpoints = (data as { endpoints?: { api?: unknown } }).endpoints;
|
|
244
|
-
return typeof endpoints?.api === "string" ? normalizeGitHubCopilotApiEndpoint(endpoints.api) : undefined;
|
|
245
|
-
} catch {
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
229
|
/**
|
|
251
230
|
* Enable a model for the user's GitHub Copilot account.
|
|
252
231
|
* This is required for some models (like Claude, Grok) before they can be used.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ALIBABA_TOKEN_PLAN_CN_BASE_URL,
|
|
4
|
+
parseAlibabaTokenPlanCredential,
|
|
5
|
+
} from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
|
|
3
6
|
import type {
|
|
4
7
|
CredentialRankingStrategy,
|
|
5
8
|
UsageFetchContext,
|
|
@@ -12,21 +15,45 @@ import { isRecord } from "../utils";
|
|
|
12
15
|
import { HOUR_MS, parsePositiveTimestamp, WEEK_MS } from "./shared";
|
|
13
16
|
|
|
14
17
|
const PROVIDER = "alibaba-token-plan";
|
|
15
|
-
const CONSOLE_ORIGIN = "https://home.qwencloud.com";
|
|
16
|
-
const DASHBOARD_URL = `${CONSOLE_ORIGIN}/billing/subscription/token-plan-individual`;
|
|
17
|
-
const USER_INFO_URL = `${CONSOLE_ORIGIN}/tool/user/info.json`;
|
|
18
|
-
const GATEWAY_ACTION = "IntlBroadScopeAspnGateway";
|
|
19
18
|
const USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
|
20
|
-
const USAGE_URL = `https://cs-data.qwencloud.com/data/api.json?product=sfm_bailian&action=${GATEWAY_ACTION}&api=${encodeURIComponent(USAGE_API)}`;
|
|
21
19
|
const BROWSER_USER_AGENT =
|
|
22
20
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
const INTERNATIONAL_CONSOLE = {
|
|
22
|
+
origin: "https://home.qwencloud.com",
|
|
23
|
+
dashboardUrl: "https://home.qwencloud.com/billing/subscription/token-plan-individual",
|
|
24
|
+
sessionUrl: "https://home.qwencloud.com/tool/user/info.json",
|
|
25
|
+
gatewayAction: "IntlBroadScopeAspnGateway",
|
|
26
|
+
region: "ap-southeast-1",
|
|
27
|
+
usageUrl: `https://cs-data.qwencloud.com/data/api.json?product=sfm_bailian&action=IntlBroadScopeAspnGateway&api=${encodeURIComponent(USAGE_API)}`,
|
|
28
|
+
cornerstoneParam: {
|
|
29
|
+
domain: "home.qwencloud.com",
|
|
30
|
+
consoleSite: "QWENCLOUD",
|
|
31
|
+
console: "ONE_CONSOLE",
|
|
32
|
+
xsp_lang: "en-US",
|
|
33
|
+
protocol: "V2",
|
|
34
|
+
productCode: "p_efm",
|
|
35
|
+
},
|
|
36
|
+
} as const;
|
|
37
|
+
const CHINA_CONSOLE = {
|
|
38
|
+
origin: "https://bailian.console.aliyun.com",
|
|
39
|
+
dashboardUrl: "https://bailian.console.aliyun.com/cn-beijing?tab=plan",
|
|
40
|
+
sessionUrl: "https://bailian.console.aliyun.com/cn-beijing?tab=plan",
|
|
41
|
+
gatewayAction: "BroadScopeAspnGateway",
|
|
42
|
+
region: "cn-beijing",
|
|
43
|
+
usageUrl: `https://bailian-cs.console.aliyun.com/data/api.json?action=BroadScopeAspnGateway&product=sfm_bailian&api=${encodeURIComponent(USAGE_API)}`,
|
|
44
|
+
cornerstoneParam: {
|
|
45
|
+
feURL: "https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/token-plan/personal",
|
|
46
|
+
protocol: "V2",
|
|
47
|
+
console: "ONE_CONSOLE",
|
|
48
|
+
productCode: "p_efm",
|
|
49
|
+
switchAgent: 12608464,
|
|
50
|
+
switchUserType: 3,
|
|
51
|
+
domain: "bailian.console.aliyun.com",
|
|
52
|
+
consoleSite: "BAILIAN_ALIYUN",
|
|
53
|
+
userNickName: "",
|
|
54
|
+
userPrincipalName: "",
|
|
55
|
+
xsp_lang: "zh-CN",
|
|
56
|
+
},
|
|
30
57
|
} as const;
|
|
31
58
|
|
|
32
59
|
function extractCookieValue(header: string, name: string): string | undefined {
|
|
@@ -102,35 +129,56 @@ async function fetchAlibabaTokenPlanUsage(
|
|
|
102
129
|
const credential = parseAlibabaTokenPlanCredential(params.credential.apiKey);
|
|
103
130
|
if (!credential?.cookie) return null;
|
|
104
131
|
const cookie = credential.cookie;
|
|
132
|
+
const isChina = credential.baseUrl === ALIBABA_TOKEN_PLAN_CN_BASE_URL;
|
|
133
|
+
const consoleConfig = isChina ? CHINA_CONSOLE : INTERNATIONAL_CONSOLE;
|
|
105
134
|
|
|
106
135
|
try {
|
|
107
|
-
const
|
|
136
|
+
const sessionResponse = await ctx.fetch(consoleConfig.sessionUrl, {
|
|
108
137
|
headers: {
|
|
109
|
-
Accept:
|
|
138
|
+
Accept: isChina
|
|
139
|
+
? "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8"
|
|
140
|
+
: "application/json, text/plain, */*",
|
|
110
141
|
Cookie: cookie,
|
|
111
|
-
Referer: `${
|
|
142
|
+
Referer: `${consoleConfig.origin}/`,
|
|
112
143
|
"User-Agent": BROWSER_USER_AGENT,
|
|
113
144
|
},
|
|
114
145
|
redirect: "manual",
|
|
115
146
|
signal: params.signal,
|
|
116
147
|
});
|
|
117
|
-
if (!
|
|
118
|
-
ctx.logger?.warn("
|
|
148
|
+
if (!sessionResponse.ok) {
|
|
149
|
+
ctx.logger?.warn("Alibaba Token Plan session lookup failed", {
|
|
150
|
+
provider: PROVIDER,
|
|
151
|
+
status: sessionResponse.status,
|
|
152
|
+
});
|
|
119
153
|
return null;
|
|
120
154
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
|
|
156
|
+
let secToken: string | undefined;
|
|
157
|
+
let accountId: string | undefined;
|
|
158
|
+
if (isChina) {
|
|
159
|
+
const html = await sessionResponse.text();
|
|
160
|
+
secToken = /\bSEC_TOKEN\s*:\s*"([^"]+)"/.exec(html)?.[1];
|
|
161
|
+
if (!secToken) {
|
|
162
|
+
ctx.logger?.warn("Alibaba Token Plan China session response invalid", { provider: PROVIDER });
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
const userPayload: unknown = await sessionResponse.json();
|
|
167
|
+
if (!isRecord(userPayload) || !isRecord(userPayload.data) || typeof userPayload.data.secToken !== "string") {
|
|
168
|
+
ctx.logger?.warn("QwenCloud session response invalid", { provider: PROVIDER });
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
secToken = userPayload.data.secToken;
|
|
172
|
+
accountId = accountIdFromUserData(userPayload.data);
|
|
125
173
|
}
|
|
126
|
-
|
|
174
|
+
|
|
127
175
|
const csrf = extractCookieValue(cookie, "login_aliyunid_csrf") ?? extractCookieValue(cookie, "csrf");
|
|
128
176
|
const headers: Record<string, string> = {
|
|
129
177
|
Accept: "application/json, text/plain, */*",
|
|
130
178
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
131
179
|
Cookie: cookie,
|
|
132
|
-
Origin:
|
|
133
|
-
Referer:
|
|
180
|
+
Origin: consoleConfig.origin,
|
|
181
|
+
Referer: consoleConfig.dashboardUrl,
|
|
134
182
|
"User-Agent": BROWSER_USER_AGENT,
|
|
135
183
|
"X-Requested-With": "XMLHttpRequest",
|
|
136
184
|
};
|
|
@@ -140,16 +188,21 @@ async function fetchAlibabaTokenPlanUsage(
|
|
|
140
188
|
}
|
|
141
189
|
const body = new URLSearchParams({
|
|
142
190
|
product: "sfm_bailian",
|
|
143
|
-
action:
|
|
144
|
-
region:
|
|
191
|
+
action: consoleConfig.gatewayAction,
|
|
192
|
+
region: consoleConfig.region,
|
|
145
193
|
sec_token: secToken,
|
|
146
194
|
params: JSON.stringify({
|
|
147
195
|
Api: USAGE_API,
|
|
148
|
-
Data: {
|
|
196
|
+
Data: {
|
|
197
|
+
cornerstoneParam: {
|
|
198
|
+
...(isChina ? { feTraceId: crypto.randomUUID() } : {}),
|
|
199
|
+
...consoleConfig.cornerstoneParam,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
149
202
|
V: "1.0",
|
|
150
203
|
}),
|
|
151
204
|
});
|
|
152
|
-
const usageResponse = await ctx.fetch(
|
|
205
|
+
const usageResponse = await ctx.fetch(consoleConfig.usageUrl, {
|
|
153
206
|
method: "POST",
|
|
154
207
|
headers,
|
|
155
208
|
body,
|
|
@@ -157,16 +210,18 @@ async function fetchAlibabaTokenPlanUsage(
|
|
|
157
210
|
signal: params.signal,
|
|
158
211
|
});
|
|
159
212
|
if (!usageResponse.ok) {
|
|
160
|
-
ctx.logger?.warn("
|
|
213
|
+
ctx.logger?.warn("Alibaba Token Plan usage fetch failed", {
|
|
214
|
+
provider: PROVIDER,
|
|
215
|
+
status: usageResponse.status,
|
|
216
|
+
});
|
|
161
217
|
return null;
|
|
162
218
|
}
|
|
163
219
|
const payload: unknown = await usageResponse.json();
|
|
164
220
|
if (!isRecord(payload) || payload.successResponse === false || !isRecord(payload.data)) {
|
|
165
|
-
ctx.logger?.warn("
|
|
221
|
+
ctx.logger?.warn("Alibaba Token Plan usage response invalid", { provider: PROVIDER });
|
|
166
222
|
return null;
|
|
167
223
|
}
|
|
168
224
|
const responseData = unwrapGatewayData(payload.data);
|
|
169
|
-
const accountId = accountIdFromUserData(userPayload.data);
|
|
170
225
|
const limits = [
|
|
171
226
|
buildLimit(
|
|
172
227
|
"5h",
|
|
@@ -190,10 +245,10 @@ async function fetchAlibabaTokenPlanUsage(
|
|
|
190
245
|
provider: PROVIDER,
|
|
191
246
|
fetchedAt: Date.now(),
|
|
192
247
|
limits,
|
|
193
|
-
metadata: { source: "qwencloud-console", ...(accountId ? { accountId } : {}) },
|
|
248
|
+
metadata: { source: isChina ? "bailian-console" : "qwencloud-console", ...(accountId ? { accountId } : {}) },
|
|
194
249
|
};
|
|
195
250
|
} catch (error) {
|
|
196
|
-
ctx.logger?.warn("
|
|
251
|
+
ctx.logger?.warn("Alibaba Token Plan usage request failed", {
|
|
197
252
|
provider: PROVIDER,
|
|
198
253
|
error: error instanceof Error ? error.name : "unknown",
|
|
199
254
|
});
|