cartethyia 2.5.2 → 2.5.3
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-MsQaeQTs.js → BannedPage-hfCzi-Ux.js} +1 -1
- package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BO9nKg9w.js → CliToolDetailPage-DLGqYEwI.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-DEDyKBlC.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-CfuYiQ8Z.js} +1 -1
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-bsLuZmYH.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-_UWhwziJ.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-qomXThxX.js → HealthEventsModal-Cjfc5WFS.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-DDVIfZGW.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-C-ExCy1V.js → OverviewPage-B53YYMng.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-BS9IEpvv.js → ProvidersPage-Dx31DgIO.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-Rn_qXScb.js → ProxyPage-BpnR4kXT.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BihkgQSq.js → QuotaPage-BpOQCnun.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +1 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-D6pmQ86L.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-DWvKiuSl.js} +1 -1
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-CmkUrgdj.js} +1 -1
- package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-B47uOydr.js} +1 -1
- package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Dj9AR0gY.js → entry-CkryrIea.js} +3 -3
- package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
- package/dist/dashboard/assets/{index-DCBtROoK.js → index-BydyKaDz.js} +2 -2
- package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
- package/dist/dashboard/assets/{select-DsX5QT8K.js → select-BO6h8yNv.js} +1 -1
- package/dist/dashboard/assets/select-BO6h8yNv.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 +21 -21
- package/package.json +1 -1
- package/scripts/cli/command/console-menu.ts +248 -246
- package/scripts/cli/command/tray.ts +208 -204
- package/src/config/spec.ts +26 -2
- package/src/console/backup/routes.ts +52 -1
- package/src/console/backup/service.ts +60 -4
- package/src/console/share/share-router.ts +6 -9
- package/src/observability/payload-capture.ts +366 -344
- package/src/observability/payload-store.ts +12 -2
- package/src/observability/telemetry-status.ts +28 -16
- package/src/persistence/stores/api-key-store.ts +6 -0
- package/src/persistence/stores/share-store.ts +583 -556
- package/src/persistence/stores/telemetry-store.ts +1 -1
- package/src/protocol/response/gemini.ts +18 -4
- package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
- package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
- package/src/providers/integrations/cline/cline-quota.ts +8 -11
- package/src/providers/integrations/opencode/provider.ts +532 -532
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae-catalog.ts +5 -3
- package/src/providers/operations/account-health-service.ts +23 -0
- package/src/providers/operations/client-versions.ts +9 -2
- package/src/providers/provider-metadata.ts +7 -1
- package/src/transport/dispatch/attempt-finalize.ts +46 -5
- package/src/transport/dispatch/proxy-request.ts +9 -0
- package/src/transport/dispatch/streaming/stream.ts +16 -2
- package/src/transport/routing/route-catalog.ts +806 -799
- package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
- package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
- package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
|
@@ -35,7 +35,7 @@ export const TOKENHARBOR_FALLBACK_MODELS: readonly ModelDefinition[] = [
|
|
|
35
35
|
defineModel({ id: "muse-spark-1.2", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
36
36
|
defineModel({ id: "muse-spark-1-3", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
37
37
|
defineModel({ id: "gpt-6-astra", providerId: "tokenharbor", ctx: 1_050_000, reasoning: true, vision: true }),
|
|
38
|
-
defineModel({ id: "deepseek-v4.1-flash", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true }),
|
|
38
|
+
defineModel({ id: "deepseek-v4.1-flash", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
39
39
|
defineModel({ id: "gemini-3.6-flash", providerId: "tokenharbor", ctx: 1_000_000, reasoning: true, vision: true }),
|
|
40
40
|
defineModel({ id: "glm-5.2", providerId: "tokenharbor", ctx: 1_000_000, reasoning: true }),
|
|
41
41
|
defineModel({ id: "gpt-5.6-luna", providerId: "tokenharbor", reasoning: true, vision: true }),
|
|
@@ -62,7 +62,7 @@ export const TOKENHARBOR_FALLBACK_MODELS: readonly ModelDefinition[] = [
|
|
|
62
62
|
defineModel({ id: "mimo-v2.6-pro", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
63
63
|
defineModel({ id: "mimo-v2.5:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true, free: true }),
|
|
64
64
|
defineModel({ id: "deepseek-v4-flash:free", providerId: "tokenharbor", reasoning: true, free: true }),
|
|
65
|
-
defineModel({ id: "deepseek-v4.1-flash:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, free: true }),
|
|
65
|
+
defineModel({ id: "deepseek-v4.1-flash:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true, free: true }),
|
|
66
66
|
defineModel({ id: "qwen3.8-flash:free", providerId: "tokenharbor", reasoning: true, free: true }),
|
|
67
67
|
defineModel({ id: "mimo-v2.6-flash:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true, free: true }),
|
|
68
68
|
];
|
|
@@ -79,7 +79,9 @@ const TRAE_CN_ROWS: readonly TraeModelRow[] = [
|
|
|
79
79
|
* (DeepSeek-V4 rows: 200k active, 1M selectable), so reading `dev` makes a
|
|
80
80
|
* client under-request the window it is entitled to. The maximum is per model
|
|
81
81
|
* and is **not** uniform: the GPT rows cap at 272k and Kimi-K2.5 / MiniMax-M2.7
|
|
82
|
-
* offer no selectable upgrade at all, staying at 200k.
|
|
82
|
+
* offer no selectable upgrade at all, staying at 200k. `gpt-5.4` is the
|
|
83
|
+
* exception: upstream raised its selectable maximum to 1M, so it is filed at
|
|
84
|
+
* 1M while the rest of the GPT rows stay at 272k.
|
|
83
85
|
*
|
|
84
86
|
* `out` is what the same model is filed at in the CodeBuddy roster where that
|
|
85
87
|
* roster carries it (DeepSeek-V4 384k, MiniMax-M3 512k, Kimi-K2.5 262k,
|
|
@@ -98,13 +100,13 @@ const TRAE_CN_ROWS: readonly TraeModelRow[] = [
|
|
|
98
100
|
* one.
|
|
99
101
|
*/
|
|
100
102
|
const TRAE_INTL_ROWS: readonly TraeModelRow[] = [
|
|
101
|
-
{ id: "gpt-5.4", displayName: "GPT-5.4", ctx:
|
|
103
|
+
{ id: "gpt-5.4", displayName: "GPT-5.4", ctx: 1_000_000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
102
104
|
{ id: "gpt-5.2", displayName: "GPT-5.2", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
103
105
|
{ id: "gpt-5.2-codex-paygo", displayName: "GPT-5.2-Codex", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
104
106
|
{ id: "gpt-5.1-paygo", displayName: "GPT-5.1", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
105
107
|
{ id: "gpt-5-paygo", displayName: "GPT-5", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
106
108
|
{ id: "Dola-Seed-2.0-Code", displayName: "Seed-2.1-Turbo", ctx: 1000000, out: 131_072, vision: true, reasoning: true, surface: "agent" },
|
|
107
|
-
{ id: "deepseek-v4.1-flash", displayName: "DeepSeek-V4.1-Flash", ctx: 1000000, out: 384_000, vision:
|
|
109
|
+
{ id: "deepseek-v4.1-flash", displayName: "DeepSeek-V4.1-Flash", ctx: 1000000, out: 384_000, vision: true, reasoning: true, surface: "agent" },
|
|
108
110
|
{ id: "deepseek-v4-flash-0731", displayName: "DeepSeek-V4-Flash", ctx: 1000000, out: 384_000, vision: false, reasoning: true, surface: "agent" },
|
|
109
111
|
{ id: "deepseek-v4-pro-0813", displayName: "DeepSeek-V4-Pro", ctx: 1000000, out: 384_000, vision: false, reasoning: true, surface: "agent" },
|
|
110
112
|
{ id: "kimi-k3", displayName: "Kimi-K3", ctx: 1000000, out: 131_072, vision: true, reasoning: true, surface: "agent" },
|
|
@@ -637,11 +637,33 @@ export function classifyAccountError(
|
|
|
637
637
|
lower.includes("no access to model") ||
|
|
638
638
|
lower.includes("model_not_supported") ||
|
|
639
639
|
lower.includes("not available in your region"));
|
|
640
|
+
/**
|
|
641
|
+
* The upstream accepted the request and put it in a queue behind other
|
|
642
|
+
* callers — it is busy, not rejecting the caller's credential.
|
|
643
|
+
*
|
|
644
|
+
* Qoder answers `403 {"code":"10605","message":"{\"isQueued\":true,
|
|
645
|
+
* \"modelKey\":\"dfmodel\",\"queueCount\":1395,...}"}`. The 403 is what made
|
|
646
|
+
* this misclassify: the generic `(401 || 403) && accountEvidence` arm claimed
|
|
647
|
+
* it as `auth_invalidated`, so a healthy account was parked as "Re-login
|
|
648
|
+
* required" purely because the provider was busy, and an operator was sent
|
|
649
|
+
* to re-authenticate a credential that was never rejected.
|
|
650
|
+
*
|
|
651
|
+
* Matched on the queue evidence rather than on the bare `10605`, so an
|
|
652
|
+
* unrelated 403 that happens to share the code is not swept in. The bodies
|
|
653
|
+
* are nested JSON, so the tell is the key, not a parse of the envelope.
|
|
654
|
+
*/
|
|
655
|
+
const queuedUpstream =
|
|
656
|
+
providerCode === "10605" ||
|
|
657
|
+
lower.includes("isqueued") ||
|
|
658
|
+
lower.includes("queuecount") ||
|
|
659
|
+
lower.includes("queuetype") ||
|
|
660
|
+
(lower.includes("queue") && lower.includes("modelkey"));
|
|
640
661
|
const authSignal =
|
|
641
662
|
!quotaSignal &&
|
|
642
663
|
!policyRejection &&
|
|
643
664
|
!hostedToolFailure &&
|
|
644
665
|
!modelEntitlementFailure &&
|
|
666
|
+
!queuedUpstream &&
|
|
645
667
|
(providerCode === "authentication_failed" ||
|
|
646
668
|
folded.includes("invalid api key") ||
|
|
647
669
|
folded.includes("incorrect api key") ||
|
|
@@ -805,6 +827,7 @@ export function classifyAccountError(
|
|
|
805
827
|
statusCode === 503 ||
|
|
806
828
|
statusCode === 529 ||
|
|
807
829
|
providerCode === "model_capacity" ||
|
|
830
|
+
queuedUpstream ||
|
|
808
831
|
lower.includes("model overloaded") ||
|
|
809
832
|
lower.includes("capacity exceeded") ||
|
|
810
833
|
lower.includes("temporarily unavailable");
|
|
@@ -632,12 +632,19 @@ export function buildWorkBuddyUserAgent(
|
|
|
632
632
|
cliVersion = getWorkBuddyCliVersion(),
|
|
633
633
|
): string {
|
|
634
634
|
// 1:1 with codebuddy2api. The CLI profile is `CLI/<v> CodeBuddy/<v>` and the
|
|
635
|
-
// WorkBuddy profile is `WorkBuddy/<wb>
|
|
635
|
+
// WorkBuddy profile is `WorkBuddy/<wb> workbuddy-ai/<wb> CLI/<cli>`, which
|
|
636
636
|
// sends NO SDK headers — two separate profiles, not one string with a switch.
|
|
637
637
|
// Whether SDK headers ride along is decided by `workbuddyHeaders`, not here, so
|
|
638
638
|
// this builder only decides the UA text.
|
|
639
|
+
//
|
|
640
|
+
// The second token is the wire spelling `workbuddy-ai`, NOT the display
|
|
641
|
+
// brand `WorkBuddy AI`. This is what an official client actually sends; the
|
|
642
|
+
// product is branded with a space in the UI and in docs, and copying that
|
|
643
|
+
// branding into the header produced a UA no real client emits — which the
|
|
644
|
+
// intl gateway answers with 403 `11140`, the same refusal it gives a
|
|
645
|
+
// wrong-brand header. Product name ≠ wire token.
|
|
639
646
|
if (identity === "CLI") return `CLI/${cliVersion} CodeBuddy/${cliVersion}`;
|
|
640
|
-
return `WorkBuddy/${clientVersion}
|
|
647
|
+
return `WorkBuddy/${clientVersion} workbuddy-ai/${clientVersion} CLI/${cliVersion}`;
|
|
641
648
|
}
|
|
642
649
|
|
|
643
650
|
export const _resetWorkBuddyCnClientVersionCache = accessor("workbuddyCnClient").reset;
|
|
@@ -86,6 +86,9 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
86
86
|
serviceKinds: ["llm", "websearch", "imageGeneration"],
|
|
87
87
|
credentialUrl: "https://antigravity.google",
|
|
88
88
|
credentialHint: "Signed in with a Google account through the Antigravity login flow.",
|
|
89
|
+
// Per-account credential, so rotating the egress address buys nothing —
|
|
90
|
+
// the same reason Hermes, InferHub and Cline skip.
|
|
91
|
+
defaultSkipProxy: true,
|
|
89
92
|
},
|
|
90
93
|
{ id: "muse", displayName: "Muse Code", baseUrl: "https://api.meta.ai" },
|
|
91
94
|
{
|
|
@@ -109,6 +112,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
109
112
|
baseUrl: "https://q.us-east-1.amazonaws.com",
|
|
110
113
|
bespokeWire: true,
|
|
111
114
|
hasAdapterUserAgent: true,
|
|
115
|
+
defaultSkipProxy: true,
|
|
112
116
|
credentialUrl: "https://kiro.dev",
|
|
113
117
|
credentialHint: "Sign in with Google or GitHub in the browser, or use the device-code login with an Identity Center organization, then paste a Kiro API key if you prefer. Prompt caching is not reliably working for Kiro: the upstream recomputes the prompt on every turn, so cached-token savings should not be expected.",
|
|
114
118
|
},
|
|
@@ -331,6 +335,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
331
335
|
displayName: "Kilo Code",
|
|
332
336
|
baseUrl: "https://api.kilo.ai/api/openrouter",
|
|
333
337
|
credentialUrl: "https://app.kilo.ai/device-auth",
|
|
338
|
+
defaultSkipProxy: true,
|
|
334
339
|
},
|
|
335
340
|
{
|
|
336
341
|
// Command Code's adapter stamps its own client identity headers
|
|
@@ -366,7 +371,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
366
371
|
credentialUrl: `${QODER_REALMS.cn.website}/account/integrations`,
|
|
367
372
|
credentialHint: "Paste a Personal Access Token (pt-...) from the Qoder CN integrations page, or sign in with your Qoder China account.",
|
|
368
373
|
},
|
|
369
|
-
{ id: "trae", displayName: "Trae", baseUrl: `${TRAE_REALMS.intl.core}${TRAE_CHAT_PATH}`, hasAdapterUserAgent: true, credentialUrl: "https://www.trae.ai/", credentialHint: "Paste a Trae account export (accessToken + refreshToken) or the raw access token from an international Trae account." },
|
|
374
|
+
{ id: "trae", displayName: "Trae", baseUrl: `${TRAE_REALMS.intl.core}${TRAE_CHAT_PATH}`, hasAdapterUserAgent: true, defaultSkipProxy: true, credentialUrl: "https://www.trae.ai/", credentialHint: "Paste a Trae account export (accessToken + refreshToken) or the raw access token from an international Trae account." },
|
|
370
375
|
// A separate provider, not a region flag on `trae`: the China deployment has
|
|
371
376
|
// its own core host, authorization scheme, and model roster, and a CN account
|
|
372
377
|
// dispatched to the intl host is refused rather than degraded.
|
|
@@ -392,6 +397,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
392
397
|
baseUrl: "https://api.individual.githubcopilot.com",
|
|
393
398
|
hasAdapterUserAgent: true,
|
|
394
399
|
credentialUrl: "https://github.com/settings/copilot",
|
|
400
|
+
defaultSkipProxy: true,
|
|
395
401
|
},
|
|
396
402
|
{ id: "perplexity", displayName: "Perplexity", baseUrl: "https://api.perplexity.ai", credentialUrl: "https://www.perplexity.ai/settings/api" },
|
|
397
403
|
// Native web-search providers expose only the search service. Their catalog
|
|
@@ -39,6 +39,27 @@ export async function shouldCaptureProviderBody(
|
|
|
39
39
|
return payloadCaptureKeepsBodies(db, tenantId);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Per-stage cap for the provider request/response tee at one capture depth.
|
|
44
|
+
*
|
|
45
|
+
* The tee is what the drawer's "Provider Request" and "Provider Response"
|
|
46
|
+
* panels read, so its cap is the most a provider panel can ever show. It used
|
|
47
|
+
* to be a flat 256 KiB at every depth: on a large request the translated
|
|
48
|
+
* provider body was clipped to 256 KiB before the shared record budget ever
|
|
49
|
+
* ran, so a tenant who raised the depth to Maximum — 16 MiB — still saw a
|
|
50
|
+
* quarter-megabyte provider request and an empty provider response. Derived
|
|
51
|
+
* from the same depth tiers as the record budget, so raising the depth now
|
|
52
|
+
* raises both.
|
|
53
|
+
*
|
|
54
|
+
* Capped at a quarter of the depth's total budget: four panels share it, and
|
|
55
|
+
* one of them must never be able to spend the whole thing.
|
|
56
|
+
*/
|
|
57
|
+
export function providerCaptureMaxBytes(depth?: TelemetryPayloadDepth): number {
|
|
58
|
+
if (depth === "moderate") return 2 * 1024 * 1024;
|
|
59
|
+
if (depth === "maximum") return 4 * 1024 * 1024;
|
|
60
|
+
return 256 * 1024;
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
/**
|
|
43
64
|
* Same answer as {@link shouldCaptureProviderBody}, memoized on the request.
|
|
44
65
|
*
|
|
@@ -62,6 +83,28 @@ export async function shouldCaptureProviderBodyForRequest(
|
|
|
62
83
|
return resolved.ok === true && resolved.mode === "full";
|
|
63
84
|
}
|
|
64
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The tenant's capture depth, memoized on the request like the mode above.
|
|
88
|
+
*
|
|
89
|
+
* Read *after* {@link shouldCaptureProviderBodyForRequest} has populated the
|
|
90
|
+
* memo, so the two answers come from one preference read. It sizes the
|
|
91
|
+
* provider tee: a tenant who raised the depth is asking for more of every
|
|
92
|
+
* panel, and a flat cap would answer with the same clipped body regardless.
|
|
93
|
+
*/
|
|
94
|
+
export async function resolveCaptureDepthForRequest(
|
|
95
|
+
db: CartethyiaDatabase,
|
|
96
|
+
tenantId: string | null,
|
|
97
|
+
state: { payloadCapturePreference?: unknown },
|
|
98
|
+
): Promise<TelemetryPayloadDepth> {
|
|
99
|
+
if (state.payloadCapturePreference === undefined) {
|
|
100
|
+
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
101
|
+
}
|
|
102
|
+
const resolved = state.payloadCapturePreference as
|
|
103
|
+
| { readonly ok: true; readonly depth: TelemetryPayloadDepth }
|
|
104
|
+
| { readonly ok: false };
|
|
105
|
+
return resolved.ok === true ? resolved.depth : "minimum";
|
|
106
|
+
}
|
|
107
|
+
|
|
65
108
|
/**
|
|
66
109
|
* Combined body cap for one capture depth. Fixed size tiers: every depth
|
|
67
110
|
* stores all four drawer panels (client request, translated provider
|
|
@@ -103,7 +146,6 @@ export function parseCapturedBody(value: unknown): unknown {
|
|
|
103
146
|
}
|
|
104
147
|
}
|
|
105
148
|
|
|
106
|
-
const PROVIDER_CAPTURE_MAX_BYTES = 256 * 1024;
|
|
107
149
|
const PROVIDER_CAPTURE_AWAIT_MS = 2_000;
|
|
108
150
|
const CAPTURABLE_RESPONSE_TYPES = /^(text\/|application\/(json|x-ndjson|sse))/i;
|
|
109
151
|
|
|
@@ -258,6 +300,7 @@ export function captureProviderExchange(
|
|
|
258
300
|
inner: ValidatedOutboundFetch,
|
|
259
301
|
sink: ProviderExchangeCapture,
|
|
260
302
|
captureBody = true,
|
|
303
|
+
maxBytes = providerCaptureMaxBytes(),
|
|
261
304
|
): ValidatedOutboundFetch {
|
|
262
305
|
return async (input, init) => {
|
|
263
306
|
const url =
|
|
@@ -276,14 +319,12 @@ export function captureProviderExchange(
|
|
|
276
319
|
// unbounded translated request body per attempt — up to `maxAttempts` of
|
|
277
320
|
// them, all alive until the loop returns — costs more than it can tell
|
|
278
321
|
// an operator.
|
|
279
|
-
...(rawBody === null
|
|
280
|
-
? {}
|
|
281
|
-
: { body: parseCapturedBody(rawBody.slice(0, PROVIDER_CAPTURE_MAX_BYTES)) }),
|
|
322
|
+
...(rawBody === null ? {} : { body: parseCapturedBody(rawBody.slice(0, maxBytes)) }),
|
|
282
323
|
};
|
|
283
324
|
const response = await inner(input, init);
|
|
284
325
|
sink.response =
|
|
285
326
|
captureBody && shouldCaptureProviderResponse(response)
|
|
286
|
-
? readBoundedBody(response.clone(),
|
|
327
|
+
? readBoundedBody(response.clone(), maxBytes).then(parseCapturedBody)
|
|
287
328
|
: Promise.resolve(null);
|
|
288
329
|
// Read regardless of `captureBody`: this is metadata about the dispatch, not
|
|
289
330
|
// payload retention, so a tenant in `metadata` mode still records which
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
completionContext,
|
|
29
29
|
estimatedUsage,
|
|
30
30
|
parseCapturedBody,
|
|
31
|
+
providerCaptureMaxBytes,
|
|
32
|
+
resolveCaptureDepthForRequest,
|
|
31
33
|
shouldCaptureProviderBodyForRequest,
|
|
32
34
|
terminalFailure,
|
|
33
35
|
} from "./attempt-finalize";
|
|
@@ -239,6 +241,13 @@ export async function handleProviderProxyRequest(
|
|
|
239
241
|
prepared.authorization.snapshot.tenant_id,
|
|
240
242
|
state,
|
|
241
243
|
),
|
|
244
|
+
providerCaptureMaxBytes(
|
|
245
|
+
await resolveCaptureDepthForRequest(
|
|
246
|
+
deps.db,
|
|
247
|
+
prepared.authorization.snapshot.tenant_id,
|
|
248
|
+
state,
|
|
249
|
+
),
|
|
250
|
+
),
|
|
242
251
|
),
|
|
243
252
|
}
|
|
244
253
|
: {}),
|
|
@@ -43,7 +43,12 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
|
|
|
43
43
|
import type { NetworkPoolSelector } from "../../../network/pool/selector";
|
|
44
44
|
import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
|
|
45
45
|
import type { ProviderExchangeCapture } from "../attempt-finalize";
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
captureProviderExchange,
|
|
48
|
+
providerCaptureMaxBytes,
|
|
49
|
+
resolveCaptureDepthForRequest,
|
|
50
|
+
shouldCaptureProviderBodyForRequest,
|
|
51
|
+
} from "../attempt-finalize";
|
|
47
52
|
import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
|
|
48
53
|
import type { AttemptLeaseOwnership } from "../leases";
|
|
49
54
|
import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
|
|
@@ -165,11 +170,20 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
165
170
|
networkPoolId,
|
|
166
171
|
prepared.authorization.snapshot.tenant_id,
|
|
167
172
|
);
|
|
173
|
+
const captureBody = outboundFetch
|
|
174
|
+
? await shouldCaptureProviderBodyForRequest(deps.db, prepared.authorization.snapshot.tenant_id, state)
|
|
175
|
+
: false;
|
|
176
|
+
const captureDepth = await resolveCaptureDepthForRequest(
|
|
177
|
+
deps.db,
|
|
178
|
+
prepared.authorization.snapshot.tenant_id,
|
|
179
|
+
state,
|
|
180
|
+
);
|
|
168
181
|
const wrappedOutboundFetch = outboundFetch
|
|
169
182
|
? captureProviderExchange(
|
|
170
183
|
outboundFetch,
|
|
171
184
|
providerCapture,
|
|
172
|
-
|
|
185
|
+
captureBody,
|
|
186
|
+
providerCaptureMaxBytes(captureDepth),
|
|
173
187
|
)
|
|
174
188
|
: undefined;
|
|
175
189
|
const dispatchContext = buildUpstreamDispatchContext({
|