cartethyia 2.6.3 → 2.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/assets/{BannedPage-C8pfspAj.js → BannedPage-B7OjIxCp.js} +1 -1
- package/dist/dashboard/assets/BannedPage-B7OjIxCp.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-D-OwfzDV.js → CliToolDetailPage-Bt-6y24w.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bt-6y24w.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DqRcDO5j.js → CliToolsPage-C6LunsuE.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-C6LunsuE.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-B-a0l8yE.js → CombosPage-CgyfN2Yy.js} +1 -1
- package/dist/dashboard/assets/CombosPage-CgyfN2Yy.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-BVVGUdqt.js → ConsoleLogPage-BHWU7y6j.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BHWU7y6j.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BzmbE05k.js → CustomizationPage-xIjU736m.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-xIjU736m.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DeT1zkYG.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-DeT1zkYG.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdSGc5J0.js → LoginPage-ChVqIAam.js} +1 -1
- package/dist/dashboard/assets/LoginPage-ChVqIAam.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DVny4S5_.js → ModelPicker-CA1kLs45.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-D3fMRkpP.js → OverviewPage-BD4GkRtS.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-BD4GkRtS.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BJXwqpKj.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-BJXwqpKj.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-T7WamMuq.js → ProvidersPage-Ck64riM5.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Ck64riM5.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-CKJb_Kj4.js → ProxyPage-Br_Leqq0.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-Br_Leqq0.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-hxJ1hGES.js → QuotaPage-D2gyhfa8.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-D2gyhfa8.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CpEErAq5.js → SettingsPage-1dbYb8cM.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-1dbYb8cM.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-DRsoneXm.js → SetupPage-DUTFy9wH.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DUTFy9wH.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-QvDz0eQo.js → SortableList-BZEhfDpZ.js} +1 -1
- package/dist/dashboard/assets/SortableList-BZEhfDpZ.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-6yLBksbv.js → StudioPage-GBDtsJjh.js} +1 -1
- package/dist/dashboard/assets/StudioPage-GBDtsJjh.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-CpB88NH4.js → UsagePage-CxidMvxq.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CxidMvxq.js.gz +0 -0
- package/dist/dashboard/assets/app-BUSiYL6F.css +2 -0
- package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
- package/dist/dashboard/assets/{badge-CKsF123d.js → badge-Bkz_-RrM.js} +1 -1
- package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
- package/dist/dashboard/assets/{banner-8j7uChA9.js → banner-C-7y3VKV.js} +1 -1
- package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
- package/dist/dashboard/assets/{card-CdsSlWA3.js → card-BOPBuBzD.js} +1 -1
- package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-uL5aYLfW.js → clipboard-button-e-Mlk0FH.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-C07hr5_G.js → dialog-Iec_ywbD.js} +1 -1
- package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BPO1TcKH.js → drawer-Bs8MIIHc.js} +1 -1
- package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bz_CtDfC.js → entry-BRszm4_Z.js} +1 -1
- package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
- package/dist/dashboard/assets/{entry-8U_8IAW2.js → entry-BrksMru8.js} +3 -3
- package/dist/dashboard/assets/entry-BrksMru8.js.gz +0 -0
- package/dist/dashboard/assets/{entry-GJRbmrpA.js → entry-DcobaKFu.js} +1 -1
- package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-DmQreSDd.js → github-badge-BuDsrsPn.js} +1 -1
- package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
- package/dist/dashboard/assets/{index-BG48fsZ2.js → index-DU61iviS.js} +2 -2
- package/dist/dashboard/assets/index-DU61iviS.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C9dCYaBZ.js → inline-DToMICwl.js} +1 -1
- package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
- package/dist/dashboard/assets/{input-CjCnXweJ.js → input-WlyHWeG2.js} +1 -1
- package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
- package/dist/dashboard/assets/{layout-V6y8DAkl.js → layout-DaJ5baJr.js} +1 -1
- package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CM1AaG_y.js → list-row-B3oSK13z.js} +1 -1
- package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BOevJzdz.js → numbers-Dulqgys5.js} +1 -1
- package/dist/dashboard/assets/numbers-Dulqgys5.js.gz +0 -0
- package/dist/dashboard/assets/{select-AnGKpdl1.js → select-Ciq_KFQ1.js} +1 -1
- package/dist/dashboard/assets/{select-AnGKpdl1.js.gz → select-Ciq_KFQ1.js.gz} +0 -0
- package/dist/dashboard/assets/{stack-_gfTdLVB.js → stack-B1w5BM0u.js} +1 -1
- package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
- package/dist/dashboard/assets/{state-2c5V4zo7.js → state-DhZBfWgx.js} +1 -1
- package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Dkke7Naf.js → switch-yD7D99Lg.js} +1 -1
- package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +41 -41
- package/package.json +96 -96
- package/src/boot/app.ts +11 -1
- package/src/console/providers/catalog/contracts.ts +7 -0
- package/src/console/providers/catalog/store.ts +25 -2
- package/src/main.ts +6 -0
- package/src/observability/event-query.ts +10 -5
- package/src/providers/operations/account-health-service.ts +211 -17
- package/dist/dashboard/assets/BannedPage-C8pfspAj.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-D-OwfzDV.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DqRcDO5j.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-B-a0l8yE.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-BVVGUdqt.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BzmbE05k.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BRDc_wx8.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BRDc_wx8.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdSGc5J0.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D3fMRkpP.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DlBwPsHM.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-DlBwPsHM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-T7WamMuq.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CKJb_Kj4.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-hxJ1hGES.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CpEErAq5.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DRsoneXm.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-QvDz0eQo.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-6yLBksbv.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CpB88NH4.js.gz +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +0 -2
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/entry-8U_8IAW2.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/index-BG48fsZ2.js.gz +0 -0
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- /package/dist/dashboard/assets/{app-2KhtHCv9.js → app-MdVfwraB.js} +0 -0
- /package/dist/dashboard/assets/{app-2KhtHCv9.js.gz → app-MdVfwraB.js.gz} +0 -0
|
@@ -160,7 +160,29 @@ export type AccountErrorCategory =
|
|
|
160
160
|
| "model_capacity"
|
|
161
161
|
| "model_unreachable"
|
|
162
162
|
| "auth_invalidated"
|
|
163
|
+
/**
|
|
164
|
+
* Deprecated: an upstream ban. Kept so existing health rows keep rendering
|
|
165
|
+
* instead of showing a blank cause. No longer produced — see
|
|
166
|
+
* `policy_blocked_banned` and `policy_blocked_invalid_request`.
|
|
167
|
+
*/
|
|
163
168
|
| "policy_blocked"
|
|
169
|
+
/**
|
|
170
|
+
* The upstream banned this credential: a verdict on the account, e.g. buddy
|
|
171
|
+
* `11140` "request illegal" with "Your account has been restricted due to
|
|
172
|
+
* unusual activity". Cooldown cannot help — the account is not throttled, it
|
|
173
|
+
* is refused. Disabling immediately is a *feature*: a client's retry budget
|
|
174
|
+
* is spent in seconds, so surviving attempts burn sibling accounts that are
|
|
175
|
+
* still healthy.
|
|
176
|
+
*/
|
|
177
|
+
| "policy_blocked_banned"
|
|
178
|
+
/**
|
|
179
|
+
* The upstream refused this request: a verdict on the request. e.g. buddy
|
|
180
|
+
* `11-128` "request illegal" with no account restriction. Distinguished from
|
|
181
|
+
* {@link policy_blocked_banned} because the correct responses differ — a
|
|
182
|
+
* banned account must be disabled, a malformed request must only stop being
|
|
183
|
+
* retried verbatim.
|
|
184
|
+
*/
|
|
185
|
+
| "policy_blocked_invalid_request"
|
|
164
186
|
| "server_error"
|
|
165
187
|
| "timeout"
|
|
166
188
|
| "unknown";
|
|
@@ -338,6 +360,35 @@ function accountDeadlineMs(
|
|
|
338
360
|
return latest > 0 ? new Date(latest) : null;
|
|
339
361
|
}
|
|
340
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Whether the upstream named a restriction on the account itself.
|
|
365
|
+
*
|
|
366
|
+
* The buddy family reuses "request illegal" for two unrelated rejections, and
|
|
367
|
+
* the difference is only in the body:
|
|
368
|
+
*
|
|
369
|
+
* - `11140` carries `displayMsg` about the account being restricted ("Your
|
|
370
|
+
* account has been restricted due to unusual activity") and `actions`
|
|
371
|
+
* offering an appeal. That is a ban — the account is refused, not throttled.
|
|
372
|
+
* - `11-128` is a refusal of this request; the account is fine.
|
|
373
|
+
*
|
|
374
|
+
* Proven account-scoped (2026-10-16): two live share-page requests carrying
|
|
375
|
+
* byte-identical bodies and headers to the same model differed only in the
|
|
376
|
+
* pooled account, and the blocked one 400'd while the other 200'd.
|
|
377
|
+
*
|
|
378
|
+
* Matched on the code *and* the account-restriction wording, so an unrelated
|
|
379
|
+
* upstream that happens to say "restricted" in prose cannot disable accounts.
|
|
380
|
+
*/
|
|
381
|
+
function isBuddyAccountBanned(lower: string, providerCode: string): boolean {
|
|
382
|
+
if (providerCode !== "11140") return false;
|
|
383
|
+
return (
|
|
384
|
+
lower.includes("restricted") ||
|
|
385
|
+
lower.includes("has been banned") ||
|
|
386
|
+
lower.includes("unusual activity") ||
|
|
387
|
+
lower.includes("appeal this restriction") ||
|
|
388
|
+
lower.includes("submit feedback to appeal")
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
341
392
|
/**
|
|
342
393
|
* Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
|
|
343
394
|
* window longer than that is a monthly or plan-level limit, and parking an
|
|
@@ -421,6 +472,12 @@ interface AccountErrorContext {
|
|
|
421
472
|
readonly canMutate: boolean;
|
|
422
473
|
readonly providerCode: string;
|
|
423
474
|
readonly statedCooldownMs: number | null;
|
|
475
|
+
/**
|
|
476
|
+
* The upstream named a restriction on the account, not a refusal of the
|
|
477
|
+
* request. Read by the policy arm to pick between disabling the account and
|
|
478
|
+
* merely refusing the request.
|
|
479
|
+
*/
|
|
480
|
+
readonly buddyBanned: boolean;
|
|
424
481
|
readonly reason: (prefix: string) => string;
|
|
425
482
|
readonly result: (
|
|
426
483
|
category: AccountErrorCategory,
|
|
@@ -476,6 +533,13 @@ interface AccountErrorSignals {
|
|
|
476
533
|
readonly credentialKind: string | undefined;
|
|
477
534
|
/** Buddy-family provider: the only family with provider-specific codes below. */
|
|
478
535
|
readonly buddy: boolean;
|
|
536
|
+
/**
|
|
537
|
+
* The upstream named a restriction on this account, not a refusal of the
|
|
538
|
+
* request. Only the `11140` shape carries `displayMsg` about the account
|
|
539
|
+
* being restricted, so the policy arm can tell a ban from a bad request
|
|
540
|
+
* instead of treating both as one cooldown.
|
|
541
|
+
*/
|
|
542
|
+
readonly buddyBanned: boolean;
|
|
479
543
|
readonly priceRefusal: boolean;
|
|
480
544
|
readonly deploymentInfraError: boolean;
|
|
481
545
|
readonly quotaSignal: boolean;
|
|
@@ -601,6 +665,7 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
|
|
|
601
665
|
canMutate,
|
|
602
666
|
providerCode,
|
|
603
667
|
statedCooldownMs,
|
|
668
|
+
buddyBanned: isBuddyAccountBanned(lower, providerCode),
|
|
604
669
|
reason,
|
|
605
670
|
result,
|
|
606
671
|
};
|
|
@@ -623,6 +688,7 @@ export function classifyAccountError(
|
|
|
623
688
|
providerCode,
|
|
624
689
|
|
|
625
690
|
statedCooldownMs,
|
|
691
|
+
buddyBanned,
|
|
626
692
|
reason,
|
|
627
693
|
result,
|
|
628
694
|
} = accountErrorContext(error, options);
|
|
@@ -761,19 +827,46 @@ export function classifyAccountError(
|
|
|
761
827
|
lower.includes("request illegal") ||
|
|
762
828
|
lower.includes("content blocked") ||
|
|
763
829
|
lower.includes("account suspended");
|
|
830
|
+
// A ban is a verdict on the credential, and cooldown cannot answer it: the
|
|
831
|
+
// account is refused, not throttled. Disabling is the fix, and disabling
|
|
832
|
+
// *immediately* is a feature — a client's retry budget is spent in seconds,
|
|
833
|
+
// so a banned account that merely cooled stayed in the plan and burned
|
|
834
|
+
// sibling accounts that were still healthy before the window expired.
|
|
835
|
+
//
|
|
836
|
+
// The two shapes are told apart by the body, not the code alone: `11140`
|
|
837
|
+
// names a restriction on the account, `11-128` refuses the request.
|
|
764
838
|
const policyBlock =
|
|
765
839
|
policyRejection &&
|
|
766
840
|
options?.providerId !== undefined &&
|
|
767
841
|
BUDDY_PROVIDER_IDS.has(options.providerId.toLowerCase());
|
|
768
842
|
if (policyBlock) {
|
|
769
|
-
|
|
843
|
+
if (buddyBanned) {
|
|
844
|
+
return result(
|
|
845
|
+
"policy_blocked_banned",
|
|
846
|
+
"disabled",
|
|
847
|
+
0,
|
|
848
|
+
null,
|
|
849
|
+
reason("Account banned by upstream"),
|
|
850
|
+
// A ban is evidence about the credential even at provider scope, for
|
|
851
|
+
// the same reason a named revocation is: the message is the evidence.
|
|
852
|
+
// Without this a stream frame's missing credential detail would leave a
|
|
853
|
+
// banned account serving.
|
|
854
|
+
true,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
// Not a ban: the upstream refused this request. The account is fine, so the
|
|
858
|
+
// only correct move is to stop retrying it verbatim. Cooling the account
|
|
859
|
+
// here was the bug that made one malformed request look like a dead
|
|
860
|
+
// credential.
|
|
770
861
|
return result(
|
|
771
|
-
"
|
|
862
|
+
"policy_blocked_invalid_request",
|
|
772
863
|
"cooldown",
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
reason("
|
|
776
|
-
|
|
864
|
+
0,
|
|
865
|
+
null,
|
|
866
|
+
reason("Upstream refused this request"),
|
|
867
|
+
// Never mutates: a malformed body fails identically on every account, so
|
|
868
|
+
// cooling would burn a healthy credential on the gateway's own fault.
|
|
869
|
+
false,
|
|
777
870
|
);
|
|
778
871
|
}
|
|
779
872
|
// These codes are a verdict on the request, not the account. Cooling the
|
|
@@ -829,14 +922,30 @@ export function classifyAccountError(
|
|
|
829
922
|
options?.providerId !== undefined &&
|
|
830
923
|
BUDDY_PROVIDER_IDS.has(options.providerId.toLowerCase());
|
|
831
924
|
if (channelBlock) {
|
|
832
|
-
|
|
925
|
+
// Same split as the policy arm above: a ban disables, a channel refusal
|
|
926
|
+
// only stops this request. `11-128` is the request shape, so this branch
|
|
927
|
+
// nearly always takes the refusal path — but it shares the ban test so a
|
|
928
|
+
// `11140`-shaped body arriving as a channel rejection still disables.
|
|
929
|
+
if (buddyBanned) {
|
|
930
|
+
return result(
|
|
931
|
+
"policy_blocked_banned",
|
|
932
|
+
"disabled",
|
|
933
|
+
0,
|
|
934
|
+
null,
|
|
935
|
+
reason("Account banned by upstream"),
|
|
936
|
+
true,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
833
939
|
return result(
|
|
834
|
-
"
|
|
940
|
+
"policy_blocked_invalid_request",
|
|
835
941
|
"cooldown",
|
|
836
|
-
|
|
837
|
-
|
|
942
|
+
0,
|
|
943
|
+
null,
|
|
838
944
|
reason("Provider channel rejection"),
|
|
839
|
-
|
|
945
|
+
// The credential is valid; the upstream refuses this calling channel.
|
|
946
|
+
// Cooling was the bug: it kept a refused account in the plan, where a
|
|
947
|
+
// client's retry budget burned healthy siblings for the whole window.
|
|
948
|
+
false,
|
|
840
949
|
);
|
|
841
950
|
}
|
|
842
951
|
// server-side tool (web search, x search, web fetch), not evidence about the
|
|
@@ -907,6 +1016,7 @@ export function classifyAccountError(
|
|
|
907
1016
|
staticToken,
|
|
908
1017
|
credentialKind,
|
|
909
1018
|
buddy,
|
|
1019
|
+
buddyBanned,
|
|
910
1020
|
priceRefusal,
|
|
911
1021
|
deploymentInfraError,
|
|
912
1022
|
quotaSignal,
|
|
@@ -941,6 +1051,62 @@ export function classifyAccountError(
|
|
|
941
1051
|
* {@link AccountErrorRule}.
|
|
942
1052
|
*/
|
|
943
1053
|
const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
1054
|
+
{
|
|
1055
|
+
// Must precede "rate-limit": this rejection is a 429 whose own text says
|
|
1056
|
+
// "rate limit", so the broader arm claimed it and cooled the account for
|
|
1057
|
+
// the full 15-minute rate-limit window.
|
|
1058
|
+
//
|
|
1059
|
+
// A full max-inflight is a statement about the gateway's own routing, not
|
|
1060
|
+
// about the credential: every account serving the model is busy right now.
|
|
1061
|
+
// Cooling them marks healthy accounts as `cooldown` for a limit their
|
|
1062
|
+
// provider never applied, which is what made every account read busy while
|
|
1063
|
+
// incoming requests piled up — the accounts were never the problem, the
|
|
1064
|
+
// ceiling was.
|
|
1065
|
+
name: "gateway-capacity-exhausted",
|
|
1066
|
+
match: (s) =>
|
|
1067
|
+
s.lower.includes("at its concurrency limit") ||
|
|
1068
|
+
s.lower.includes("every proxy pool for this provider is at its concurrency limit") ||
|
|
1069
|
+
s.lower.includes("the gateway is at its concurrency limit"),
|
|
1070
|
+
decide: (s) =>
|
|
1071
|
+
s.result(
|
|
1072
|
+
"model_capacity",
|
|
1073
|
+
// `status` is the status the account would move to; it is never
|
|
1074
|
+
// applied because `mutatesAccount` is false below, so this names the
|
|
1075
|
+
// shape rather than a transition. Zero cooldown is the point: no wait
|
|
1076
|
+
// is attached to the account, so the next request may use it at once.
|
|
1077
|
+
"cooldown",
|
|
1078
|
+
0,
|
|
1079
|
+
null,
|
|
1080
|
+
s.reason("Gateway concurrency ceiling reached; no account was at fault"),
|
|
1081
|
+
// Never mutates: there is nothing wrong with the account to record.
|
|
1082
|
+
false,
|
|
1083
|
+
),
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
// Must precede the unclassified fallback: the fallback is account-blind and
|
|
1087
|
+
// would stamp this as a provider-wide failure, which is how one proxy
|
|
1088
|
+
// refusing CONNECT put every account of every provider behind that pool
|
|
1089
|
+
// into cooldown — opencode's dead front door taking codebuddy down with it.
|
|
1090
|
+
//
|
|
1091
|
+
// A CONNECT refusal is a statement about the proxy pool, not about a
|
|
1092
|
+
// credential: the tunnel was never established, so no upstream ever saw the
|
|
1093
|
+
// request and no account can be at fault. Scoped to the pool so the
|
|
1094
|
+
// provider's own accounts stay in rotation.
|
|
1095
|
+
name: "proxy-connect-refusal",
|
|
1096
|
+
match: (s) =>
|
|
1097
|
+
s.lower.includes("proxy connect failed") ||
|
|
1098
|
+
(s.lower.includes("connect") && s.lower.includes("tunnel") && s.lower.includes("refus")),
|
|
1099
|
+
decide: (s) =>
|
|
1100
|
+
s.result(
|
|
1101
|
+
"model_unreachable",
|
|
1102
|
+
"cooldown",
|
|
1103
|
+
0,
|
|
1104
|
+
null,
|
|
1105
|
+
s.reason("Proxy pool refused the CONNECT tunnel"),
|
|
1106
|
+
// Never mutates: the pool owns this failure, not the account.
|
|
1107
|
+
false,
|
|
1108
|
+
),
|
|
1109
|
+
},
|
|
944
1110
|
{
|
|
945
1111
|
name: "deployment-infrastructure",
|
|
946
1112
|
match: (s) => s.deploymentInfraError,
|
|
@@ -976,14 +1142,42 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
976
1142
|
name: "content-policy-block",
|
|
977
1143
|
match: (s) => s.policyRejection && s.buddy,
|
|
978
1144
|
decide: (s) => {
|
|
979
|
-
|
|
1145
|
+
// A ban is a verdict on the credential, and cooldown cannot answer it:
|
|
1146
|
+
// the account is refused, not throttled. Disabling is the fix, and
|
|
1147
|
+
// disabling *immediately* is a feature — a client's retry budget is spent
|
|
1148
|
+
// in seconds, so a banned account that merely cooled stayed in the plan
|
|
1149
|
+
// and burned sibling accounts that were still healthy before the window
|
|
1150
|
+
// expired.
|
|
1151
|
+
//
|
|
1152
|
+
// The two shapes are told apart by the body, not the code alone: `11140`
|
|
1153
|
+
// names a restriction on the account, `11-128` refuses the request.
|
|
1154
|
+
if (s.buddyBanned) {
|
|
1155
|
+
return s.result(
|
|
1156
|
+
"policy_blocked_banned",
|
|
1157
|
+
"disabled",
|
|
1158
|
+
0,
|
|
1159
|
+
null,
|
|
1160
|
+
s.reason("Account banned by upstream"),
|
|
1161
|
+
// A ban is evidence about the credential even at provider scope, for
|
|
1162
|
+
// the same reason a named revocation is: the message is the evidence.
|
|
1163
|
+
// Without this a stream frame's missing credential detail would leave
|
|
1164
|
+
// a banned account serving.
|
|
1165
|
+
true,
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
// Not a ban: the upstream refused this request. The account is fine, so
|
|
1169
|
+
// the only correct move is to stop retrying it verbatim. Cooling the
|
|
1170
|
+
// account here was the bug that made one malformed request look like a
|
|
1171
|
+
// dead credential.
|
|
980
1172
|
return s.result(
|
|
981
|
-
"
|
|
1173
|
+
"policy_blocked_invalid_request",
|
|
982
1174
|
"cooldown",
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
s.reason("
|
|
986
|
-
|
|
1175
|
+
0,
|
|
1176
|
+
null,
|
|
1177
|
+
s.reason("Upstream refused this request"),
|
|
1178
|
+
// Never mutates: a malformed body fails identically on every account,
|
|
1179
|
+
// so cooling would burn a healthy credential on the gateway's own fault.
|
|
1180
|
+
false,
|
|
987
1181
|
);
|
|
988
1182
|
},
|
|
989
1183
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{o as e,r as t,t as n}from"./jsx-runtime-CXUMngI_.js";import{i as r}from"./terminal-0_IjPWTk.js";import{c as i,s as a}from"./numbers-BOevJzdz.js";import{n as o,r as s,t as c}from"./state-2c5V4zo7.js";import{t as l}from"./dialog-C07hr5_G.js";import{t as u}from"./rotate-ccw-CufcqITi.js";import{t as d}from"./trash-Cppn5c3R.js";import{C as f,T as p,v as m}from"./assert-DU6qrMA_.js";import{o as h,t as g}from"./query-keys-UETqfytp.js";import{t as _}from"./useMutation-EDFRkn1i.js";import{k as v,n as y,v as b,y as x,z as S}from"./entry-8U_8IAW2.js";import{t as C}from"./badge-CKsF123d.js";import{n as w}from"./inline-C9dCYaBZ.js";import{t as T}from"./layout-V6y8DAkl.js";async function E(e){return h(`/quota/overview`,e?{signal:e}:void 0)}function D(){return f({queryKey:g.quota.all,queryFn:e=>E(m(e)),staleTime:1/0,refetchInterval:!1,refetchIntervalInBackground:!1,refetchOnMount:!1,refetchOnWindowFocus:!1,placeholderData:e=>e})}function O(e){let t=p(),n=g.quota.account(e);return _({mutationFn:async()=>h(`/accounts/${encodeURIComponent(e)}/quota/refresh`,{method:`POST`}),onSuccess:async()=>{await Promise.all([t.invalidateQueries({queryKey:n,exact:!0}),t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}function k(){let e=p();return _({mutationFn:async({accountIds:e,onProgress:t})=>{let n=e.length,r=0,i=await h(`/quota/refresh`,{method:`POST`,body:JSON.stringify({accountIds:e})});return r=i.queued,t?.(r,n),i},onSuccess:async(t,n)=>{let r=n.accountIds,i=new Set(t.failures.map(e=>e.accountId)),a=r.filter(e=>!i.has(e));await Promise.all([...a.map(t=>e.invalidateQueries({queryKey:g.quota.account(t),exact:!0})),e.invalidateQueries({queryKey:g.quota.all})])}})}function A(){let e=p();return _({mutationFn:({id:e,active:t})=>h(`/accounts/${encodeURIComponent(e)}`,{method:`PATCH`,body:JSON.stringify({active:t})}),onSuccess:async()=>{await Promise.all([e.invalidateQueries({queryKey:g.quota.all}),e.invalidateQueries({queryKey:g.providers.all})])}})}function j(){let e=p();return _({mutationFn:async({ids:e,active:t})=>{e.length!==0&&await h(`/accounts/batch`,{method:`PATCH`,body:JSON.stringify({ids:e,active:t})})},onSuccess:async()=>{await Promise.all([e.invalidateQueries({queryKey:g.quota.all}),e.invalidateQueries({queryKey:g.providers.all})])}})}function M(){let e=p();return _({mutationFn:async e=>{await h(`/accounts/${encodeURIComponent(e)}`,{method:`DELETE`})},onSuccess:async(t,n)=>{e.removeQueries({queryKey:g.quota.account(n),exact:!0}),await Promise.all([e.invalidateQueries({queryKey:g.quota.all}),e.invalidateQueries({queryKey:g.providers.all})])}})}var N=new Set([`workbuddy`,`workbuddycn`,`cb`,`cbcn`]);function P(e){return N.has(e.trim().toLowerCase())}function F(e){let t=p();return _({mutationFn:async()=>{if(e.length===0)throw Error(`Select a provider first`);return h(`/quota/daily-growth`,{method:`POST`,body:JSON.stringify({providerId:e})})},onSuccess:async()=>{await Promise.all([t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}function I(e){let t=p();return _({mutationFn:async()=>{let t=await h(`/accounts/${encodeURIComponent(e)}/activity-report`,{method:`POST`}),n=t.message??`Activity reported`,r=await h(`/accounts/${encodeURIComponent(e)}/checkin`,{method:`POST`}),i=r.message??`Daily check-in done`;return{ok:t.ok,message:t.ok?`Report success · Check-in ${i}`:`Report failed: ${n} · Check-in ${i}`,checkin:r,report:t}},onSuccess:async()=>{await Promise.all([t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}var L=new Set([`codex`,`claude`,`anthropic`]);function R(e){return L.has(e.trim().toLowerCase())}function z(e,t){return f({queryKey:g.quota.resets(e),queryFn:({signal:t})=>h(`/accounts/${encodeURIComponent(e)}/resets`,t?{signal:t}:void 0),enabled:t,staleTime:3e4})}function B(e){let t=p();return _({mutationFn:async t=>h(`/accounts/${encodeURIComponent(e)}/reset`,{method:`POST`,body:JSON.stringify(t??{})}),onSettled:async()=>{await Promise.all([t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}var V=e(t(),1),H=n();function U(e){return e===`cooldown`}function W(e){return e===`active`?`ok`:e===`disabled`?`disabled`:`warn`}function G(e,t){return e?{title:`Enable static-token mode?`,message:`${t} will be used exactly as issued and will not be refreshed. Re-enable refresh before this token expires.`,confirmLabel:`Enable static mode`}:{title:`Re-enable credential refresh?`,message:`${t} will return to the OAuth refresh sweep. Use this only when a refresh token is available.`,confirmLabel:`Re-enable refresh`}}function K({summary:e,onClose:t}){let{providerId:n,accountId:f,title:p,status:m,errorCategory:h,errorMessage:g}=e,_=y(n,f),E=x(),D=_.data??[],O=v(),k=e.credentialKind===`oauth`||e.credentialMode===`jwt`||e.staticToken===!0,[A,j]=(0,V.useState)(e.staticToken===!0),[M,N]=(0,V.useState)(null),[P,F]=(0,V.useState)(!1);(0,V.useEffect)(()=>j(e.staticToken===!0),[e.staticToken]);let I=b();return(0,H.jsx)(l,{open:!0,onClose:t,title:`Health & Error Log — ${p}`,size:`lg`,children:(0,H.jsxs)(`div`,{className:`flex flex-col gap-7 min-w-0`,children:[(0,H.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-5 rounded-control border border-inner-border bg-surface-2 py-5 px-7`,children:[(0,H.jsxs)(`div`,{className:`min-w-0`,children:[(0,H.jsxs)(w,{gap:`8px`,className:`flex-wrap`,children:[(0,H.jsx)(`span`,{className:`text-body font-semibold`,children:`Account status:`}),(0,H.jsx)(C,{tone:W(m),dot:!0,children:m.toUpperCase()}),h?(0,H.jsx)(C,{tone:`warn`,title:g??void 0,children:h}):null]}),g?(0,H.jsx)(`p`,{className:`text-badge text-red mt-2 wrap-anywhere`,children:g}):null]}),U(m)?(0,H.jsx)(i,{size:`sm`,variant:`secondary`,icon:(0,H.jsx)(u,{size:12,className:E.isPending?`animate-spin`:``}),disabled:E.isPending,onClick:()=>E.mutate({providerId:n,accountId:f},{onSuccess:()=>void _.refetch()}),children:E.isPending?`Recovering...`:`Recover Now`}):null]}),k?(0,H.jsxs)(`div`,{className:`flex items-center justify-between gap-6 rounded-control border border-inner-border bg-surface-2 py-5 px-7`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`text-caption font-semibold`,children:`Credential management`}),(0,H.jsx)(`div`,{className:`text-badge text-text-tertiary`,children:A?`Static mode: no refresh request will be sent.`:`OAuth refresh is enabled for this account.`})]}),(0,H.jsx)(i,{size:`sm`,variant:A?`primary`:`secondary`,disabled:O.isPending,onClick:()=>N(!A),children:A?`Static · no refresh`:`Enable static mode`})]}):null,(0,H.jsxs)(`div`,{className:`min-w-0`,children:[(0,H.jsxs)(`div`,{className:`flex items-center justify-between gap-5 mb-4`,children:[(0,H.jsxs)(`h4`,{className:`text-caption font-semibold text-text-secondary`,children:[`Transition Events & Audit (`,D.length,`)`]}),(0,H.jsx)(i,{size:`sm`,variant:`secondary`,icon:(0,H.jsx)(d,{size:12}),disabled:D.length===0||I.isPending,onClick:()=>F(!0),title:`Deletes this account's recorded transitions and audit entries.`,children:`Purge log`})]}),_.isPending?(0,H.jsx)(s,{compact:!0,label:`Loading health events...`}):_.isError?(0,H.jsx)(o,{compact:!0,message:`Failed to load health events`,onRetry:()=>void _.refetch()}):D.length===0?(0,H.jsx)(c,{compact:!0,title:`No health events recorded`,message:e.emptyMessage,icon:(0,H.jsx)(r,{size:20})}):(0,H.jsx)(`div`,{className:`rounded-inset border border-inner-border`,children:(0,H.jsx)(T,{maxHeight:480,scrollRegion:!0,headers:[`Timestamp`,`From → To`,`Model`,`Category`,`Reason / Details`],children:D.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`text-badge text-text-tertiary whitespace-nowrap`,children:new Date(e.createdAt).toLocaleString()}),(0,H.jsxs)(`td`,{className:`whitespace-nowrap`,children:[e.fromStatus!==null&&e.fromStatus!==e.toStatus?(0,H.jsxs)(`span`,{className:`text-badge text-text-secondary`,children:[e.fromStatus,` →`,` `]}):(0,H.jsx)(`span`,{className:`text-badge text-text-tertiary`,children:`logged · `}),(0,H.jsx)(C,{tone:W(e.toStatus),children:e.toStatus})]}),(0,H.jsx)(`td`,{className:`font-mono tabular-nums text-badge text-text-secondary wrap-anywhere`,children:e.modelId??`—`}),(0,H.jsx)(`td`,{className:`font-mono tabular-nums text-badge text-text-secondary`,children:e.errorCategory??`—`}),(0,H.jsx)(`td`,{className:`text-badge text-text-primary min-w-110 max-w-260 wrap-anywhere`,children:e.reason??`—`})]},e.id))})})]}),(0,H.jsx)(S,{open:M!==null,onClose:()=>N(null),onConfirm:()=>{let e=M===!0;O.mutate({providerId:n,accountId:f,request:{staticToken:e}},{onSuccess:()=>{j(e),N(null),a.success(e?`Static mode enabled`:`Credential refresh re-enabled`,p),_.refetch()},onError:e=>a.error(`Credential update failed`,e instanceof Error?e.message:`Unable to update credential mode`)})},title:G(M===!0,p).title,message:G(M===!0,p).message,confirmLabel:G(M===!0,p).confirmLabel,cancelLabel:`Cancel`}),(0,H.jsx)(S,{open:P,onClose:()=>F(!1),onConfirm:()=>{I.mutate({providerId:n,accountId:f,title:p},{onSuccess:()=>F(!1)})},title:`Purge the health & error log for ${p}?`,message:`Deletes every transition and audit entry recorded for this account. Its status, credentials, and counters are untouched. This cannot be undone.`,confirmLabel:`Purge log`,cancelLabel:`Cancel`}),(0,H.jsx)(w,{justify:`flex-end`,gap:`0px`,children:(0,H.jsx)(i,{variant:`secondary`,size:`sm`,onClick:t,children:`Close`})})]})})}export{M as a,k as c,I as d,F as f,z as i,j as l,P as n,D as o,A as p,R as r,O as s,K as t,B as u};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|