cartethyia 2.5.4 → 2.5.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 +13 -4
- package/README.md +2 -2
- package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-CADDq4IU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-DuO1Z7dq.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-V5faV9V2.js} +1 -1
- package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-D5C2raAc.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/tokens-COzario9.js +1 -0
- package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.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 +54 -54
- package/migrations/README.md +52 -52
- package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
- package/package.json +96 -95
- package/scripts/cli/admin.ts +125 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
- package/scripts/cli/doctor.ts +37 -0
- package/scripts/cli/{support/env.ts → env.ts} +99 -99
- package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
- package/scripts/cli/instance.ts +224 -0
- package/scripts/cli/main.ts +61 -18
- package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
- package/scripts/cli/port.ts +100 -0
- package/scripts/cli/{support/services.ts → services.ts} +88 -88
- package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
- package/scripts/cli/{command/start.ts → start.ts} +10 -5
- package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
- package/scripts/cli/tray/index.ts +62 -12
- package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
- package/scripts/cli/{command/update.ts → update.ts} +39 -39
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +697 -693
- package/src/boot/dependencies.ts +647 -646
- package/src/boot/timeout.ts +2 -13
- package/src/config/cooldowns.ts +186 -138
- package/src/config/network.ts +8 -14
- package/src/config/providers.ts +7 -10
- package/src/config/runtime.ts +76 -76
- package/src/config/security.ts +15 -0
- package/src/config/spec.ts +20 -30
- package/src/config/telemetry.ts +0 -20
- package/src/config/timeouts.ts +0 -6
- package/src/console/admin/model-abuse/contracts.ts +28 -6
- package/src/console/admin/model-abuse/routes.ts +56 -3
- package/src/console/api-keys/contracts.ts +410 -414
- package/src/console/api-keys/routes.ts +881 -880
- package/src/console/auth/credentials.ts +3 -3
- package/src/console/auth/setup-service.ts +2 -2
- package/src/console/backup/export-endpoint.ts +6 -2
- package/src/console/backup/nine-router.ts +2 -5
- package/src/console/backup/service.ts +4 -0
- package/src/console/backup/store.ts +192 -29
- package/src/console/backup/validate.ts +3 -45
- package/src/console/cli-tools/fs-ops.ts +487 -487
- package/src/console/console-router-guards.ts +1 -1
- package/src/console/data-management/contracts.ts +13 -6
- package/src/console/data-management/operations.ts +23 -18
- package/src/console/data-management/routes.ts +0 -1
- package/src/console/domain-registration.ts +10 -5
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +39 -16
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +4 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/store.ts +1553 -1553
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/contracts.ts +61 -0
- package/src/console/remote-support/routes.ts +65 -0
- package/src/console/remote-support/service.ts +207 -0
- package/src/console/response-compression.ts +0 -3
- package/src/console/routing/pools/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +58 -2
- package/src/console/settings/store.ts +30 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/network/http2-bidi.ts +2 -4
- package/src/network/pool/agent.ts +478 -478
- package/src/network/pool/selector.ts +5 -4
- package/src/network/retry.ts +1 -2
- package/src/network/ssrf.ts +5 -4
- package/src/observability/event-compaction.ts +80 -78
- package/src/observability/event-day-index.ts +13 -15
- package/src/observability/event-log.ts +4 -4
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +151 -33
- package/src/observability/payload-store.ts +397 -440
- package/src/observability/redaction.ts +5 -6
- package/src/observability/request-notify.ts +43 -0
- package/src/observability/telemetry-status.ts +8 -9
- package/src/observability/token-speed.ts +5 -4
- package/src/persistence/db-mode.ts +7 -7
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +1406 -1402
- package/src/persistence/shared-cache.ts +1 -2
- package/src/persistence/stores/api-key-store.ts +18 -6
- package/src/persistence/stores/share-store.ts +4 -3
- package/src/persistence/stores/telemetry-store.ts +236 -237
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +138 -4
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/request/responses.ts +9 -1
- package/src/protocol/response/chat.ts +34 -4
- package/src/protocol/response/codex.ts +7 -4
- package/src/protocol/response/dsml.ts +140 -28
- package/src/protocol/response/responses.ts +11 -5
- package/src/protocol/stream-error-frames.ts +4 -5
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/configured-openai-providers.ts +134 -136
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/catalog/catalog-format.ts +3 -6
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +4 -4
- package/src/providers/discovery/probe-wire.ts +6 -13
- package/src/providers/discovery/probing-service.ts +1117 -1045
- package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
- package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
- package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
- package/src/providers/integrations/buddy/buddy-media.ts +2 -2
- package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
- package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
- package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
- package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
- package/src/providers/integrations/claude/claude.ts +608 -608
- package/src/providers/integrations/cline/cline-quota.ts +132 -132
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/commandcode/commandcode.ts +674 -675
- package/src/providers/integrations/commandcode/discovery.ts +117 -116
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
- package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
- package/src/providers/integrations/cursor/cursor.ts +11 -163
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok.ts +6 -7
- package/src/providers/integrations/hermes.ts +14 -18
- package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
- package/src/providers/integrations/kiro/kiro.ts +1 -1
- package/src/providers/integrations/mistral.ts +6 -6
- package/src/providers/integrations/opencode/fingerprint.ts +3 -3
- package/src/providers/integrations/opencode/provider.ts +8 -8
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
- package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
- package/src/providers/integrations/qoder/qoder.ts +884 -865
- package/src/providers/integrations/shared/connect.ts +6 -6
- package/src/providers/integrations/trae/trae-catalog.ts +1 -2
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +1528 -1523
- package/src/providers/operations/account-reset-service.ts +1 -1
- package/src/providers/operations/client-versions.ts +6 -6
- package/src/providers/operations/custom-cli-headers.ts +3 -4
- package/src/providers/operations/provider-catalog-seeder.ts +95 -95
- package/src/providers/operations/provider-catalog-service.ts +3 -4
- package/src/providers/provider-metadata.ts +4 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/security/abuse.ts +15 -10
- package/src/security/access-control.ts +144 -146
- package/src/security/admission/contracts.ts +11 -0
- package/src/security/admission/in-memory-store.ts +4 -3
- package/src/security/admission/lease-sweep.ts +6 -6
- package/src/security/admission/reasons.ts +5 -1
- package/src/security/admission/redis-store.ts +211 -210
- package/src/security/admission/service.ts +4 -0
- package/src/security/api-key-auth.ts +504 -504
- package/src/security/ip-boundary.ts +155 -15
- package/src/security/model-abuse.ts +366 -94
- package/src/security/outbound-headers.ts +3 -4
- package/src/shared/sleep.ts +4 -0
- package/src/transport/dispatch/attempt-finalize.ts +47 -5
- package/src/transport/dispatch/attempt-loop.ts +531 -530
- package/src/transport/dispatch/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +5 -6
- package/src/transport/failure-policy.ts +162 -8
- package/src/transport/guards/auth-guard.ts +7 -4
- package/src/transport/guards/concurrency-guard.ts +0 -5
- package/src/transport/guards/error-lifecycle.ts +13 -6
- package/src/transport/guards/pipeline.ts +15 -3
- package/src/transport/guards/request-context.ts +8 -2
- package/src/transport/request/inflight.ts +5 -6
- package/src/transport/request/preparer.ts +2 -2
- package/src/transport/request/state.ts +12 -8
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +8 -1
- package/src/transport/routing/route-model.ts +46 -14
- package/src/transport/routing/router.ts +21 -14
- package/src/transport/surface/completion.ts +2 -3
- package/src/transport/surface/dialects.ts +5 -6
- package/src/transport/surface/responses/encode.ts +38 -0
- package/src/transport/surface/responses/parse.ts +4 -0
- package/src/transport/translation/capabilities.ts +2 -3
- package/src/transport/translation/quirks.ts +3 -3
- package/src/workers/buddy-growth-tasks.ts +5 -10
- package/src/workers/daily-checkin.ts +17 -23
- package/src/workers/quota-refresh-sweep.ts +26 -26
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +0 -2
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/gateway/drain-token.ts +0 -78
- package/scripts/cli/gateway/instance-lock.ts +0 -105
- package/scripts/cli/gateway/stop.ts +0 -51
- package/scripts/cli/support/bun.ts +0 -27
- package/scripts/cli/support/version.ts +0 -37
- /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -1,608 +1,608 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dedicated `claude` adapter.
|
|
3
|
-
*
|
|
4
|
-
* Wire mechanics live in `src/protocol/`:
|
|
5
|
-
* - `protocol/codecs.ts` shared guards, tool-id/schema/tool-name helpers, endpoint URL
|
|
6
|
-
* - `protocol/request/messages.ts` canonical -> Claude Messages body
|
|
7
|
-
* - `protocol/response/messages.ts` Claude JSON/SSE -> canonical events
|
|
8
|
-
* - `protocol/upstream/send-claude-messages.ts` shared HTTP send helper
|
|
9
|
-
* - `errors.ts` upstream HTTP error -> `GatewayError`
|
|
10
|
-
* - `policy.ts` header/beta/credential policy (unchanged)
|
|
11
|
-
* - `cch.ts` CCH billing-header body patch (unchanged)
|
|
12
|
-
*
|
|
13
|
-
* Bespoke by wire protocol (Phase C5): the Claude Messages envelope
|
|
14
|
-
* (thinking blocks, cache_control, beta negotiation) is outside the
|
|
15
|
-
* OpenAI-compatible factory's reach — stays bespoke, never re-audit.
|
|
16
|
-
*/
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
|
-
import type { CanonicalEvent, CanonicalRequest } from "../../../protocol/canonical-model";
|
|
19
|
-
import { GatewayError } from "../../../protocol/gateway-error";
|
|
20
|
-
import { firstUserText, getHeader } from "../../../protocol/canonical-model";
|
|
21
|
-
import { log } from "../../../observability/logger";
|
|
22
|
-
import {
|
|
23
|
-
assessClaudeCodeCompatibility,
|
|
24
|
-
assertClaudeCodeCompatibility,
|
|
25
|
-
requestHasClaudeTools,
|
|
26
|
-
} from "./claude-compatibility";
|
|
27
|
-
import { buildClaudeHeaders, type ClaudeHeaderOptions } from "./claude-credentials";
|
|
28
|
-
import { isRecord } from "../../../protocol/codecs";
|
|
29
|
-
import { HEADER_CONTROL } from "../../../security/outbound-headers";
|
|
30
|
-
import { CLAUDE_USER_PROFILES_BETA } from "./claude-user-profiles";
|
|
31
|
-
import { sharedClaudeSlowLanes } from "./claude-slow-lane";
|
|
32
|
-
import { planSlowLaneRetry } from "./claude-slow-lane-retry";
|
|
33
|
-
import { sleep } from "../../../transport/failure-policy";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Floor reserved out of the request deadline before a slow-lane wait, so a
|
|
37
|
-
* wait can never consume the budget the next real attempt needs. Mirrors
|
|
38
|
-
* `MIN_RETRY_BUDGET_MS` in the attempt loop.
|
|
39
|
-
*/
|
|
40
|
-
const SLOW_LANE_DEADLINE_FLOOR_MS = 15_000;
|
|
41
|
-
import { resolvePromptCacheKey } from "../../operations/session-resolution";
|
|
42
|
-
import { ensureAnthropicBeta, filterClaudeCustomHeaders } from "../shared/claude-messages";
|
|
43
|
-
import {
|
|
44
|
-
CLAUDE_CODE_SYSTEM_INSTRUCTION,
|
|
45
|
-
createClaudeBillingText,
|
|
46
|
-
patchClaudeCchBody,
|
|
47
|
-
} from "./claude-cch";
|
|
48
|
-
import { getClaudeCliVersion, adoptClaudeCliVersion, getClaudeSdkVersion } from "../../operations/client-versions";
|
|
49
|
-
import { adoptRequiredVersion } from "../../operations/client-version-resolver";
|
|
50
|
-
import { getOrCreateInstallId } from "../shared/install-id";
|
|
51
|
-
import { canonicalToClaudeMessagesPayload } from "../../../protocol/request/messages";
|
|
52
|
-
import { applyParamQuirks } from "../../../transport/translation/quirks";
|
|
53
|
-
import { CLAUDE_BILLING_HEADER_PREFIX, endpointUrl } from "../../../protocol/codecs";
|
|
54
|
-
import { sendClaudeMessagesRequest } from "../../../protocol/upstream/send-claude-messages";
|
|
55
|
-
import {
|
|
56
|
-
FALLBACK_CREDIT_BETA,
|
|
57
|
-
fallbackCreditRequestBody,
|
|
58
|
-
fallbackCreditTargetModel,
|
|
59
|
-
hashBody,
|
|
60
|
-
isFallbackCreditTokenRejection,
|
|
61
|
-
refusalCreditToken,
|
|
62
|
-
sharedFallbackCreditCache,
|
|
63
|
-
} from "./fallback-credit";
|
|
64
|
-
import type { ProviderDispatchTarget, ProviderAdapter, ProviderDispatchContext } from "../../provider-contracts";
|
|
65
|
-
import { defineModel } from "../../provider-model-definition";
|
|
66
|
-
import type { ModelDefinition } from "../../provider-model-definition";
|
|
67
|
-
|
|
68
|
-
const claudeMessagesModel = (
|
|
69
|
-
modelId: string,
|
|
70
|
-
contextLimit: number,
|
|
71
|
-
outputLimit: number,
|
|
72
|
-
reasoning: boolean,
|
|
73
|
-
): ModelDefinition =>
|
|
74
|
-
defineModel({
|
|
75
|
-
id: modelId,
|
|
76
|
-
wireFamily: "messages",
|
|
77
|
-
endpoint: "/v1/messages",
|
|
78
|
-
ctx: contextLimit,
|
|
79
|
-
out: outputLimit,
|
|
80
|
-
vision: true,
|
|
81
|
-
reasoning,
|
|
82
|
-
toolCall: true,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
export const CLAUDE_MODELS: readonly ModelDefinition[] = [
|
|
86
|
-
claudeMessagesModel("claude-fable-5", 1_000_000, 128_000, true),
|
|
87
|
-
claudeMessagesModel("claude-fable-5-1", 1_000_000, 128_000, true),
|
|
88
|
-
claudeMessagesModel("claude-mythos-5", 1_000_000, 128_000, true),
|
|
89
|
-
claudeMessagesModel("claude-mythos-5-1", 1_000_000, 128_000, true),
|
|
90
|
-
claudeMessagesModel("claude-opus-5", 1_000_000, 128_000, true),
|
|
91
|
-
claudeMessagesModel("claude-opus-5-5", 1_000_000, 128_000, true),
|
|
92
|
-
claudeMessagesModel("claude-opus-4-8", 1_000_000, 128_000, true),
|
|
93
|
-
claudeMessagesModel("claude-opus-4-7", 1_000_000, 128_000, true),
|
|
94
|
-
claudeMessagesModel("claude-opus-4-6", 1_000_000, 128_000, true),
|
|
95
|
-
claudeMessagesModel("claude-sonnet-5-5", 1_000_000, 128_000, true),
|
|
96
|
-
claudeMessagesModel("claude-sonnet-5", 1_000_000, 128_000, true),
|
|
97
|
-
claudeMessagesModel("claude-sonnet-4-6", 1_000_000, 128_000, true),
|
|
98
|
-
claudeMessagesModel("claude-opus-4-5", 200_000, 64_000, true),
|
|
99
|
-
// 200K: the 4.5 trio predates the million-token window, which arrived with
|
|
100
|
-
// Opus 4.6. Only this row claimed 1M while its own siblings said 200K.
|
|
101
|
-
claudeMessagesModel("claude-sonnet-4-5", 200_000, 64_000, true),
|
|
102
|
-
claudeMessagesModel("claude-haiku-4-5", 200_000, 64_000, true),
|
|
103
|
-
];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const CLAUDE_DEVICE_ID_DOMAIN = "cartethyia-claude-device-id-v1";
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Stable per-installation id, persisted once like the Codex install id. The
|
|
112
|
-
* assistant metadata `device_id` is derived from it so a machine keeps one
|
|
113
|
-
* identity across restarts instead of minting a new one per request.
|
|
114
|
-
*
|
|
115
|
-
* The persistence lives in the shared install-id module: it owns the writable-
|
|
116
|
-
* location fallback (a container runs with `HOME=/root` but as uid 10001) and
|
|
117
|
-
* the policy that a telemetry identity never fails a request.
|
|
118
|
-
*/
|
|
119
|
-
async function getClaudeInstallId(): Promise<string> {
|
|
120
|
-
return getOrCreateInstallId("claude");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** The stable device id the assistant metadata carries for this installation. */
|
|
124
|
-
export async function claudeDeviceId(accountId?: string): Promise<string> {
|
|
125
|
-
const hash = createHash("sha256").update(CLAUDE_DEVICE_ID_DOMAIN).update("\0").update(await getClaudeInstallId());
|
|
126
|
-
if (accountId !== undefined && accountId.length > 0) hash.update("\0").update(accountId);
|
|
127
|
-
return hash.digest("hex");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
131
|
-
|
|
132
|
-
function isUuid(value: unknown): value is string {
|
|
133
|
-
return typeof value === "string" && UUID_RE.test(value);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Whether a caller-supplied user id is already in an accepted shape: the
|
|
138
|
-
* cloaked `user_<64hex>_account_<uuid>_session_<uuid>` form, or a JSON
|
|
139
|
-
* envelope carrying a non-empty `session_id`.
|
|
140
|
-
*/
|
|
141
|
-
export function isClaudeMetadataUserId(value: string): boolean {
|
|
142
|
-
if (/^user_[0-9a-f]{64}_account_[0-9a-f-]{36}_session_[0-9a-f-]{36}$/i.test(value)) return true;
|
|
143
|
-
if (!value.startsWith("{")) return false;
|
|
144
|
-
try {
|
|
145
|
-
const parsed: unknown = JSON.parse(value);
|
|
146
|
-
return (
|
|
147
|
-
isRecord(parsed) &&
|
|
148
|
-
typeof parsed["session_id"] === "string" &&
|
|
149
|
-
(parsed["session_id"] as string).length > 0
|
|
150
|
-
);
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Adapter construction options used by deterministic local tests and custom endpoints. */
|
|
157
|
-
interface ClaudeAdapterOptions {
|
|
158
|
-
readonly fetch?: typeof fetch;
|
|
159
|
-
readonly base_url?: string;
|
|
160
|
-
readonly custom_headers?: Readonly<Record<string, unknown>>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Claude Code CLI impersonation adapter: builds Claude-branded headers via
|
|
165
|
-
* `buildClaudeHeaders`, negotiates beta features, applies CCH billing-header
|
|
166
|
-
* injection, and biases the payload/endpoint for OAuth-style credentials.
|
|
167
|
-
* `provider_id` is always `"claude"` — plain Anthropic API-key traffic uses
|
|
168
|
-
* `AnthropicApiKeyAdapter` instead, which never touches this impersonation
|
|
169
|
-
* machinery.
|
|
170
|
-
*/
|
|
171
|
-
export class ClaudeAdapter implements ProviderAdapter {
|
|
172
|
-
readonly provider_id = "claude" as const;
|
|
173
|
-
private readonly fetchFn: typeof fetch;
|
|
174
|
-
private readonly baseUrl: string | undefined;
|
|
175
|
-
private readonly customHeaders: Readonly<Record<string, unknown>> | undefined;
|
|
176
|
-
|
|
177
|
-
constructor(options: ClaudeAdapterOptions = {}) {
|
|
178
|
-
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
179
|
-
this.baseUrl = options.base_url;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async *dispatch(
|
|
183
|
-
request: CanonicalRequest,
|
|
184
|
-
candidate: ProviderDispatchTarget,
|
|
185
|
-
context: ProviderDispatchContext,
|
|
186
|
-
): AsyncIterable<CanonicalEvent> {
|
|
187
|
-
if (candidate.wire_family !== "messages") {
|
|
188
|
-
throw new GatewayError(
|
|
189
|
-
"capability_unsupported",
|
|
190
|
-
400,
|
|
191
|
-
"claude supports messages only",
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
if (
|
|
195
|
-
context.credential.credential_kind !== "oauth" &&
|
|
196
|
-
context.credential.credential_kind !== "scoped_access_token"
|
|
197
|
-
) {
|
|
198
|
-
throw new GatewayError(
|
|
199
|
-
"invalid_request",
|
|
200
|
-
400,
|
|
201
|
-
`claude: the assistant OAuth credential required (got credential_kind=${context.credential.credential_kind})`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
const customHeaders = filterClaudeCustomHeaders({
|
|
205
|
-
...this.customHeaders,
|
|
206
|
-
...context.credential.custom_headers,
|
|
207
|
-
});
|
|
208
|
-
const isClaudeCodeOAuth =
|
|
209
|
-
context.credential.credential_kind === "oauth" ||
|
|
210
|
-
context.credential.credential_kind === "scoped_access_token";
|
|
211
|
-
const incomingBeta = getHeader(context.request_headers, "anthropic-beta");
|
|
212
|
-
const assessment = assessClaudeCodeCompatibility(request, {
|
|
213
|
-
capabilities: candidate.capabilities,
|
|
214
|
-
beta_header: incomingBeta,
|
|
215
|
-
beta_policy: "reject",
|
|
216
|
-
credential_kind: context.credential.credential_kind,
|
|
217
|
-
target_provider: "claude",
|
|
218
|
-
});
|
|
219
|
-
assertClaudeCodeCompatibility(assessment);
|
|
220
|
-
const hasThinking =
|
|
221
|
-
request.reasoning !== undefined &&
|
|
222
|
-
request.reasoning.thinking_type !== "disabled";
|
|
223
|
-
const hasTools = requestHasClaudeTools(request);
|
|
224
|
-
const cliVersion = getClaudeCliVersion();
|
|
225
|
-
const sdkVersion = getClaudeSdkVersion();
|
|
226
|
-
const headerOptions: ClaudeHeaderOptions = {
|
|
227
|
-
credential_kind: context.credential.credential_kind,
|
|
228
|
-
custom_headers: customHeaders,
|
|
229
|
-
capabilities: candidate.capabilities,
|
|
230
|
-
unsupported_beta_policy: "reject",
|
|
231
|
-
target_provider: "claude",
|
|
232
|
-
has_thinking: hasThinking,
|
|
233
|
-
has_tools: hasTools,
|
|
234
|
-
stream: request.stream,
|
|
235
|
-
cli_version: cliVersion,
|
|
236
|
-
sdk_version: sdkVersion,
|
|
237
|
-
...(context.request_headers === undefined
|
|
238
|
-
? {}
|
|
239
|
-
: { request_headers: context.request_headers }),
|
|
240
|
-
...(incomingBeta === undefined ? {} : { anthropic_beta: incomingBeta }),
|
|
241
|
-
};
|
|
242
|
-
const headers: Record<string, string> = buildClaudeHeaders(
|
|
243
|
-
context.credential.secret,
|
|
244
|
-
context.credential.account_id,
|
|
245
|
-
headerOptions,
|
|
246
|
-
);
|
|
247
|
-
const profileId = request.generation_controls["extension:user_profile_id"];
|
|
248
|
-
// A profile id travels two ways on the same request: the `user-profiles`
|
|
249
|
-
// beta that unlocks profile scoping, and the header that names *which*
|
|
250
|
-
// profile. Carrying only the beta
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
if (typeof profileId === "string" && profileId.length > 0) {
|
|
254
|
-
// Dated, not bare: the donor sends `user-profiles-2026-09-04`
|
|
255
|
-
// (the reference client `packages/ai/src/providers/anthropic-user-profiles.ts:4`).
|
|
256
|
-
// `ensureAnthropicBeta` takes any string, so
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
ensureAnthropicBeta(headers, CLAUDE_USER_PROFILES_BETA);
|
|
260
|
-
if (!HEADER_CONTROL.test(profileId)) {
|
|
261
|
-
headers["anthropic-user-profile-id"] = profileId;
|
|
262
|
-
} else {
|
|
263
|
-
// Never let a caller's newline reach the wire: an injected header is
|
|
264
|
-
// the one thing a filter downstream cannot undo.
|
|
265
|
-
log.warn("claude: user_profile_id dropped, contains control characters", {
|
|
266
|
-
provider: "claude",
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const sessionId =
|
|
271
|
-
headers["X-Claude-Code-Session-Id"] ?? resolvePromptCacheKey(request, context);
|
|
272
|
-
// Provider/model parameter quirks run before translation, exactly as they do
|
|
273
|
-
// on the API-key (`anthropic`) path. Without this call the Claude Code
|
|
274
|
-
// adapter never reached the table, so a model that rejects `temperature`
|
|
275
|
-
// (`claude-opus-4-7` and later, `claude-sonnet-5`, `claude-fable-5`) got a
|
|
276
|
-
// 400 on every request that carried one — including the probe, which sets
|
|
277
|
-
// `temperature: 0.2` itself.
|
|
278
|
-
const payload = canonicalToClaudeMessagesPayload(
|
|
279
|
-
applyParamQuirks(request, candidate.provider_id),
|
|
280
|
-
{ isOAuth: isClaudeCodeOAuth },
|
|
281
|
-
);
|
|
282
|
-
if (isClaudeCodeOAuth) {
|
|
283
|
-
// Session attribution follows the reference rules: a valid caller id
|
|
284
|
-
// travels verbatim; otherwise a JSON envelope is generated from the
|
|
285
|
-
// resolved session identity (never a bare UUID, which matches neither
|
|
286
|
-
// accepted grammar). API-key callers without an id send none.
|
|
287
|
-
const callerUserId = request.generation_controls["extension:metadata_user_id"];
|
|
288
|
-
if (typeof callerUserId === "string" && isClaudeMetadataUserId(callerUserId)) {
|
|
289
|
-
payload.metadata = { user_id: callerUserId };
|
|
290
|
-
} else if (sessionId !== undefined && sessionId.length > 0) {
|
|
291
|
-
const accountUuid = isUuid(context.credential.account_id)
|
|
292
|
-
? context.credential.account_id
|
|
293
|
-
: undefined;
|
|
294
|
-
payload.metadata = {
|
|
295
|
-
user_id: JSON.stringify({
|
|
296
|
-
device_id: await claudeDeviceId(accountUuid),
|
|
297
|
-
session_id: sessionId,
|
|
298
|
-
...(accountUuid === undefined ? {} : { account_uuid: accountUuid }),
|
|
299
|
-
}),
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
const system = Array.isArray(payload.system) ? payload.system : [];
|
|
303
|
-
const userText = firstUserText(request);
|
|
304
|
-
const billing = {
|
|
305
|
-
type: "text",
|
|
306
|
-
text: createClaudeBillingText(userText, cliVersion),
|
|
307
|
-
};
|
|
308
|
-
// The breakpoint is NOT pinned here. The reference client caches the
|
|
309
|
-
// *last* system block, so when the caller supplies its own prompt the
|
|
310
|
-
// breakpoint belongs on that, not on the identity — pinning it here put
|
|
311
|
-
// a breakpoint mid-prefix and left the caller's prompt uncached, which
|
|
312
|
-
// is both a fingerprint delta and a cache miss on every request.
|
|
313
|
-
const instruction = {
|
|
314
|
-
type: "text",
|
|
315
|
-
text: CLAUDE_CODE_SYSTEM_INSTRUCTION,
|
|
316
|
-
};
|
|
317
|
-
const hasInstruction = system.some(
|
|
318
|
-
(block) => isRecord(block) && block["text"] === CLAUDE_CODE_SYSTEM_INSTRUCTION,
|
|
319
|
-
);
|
|
320
|
-
const hasBilling = system.some(
|
|
321
|
-
(block) =>
|
|
322
|
-
isRecord(block) &&
|
|
323
|
-
typeof block["text"] === "string" &&
|
|
324
|
-
(block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX),
|
|
325
|
-
);
|
|
326
|
-
// Captured-client placement: billing first, instruction second. The
|
|
327
|
-
// patch anchor assumes this order — never reorder around it.
|
|
328
|
-
const assembled = [
|
|
329
|
-
...(hasBilling ? [] : [billing]),
|
|
330
|
-
...(hasInstruction ? [] : [instruction]),
|
|
331
|
-
...system,
|
|
332
|
-
];
|
|
333
|
-
// The cache breakpoint rides the last system block: everything before it
|
|
334
|
-
// is stable prefix, and caching the tail is what the reference client
|
|
335
|
-
// does. Billing is never breakpointed — it varies per request.
|
|
336
|
-
const lastIndex = assembled.length - 1;
|
|
337
|
-
if (lastIndex > 0) {
|
|
338
|
-
const last = assembled[lastIndex];
|
|
339
|
-
const existing = isRecord(last) ? last["cache_control"] : undefined;
|
|
340
|
-
// A caller's own breakpoint is kept, but upgraded to the reference
|
|
341
|
-
// client's 1h retention when it stated none: that is the default the
|
|
342
|
-
// real CLI sends, and leaving it bare caches a shorter prefix than the
|
|
343
|
-
// upstream will honour.
|
|
344
|
-
const breakpoint =
|
|
345
|
-
isRecord(existing) && existing["ttl"] !== undefined
|
|
346
|
-
? existing
|
|
347
|
-
: { type: "ephemeral", ttl: "1h" };
|
|
348
|
-
if (isRecord(last)) {
|
|
349
|
-
assembled[lastIndex] = { ...last, cache_control: breakpoint };
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
payload.system = assembled;
|
|
353
|
-
}
|
|
354
|
-
const url = endpointUrl(
|
|
355
|
-
this.baseUrl,
|
|
356
|
-
candidate.endpoint_path || "/v1/messages",
|
|
357
|
-
isClaudeCodeOAuth,
|
|
358
|
-
);
|
|
359
|
-
const requestBody = JSON.stringify(payload);
|
|
360
|
-
const patchedBody = isClaudeCodeOAuth ? patchClaudeCchBody(requestBody) : undefined;
|
|
361
|
-
if (isClaudeCodeOAuth && patchedBody === undefined) {
|
|
362
|
-
// Unanchored billing block: warn and send as-is, exactly like the
|
|
363
|
-
// reference client. Availability wins over attestation here — an
|
|
364
|
-
// unattested send is the accepted fallback, a 500 is a self-DoS.
|
|
365
|
-
log.warn("claude billing attestation unanchored; sending unattested", {
|
|
366
|
-
provider: "claude",
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
const outboundBody =
|
|
370
|
-
patchedBody === undefined ? requestBody : new TextDecoder().decode(patchedBody);
|
|
371
|
-
// Fallback-credit redemption: a previous refusal of *this exact body* may
|
|
372
|
-
// have left a token that buys one free replay. Keyed by the frozen body
|
|
373
|
-
// because the donor binds the token to the request that was refused
|
|
374
|
-
// (`anthropic.ts:3303`, `params: structuredClone(params)`).
|
|
375
|
-
const creditKey = hashBody(outboundBody);
|
|
376
|
-
const creditToken = sharedFallbackCreditCache.get(creditKey);
|
|
377
|
-
let creditSent = false;
|
|
378
|
-
let body = outboundBody;
|
|
379
|
-
if (creditToken !== undefined) {
|
|
380
|
-
const target = fallbackCreditTargetModel(request.provider_options);
|
|
381
|
-
const redemption = fallbackCreditRequestBody(outboundBody, creditToken, target);
|
|
382
|
-
if (redemption !== undefined) {
|
|
383
|
-
body = redemption;
|
|
384
|
-
creditSent = true;
|
|
385
|
-
// The donor guarantees the beta is present on every redemption
|
|
386
|
-
// (`anthropic.ts:2433-2435`, `:2782-2784`); on this side it is only
|
|
387
|
-
// on the agent profile (`claude-credentials.ts:124`), so add it here.
|
|
388
|
-
ensureAnthropicBeta(headers, FALLBACK_CREDIT_BETA);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
// Slow lane (design doc §5): attach the usage limit when this account's
|
|
392
|
-
// lane is active, so the upstream knows it may serve us on spare capacity.
|
|
393
|
-
// First-party OAuth only, mirroring the donor's gate
|
|
394
|
-
// (`anthropic.ts:2695-2705`). Nothing changes for every other request.
|
|
395
|
-
const accountId = context.credential.account_id;
|
|
396
|
-
const slowEligible =
|
|
397
|
-
isClaudeCodeOAuth && accountId !== undefined && sharedClaudeSlowLanes.isActive(accountId);
|
|
398
|
-
const slowHeaders: Record<string, string> = slowEligible
|
|
399
|
-
? { ...headers, "anthropic-usage-limit": "slow" }
|
|
400
|
-
: headers;
|
|
401
|
-
|
|
402
|
-
// Adopt-on-rejection: a stale pinned CLI version costs one rejected
|
|
403
|
-
// request, not an outage. The shared helper parses the server-named
|
|
404
|
-
// minimum and promotes it process-wide; a retry at the same version
|
|
405
|
-
// cannot loop because the helper only reports a forward move.
|
|
406
|
-
const sendOnce = (
|
|
407
|
-
headers: Record<string, string>,
|
|
408
|
-
body: string,
|
|
409
|
-
): AsyncIterable<CanonicalEvent> =>
|
|
410
|
-
sendClaudeMessagesRequest(
|
|
411
|
-
url,
|
|
412
|
-
headers,
|
|
413
|
-
body,
|
|
414
|
-
context,
|
|
415
|
-
request,
|
|
416
|
-
this.fetchFn,
|
|
417
|
-
isClaudeCodeOAuth,
|
|
418
|
-
);
|
|
419
|
-
try {
|
|
420
|
-
yield* this.sendWithCreditCapture(
|
|
421
|
-
sendOnce,
|
|
422
|
-
slowHeaders,
|
|
423
|
-
body,
|
|
424
|
-
creditSent ? undefined : creditKey,
|
|
425
|
-
);
|
|
426
|
-
if (creditSent) {
|
|
427
|
-
// One-shot spend. The donor consumes the handle once a turn is served
|
|
428
|
-
// (`consumeActiveFallbackCreditRedemption`, `turn-recovery.ts:510-519`);
|
|
429
|
-
// leaving it here would replay a spent token on the next identical
|
|
430
|
-
// request and cost it the 400 round trip below.
|
|
431
|
-
sharedFallbackCreditCache.delete(creditKey);
|
|
432
|
-
}
|
|
433
|
-
return;
|
|
434
|
-
} catch (error: unknown) {
|
|
435
|
-
// Slow-lane capacity wait (design doc §5): a 429 `slot_busy` or a 529 /
|
|
436
|
-
// `overloaded_error` while the slow limit was sent means "no spare
|
|
437
|
-
// capacity right now", not "this account is done". Retry the SAME
|
|
438
|
-
// account after the server-stated interval rather than failing over —
|
|
439
|
-
// and record no health, because nothing failed.
|
|
440
|
-
//
|
|
441
|
-
// Runs before every other error branch so a capacity wait is never
|
|
442
|
-
// misread as a version rejection or a credential problem.
|
|
443
|
-
if (slowEligible && accountId !== undefined) {
|
|
444
|
-
const waited = await this.planAndWaitSlowLane(error, accountId, context);
|
|
445
|
-
if (waited) {
|
|
446
|
-
// Same account, same bytes: the capacity wait is not a failover.
|
|
447
|
-
yield* this.sendWithCreditCapture(
|
|
448
|
-
sendOnce,
|
|
449
|
-
slowHeaders,
|
|
450
|
-
body,
|
|
451
|
-
creditSent ? undefined : creditKey,
|
|
452
|
-
);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if (creditSent && isFallbackCreditTokenRejection(error)) {
|
|
457
|
-
// Donor `anthropic.ts:3469-3481`: the token is rejected, so forfeit it
|
|
458
|
-
// and retry the standard request once without it. A rebuilt body no
|
|
459
|
-
// longer matches the refused one, so the credit cannot ride along.
|
|
460
|
-
sharedFallbackCreditCache.delete(creditKey);
|
|
461
|
-
log.warn("claude: fallback credit token rejected, retrying without it", {
|
|
462
|
-
provider: "claude",
|
|
463
|
-
error: error instanceof Error ? error.message : String(error),
|
|
464
|
-
});
|
|
465
|
-
yield* sendOnce(headers, outboundBody);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const adopted = adoptRequiredVersion(
|
|
469
|
-
{ get: getClaudeCliVersion },
|
|
470
|
-
adoptClaudeCliVersion,
|
|
471
|
-
error,
|
|
472
|
-
{
|
|
473
|
-
marker: "claude_code_version_too_old",
|
|
474
|
-
pattern: /version (\d+\.\d+\.\d+) or newer is required/i,
|
|
475
|
-
},
|
|
476
|
-
);
|
|
477
|
-
if (adopted === null) throw error;
|
|
478
|
-
const retryCliVersion = getClaudeCliVersion();
|
|
479
|
-
log.warn("claude: adopted server-named CLI version, retrying once", {
|
|
480
|
-
provider: "claude",
|
|
481
|
-
version: retryCliVersion,
|
|
482
|
-
});
|
|
483
|
-
const retryBilling = {
|
|
484
|
-
type: "text",
|
|
485
|
-
text: createClaudeBillingText(firstUserText(request), retryCliVersion),
|
|
486
|
-
};
|
|
487
|
-
const parsedBody = JSON.parse(requestBody) as Record<string, unknown>;
|
|
488
|
-
const existingSystem = Array.isArray(parsedBody["system"])
|
|
489
|
-
? (parsedBody["system"] as unknown[])
|
|
490
|
-
: [];
|
|
491
|
-
const retryBody = isClaudeCodeOAuth
|
|
492
|
-
? patchClaudeCchBody(
|
|
493
|
-
JSON.stringify({
|
|
494
|
-
...parsedBody,
|
|
495
|
-
system: [
|
|
496
|
-
retryBilling,
|
|
497
|
-
...existingSystem.filter(
|
|
498
|
-
(block) =>
|
|
499
|
-
!(
|
|
500
|
-
isRecord(block) &&
|
|
501
|
-
typeof block["text"] === "string" &&
|
|
502
|
-
(block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX)
|
|
503
|
-
),
|
|
504
|
-
),
|
|
505
|
-
],
|
|
506
|
-
}),
|
|
507
|
-
)
|
|
508
|
-
: undefined;
|
|
509
|
-
const retryHeaders: Record<string, string> = {
|
|
510
|
-
...headers,
|
|
511
|
-
"User-Agent": `claude-cli/${retryCliVersion} (external, cli)`,
|
|
512
|
-
};
|
|
513
|
-
yield* sendOnce(
|
|
514
|
-
retryHeaders,
|
|
515
|
-
retryBody === undefined ? outboundBody : new TextDecoder().decode(retryBody),
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Sends once, passing every event through, and banks a refusal's credit token
|
|
522
|
-
* against the body that was refused.
|
|
523
|
-
*
|
|
524
|
-
* Donor rule (`anthropic.ts:3287-3309`): only a refusal carries a token, and
|
|
525
|
-
* the frozen body is the request that produced it (`params: structuredClone(params)`).
|
|
526
|
-
* On this side the refusal surfaces as a terminal event whose canonical
|
|
527
|
-
* `stop_reason` is `error` (`mapClaudeStopReason`, `response/messages.ts:32-34`)
|
|
528
|
-
* carrying the provider's `stop_details`.
|
|
529
|
-
*
|
|
530
|
-
* `captureKey` is omitted for a redemption send: a body already carrying a
|
|
531
|
-
* token is never the body that was refused, so banking against it would only
|
|
532
|
-
* occupy a cache slot for five minutes with a token that cannot be replayed.
|
|
533
|
-
*/
|
|
534
|
-
/**
|
|
535
|
-
* Waits out a slow-lane capacity signal and reports whether to retry.
|
|
536
|
-
*
|
|
537
|
-
* Returns true when the failure was a capacity wait that has now elapsed and
|
|
538
|
-
* the caller should resend; false when it was anything else, so the normal
|
|
539
|
-
* error path proceeds untouched.
|
|
540
|
-
*
|
|
541
|
-
* Bounded by the server's own max-wait and by the request deadline minus the
|
|
542
|
-
* attempt-loop's floor, so a wait can never eat the budget a real retry
|
|
543
|
-
* needs. No health is recorded here — a capacity wait is not a failure.
|
|
544
|
-
*/
|
|
545
|
-
private async planAndWaitSlowLane(
|
|
546
|
-
error: unknown,
|
|
547
|
-
accountId: string,
|
|
548
|
-
context: ProviderDispatchContext,
|
|
549
|
-
): Promise<boolean> {
|
|
550
|
-
const httpStatus =
|
|
551
|
-
error instanceof GatewayError && typeof error.status === "number" ? error.status : undefined;
|
|
552
|
-
const decision = planSlowLaneRetry(
|
|
553
|
-
{
|
|
554
|
-
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
555
|
-
message: error instanceof Error ? error.message : String(error),
|
|
556
|
-
sentSlow: true,
|
|
557
|
-
// One wait per dispatch: this adapter carries no cross-attempt counter,
|
|
558
|
-
// and a single wait is the conservative bound.
|
|
559
|
-
waitedMs: 0,
|
|
560
|
-
attempts: 0,
|
|
561
|
-
},
|
|
562
|
-
sharedClaudeSlowLanes,
|
|
563
|
-
accountId,
|
|
564
|
-
);
|
|
565
|
-
if (decision === null) return false;
|
|
566
|
-
|
|
567
|
-
const allowance = context.deadline - Date.now() - SLOW_LANE_DEADLINE_FLOOR_MS;
|
|
568
|
-
if (allowance <= 0) return false;
|
|
569
|
-
const waitMs = Math.min(decision.delayMs, allowance);
|
|
570
|
-
log.warn("claude: slow lane busy, waiting for capacity on the same account", {
|
|
571
|
-
provider: "claude",
|
|
572
|
-
waitMs,
|
|
573
|
-
});
|
|
574
|
-
try {
|
|
575
|
-
await sleep(waitMs, context.abort_signal);
|
|
576
|
-
} catch {
|
|
577
|
-
// Aborted mid-wait: let the caller surface the original failure rather
|
|
578
|
-
// than a bare cancel, so the client still sees the rate limit.
|
|
579
|
-
return false;
|
|
580
|
-
}
|
|
581
|
-
return true;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private async *sendWithCreditCapture(
|
|
585
|
-
sendOnce: (
|
|
586
|
-
headers: Record<string, string>,
|
|
587
|
-
body: string,
|
|
588
|
-
) => AsyncIterable<CanonicalEvent>,
|
|
589
|
-
headers: Record<string, string>,
|
|
590
|
-
body: string,
|
|
591
|
-
captureKey: string | undefined,
|
|
592
|
-
): AsyncIterable<CanonicalEvent> {
|
|
593
|
-
for await (const event of sendOnce(headers, body)) {
|
|
594
|
-
if (captureKey !== undefined && event.type === "terminal" && event.stop_reason === "error") {
|
|
595
|
-
const token = refusalCreditToken(event.stop_details);
|
|
596
|
-
if (token !== undefined) {
|
|
597
|
-
sharedFallbackCreditCache.set(captureKey, token);
|
|
598
|
-
log.warn("claude: refusal carried a fallback credit, banked for replay", {
|
|
599
|
-
provider: "claude",
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
yield event;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
export const claudeAdapter = new ClaudeAdapter();
|
|
1
|
+
/**
|
|
2
|
+
* Dedicated `claude` adapter.
|
|
3
|
+
*
|
|
4
|
+
* Wire mechanics live in `src/protocol/`:
|
|
5
|
+
* - `protocol/codecs.ts` shared guards, tool-id/schema/tool-name helpers, endpoint URL
|
|
6
|
+
* - `protocol/request/messages.ts` canonical -> Claude Messages body
|
|
7
|
+
* - `protocol/response/messages.ts` Claude JSON/SSE -> canonical events
|
|
8
|
+
* - `protocol/upstream/send-claude-messages.ts` shared HTTP send helper
|
|
9
|
+
* - `errors.ts` upstream HTTP error -> `GatewayError`
|
|
10
|
+
* - `policy.ts` header/beta/credential policy (unchanged)
|
|
11
|
+
* - `cch.ts` CCH billing-header body patch (unchanged)
|
|
12
|
+
*
|
|
13
|
+
* Bespoke by wire protocol (Phase C5): the Claude Messages envelope
|
|
14
|
+
* (thinking blocks, cache_control, beta negotiation) is outside the
|
|
15
|
+
* OpenAI-compatible factory's reach — stays bespoke, never re-audit.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import type { CanonicalEvent, CanonicalRequest } from "../../../protocol/canonical-model";
|
|
19
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
20
|
+
import { firstUserText, getHeader } from "../../../protocol/canonical-model";
|
|
21
|
+
import { log } from "../../../observability/logger";
|
|
22
|
+
import {
|
|
23
|
+
assessClaudeCodeCompatibility,
|
|
24
|
+
assertClaudeCodeCompatibility,
|
|
25
|
+
requestHasClaudeTools,
|
|
26
|
+
} from "./claude-compatibility";
|
|
27
|
+
import { buildClaudeHeaders, type ClaudeHeaderOptions } from "./claude-credentials";
|
|
28
|
+
import { isRecord } from "../../../protocol/codecs";
|
|
29
|
+
import { HEADER_CONTROL } from "../../../security/outbound-headers";
|
|
30
|
+
import { CLAUDE_USER_PROFILES_BETA } from "./claude-user-profiles";
|
|
31
|
+
import { sharedClaudeSlowLanes } from "./claude-slow-lane";
|
|
32
|
+
import { planSlowLaneRetry } from "./claude-slow-lane-retry";
|
|
33
|
+
import { sleep } from "../../../transport/failure-policy";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Floor reserved out of the request deadline before a slow-lane wait, so a
|
|
37
|
+
* wait can never consume the budget the next real attempt needs. Mirrors
|
|
38
|
+
* `MIN_RETRY_BUDGET_MS` in the attempt loop.
|
|
39
|
+
*/
|
|
40
|
+
const SLOW_LANE_DEADLINE_FLOOR_MS = 15_000;
|
|
41
|
+
import { resolvePromptCacheKey } from "../../operations/session-resolution";
|
|
42
|
+
import { ensureAnthropicBeta, filterClaudeCustomHeaders } from "../shared/claude-messages";
|
|
43
|
+
import {
|
|
44
|
+
CLAUDE_CODE_SYSTEM_INSTRUCTION,
|
|
45
|
+
createClaudeBillingText,
|
|
46
|
+
patchClaudeCchBody,
|
|
47
|
+
} from "./claude-cch";
|
|
48
|
+
import { getClaudeCliVersion, adoptClaudeCliVersion, getClaudeSdkVersion } from "../../operations/client-versions";
|
|
49
|
+
import { adoptRequiredVersion } from "../../operations/client-version-resolver";
|
|
50
|
+
import { getOrCreateInstallId } from "../shared/install-id";
|
|
51
|
+
import { canonicalToClaudeMessagesPayload } from "../../../protocol/request/messages";
|
|
52
|
+
import { applyParamQuirks } from "../../../transport/translation/quirks";
|
|
53
|
+
import { CLAUDE_BILLING_HEADER_PREFIX, endpointUrl } from "../../../protocol/codecs";
|
|
54
|
+
import { sendClaudeMessagesRequest } from "../../../protocol/upstream/send-claude-messages";
|
|
55
|
+
import {
|
|
56
|
+
FALLBACK_CREDIT_BETA,
|
|
57
|
+
fallbackCreditRequestBody,
|
|
58
|
+
fallbackCreditTargetModel,
|
|
59
|
+
hashBody,
|
|
60
|
+
isFallbackCreditTokenRejection,
|
|
61
|
+
refusalCreditToken,
|
|
62
|
+
sharedFallbackCreditCache,
|
|
63
|
+
} from "./fallback-credit";
|
|
64
|
+
import type { ProviderDispatchTarget, ProviderAdapter, ProviderDispatchContext } from "../../provider-contracts";
|
|
65
|
+
import { defineModel } from "../../provider-model-definition";
|
|
66
|
+
import type { ModelDefinition } from "../../provider-model-definition";
|
|
67
|
+
|
|
68
|
+
const claudeMessagesModel = (
|
|
69
|
+
modelId: string,
|
|
70
|
+
contextLimit: number,
|
|
71
|
+
outputLimit: number,
|
|
72
|
+
reasoning: boolean,
|
|
73
|
+
): ModelDefinition =>
|
|
74
|
+
defineModel({
|
|
75
|
+
id: modelId,
|
|
76
|
+
wireFamily: "messages",
|
|
77
|
+
endpoint: "/v1/messages",
|
|
78
|
+
ctx: contextLimit,
|
|
79
|
+
out: outputLimit,
|
|
80
|
+
vision: true,
|
|
81
|
+
reasoning,
|
|
82
|
+
toolCall: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const CLAUDE_MODELS: readonly ModelDefinition[] = [
|
|
86
|
+
claudeMessagesModel("claude-fable-5", 1_000_000, 128_000, true),
|
|
87
|
+
claudeMessagesModel("claude-fable-5-1", 1_000_000, 128_000, true),
|
|
88
|
+
claudeMessagesModel("claude-mythos-5", 1_000_000, 128_000, true),
|
|
89
|
+
claudeMessagesModel("claude-mythos-5-1", 1_000_000, 128_000, true),
|
|
90
|
+
claudeMessagesModel("claude-opus-5", 1_000_000, 128_000, true),
|
|
91
|
+
claudeMessagesModel("claude-opus-5-5", 1_000_000, 128_000, true),
|
|
92
|
+
claudeMessagesModel("claude-opus-4-8", 1_000_000, 128_000, true),
|
|
93
|
+
claudeMessagesModel("claude-opus-4-7", 1_000_000, 128_000, true),
|
|
94
|
+
claudeMessagesModel("claude-opus-4-6", 1_000_000, 128_000, true),
|
|
95
|
+
claudeMessagesModel("claude-sonnet-5-5", 1_000_000, 128_000, true),
|
|
96
|
+
claudeMessagesModel("claude-sonnet-5", 1_000_000, 128_000, true),
|
|
97
|
+
claudeMessagesModel("claude-sonnet-4-6", 1_000_000, 128_000, true),
|
|
98
|
+
claudeMessagesModel("claude-opus-4-5", 200_000, 64_000, true),
|
|
99
|
+
// 200K: the 4.5 trio predates the million-token window, which arrived with
|
|
100
|
+
// Opus 4.6. Only this row claimed 1M while its own siblings said 200K.
|
|
101
|
+
claudeMessagesModel("claude-sonnet-4-5", 200_000, 64_000, true),
|
|
102
|
+
claudeMessagesModel("claude-haiku-4-5", 200_000, 64_000, true),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
const CLAUDE_DEVICE_ID_DOMAIN = "cartethyia-claude-device-id-v1";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Stable per-installation id, persisted once like the Codex install id. The
|
|
112
|
+
* assistant metadata `device_id` is derived from it so a machine keeps one
|
|
113
|
+
* identity across restarts instead of minting a new one per request.
|
|
114
|
+
*
|
|
115
|
+
* The persistence lives in the shared install-id module: it owns the writable-
|
|
116
|
+
* location fallback (a container runs with `HOME=/root` but as uid 10001) and
|
|
117
|
+
* the policy that a telemetry identity never fails a request.
|
|
118
|
+
*/
|
|
119
|
+
async function getClaudeInstallId(): Promise<string> {
|
|
120
|
+
return getOrCreateInstallId("claude");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The stable device id the assistant metadata carries for this installation. */
|
|
124
|
+
export async function claudeDeviceId(accountId?: string): Promise<string> {
|
|
125
|
+
const hash = createHash("sha256").update(CLAUDE_DEVICE_ID_DOMAIN).update("\0").update(await getClaudeInstallId());
|
|
126
|
+
if (accountId !== undefined && accountId.length > 0) hash.update("\0").update(accountId);
|
|
127
|
+
return hash.digest("hex");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
131
|
+
|
|
132
|
+
function isUuid(value: unknown): value is string {
|
|
133
|
+
return typeof value === "string" && UUID_RE.test(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether a caller-supplied user id is already in an accepted shape: the
|
|
138
|
+
* cloaked `user_<64hex>_account_<uuid>_session_<uuid>` form, or a JSON
|
|
139
|
+
* envelope carrying a non-empty `session_id`.
|
|
140
|
+
*/
|
|
141
|
+
export function isClaudeMetadataUserId(value: string): boolean {
|
|
142
|
+
if (/^user_[0-9a-f]{64}_account_[0-9a-f-]{36}_session_[0-9a-f-]{36}$/i.test(value)) return true;
|
|
143
|
+
if (!value.startsWith("{")) return false;
|
|
144
|
+
try {
|
|
145
|
+
const parsed: unknown = JSON.parse(value);
|
|
146
|
+
return (
|
|
147
|
+
isRecord(parsed) &&
|
|
148
|
+
typeof parsed["session_id"] === "string" &&
|
|
149
|
+
(parsed["session_id"] as string).length > 0
|
|
150
|
+
);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Adapter construction options used by deterministic local tests and custom endpoints. */
|
|
157
|
+
interface ClaudeAdapterOptions {
|
|
158
|
+
readonly fetch?: typeof fetch;
|
|
159
|
+
readonly base_url?: string;
|
|
160
|
+
readonly custom_headers?: Readonly<Record<string, unknown>>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Claude Code CLI impersonation adapter: builds Claude-branded headers via
|
|
165
|
+
* `buildClaudeHeaders`, negotiates beta features, applies CCH billing-header
|
|
166
|
+
* injection, and biases the payload/endpoint for OAuth-style credentials.
|
|
167
|
+
* `provider_id` is always `"claude"` — plain Anthropic API-key traffic uses
|
|
168
|
+
* `AnthropicApiKeyAdapter` instead, which never touches this impersonation
|
|
169
|
+
* machinery.
|
|
170
|
+
*/
|
|
171
|
+
export class ClaudeAdapter implements ProviderAdapter {
|
|
172
|
+
readonly provider_id = "claude" as const;
|
|
173
|
+
private readonly fetchFn: typeof fetch;
|
|
174
|
+
private readonly baseUrl: string | undefined;
|
|
175
|
+
private readonly customHeaders: Readonly<Record<string, unknown>> | undefined;
|
|
176
|
+
|
|
177
|
+
constructor(options: ClaudeAdapterOptions = {}) {
|
|
178
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
179
|
+
this.baseUrl = options.base_url;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async *dispatch(
|
|
183
|
+
request: CanonicalRequest,
|
|
184
|
+
candidate: ProviderDispatchTarget,
|
|
185
|
+
context: ProviderDispatchContext,
|
|
186
|
+
): AsyncIterable<CanonicalEvent> {
|
|
187
|
+
if (candidate.wire_family !== "messages") {
|
|
188
|
+
throw new GatewayError(
|
|
189
|
+
"capability_unsupported",
|
|
190
|
+
400,
|
|
191
|
+
"claude supports messages only",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
context.credential.credential_kind !== "oauth" &&
|
|
196
|
+
context.credential.credential_kind !== "scoped_access_token"
|
|
197
|
+
) {
|
|
198
|
+
throw new GatewayError(
|
|
199
|
+
"invalid_request",
|
|
200
|
+
400,
|
|
201
|
+
`claude: the assistant OAuth credential required (got credential_kind=${context.credential.credential_kind})`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const customHeaders = filterClaudeCustomHeaders({
|
|
205
|
+
...this.customHeaders,
|
|
206
|
+
...context.credential.custom_headers,
|
|
207
|
+
});
|
|
208
|
+
const isClaudeCodeOAuth =
|
|
209
|
+
context.credential.credential_kind === "oauth" ||
|
|
210
|
+
context.credential.credential_kind === "scoped_access_token";
|
|
211
|
+
const incomingBeta = getHeader(context.request_headers, "anthropic-beta");
|
|
212
|
+
const assessment = assessClaudeCodeCompatibility(request, {
|
|
213
|
+
capabilities: candidate.capabilities,
|
|
214
|
+
beta_header: incomingBeta,
|
|
215
|
+
beta_policy: "reject",
|
|
216
|
+
credential_kind: context.credential.credential_kind,
|
|
217
|
+
target_provider: "claude",
|
|
218
|
+
});
|
|
219
|
+
assertClaudeCodeCompatibility(assessment);
|
|
220
|
+
const hasThinking =
|
|
221
|
+
request.reasoning !== undefined &&
|
|
222
|
+
request.reasoning.thinking_type !== "disabled";
|
|
223
|
+
const hasTools = requestHasClaudeTools(request);
|
|
224
|
+
const cliVersion = getClaudeCliVersion();
|
|
225
|
+
const sdkVersion = getClaudeSdkVersion();
|
|
226
|
+
const headerOptions: ClaudeHeaderOptions = {
|
|
227
|
+
credential_kind: context.credential.credential_kind,
|
|
228
|
+
custom_headers: customHeaders,
|
|
229
|
+
capabilities: candidate.capabilities,
|
|
230
|
+
unsupported_beta_policy: "reject",
|
|
231
|
+
target_provider: "claude",
|
|
232
|
+
has_thinking: hasThinking,
|
|
233
|
+
has_tools: hasTools,
|
|
234
|
+
stream: request.stream,
|
|
235
|
+
cli_version: cliVersion,
|
|
236
|
+
sdk_version: sdkVersion,
|
|
237
|
+
...(context.request_headers === undefined
|
|
238
|
+
? {}
|
|
239
|
+
: { request_headers: context.request_headers }),
|
|
240
|
+
...(incomingBeta === undefined ? {} : { anthropic_beta: incomingBeta }),
|
|
241
|
+
};
|
|
242
|
+
const headers: Record<string, string> = buildClaudeHeaders(
|
|
243
|
+
context.credential.secret,
|
|
244
|
+
context.credential.account_id,
|
|
245
|
+
headerOptions,
|
|
246
|
+
);
|
|
247
|
+
const profileId = request.generation_controls["extension:user_profile_id"];
|
|
248
|
+
// A profile id travels two ways on the same request: the `user-profiles`
|
|
249
|
+
// beta that unlocks profile scoping, and the header that names *which*
|
|
250
|
+
// profile. Carrying only the beta declares the capability without ever
|
|
251
|
+
// addressing a profile, so a profile-scoped credential could not be
|
|
252
|
+
// selected.
|
|
253
|
+
if (typeof profileId === "string" && profileId.length > 0) {
|
|
254
|
+
// Dated, not bare: the donor sends `user-profiles-2026-09-04`
|
|
255
|
+
// (the reference client `packages/ai/src/providers/anthropic-user-profiles.ts:4`).
|
|
256
|
+
// `ensureAnthropicBeta` takes any string, so a bare "user-profiles" would
|
|
257
|
+
// pass every check while being a beta Anthropic has never published — an
|
|
258
|
+
// undocumented value the upstream ignores.
|
|
259
|
+
ensureAnthropicBeta(headers, CLAUDE_USER_PROFILES_BETA);
|
|
260
|
+
if (!HEADER_CONTROL.test(profileId)) {
|
|
261
|
+
headers["anthropic-user-profile-id"] = profileId;
|
|
262
|
+
} else {
|
|
263
|
+
// Never let a caller's newline reach the wire: an injected header is
|
|
264
|
+
// the one thing a filter downstream cannot undo.
|
|
265
|
+
log.warn("claude: user_profile_id dropped, contains control characters", {
|
|
266
|
+
provider: "claude",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const sessionId =
|
|
271
|
+
headers["X-Claude-Code-Session-Id"] ?? resolvePromptCacheKey(request, context);
|
|
272
|
+
// Provider/model parameter quirks run before translation, exactly as they do
|
|
273
|
+
// on the API-key (`anthropic`) path. Without this call the Claude Code
|
|
274
|
+
// adapter never reached the table, so a model that rejects `temperature`
|
|
275
|
+
// (`claude-opus-4-7` and later, `claude-sonnet-5`, `claude-fable-5`) got a
|
|
276
|
+
// 400 on every request that carried one — including the probe, which sets
|
|
277
|
+
// `temperature: 0.2` itself.
|
|
278
|
+
const payload = canonicalToClaudeMessagesPayload(
|
|
279
|
+
applyParamQuirks(request, candidate.provider_id),
|
|
280
|
+
{ isOAuth: isClaudeCodeOAuth },
|
|
281
|
+
);
|
|
282
|
+
if (isClaudeCodeOAuth) {
|
|
283
|
+
// Session attribution follows the reference rules: a valid caller id
|
|
284
|
+
// travels verbatim; otherwise a JSON envelope is generated from the
|
|
285
|
+
// resolved session identity (never a bare UUID, which matches neither
|
|
286
|
+
// accepted grammar). API-key callers without an id send none.
|
|
287
|
+
const callerUserId = request.generation_controls["extension:metadata_user_id"];
|
|
288
|
+
if (typeof callerUserId === "string" && isClaudeMetadataUserId(callerUserId)) {
|
|
289
|
+
payload.metadata = { user_id: callerUserId };
|
|
290
|
+
} else if (sessionId !== undefined && sessionId.length > 0) {
|
|
291
|
+
const accountUuid = isUuid(context.credential.account_id)
|
|
292
|
+
? context.credential.account_id
|
|
293
|
+
: undefined;
|
|
294
|
+
payload.metadata = {
|
|
295
|
+
user_id: JSON.stringify({
|
|
296
|
+
device_id: await claudeDeviceId(accountUuid),
|
|
297
|
+
session_id: sessionId,
|
|
298
|
+
...(accountUuid === undefined ? {} : { account_uuid: accountUuid }),
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const system = Array.isArray(payload.system) ? payload.system : [];
|
|
303
|
+
const userText = firstUserText(request);
|
|
304
|
+
const billing = {
|
|
305
|
+
type: "text",
|
|
306
|
+
text: createClaudeBillingText(userText, cliVersion),
|
|
307
|
+
};
|
|
308
|
+
// The breakpoint is NOT pinned here. The reference client caches the
|
|
309
|
+
// *last* system block, so when the caller supplies its own prompt the
|
|
310
|
+
// breakpoint belongs on that, not on the identity — pinning it here put
|
|
311
|
+
// a breakpoint mid-prefix and left the caller's prompt uncached, which
|
|
312
|
+
// is both a fingerprint delta and a cache miss on every request.
|
|
313
|
+
const instruction = {
|
|
314
|
+
type: "text",
|
|
315
|
+
text: CLAUDE_CODE_SYSTEM_INSTRUCTION,
|
|
316
|
+
};
|
|
317
|
+
const hasInstruction = system.some(
|
|
318
|
+
(block) => isRecord(block) && block["text"] === CLAUDE_CODE_SYSTEM_INSTRUCTION,
|
|
319
|
+
);
|
|
320
|
+
const hasBilling = system.some(
|
|
321
|
+
(block) =>
|
|
322
|
+
isRecord(block) &&
|
|
323
|
+
typeof block["text"] === "string" &&
|
|
324
|
+
(block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX),
|
|
325
|
+
);
|
|
326
|
+
// Captured-client placement: billing first, instruction second. The
|
|
327
|
+
// patch anchor assumes this order — never reorder around it.
|
|
328
|
+
const assembled = [
|
|
329
|
+
...(hasBilling ? [] : [billing]),
|
|
330
|
+
...(hasInstruction ? [] : [instruction]),
|
|
331
|
+
...system,
|
|
332
|
+
];
|
|
333
|
+
// The cache breakpoint rides the last system block: everything before it
|
|
334
|
+
// is stable prefix, and caching the tail is what the reference client
|
|
335
|
+
// does. Billing is never breakpointed — it varies per request.
|
|
336
|
+
const lastIndex = assembled.length - 1;
|
|
337
|
+
if (lastIndex > 0) {
|
|
338
|
+
const last = assembled[lastIndex];
|
|
339
|
+
const existing = isRecord(last) ? last["cache_control"] : undefined;
|
|
340
|
+
// A caller's own breakpoint is kept, but upgraded to the reference
|
|
341
|
+
// client's 1h retention when it stated none: that is the default the
|
|
342
|
+
// real CLI sends, and leaving it bare caches a shorter prefix than the
|
|
343
|
+
// upstream will honour.
|
|
344
|
+
const breakpoint =
|
|
345
|
+
isRecord(existing) && existing["ttl"] !== undefined
|
|
346
|
+
? existing
|
|
347
|
+
: { type: "ephemeral", ttl: "1h" };
|
|
348
|
+
if (isRecord(last)) {
|
|
349
|
+
assembled[lastIndex] = { ...last, cache_control: breakpoint };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
payload.system = assembled;
|
|
353
|
+
}
|
|
354
|
+
const url = endpointUrl(
|
|
355
|
+
this.baseUrl,
|
|
356
|
+
candidate.endpoint_path || "/v1/messages",
|
|
357
|
+
isClaudeCodeOAuth,
|
|
358
|
+
);
|
|
359
|
+
const requestBody = JSON.stringify(payload);
|
|
360
|
+
const patchedBody = isClaudeCodeOAuth ? patchClaudeCchBody(requestBody) : undefined;
|
|
361
|
+
if (isClaudeCodeOAuth && patchedBody === undefined) {
|
|
362
|
+
// Unanchored billing block: warn and send as-is, exactly like the
|
|
363
|
+
// reference client. Availability wins over attestation here — an
|
|
364
|
+
// unattested send is the accepted fallback, a 500 is a self-DoS.
|
|
365
|
+
log.warn("claude billing attestation unanchored; sending unattested", {
|
|
366
|
+
provider: "claude",
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const outboundBody =
|
|
370
|
+
patchedBody === undefined ? requestBody : new TextDecoder().decode(patchedBody);
|
|
371
|
+
// Fallback-credit redemption: a previous refusal of *this exact body* may
|
|
372
|
+
// have left a token that buys one free replay. Keyed by the frozen body
|
|
373
|
+
// because the donor binds the token to the request that was refused
|
|
374
|
+
// (`anthropic.ts:3303`, `params: structuredClone(params)`).
|
|
375
|
+
const creditKey = hashBody(outboundBody);
|
|
376
|
+
const creditToken = sharedFallbackCreditCache.get(creditKey);
|
|
377
|
+
let creditSent = false;
|
|
378
|
+
let body = outboundBody;
|
|
379
|
+
if (creditToken !== undefined) {
|
|
380
|
+
const target = fallbackCreditTargetModel(request.provider_options);
|
|
381
|
+
const redemption = fallbackCreditRequestBody(outboundBody, creditToken, target);
|
|
382
|
+
if (redemption !== undefined) {
|
|
383
|
+
body = redemption;
|
|
384
|
+
creditSent = true;
|
|
385
|
+
// The donor guarantees the beta is present on every redemption
|
|
386
|
+
// (`anthropic.ts:2433-2435`, `:2782-2784`); on this side it is only
|
|
387
|
+
// on the agent profile (`claude-credentials.ts:124`), so add it here.
|
|
388
|
+
ensureAnthropicBeta(headers, FALLBACK_CREDIT_BETA);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Slow lane (design doc §5): attach the usage limit when this account's
|
|
392
|
+
// lane is active, so the upstream knows it may serve us on spare capacity.
|
|
393
|
+
// First-party OAuth only, mirroring the donor's gate
|
|
394
|
+
// (`anthropic.ts:2695-2705`). Nothing changes for every other request.
|
|
395
|
+
const accountId = context.credential.account_id;
|
|
396
|
+
const slowEligible =
|
|
397
|
+
isClaudeCodeOAuth && accountId !== undefined && sharedClaudeSlowLanes.isActive(accountId);
|
|
398
|
+
const slowHeaders: Record<string, string> = slowEligible
|
|
399
|
+
? { ...headers, "anthropic-usage-limit": "slow" }
|
|
400
|
+
: headers;
|
|
401
|
+
|
|
402
|
+
// Adopt-on-rejection: a stale pinned CLI version costs one rejected
|
|
403
|
+
// request, not an outage. The shared helper parses the server-named
|
|
404
|
+
// minimum and promotes it process-wide; a retry at the same version
|
|
405
|
+
// cannot loop because the helper only reports a forward move.
|
|
406
|
+
const sendOnce = (
|
|
407
|
+
headers: Record<string, string>,
|
|
408
|
+
body: string,
|
|
409
|
+
): AsyncIterable<CanonicalEvent> =>
|
|
410
|
+
sendClaudeMessagesRequest(
|
|
411
|
+
url,
|
|
412
|
+
headers,
|
|
413
|
+
body,
|
|
414
|
+
context,
|
|
415
|
+
request,
|
|
416
|
+
this.fetchFn,
|
|
417
|
+
isClaudeCodeOAuth,
|
|
418
|
+
);
|
|
419
|
+
try {
|
|
420
|
+
yield* this.sendWithCreditCapture(
|
|
421
|
+
sendOnce,
|
|
422
|
+
slowHeaders,
|
|
423
|
+
body,
|
|
424
|
+
creditSent ? undefined : creditKey,
|
|
425
|
+
);
|
|
426
|
+
if (creditSent) {
|
|
427
|
+
// One-shot spend. The donor consumes the handle once a turn is served
|
|
428
|
+
// (`consumeActiveFallbackCreditRedemption`, `turn-recovery.ts:510-519`);
|
|
429
|
+
// leaving it here would replay a spent token on the next identical
|
|
430
|
+
// request and cost it the 400 round trip below.
|
|
431
|
+
sharedFallbackCreditCache.delete(creditKey);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
} catch (error: unknown) {
|
|
435
|
+
// Slow-lane capacity wait (design doc §5): a 429 `slot_busy` or a 529 /
|
|
436
|
+
// `overloaded_error` while the slow limit was sent means "no spare
|
|
437
|
+
// capacity right now", not "this account is done". Retry the SAME
|
|
438
|
+
// account after the server-stated interval rather than failing over —
|
|
439
|
+
// and record no health, because nothing failed.
|
|
440
|
+
//
|
|
441
|
+
// Runs before every other error branch so a capacity wait is never
|
|
442
|
+
// misread as a version rejection or a credential problem.
|
|
443
|
+
if (slowEligible && accountId !== undefined) {
|
|
444
|
+
const waited = await this.planAndWaitSlowLane(error, accountId, context);
|
|
445
|
+
if (waited) {
|
|
446
|
+
// Same account, same bytes: the capacity wait is not a failover.
|
|
447
|
+
yield* this.sendWithCreditCapture(
|
|
448
|
+
sendOnce,
|
|
449
|
+
slowHeaders,
|
|
450
|
+
body,
|
|
451
|
+
creditSent ? undefined : creditKey,
|
|
452
|
+
);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (creditSent && isFallbackCreditTokenRejection(error)) {
|
|
457
|
+
// Donor `anthropic.ts:3469-3481`: the token is rejected, so forfeit it
|
|
458
|
+
// and retry the standard request once without it. A rebuilt body no
|
|
459
|
+
// longer matches the refused one, so the credit cannot ride along.
|
|
460
|
+
sharedFallbackCreditCache.delete(creditKey);
|
|
461
|
+
log.warn("claude: fallback credit token rejected, retrying without it", {
|
|
462
|
+
provider: "claude",
|
|
463
|
+
error: error instanceof Error ? error.message : String(error),
|
|
464
|
+
});
|
|
465
|
+
yield* sendOnce(headers, outboundBody);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const adopted = adoptRequiredVersion(
|
|
469
|
+
{ get: getClaudeCliVersion },
|
|
470
|
+
adoptClaudeCliVersion,
|
|
471
|
+
error,
|
|
472
|
+
{
|
|
473
|
+
marker: "claude_code_version_too_old",
|
|
474
|
+
pattern: /version (\d+\.\d+\.\d+) or newer is required/i,
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
if (adopted === null) throw error;
|
|
478
|
+
const retryCliVersion = getClaudeCliVersion();
|
|
479
|
+
log.warn("claude: adopted server-named CLI version, retrying once", {
|
|
480
|
+
provider: "claude",
|
|
481
|
+
version: retryCliVersion,
|
|
482
|
+
});
|
|
483
|
+
const retryBilling = {
|
|
484
|
+
type: "text",
|
|
485
|
+
text: createClaudeBillingText(firstUserText(request), retryCliVersion),
|
|
486
|
+
};
|
|
487
|
+
const parsedBody = JSON.parse(requestBody) as Record<string, unknown>;
|
|
488
|
+
const existingSystem = Array.isArray(parsedBody["system"])
|
|
489
|
+
? (parsedBody["system"] as unknown[])
|
|
490
|
+
: [];
|
|
491
|
+
const retryBody = isClaudeCodeOAuth
|
|
492
|
+
? patchClaudeCchBody(
|
|
493
|
+
JSON.stringify({
|
|
494
|
+
...parsedBody,
|
|
495
|
+
system: [
|
|
496
|
+
retryBilling,
|
|
497
|
+
...existingSystem.filter(
|
|
498
|
+
(block) =>
|
|
499
|
+
!(
|
|
500
|
+
isRecord(block) &&
|
|
501
|
+
typeof block["text"] === "string" &&
|
|
502
|
+
(block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX)
|
|
503
|
+
),
|
|
504
|
+
),
|
|
505
|
+
],
|
|
506
|
+
}),
|
|
507
|
+
)
|
|
508
|
+
: undefined;
|
|
509
|
+
const retryHeaders: Record<string, string> = {
|
|
510
|
+
...headers,
|
|
511
|
+
"User-Agent": `claude-cli/${retryCliVersion} (external, cli)`,
|
|
512
|
+
};
|
|
513
|
+
yield* sendOnce(
|
|
514
|
+
retryHeaders,
|
|
515
|
+
retryBody === undefined ? outboundBody : new TextDecoder().decode(retryBody),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Sends once, passing every event through, and banks a refusal's credit token
|
|
522
|
+
* against the body that was refused.
|
|
523
|
+
*
|
|
524
|
+
* Donor rule (`anthropic.ts:3287-3309`): only a refusal carries a token, and
|
|
525
|
+
* the frozen body is the request that produced it (`params: structuredClone(params)`).
|
|
526
|
+
* On this side the refusal surfaces as a terminal event whose canonical
|
|
527
|
+
* `stop_reason` is `error` (`mapClaudeStopReason`, `response/messages.ts:32-34`)
|
|
528
|
+
* carrying the provider's `stop_details`.
|
|
529
|
+
*
|
|
530
|
+
* `captureKey` is omitted for a redemption send: a body already carrying a
|
|
531
|
+
* token is never the body that was refused, so banking against it would only
|
|
532
|
+
* occupy a cache slot for five minutes with a token that cannot be replayed.
|
|
533
|
+
*/
|
|
534
|
+
/**
|
|
535
|
+
* Waits out a slow-lane capacity signal and reports whether to retry.
|
|
536
|
+
*
|
|
537
|
+
* Returns true when the failure was a capacity wait that has now elapsed and
|
|
538
|
+
* the caller should resend; false when it was anything else, so the normal
|
|
539
|
+
* error path proceeds untouched.
|
|
540
|
+
*
|
|
541
|
+
* Bounded by the server's own max-wait and by the request deadline minus the
|
|
542
|
+
* attempt-loop's floor, so a wait can never eat the budget a real retry
|
|
543
|
+
* needs. No health is recorded here — a capacity wait is not a failure.
|
|
544
|
+
*/
|
|
545
|
+
private async planAndWaitSlowLane(
|
|
546
|
+
error: unknown,
|
|
547
|
+
accountId: string,
|
|
548
|
+
context: ProviderDispatchContext,
|
|
549
|
+
): Promise<boolean> {
|
|
550
|
+
const httpStatus =
|
|
551
|
+
error instanceof GatewayError && typeof error.status === "number" ? error.status : undefined;
|
|
552
|
+
const decision = planSlowLaneRetry(
|
|
553
|
+
{
|
|
554
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
555
|
+
message: error instanceof Error ? error.message : String(error),
|
|
556
|
+
sentSlow: true,
|
|
557
|
+
// One wait per dispatch: this adapter carries no cross-attempt counter,
|
|
558
|
+
// and a single wait is the conservative bound.
|
|
559
|
+
waitedMs: 0,
|
|
560
|
+
attempts: 0,
|
|
561
|
+
},
|
|
562
|
+
sharedClaudeSlowLanes,
|
|
563
|
+
accountId,
|
|
564
|
+
);
|
|
565
|
+
if (decision === null) return false;
|
|
566
|
+
|
|
567
|
+
const allowance = context.deadline - Date.now() - SLOW_LANE_DEADLINE_FLOOR_MS;
|
|
568
|
+
if (allowance <= 0) return false;
|
|
569
|
+
const waitMs = Math.min(decision.delayMs, allowance);
|
|
570
|
+
log.warn("claude: slow lane busy, waiting for capacity on the same account", {
|
|
571
|
+
provider: "claude",
|
|
572
|
+
waitMs,
|
|
573
|
+
});
|
|
574
|
+
try {
|
|
575
|
+
await sleep(waitMs, context.abort_signal);
|
|
576
|
+
} catch {
|
|
577
|
+
// Aborted mid-wait: let the caller surface the original failure rather
|
|
578
|
+
// than a bare cancel, so the client still sees the rate limit.
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private async *sendWithCreditCapture(
|
|
585
|
+
sendOnce: (
|
|
586
|
+
headers: Record<string, string>,
|
|
587
|
+
body: string,
|
|
588
|
+
) => AsyncIterable<CanonicalEvent>,
|
|
589
|
+
headers: Record<string, string>,
|
|
590
|
+
body: string,
|
|
591
|
+
captureKey: string | undefined,
|
|
592
|
+
): AsyncIterable<CanonicalEvent> {
|
|
593
|
+
for await (const event of sendOnce(headers, body)) {
|
|
594
|
+
if (captureKey !== undefined && event.type === "terminal" && event.stop_reason === "error") {
|
|
595
|
+
const token = refusalCreditToken(event.stop_details);
|
|
596
|
+
if (token !== undefined) {
|
|
597
|
+
sharedFallbackCreditCache.set(captureKey, token);
|
|
598
|
+
log.warn("claude: refusal carried a fallback credit, banked for replay", {
|
|
599
|
+
provider: "claude",
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
yield event;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export const claudeAdapter = new ClaudeAdapter();
|