cartethyia 2.7.5 → 2.7.6
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/.env.example +4 -1
- package/README.md +1 -1
- package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
- package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
- package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
- package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-D5rol2j2.js → ConsoleLogPage-KRYjHZsX.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-B5nZvmf0.js → InjectSystemPromptPanel-d5v3K_Yc.js} +245 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
- package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DnQPSIA8.js → ModelPicker-C6WlpU5E.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-5SnUSy3j.js → ProxyPage-DxjvUzSD.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BJtRZ4qD.js → SettingsPage-BW4AKj2H.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
- package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BfAb5-o_.js → StudioPage-DRPavCdU.js} +4 -4
- package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BNzeZ2Vk.js → UsagePage-B7M7LvMM.js} +13 -13
- package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
- package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
- package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C0K-c4eo.js → assert-CidQVoNo.js} +1 -1
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DfbhQU4k.js → badge-BaX2GTrn.js} +1 -1
- package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BllT4LcF.js → banner-DFH4Bkte.js} +1 -1
- package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
- package/dist/dashboard/assets/{card-CtkX66Wd.js → card-CxDw0lK6.js} +1 -1
- package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CQXVMn4P.js → cli-tools-Bwv5-zGZ.js} +1 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-BQqBQd2n.js → clipboard-button-DfS1ntiq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BrTEZ8zu.js → drawer-QqPul6sC.js} +1 -1
- package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-BAACA727.js → formatters-u7JPB5BX.js} +1 -1
- package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
- package/dist/dashboard/assets/{index-FQlj0HUT.js → index-Ko4dLRnV.js} +2 -2
- package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C4gU5iOL.js → inline-CHJQwxLa.js} +1 -1
- package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
- package/dist/dashboard/assets/{input-BHHX_Tmm.js → input-BlmlizVV.js} +1 -1
- package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CajrYUnD.js → layout-Bz93I-yE.js} +1 -1
- package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Bx2I7XUe.js → list-row-Ml16KCfu.js} +1 -1
- package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
- package/dist/dashboard/assets/{live-DNNqRbj2.js → live-7_QpnYGu.js} +1 -1
- package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/{network-C5UBACrJ.js → network-CuMDaTzT.js} +1 -1
- package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-Xsi9Qx04.js → pagination-B5aOoweZ.js} +1 -1
- package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai.webp +0 -0
- package/dist/dashboard/assets/providers/zcode.webp +0 -0
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/{routing-in0f_IfK.js → routing-fnCsxgNO.js} +1 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/{select-BLULNqTp.js → select-BHqpd-bg.js} +2 -2
- package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CcGczj6I.js → settings-BxslsErn.js} +1 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D2lIR0tg.js → stack-j7cXjgYX.js} +1 -1
- package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
- package/dist/dashboard/assets/{state-DTARjfiW.js → state-DHmEnLa2.js} +1 -1
- package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
- package/dist/dashboard/assets/{switch-c0oPSnqW.js → switch-DmZcyTIC.js} +1 -1
- package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-m8WGNPrz.js → useMutation-CbCP4w1o.js} +1 -1
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +57 -57
- package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
- package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
- package/package.json +1 -1
- package/src/boot/dependencies.ts +10 -2
- package/src/config/providers.ts +13 -7
- package/src/config/spec.ts +18 -11
- package/src/config/usage.ts +27 -0
- package/src/console/api-keys/routes.ts +19 -23
- package/src/console/cli-tools/contracts.ts +39 -295
- package/src/console/cli-tools/fs-ops.ts +29 -0
- package/src/console/cli-tools/injectors/claude.ts +3 -3
- package/src/console/cli-tools/injectors/codex.ts +6 -6
- package/src/console/cli-tools/injectors/driver.ts +8 -105
- package/src/console/cli-tools/injectors/kilo.ts +146 -21
- package/src/console/cli-tools/injectors/opencode.ts +28 -5
- package/src/console/cli-tools/routes.ts +6 -0
- package/src/console/cli-tools/service.ts +7 -6
- package/src/console/observability/contracts.ts +6 -0
- package/src/console/observability/store.ts +3 -0
- package/src/console/share/share-stats.ts +7 -9
- package/src/observability/event-log.ts +7 -0
- package/src/observability/event-query.ts +2 -0
- package/src/observability/telemetry-buffer.ts +7 -0
- package/src/persistence/schema.ts +8 -0
- package/src/persistence/stores/api-key-store.ts +19 -1
- package/src/protocol/dsml.ts +5 -76
- package/src/providers/adapters/compatible-adapter.ts +29 -1
- package/src/providers/default-registry.ts +12 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -14
- package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
- package/src/providers/integrations/antigravity/antigravity.ts +20 -11
- package/src/providers/integrations/cursor/cursor.ts +2 -3
- package/src/providers/integrations/github/github-discovery.ts +15 -2
- package/src/providers/integrations/github/github-oauth.ts +178 -9
- package/src/providers/integrations/github/github.ts +95 -14
- package/src/providers/integrations/kiro/kiro.ts +1 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +9 -3
- package/src/providers/integrations/qoder/qoder.ts +147 -88
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/operations/account-error-classification.ts +67 -17
- package/src/providers/operations/byok-wire-profile.ts +19 -15
- package/src/providers/provider-usage.ts +15 -49
- package/src/security/admission/service.ts +11 -4
- package/src/security/api-key-auth.ts +5 -3
- package/src/transport/dispatch/attempt-finalize.ts +69 -21
- package/src/transport/dispatch/attempt-loop.ts +22 -0
- package/src/transport/failure-policy.ts +15 -1
- package/src/transport/guards/error-lifecycle.ts +4 -0
- package/src/transport/request/state.ts +6 -0
- package/src/transport/routing/router.ts +7 -0
- package/src/transport/surface/reasoning-summary.ts +9 -14
- package/src/transport/vision-handoff.ts +83 -28
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js +0 -1
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +0 -1
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +0 -1
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js +0 -1
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js +0 -1
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js +0 -1
- package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
- package/dist/dashboard/assets/app--PJWq8_C.css +0 -2
- package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
- package/dist/dashboard/assets/app-Bgd3zANQ.js +0 -9
- package/dist/dashboard/assets/app-Bgd3zANQ.js.gz +0 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
- package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
- package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
- package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BEK7KP0H.js +0 -1
- package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js +0 -1
- package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
- package/dist/dashboard/assets/entry-EBHJ3DrE.js +0 -1
- package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js +0 -4
- package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
- package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js +0 -1
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
- package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
- package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
- package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
- package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js +0 -2
- package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js +0 -1
- package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
- package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
- package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
- package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
- package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
- package/src/console/cli-tools/injectors/cline.ts +0 -110
- package/src/console/cli-tools/injectors/copilot.ts +0 -108
- package/src/console/cli-tools/injectors/cowork.ts +0 -64
- package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
- package/src/console/cli-tools/injectors/droid.ts +0 -96
- package/src/console/cli-tools/injectors/grok-build.ts +0 -70
- package/src/console/cli-tools/injectors/hermes.ts +0 -101
- package/src/console/cli-tools/injectors/jcode.ts +0 -115
- package/src/console/cli-tools/injectors/openclaw.ts +0 -161
|
@@ -998,8 +998,16 @@ export function buildQoderRequest(
|
|
|
998
998
|
// When the caller sends no intent but the model needs the push to think
|
|
999
999
|
// (verified live: Sonus/Cantus stream no reasoning without it), use the
|
|
1000
1000
|
// roster's default effort.
|
|
1001
|
+
//
|
|
1002
|
+
// Only a reasoning-capable model carries the field. A non-reasoning model
|
|
1003
|
+
// (e.g. `lite`) cannot think, so an effort is meaningless there; sending it
|
|
1004
|
+
// would also assert a capability the model does not advertise. The upstream
|
|
1005
|
+
// tolerates it today, but declaring reasoning on a model that has none is a
|
|
1006
|
+
// latent contract violation, so the field is kept off non-reasoning models.
|
|
1001
1007
|
const reasoningEffort =
|
|
1002
|
-
|
|
1008
|
+
reasoning
|
|
1009
|
+
? (reasoningEffortFromIntent(request.reasoning) ?? modelConfig.default_reasoning_effort)
|
|
1010
|
+
: undefined;
|
|
1003
1011
|
const sessionId = qoderStableHash("qoder-session", auth.userId, modelId);
|
|
1004
1012
|
const recordId = qoderStableRecordId(modelId, allMessages, maxTokens);
|
|
1005
1013
|
return {
|
|
@@ -1073,6 +1081,17 @@ export function buildQoderRequest(
|
|
|
1073
1081
|
const choice = qoderToolChoice(request.tool_choice);
|
|
1074
1082
|
return choice === undefined ? {} : { tool_choice: choice };
|
|
1075
1083
|
})(),
|
|
1084
|
+
// Passthrough knobs the IDE surface reads from `parameters` (worker `vyl`):
|
|
1085
|
+
// parallel_tool_calls, tool_choice, top_p, seed, user, response_format, …
|
|
1086
|
+
// Only include what the caller actually set.
|
|
1087
|
+
parameters: (() => {
|
|
1088
|
+
const out: Record<string, unknown> = {};
|
|
1089
|
+
const choice = qoderToolChoice(request.tool_choice);
|
|
1090
|
+
if (choice !== undefined) out["tool_choice"] = choice;
|
|
1091
|
+
const parallel = request.generation_controls.parallel_tool_calls;
|
|
1092
|
+
if (parallel !== undefined) out["parallel_tool_calls"] = parallel;
|
|
1093
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
1094
|
+
})(),
|
|
1076
1095
|
};
|
|
1077
1096
|
}
|
|
1078
1097
|
|
|
@@ -1096,11 +1115,21 @@ export function buildQoderIdeRequest(
|
|
|
1096
1115
|
): Record<string, unknown> {
|
|
1097
1116
|
const legacy = buildQoderRequest(modelId, request, modelConfig, auth, realm);
|
|
1098
1117
|
const messages = legacy.messages as Array<Record<string, unknown>>;
|
|
1118
|
+
// The IDE surface (`model/v1/chat/completions`) must ship the same tool
|
|
1119
|
+
// declaration as the legacy builder. The unpacked desktop client (worker
|
|
1120
|
+
// `PFn`) injects `tools` directly when present — without it the upstream
|
|
1121
|
+
// sees no toolset and answers with a DSML text block (or claims no tool
|
|
1122
|
+
// exists) instead of a structured `tool_calls`. The remaining knobs
|
|
1123
|
+
// (`parallel_tool_calls`, `tool_choice`, …) ride the `parameters`
|
|
1124
|
+
// passthrough (worker `vyl`), matching the reference exactly.
|
|
1125
|
+
const tools = legacy.tools as Array<Record<string, unknown>> | undefined;
|
|
1099
1126
|
return {
|
|
1100
1127
|
model: modelId,
|
|
1101
1128
|
messages,
|
|
1102
1129
|
stream: true,
|
|
1103
1130
|
stream_options: { include_usage: true },
|
|
1131
|
+
...(tools !== undefined && tools.length > 0 ? { tools } : {}),
|
|
1132
|
+
...(legacy.parameters === undefined ? {} : { parameters: legacy.parameters }),
|
|
1104
1133
|
// The same reasoning-effort field the legacy surface uses; without it the
|
|
1105
1134
|
// IDE surface streams no thinking for models that need the push (Sonus,
|
|
1106
1135
|
// Cantus). Undefined when the caller asked for none and the roster has no
|
|
@@ -1197,93 +1226,90 @@ function qoderIdeEntitlementRefused(response: Response): boolean {
|
|
|
1197
1226
|
* normal stop with reasoning only and no output text/tool calls (observed ~1
|
|
1198
1227
|
* in 4 runs with Sonus). That is not a valid completion — the lab would show a
|
|
1199
1228
|
* thought with "No text came back". Replay such a stream before committing to
|
|
1200
|
-
* it.
|
|
1201
|
-
*
|
|
1229
|
+
* it. `sniffQoderStream` below inspects just enough of the body to make that
|
|
1230
|
+
* call without buffering the whole (possibly long) stream.
|
|
1202
1231
|
*/
|
|
1203
|
-
interface QoderStreamDrain {
|
|
1204
|
-
readonly bytes: Uint8Array;
|
|
1205
|
-
readonly meaningful: boolean;
|
|
1206
|
-
readonly thoughtOnly: boolean;
|
|
1207
|
-
}
|
|
1208
1232
|
|
|
1209
|
-
|
|
1233
|
+
/**
|
|
1234
|
+
* Reads the upstream body just far enough to decide whether it carries real
|
|
1235
|
+
* output, then returns a replay stream that re-emits the already-read bytes
|
|
1236
|
+
* followed by the rest of the original body. If the body ends before any
|
|
1237
|
+
* meaningful output is seen, `meaningful` is false and the caller replays the
|
|
1238
|
+
* whole request (empty / thought-only recovery) without ever buffering a large
|
|
1239
|
+
* response: the original `drainQoderResponse` read the entire stream before
|
|
1240
|
+
* forwarding, which made every stream appear to complete in one burst at the
|
|
1241
|
+
* end.
|
|
1242
|
+
*/
|
|
1243
|
+
async function sniffQoderStream(
|
|
1210
1244
|
response: Response,
|
|
1211
1245
|
signal: AbortSignal,
|
|
1212
|
-
): Promise<
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
const reader =
|
|
1216
|
-
try {
|
|
1217
|
-
for (;;) {
|
|
1218
|
-
if (signal.aborted) break;
|
|
1219
|
-
const { done, value } = await reader.read();
|
|
1220
|
-
if (done) break;
|
|
1221
|
-
if (value) {
|
|
1222
|
-
chunks.push(value);
|
|
1223
|
-
total += value.byteLength;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
} finally {
|
|
1227
|
-
reader.releaseLock();
|
|
1228
|
-
}
|
|
1229
|
-
const bytes = new Uint8Array(total);
|
|
1230
|
-
let offset = 0;
|
|
1231
|
-
for (const chunk of chunks) {
|
|
1232
|
-
bytes.set(chunk, offset);
|
|
1233
|
-
offset += chunk.byteLength;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
let dataEvents = 0;
|
|
1237
|
-
let meaningful = false;
|
|
1238
|
-
let thoughtOnly = false;
|
|
1246
|
+
): Promise<{ meaningful: boolean; response: Response }> {
|
|
1247
|
+
const body = response.body;
|
|
1248
|
+
if (body === null) return { meaningful: true, response };
|
|
1249
|
+
const reader = body.getReader();
|
|
1239
1250
|
const decoder = new TextDecoder();
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
}
|
|
1258
|
-
const choices = json["choices"];
|
|
1259
|
-
if (!Array.isArray(choices)) continue;
|
|
1260
|
-
for (const choice of choices as Array<Record<string, unknown>>) {
|
|
1261
|
-
const delta = choice["delta"] as Record<string, unknown> | undefined;
|
|
1262
|
-
if (delta === undefined) continue;
|
|
1263
|
-
if (typeof delta["content"] === "string" && (delta["content"] as string).length > 0) {
|
|
1264
|
-
meaningful = true;
|
|
1265
|
-
return { bytes, meaningful, thoughtOnly };
|
|
1266
|
-
}
|
|
1267
|
-
if (Array.isArray(delta["tool_calls"]) && (delta["tool_calls"] as unknown[]).length > 0) {
|
|
1251
|
+
const pending: Uint8Array[] = [];
|
|
1252
|
+
let inspected = "";
|
|
1253
|
+
let meaningful = false;
|
|
1254
|
+
for (let i = 0; i < 32 && !meaningful; i += 1) {
|
|
1255
|
+
const { done, value } = await reader.read();
|
|
1256
|
+
if (signal.aborted) break;
|
|
1257
|
+
if (done) break;
|
|
1258
|
+
pending.push(value);
|
|
1259
|
+
inspected += decoder.decode(value, { stream: true });
|
|
1260
|
+
// Peek at completed SSE frames only: a content delta or a tool-call
|
|
1261
|
+
// fragment means real output is coming. Reasoning-only streams are NOT
|
|
1262
|
+
// meaningful (they are exactly what empty-stream recovery replays), and a
|
|
1263
|
+
// finish reason alone is not output either.
|
|
1264
|
+
if (/data:/.test(inspected)) {
|
|
1265
|
+
for (const frame of inspected.split(/\r?\n\r?\n/)) {
|
|
1266
|
+
if (!frame.includes("data:")) continue;
|
|
1267
|
+
if (frame.includes('"content":"') || frame.includes('"tool_calls"')) {
|
|
1268
1268
|
meaningful = true;
|
|
1269
|
-
|
|
1270
|
-
}
|
|
1271
|
-
if (
|
|
1272
|
-
typeof delta["reasoning_content"] === "string" &&
|
|
1273
|
-
(delta["reasoning_content"] as string).length > 0
|
|
1274
|
-
) {
|
|
1275
|
-
thoughtOnly = true;
|
|
1269
|
+
break;
|
|
1276
1270
|
}
|
|
1277
1271
|
}
|
|
1278
1272
|
}
|
|
1273
|
+
if (inspected.length > 65536) {
|
|
1274
|
+
meaningful = true;
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1279
1277
|
}
|
|
1280
|
-
|
|
1278
|
+
// Replay: previously-read bytes first, then continue the original body.
|
|
1279
|
+
let offset = 0;
|
|
1280
|
+
const replay = new ReadableStream<Uint8Array>({
|
|
1281
|
+
start(controller) {
|
|
1282
|
+
for (; offset < pending.length; offset += 1) controller.enqueue(pending[offset]);
|
|
1283
|
+
},
|
|
1284
|
+
async pull(controller) {
|
|
1285
|
+
if (offset < pending.length) {
|
|
1286
|
+
controller.enqueue(pending[offset]);
|
|
1287
|
+
offset += 1;
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const { done, value } = await reader.read();
|
|
1291
|
+
if (signal.aborted) {
|
|
1292
|
+
controller.close();
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (done) {
|
|
1296
|
+
controller.close();
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
controller.enqueue(value);
|
|
1300
|
+
},
|
|
1301
|
+
cancel(reason) {
|
|
1302
|
+
return reader.cancel(reason);
|
|
1303
|
+
},
|
|
1304
|
+
});
|
|
1305
|
+
return { meaningful, response: new Response(replay, response) };
|
|
1281
1306
|
}
|
|
1282
1307
|
|
|
1283
1308
|
/**
|
|
1284
1309
|
* Runs the IDE request up to `1 + maxReplays` times, returning the first
|
|
1285
1310
|
* response whose stream carried real output (text or tool calls). A 200 with
|
|
1286
|
-
* no output (empty or thought-only) is replayed rather than surfaced.
|
|
1311
|
+
* no output (empty or thought-only) is replayed rather than surfaced. The
|
|
1312
|
+
* stream is forwarded progressively (not buffered to completion).
|
|
1287
1313
|
*/
|
|
1288
1314
|
async function fetchQoderIdeWithEmptyRecovery(
|
|
1289
1315
|
requestOnce: () => Promise<Response>,
|
|
@@ -1294,12 +1320,9 @@ async function fetchQoderIdeWithEmptyRecovery(
|
|
|
1294
1320
|
for (let attempt = 0; attempt <= maxReplays; attempt += 1) {
|
|
1295
1321
|
const res = await requestOnce();
|
|
1296
1322
|
if (!res.ok || !res.body) return res;
|
|
1297
|
-
const
|
|
1298
|
-
last =
|
|
1299
|
-
|
|
1300
|
-
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
|
|
1301
|
-
});
|
|
1302
|
-
if (drain.meaningful) return last;
|
|
1323
|
+
const sniff = await sniffQoderStream(res, signal);
|
|
1324
|
+
last = sniff.response;
|
|
1325
|
+
if (sniff.meaningful) return last;
|
|
1303
1326
|
}
|
|
1304
1327
|
return last!;
|
|
1305
1328
|
}
|
|
@@ -1336,6 +1359,9 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1336
1359
|
// (recovered) or ordinary prose (released).
|
|
1337
1360
|
let leakBuf = "";
|
|
1338
1361
|
let leakFrames = 0;
|
|
1362
|
+
// Tool-call argument fragments, keyed by call id, reconciled at end of
|
|
1363
|
+
// stream (see the tool_calls handling in the loop).
|
|
1364
|
+
const toolCallBuffer = new Map<string, { index: number; name: string | undefined; arguments: string }>();
|
|
1339
1365
|
|
|
1340
1366
|
for await (const sse of decodeQoderSseEvents(body, signal)) {
|
|
1341
1367
|
const sseData = sse.data;
|
|
@@ -1492,7 +1518,14 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1492
1518
|
for (const tc of toolCalls) {
|
|
1493
1519
|
const fn = tc["function"] as Record<string, unknown> | undefined;
|
|
1494
1520
|
const index = typeof tc["index"] === "number" ? (tc["index"] as number) : undefined;
|
|
1495
|
-
|
|
1521
|
+
// `id` is only meaningful when non-empty: some Qoder slots echo
|
|
1522
|
+
// `id: ""` on continuation chunks, and treating that as a fresh
|
|
1523
|
+
// call id would orphan every argument fragment (the real call
|
|
1524
|
+
// keeps its first-seen id, the fragments pile up under "").
|
|
1525
|
+
const declaredId =
|
|
1526
|
+
typeof tc["id"] === "string" && (tc["id"] as string).length > 0
|
|
1527
|
+
? (tc["id"] as string)
|
|
1528
|
+
: undefined;
|
|
1496
1529
|
const fragName = typeof fn?.["name"] === "string" ? (fn["name"] as string) : undefined;
|
|
1497
1530
|
// OpenAI continuation chunks re-identify their call by wire
|
|
1498
1531
|
// `index` alone — the `id` appears only once, in the first chunk.
|
|
@@ -1520,14 +1553,27 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1520
1553
|
} else {
|
|
1521
1554
|
callId = `call_${String(index ?? 0)}`;
|
|
1522
1555
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1556
|
+
// Buffer tool-call argument fragments per call id until the end of
|
|
1557
|
+
// stream. Upstream streaming fragments occasionally drop or mangle
|
|
1558
|
+
// a byte mid-arguments; forwarding each fragment as an
|
|
1559
|
+
// `arguments_delta` would hand the client a malformed JSON blob to
|
|
1560
|
+
// stitch. Holding and reconciling here yields one complete,
|
|
1561
|
+
// parseable arguments string per call — flush happens at end of
|
|
1562
|
+
// stream (or when a terminal frame arrives).
|
|
1563
|
+
const existing = toolCallBuffer.get(callId);
|
|
1564
|
+
if (existing) {
|
|
1565
|
+
existing.index = index ?? existing.index;
|
|
1566
|
+
if (fragName !== undefined) existing.name = fragName;
|
|
1567
|
+
if (typeof fn?.["arguments"] === "string") {
|
|
1568
|
+
existing.arguments += (fn["arguments"] as string);
|
|
1569
|
+
}
|
|
1570
|
+
} else {
|
|
1571
|
+
toolCallBuffer.set(callId, {
|
|
1572
|
+
index: index ?? 0,
|
|
1573
|
+
name: fragName,
|
|
1574
|
+
arguments: typeof fn?.["arguments"] === "string" ? (fn["arguments"] as string) : "",
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1531
1577
|
}
|
|
1532
1578
|
}
|
|
1533
1579
|
}
|
|
@@ -1588,6 +1634,19 @@ export async function* qoderBodyToCanonicalEvents(
|
|
|
1588
1634
|
const stopReason = mapQoderFinishReason(finishReason);
|
|
1589
1635
|
const _providerStop = typeof finishReason === "string" ? finishReason : undefined;
|
|
1590
1636
|
void hasTerminal;
|
|
1637
|
+
// Flush buffered tool-call fragments as complete, stitched calls — one
|
|
1638
|
+
// `arguments_delta` per call with the full JSON string, so the client never
|
|
1639
|
+
// has to repair upstream frame-loss.
|
|
1640
|
+
for (const [callId, buffered] of toolCallBuffer) {
|
|
1641
|
+
yield {
|
|
1642
|
+
type: "tool_call_delta",
|
|
1643
|
+
sequence_number: seq++,
|
|
1644
|
+
call_id: callId,
|
|
1645
|
+
index: buffered.index,
|
|
1646
|
+
name: buffered.name,
|
|
1647
|
+
arguments_delta: buffered.arguments,
|
|
1648
|
+
} as CanonicalEvent;
|
|
1649
|
+
}
|
|
1591
1650
|
yield {
|
|
1592
1651
|
type: "terminal",
|
|
1593
1652
|
sequence_number: seq++,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { GatewayError } from "../../../protocol/gateway-error";
|
|
8
8
|
import type { CanonicalEvent, CanonicalMessage, CanonicalRequest, CanonicalStopReason } from "../../../protocol/canonical-model";
|
|
9
9
|
import type { UsageRecord } from "../../../protocol/canonical-model";
|
|
10
|
-
import {
|
|
10
|
+
import { usageFromProvider } from "../../provider-usage";
|
|
11
11
|
import {
|
|
12
12
|
readCredentialSecret,
|
|
13
13
|
assertWireFamily,
|
|
@@ -482,7 +482,7 @@ class TraeAdapter implements ProviderAdapter {
|
|
|
482
482
|
// prefixes we estimate them locally (see trae-cache.ts). Then the
|
|
483
483
|
// operator's cache mode decides whether a hit makes the whole input count
|
|
484
484
|
// as cached.
|
|
485
|
-
return
|
|
485
|
+
return estimateTraeSessionCache(record, sessionId, messages);
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
}
|
|
@@ -458,6 +458,26 @@ function banWording(lower: string): boolean {
|
|
|
458
458
|
);
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Whether GitHub Copilot stated the account lacks a license — a ban-shaped
|
|
463
|
+
* verdict on the account, not a refreshable auth rejection.
|
|
464
|
+
*
|
|
465
|
+
* Observed live (2026-11-03): the health log showed
|
|
466
|
+
* `unauthorized: not licensed to use Copilot` and `forbidden` on
|
|
467
|
+
* `gpt-6-luna`. Copilot answers 401 for a bad credential and 403 for a
|
|
468
|
+
* license/entitlement refusal, so `forbidden`/`unauthorized` on this surface
|
|
469
|
+
* is the same verdict. No amount of waiting or refreshing changes it — the
|
|
470
|
+
* subscription cannot serve this credential.
|
|
471
|
+
*/
|
|
472
|
+
function copilotNotLicensed(lower: string): boolean {
|
|
473
|
+
return (
|
|
474
|
+
lower.includes("not licensed") ||
|
|
475
|
+
lower.includes("forbidden") ||
|
|
476
|
+
lower.includes("unauthorized") ||
|
|
477
|
+
lower.includes("license")
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
461
481
|
/**
|
|
462
482
|
* Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
|
|
463
483
|
* window longer than that is a monthly or plan-level limit, and parking an
|
|
@@ -1263,6 +1283,33 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1263
1283
|
true,
|
|
1264
1284
|
),
|
|
1265
1285
|
},
|
|
1286
|
+
{
|
|
1287
|
+
// GitHub Copilot answers a license/entitlement refusal with 403 and
|
|
1288
|
+
// "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
|
|
1289
|
+
// verdict on the account: the subscription cannot serve this credential,
|
|
1290
|
+
// and no amount of waiting or refreshing changes it. Cooldown cycling
|
|
1291
|
+
// (auth_invalidated + 5m backoff) kept a dead account in rotation where a
|
|
1292
|
+
// client's retry budget burned sibling accounts — the same failure the
|
|
1293
|
+
// buddy ban exists to prevent. Disable immediately. Must run before
|
|
1294
|
+
// `credential-rejected`, which would claim the bare 403 as a refreshable
|
|
1295
|
+
// auth rejection.
|
|
1296
|
+
name: "github-copilot-not-licensed",
|
|
1297
|
+
match: (s) =>
|
|
1298
|
+
s.origin === "upstream" &&
|
|
1299
|
+
s.providerId === "github" &&
|
|
1300
|
+
copilotNotLicensed(s.lower),
|
|
1301
|
+
decide: (s) =>
|
|
1302
|
+
s.result(
|
|
1303
|
+
"policy_blocked_banned",
|
|
1304
|
+
"disabled",
|
|
1305
|
+
0,
|
|
1306
|
+
null,
|
|
1307
|
+
s.reason("GitHub Copilot account not licensed"),
|
|
1308
|
+
// A license refusal is evidence about the account even at provider
|
|
1309
|
+
// scope, for the same reason a named revocation or a buddy ban is.
|
|
1310
|
+
true,
|
|
1311
|
+
),
|
|
1312
|
+
},
|
|
1266
1313
|
{
|
|
1267
1314
|
name: "credential-rejected",
|
|
1268
1315
|
match: (s) =>
|
|
@@ -1471,7 +1518,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1471
1518
|
},
|
|
1472
1519
|
{
|
|
1473
1520
|
name: "quota-exhausted",
|
|
1474
|
-
match: (s) => s.quotaSignal,
|
|
1521
|
+
match: (s) => s.origin === "upstream" && s.quotaSignal,
|
|
1475
1522
|
decide: (s) => {
|
|
1476
1523
|
// xAI Grok Build's free tier resets on a rolling window. Use a duration
|
|
1477
1524
|
// stated by the provider when present; otherwise park for the conservative
|
|
@@ -1525,6 +1572,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1525
1572
|
// moves to the next account and the flagged one returns almost at once.
|
|
1526
1573
|
name: "inception-per-minute-limit",
|
|
1527
1574
|
match: (s) =>
|
|
1575
|
+
s.origin === "upstream" &&
|
|
1528
1576
|
s.providerId === "inception" &&
|
|
1529
1577
|
(s.statusCode === 429 ||
|
|
1530
1578
|
s.providerCode === "rate_limit_reached" ||
|
|
@@ -1549,15 +1597,16 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1549
1597
|
{
|
|
1550
1598
|
name: "rate-limit",
|
|
1551
1599
|
match: (s) =>
|
|
1552
|
-
s.
|
|
1553
|
-
s.
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1600
|
+
s.origin === "upstream" &&
|
|
1601
|
+
(s.statusCode === 429 ||
|
|
1602
|
+
s.providerCode === "rate_limit_exceeded" ||
|
|
1603
|
+
s.lower.includes("rate_limit") ||
|
|
1604
|
+
s.lower.includes("rate limit") ||
|
|
1605
|
+
s.lower.includes("too many requests") ||
|
|
1606
|
+
s.lower.includes("requests per minute") ||
|
|
1607
|
+
s.lower.includes("tokens per minute") ||
|
|
1608
|
+
s.lower.includes("tpm limit") ||
|
|
1609
|
+
s.lower.includes("rpm limit")),
|
|
1561
1610
|
decide: (s) => {
|
|
1562
1611
|
const cooldownMs = s.statedCooldownMs ?? DEFAULT_RATE_LIMIT_COOLDOWN_MS();
|
|
1563
1612
|
// Same per-account reasoning as quota above: the failing account (or
|
|
@@ -1576,13 +1625,14 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1576
1625
|
{
|
|
1577
1626
|
name: "model-capacity",
|
|
1578
1627
|
match: (s) =>
|
|
1579
|
-
s.
|
|
1580
|
-
s.statusCode ===
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1628
|
+
s.origin === "upstream" &&
|
|
1629
|
+
(s.statusCode === 503 ||
|
|
1630
|
+
s.statusCode === 529 ||
|
|
1631
|
+
s.providerCode === "model_capacity" ||
|
|
1632
|
+
s.queuedUpstream ||
|
|
1633
|
+
s.lower.includes("model overloaded") ||
|
|
1634
|
+
s.lower.includes("capacity exceeded") ||
|
|
1635
|
+
s.lower.includes("temporarily unavailable")),
|
|
1586
1636
|
decide: (s) => {
|
|
1587
1637
|
const cooldownMs = s.statedCooldownMs ?? MODEL_CAPACITY_COOLDOWN_MS();
|
|
1588
1638
|
const modelScopedMutation = s.origin === "upstream" && s.modelId !== undefined;
|
|
@@ -92,12 +92,11 @@ const ENDPOINT_TAIL = /\/(?:v\d+\/)?(?:messages|chat\/completions|responses|comp
|
|
|
92
92
|
* The root an operator's pasted URL really meant, plus the endpoint tail that
|
|
93
93
|
* was removed.
|
|
94
94
|
*
|
|
95
|
-
*
|
|
96
|
-
* `https://api.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* that is already a root comes back with an empty `stripped`.
|
|
95
|
+
* Only the **tail** is stripped. Any path before it belongs to the gateway
|
|
96
|
+
* root and survives, so `https://api.longcat.ai/anthropic/v1/messages`
|
|
97
|
+
* resolves to `https://api.longcat.ai/anthropic` (not the bare origin) — the
|
|
98
|
+
* `/anthropic` prefix is where LongCat actually serves the API. A URL that is
|
|
99
|
+
* already a root comes back with an empty `stripped`.
|
|
101
100
|
*/
|
|
102
101
|
export function splitEndpointBaseUrl(baseUrl: string): { readonly baseUrl: string; readonly stripped: string } {
|
|
103
102
|
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
@@ -110,20 +109,22 @@ export function splitEndpointBaseUrl(baseUrl: string): { readonly baseUrl: strin
|
|
|
110
109
|
const basePath = parsed.pathname.replace(/\/+$/, "");
|
|
111
110
|
const match = ENDPOINT_TAIL.exec(basePath);
|
|
112
111
|
if (match === null) return { baseUrl: trimmed, stripped: "" };
|
|
113
|
-
|
|
112
|
+
const keptPath = basePath.slice(0, match.index);
|
|
113
|
+
return { baseUrl: `${parsed.origin}${keptPath}`, stripped: basePath };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Base URL an operator's model list lives under (`<base>/models`). A bare host
|
|
118
118
|
* has no version segment, so it gets the `/v1` prefix; a base that already
|
|
119
119
|
* carries a path (conventionally `/v1`) is used as-is. A pasted endpoint path
|
|
120
|
-
* is stripped first, so `…/anthropic/v1/messages` discovers at
|
|
121
|
-
*
|
|
120
|
+
* is stripped first, so `…/anthropic/v1/messages` discovers at
|
|
121
|
+
* `<origin>/anthropic/v1/models` — the gateway prefix before the endpoint tail
|
|
122
|
+
* belongs to the root and is preserved.
|
|
122
123
|
*/
|
|
123
124
|
export function modelDiscoveryBaseUrl(baseUrl: string): string {
|
|
124
|
-
// A pasted endpoint
|
|
125
|
-
//
|
|
126
|
-
//
|
|
125
|
+
// A pasted endpoint has its tail removed; whatever path came before it is a
|
|
126
|
+
// real gateway prefix and stays. A base that already IS a root — bare host,
|
|
127
|
+
// or a version/gateway prefix — keeps that path, because
|
|
127
128
|
// `https://gateway.example/api/v1` legitimately serves its list there.
|
|
128
129
|
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
129
130
|
let parsed: URL;
|
|
@@ -133,9 +134,12 @@ export function modelDiscoveryBaseUrl(baseUrl: string): string {
|
|
|
133
134
|
return `${trimmed}/v1`;
|
|
134
135
|
}
|
|
135
136
|
const basePath = parsed.pathname.replace(/\/+$/, "");
|
|
136
|
-
const
|
|
137
|
-
const listBase =
|
|
138
|
-
|
|
137
|
+
const match = ENDPOINT_TAIL.exec(basePath);
|
|
138
|
+
const listBase =
|
|
139
|
+
match === null
|
|
140
|
+
? `${parsed.origin}${basePath}`
|
|
141
|
+
: `${parsed.origin}${basePath.slice(0, match.index)}`;
|
|
142
|
+
if (match !== null || basePath === "" || basePath === "/") return `${listBase}/v1`;
|
|
139
143
|
return listBase;
|
|
140
144
|
}
|
|
141
145
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UsageRecord } from "../protocol/canonical-model";
|
|
2
|
+
import { resolveUsageInputMode } from "../config/usage";
|
|
2
3
|
export interface OpenAiUsage {
|
|
3
4
|
prompt_tokens: number;
|
|
4
5
|
completion_tokens: number;
|
|
@@ -11,7 +12,6 @@ export interface OpenAiUsage {
|
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
import { modelsDevCatalog } from "./discovery/models-dev-catalog";
|
|
14
|
-
import { resolveCacheMode } from "../config/providers";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Extract cost multipliers from catalog model cost JSONB.
|
|
@@ -126,36 +126,6 @@ export function repriceUsage(
|
|
|
126
126
|
: { ...usage, estimated_cost: estimatedCost };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
* Simplified cache accounting: when the provider reported a cache hit, the
|
|
131
|
-
* whole prompt counts as cached — request input ber-cache = cache-eligible →
|
|
132
|
-
* input = cache. The cache-write volume is already inside the all-in input
|
|
133
|
-
* total on cache-aware shapes, so it stays separate and is not double-counted
|
|
134
|
-
* as a hit. A reported `0` or an absent report stay a cache miss. In `real`
|
|
135
|
-
* mode the provider's reported split is kept as-is.
|
|
136
|
-
*
|
|
137
|
-
* Exported so the hand-built record producers (Cursor, Kiro) apply the same
|
|
138
|
-
* rule as `normalizeUsage`.
|
|
139
|
-
*/
|
|
140
|
-
export function applyCacheMode(usage: UsageRecord): UsageRecord {
|
|
141
|
-
if (resolveCacheMode() !== "simplified") return usage;
|
|
142
|
-
if (
|
|
143
|
-
typeof usage.cached_input_tokens === "number" &&
|
|
144
|
-
usage.cached_input_tokens > 0 &&
|
|
145
|
-
usage.input_tokens > 0
|
|
146
|
-
) {
|
|
147
|
-
const writes =
|
|
148
|
-
typeof usage.cache_write_tokens === "number" ? Math.max(0, usage.cache_write_tokens) : 0;
|
|
149
|
-
const cached = Math.min(usage.input_tokens, Math.max(0, usage.input_tokens - writes));
|
|
150
|
-
return {
|
|
151
|
-
...usage,
|
|
152
|
-
cached_input_tokens: cached,
|
|
153
|
-
uncached_input_tokens: 0,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
return usage;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
129
|
/**
|
|
160
130
|
* Normalize provider-specific usage data into canonical UsageRecord.
|
|
161
131
|
* Handles Anthropic (cache_read_input_tokens, cache_creation_input_tokens, output_tokens_details.thinking_tokens)
|
|
@@ -266,10 +236,21 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
|
|
|
266
236
|
const reportedTotal = typeof input.total_tokens === "number" ? input.total_tokens : null;
|
|
267
237
|
const inputAlreadyIncludesCache =
|
|
268
238
|
reportedTotal !== null && reportedTotal === input_tokens + output_tokens;
|
|
269
|
-
|
|
239
|
+
// `inclusive` (default) folds cache into input so every provider reads alike:
|
|
240
|
+
// IN is always the whole prompt and CACHED its subset. Anthropic's raw shape
|
|
241
|
+
// is fresh-only with cache separate — and its `total_tokens` equals
|
|
242
|
+
// input+output by contract, so the `inputAlreadyIncludesCache` heuristic
|
|
243
|
+
// would otherwise mistake it for already-inclusive and never add the cache,
|
|
244
|
+
// under-reporting prompt by the entire cached span. `as-is` keeps the raw
|
|
245
|
+
// upstream semantics (and the legacy heuristic for already-inclusive bridges).
|
|
246
|
+
const accountInputInclusive = resolveUsageInputMode() === "inclusive";
|
|
247
|
+
const cacheIsSeparate = anthropic_cache_read !== null;
|
|
248
|
+
const shouldFoldCache = accountInputInclusive
|
|
249
|
+
? cacheIsSeparate
|
|
250
|
+
: cacheIsSeparate && !inputAlreadyIncludesCache;
|
|
270
251
|
let uncached_input_tokens: number | "unavailable" = "unavailable";
|
|
271
252
|
if (typeof cached_input_tokens === "number") {
|
|
272
|
-
if (
|
|
253
|
+
if (shouldFoldCache) {
|
|
273
254
|
uncached_input_tokens = input_tokens;
|
|
274
255
|
input_tokens +=
|
|
275
256
|
cached_input_tokens + (typeof cache_write_tokens === "number" ? cache_write_tokens : 0);
|
|
@@ -277,25 +258,10 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
|
|
|
277
258
|
uncached_input_tokens = Math.max(0, input_tokens - Math.min(input_tokens, cached_input_tokens));
|
|
278
259
|
cached_input_tokens = Math.min(input_tokens, cached_input_tokens);
|
|
279
260
|
}
|
|
280
|
-
} else if (
|
|
261
|
+
} else if (shouldFoldCache && typeof cache_write_tokens === "number") {
|
|
281
262
|
input_tokens += cache_write_tokens;
|
|
282
263
|
}
|
|
283
264
|
|
|
284
|
-
// Simplified cache accounting: a reported hit makes the whole prompt count as
|
|
285
|
-
// cached — request input ber-cache = cache-eligible → input = cache. The
|
|
286
|
-
// cache-write volume is already inside the all-in input total on cache-aware
|
|
287
|
-
// shapes, so it stays separate and is not double-counted as a hit. Applied
|
|
288
|
-
// before pricing so the estimated cost reads the same simplified split as the
|
|
289
|
-
// record.
|
|
290
|
-
if (typeof cached_input_tokens === "number" && cached_input_tokens > 0 && input_tokens > 0) {
|
|
291
|
-
if (resolveCacheMode() === "simplified") {
|
|
292
|
-
const writes =
|
|
293
|
-
typeof cache_write_tokens === "number" ? Math.max(0, cache_write_tokens) : 0;
|
|
294
|
-
cached_input_tokens = Math.min(input_tokens, Math.max(0, input_tokens - writes));
|
|
295
|
-
uncached_input_tokens = 0;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
265
|
// Handle reasoning tokens: Anthropic and OpenAI both use output_tokens_details
|
|
300
266
|
let reasoning_tokens: number | "unavailable" = "unavailable";
|
|
301
267
|
const details: NonNullable<UsageRecord["details"]> = {};
|
|
@@ -164,13 +164,20 @@ export class ApiKeyAdmissionService {
|
|
|
164
164
|
const actual = usageTokens(usage);
|
|
165
165
|
await this.store.reconcile(apiKeyId, estimatedTokens, actual, reservationId);
|
|
166
166
|
finalized = true;
|
|
167
|
-
// Persist against the authenticating key
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
167
|
+
// Persist the spend against BOTH the authenticating key and the family
|
|
168
|
+
// owner. The authenticating key (`snapshot.api_key_id`, the child for
|
|
169
|
+
// a share recipient) carries attribution so `sumChildrenConsumed` and
|
|
170
|
+
// recipient rows stay correct. The family owner (`admission_identity`,
|
|
171
|
+
// the template itself for a share child) is the live accumulator the
|
|
172
|
+
// console reads for total usage — without it the template column never
|
|
173
|
+
// moved and every figure depended on telemetry rows alone.
|
|
171
174
|
if (this.persistLifetimeUsage) {
|
|
175
|
+
const ownerId = snapshot.admission_identity ?? snapshot.api_key_id;
|
|
172
176
|
try {
|
|
173
177
|
await this.persistLifetimeUsage({ apiKeyId: snapshot.api_key_id, delta: actual });
|
|
178
|
+
if (ownerId !== snapshot.api_key_id) {
|
|
179
|
+
await this.persistLifetimeUsage({ apiKeyId: ownerId, delta: actual });
|
|
180
|
+
}
|
|
174
181
|
} catch {
|
|
175
182
|
// Non-fatal: the transient counter already reflects reality; a
|
|
176
183
|
// subsequent request will overwrite the row with the fresh count.
|