cartethyia 2.6.3 → 2.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/dashboard/assets/{BannedPage-C8pfspAj.js → BannedPage-B7OjIxCp.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-B7OjIxCp.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-D-OwfzDV.js → CliToolDetailPage-Bt-6y24w.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage-Bt-6y24w.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-DqRcDO5j.js → CliToolsPage-C6LunsuE.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-C6LunsuE.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-B-a0l8yE.js → CombosPage-CgyfN2Yy.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-CgyfN2Yy.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-BVVGUdqt.js → ConsoleLogPage-BHWU7y6j.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-BHWU7y6j.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-BzmbE05k.js → CustomizationPage-xIjU736m.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-xIjU736m.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-DeT1zkYG.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-DeT1zkYG.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-BdSGc5J0.js → LoginPage-ChVqIAam.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-ChVqIAam.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-DVny4S5_.js → ModelPicker-CA1kLs45.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  19. package/dist/dashboard/assets/{OverviewPage-D3fMRkpP.js → OverviewPage-BD4GkRtS.js} +1 -1
  20. package/dist/dashboard/assets/OverviewPage-BD4GkRtS.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-BJXwqpKj.js +3 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-BJXwqpKj.js.gz +0 -0
  23. package/dist/dashboard/assets/{ProvidersPage-T7WamMuq.js → ProvidersPage-Ck64riM5.js} +1 -1
  24. package/dist/dashboard/assets/ProvidersPage-Ck64riM5.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProxyPage-CKJb_Kj4.js → ProxyPage-Br_Leqq0.js} +1 -1
  26. package/dist/dashboard/assets/ProxyPage-Br_Leqq0.js.gz +0 -0
  27. package/dist/dashboard/assets/{QuotaPage-hxJ1hGES.js → QuotaPage-D2gyhfa8.js} +1 -1
  28. package/dist/dashboard/assets/QuotaPage-D2gyhfa8.js.gz +0 -0
  29. package/dist/dashboard/assets/{SettingsPage-CpEErAq5.js → SettingsPage-1dbYb8cM.js} +1 -1
  30. package/dist/dashboard/assets/SettingsPage-1dbYb8cM.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-DRsoneXm.js → SetupPage-DUTFy9wH.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DUTFy9wH.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-QvDz0eQo.js → SortableList-BZEhfDpZ.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-BZEhfDpZ.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-6yLBksbv.js → StudioPage-GBDtsJjh.js} +1 -1
  36. package/dist/dashboard/assets/StudioPage-GBDtsJjh.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-CpB88NH4.js → UsagePage-CxidMvxq.js} +1 -1
  38. package/dist/dashboard/assets/UsagePage-CxidMvxq.js.gz +0 -0
  39. package/dist/dashboard/assets/app-BUSiYL6F.css +2 -0
  40. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  41. package/dist/dashboard/assets/{badge-CKsF123d.js → badge-Bkz_-RrM.js} +1 -1
  42. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  43. package/dist/dashboard/assets/{banner-8j7uChA9.js → banner-C-7y3VKV.js} +1 -1
  44. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  45. package/dist/dashboard/assets/{card-CdsSlWA3.js → card-BOPBuBzD.js} +1 -1
  46. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  47. package/dist/dashboard/assets/{clipboard-button-uL5aYLfW.js → clipboard-button-e-Mlk0FH.js} +1 -1
  48. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  49. package/dist/dashboard/assets/{dialog-C07hr5_G.js → dialog-Iec_ywbD.js} +1 -1
  50. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  51. package/dist/dashboard/assets/{drawer-BPO1TcKH.js → drawer-Bs8MIIHc.js} +1 -1
  52. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  53. package/dist/dashboard/assets/{entry-Bz_CtDfC.js → entry-BRszm4_Z.js} +1 -1
  54. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  55. package/dist/dashboard/assets/{entry-8U_8IAW2.js → entry-BrksMru8.js} +3 -3
  56. package/dist/dashboard/assets/entry-BrksMru8.js.gz +0 -0
  57. package/dist/dashboard/assets/{entry-GJRbmrpA.js → entry-DcobaKFu.js} +1 -1
  58. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  59. package/dist/dashboard/assets/{github-badge-DmQreSDd.js → github-badge-BuDsrsPn.js} +1 -1
  60. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  61. package/dist/dashboard/assets/{index-BG48fsZ2.js → index-DU61iviS.js} +2 -2
  62. package/dist/dashboard/assets/index-DU61iviS.js.gz +0 -0
  63. package/dist/dashboard/assets/{inline-C9dCYaBZ.js → inline-DToMICwl.js} +1 -1
  64. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  65. package/dist/dashboard/assets/{input-CjCnXweJ.js → input-WlyHWeG2.js} +1 -1
  66. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  67. package/dist/dashboard/assets/{layout-V6y8DAkl.js → layout-DaJ5baJr.js} +1 -1
  68. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  69. package/dist/dashboard/assets/{list-row-CM1AaG_y.js → list-row-B3oSK13z.js} +1 -1
  70. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  71. package/dist/dashboard/assets/{numbers-BOevJzdz.js → numbers-Dulqgys5.js} +1 -1
  72. package/dist/dashboard/assets/numbers-Dulqgys5.js.gz +0 -0
  73. package/dist/dashboard/assets/{select-AnGKpdl1.js → select-Ciq_KFQ1.js} +1 -1
  74. package/dist/dashboard/assets/{select-AnGKpdl1.js.gz → select-Ciq_KFQ1.js.gz} +0 -0
  75. package/dist/dashboard/assets/{stack-_gfTdLVB.js → stack-B1w5BM0u.js} +1 -1
  76. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  77. package/dist/dashboard/assets/{state-2c5V4zo7.js → state-DhZBfWgx.js} +1 -1
  78. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  79. package/dist/dashboard/assets/{switch-Dkke7Naf.js → switch-yD7D99Lg.js} +1 -1
  80. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  81. package/dist/dashboard/index.html +1 -1
  82. package/dist/dashboard/index.html.gz +0 -0
  83. package/dist/dashboard/precompressed-assets.json +41 -41
  84. package/package.json +96 -96
  85. package/src/boot/app.ts +11 -1
  86. package/src/console/providers/catalog/contracts.ts +7 -0
  87. package/src/console/providers/catalog/store.ts +25 -2
  88. package/src/main.ts +6 -0
  89. package/src/observability/event-query.ts +10 -5
  90. package/src/providers/operations/account-health-service.ts +211 -17
  91. package/dist/dashboard/assets/BannedPage-C8pfspAj.js.gz +0 -0
  92. package/dist/dashboard/assets/CliToolDetailPage-D-OwfzDV.js.gz +0 -0
  93. package/dist/dashboard/assets/CliToolsPage-DqRcDO5j.js.gz +0 -0
  94. package/dist/dashboard/assets/CombosPage-B-a0l8yE.js.gz +0 -0
  95. package/dist/dashboard/assets/ConsoleLogPage-BVVGUdqt.js.gz +0 -0
  96. package/dist/dashboard/assets/CustomizationPage-BzmbE05k.js.gz +0 -0
  97. package/dist/dashboard/assets/HealthEventsModal-BRDc_wx8.js +0 -1
  98. package/dist/dashboard/assets/HealthEventsModal-BRDc_wx8.js.gz +0 -0
  99. package/dist/dashboard/assets/LoginPage-BdSGc5J0.js.gz +0 -0
  100. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  101. package/dist/dashboard/assets/OverviewPage-D3fMRkpP.js.gz +0 -0
  102. package/dist/dashboard/assets/ProviderDetailPage-DlBwPsHM.js +0 -3
  103. package/dist/dashboard/assets/ProviderDetailPage-DlBwPsHM.js.gz +0 -0
  104. package/dist/dashboard/assets/ProvidersPage-T7WamMuq.js.gz +0 -0
  105. package/dist/dashboard/assets/ProxyPage-CKJb_Kj4.js.gz +0 -0
  106. package/dist/dashboard/assets/QuotaPage-hxJ1hGES.js.gz +0 -0
  107. package/dist/dashboard/assets/SettingsPage-CpEErAq5.js.gz +0 -0
  108. package/dist/dashboard/assets/SetupPage-DRsoneXm.js.gz +0 -0
  109. package/dist/dashboard/assets/SortableList-QvDz0eQo.js.gz +0 -0
  110. package/dist/dashboard/assets/StudioPage-6yLBksbv.js.gz +0 -0
  111. package/dist/dashboard/assets/UsagePage-CpB88NH4.js.gz +0 -0
  112. package/dist/dashboard/assets/app-CEeaMHOy.css +0 -2
  113. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  114. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  115. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  116. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  117. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  118. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  119. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  120. package/dist/dashboard/assets/entry-8U_8IAW2.js.gz +0 -0
  121. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  122. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  123. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  124. package/dist/dashboard/assets/index-BG48fsZ2.js.gz +0 -0
  125. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  126. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  127. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  128. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  129. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  130. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  131. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  132. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  133. /package/dist/dashboard/assets/{app-2KhtHCv9.js → app-MdVfwraB.js} +0 -0
  134. /package/dist/dashboard/assets/{app-2KhtHCv9.js.gz → app-MdVfwraB.js.gz} +0 -0
package/package.json CHANGED
@@ -1,96 +1,96 @@
1
- {
2
- "name": "cartethyia",
3
- "version": "2.6.3",
4
- "description": "A powerful, self-hosted reverse proxy for OpenAI- and Anthropic-compatible AI clients, powered by native Bun and Elysia. It serves both protocols from a single server, routes every model across any configured provider, and translates requests and responses in real time — streaming and prompt caching included. Underneath it runs a full delivery engine: per-account health tracking, quota and cooldown enforcement, automatic failover across pools and providers, usage accounting, and telemetry. Point OpenCode, Cline, Claude Code, Codex, or any compatible client at Cartethyia and it keeps its native protocol while the gateway absorbs every provider-specific difference behind it. Runs on embedded SQLite or external PostgreSQL with optional Redis coordination, ships as a global CLI with a system-tray console, and needs only Bun 1.4+ alongside Node.js/npm.",
5
- "license": "GPL-3.0-only",
6
- "type": "module",
7
- "packageManager": "bun@1.4.0",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/risunCodex/Cartethyia.git"
11
- },
12
- "homepage": "https://github.com/risunCodex/Cartethyia#readme",
13
- "bugs": {
14
- "url": "https://github.com/risunCodex/Cartethyia/issues"
15
- },
16
- "keywords": [
17
- "ai",
18
- "gateway",
19
- "llm",
20
- "bun",
21
- "self-hosted"
22
- ],
23
- "bin": {
24
- "cartethyia": "scripts/cli/bin/cartethyia.cjs",
25
- "carte": "scripts/cli/bin/carte.cjs"
26
- },
27
- "files": [
28
- "scripts/cli/",
29
- "src/",
30
- "migrations/",
31
- "dist/dashboard/",
32
- ".env.example",
33
- "README.md"
34
- ],
35
- "workspaces": [
36
- "dashboard"
37
- ],
38
- "engines": {
39
- "bun": ">=1.4.0"
40
- },
41
- "scripts": {
42
- "dev": "bun run dev:stack",
43
- "dev:watch": "bun run scripts/dev/supervisor.ts",
44
- "test": "bun run scripts/test/run.ts",
45
- "build": "bun run scripts/build/build.ts",
46
- "prepack": "bun run dashboard:build",
47
- "setup": "bun run scripts/cli/setup.ts",
48
- "start": "bun run scripts/cli/main.ts start",
49
- "doctor": "bun run scripts/cli/doctor.ts",
50
- "probe:egress": "bun run scripts/tools/probe-egress.ts",
51
- "restart": "bun run scripts/build/restart.ts",
52
- "build:aot": "bun run scripts/build/aot.ts",
53
- "dev:stack": "concurrently -n BACKEND,DASHBOARD -c cyan,magenta --kill-others-on-fail \"bun run dev:backend\" \"bun run dashboard:dev\"",
54
- "typecheck": "tsc --noEmit",
55
- "test:watch": "bun run scripts/test/run.ts --watch",
56
- "test-db:up": "docker compose -f docker-compose.test.yml up -d --wait postgres-test",
57
- "dev:backend": "bun run --hot src/main.ts",
58
- "dev:console-auth": "bun run scripts/test/console-session.ts",
59
- "dev:visual-baseline": "bun run scripts/test/visual-baseline.ts",
60
- "build:binary": "bun run scripts/build/compile.ts",
61
- "test:backend": "bun run scripts/test/run.ts --scope backend",
62
- "test:integration": "bun run scripts/test/run.ts --scope integration",
63
- "test-db:down": "docker compose -f docker-compose.test.yml down",
64
- "dashboard:dev": "bun run --cwd dashboard dev",
65
- "test-db:check": "bun run scripts/test/check-db.ts",
66
- "dashboard:test": "bun run scripts/test/run.ts --scope dashboard",
67
- "test-db:reset": "docker compose -f docker-compose.test.yml down -v && docker compose -f docker-compose.test.yml up -d --wait postgres-test",
68
- "dashboard:build": "bun run --cwd dashboard build",
69
- "dashboard:typecheck": "bun run --cwd dashboard typecheck",
70
- "check:migrations": "bun run scripts/test/check-migrations.ts"
71
- },
72
- "dependencies": {
73
- "drizzle-orm": "0.45.2",
74
- "elysia": "2.0.0-beta.16",
75
- "enquirer": "^2.4.1",
76
- "ioredis": "6.0.0",
77
- "jose": "^6.2.12",
78
- "jsonrepair": "^3.15.0",
79
- "pg": "8.23.0",
80
- "pino": "^10.3.1",
81
- "pino-pretty": "^13.1.3",
82
- "protobufjs": "^7.6.5",
83
- "socks-proxy-agent": "^10.1.0",
84
- "systray2": "2.1.4",
85
- "typebox": "1.3.27",
86
- "ws": "8.22.0"
87
- },
88
- "devDependencies": {
89
- "@types/pg": "8.11.10",
90
- "@types/ws": "8.18.1",
91
- "bun-types": "1.4.0",
92
- "concurrently": "^10.0.5",
93
- "drizzle-kit": "^0.31.11",
94
- "typescript": "5.9.2"
95
- }
96
- }
1
+ {
2
+ "name": "cartethyia",
3
+ "version": "2.6.5",
4
+ "description": "A powerful, self-hosted reverse proxy for OpenAI- and Anthropic-compatible AI clients, powered by native Bun and Elysia. It serves both protocols from a single server, routes every model across any configured provider, and translates requests and responses in real time — streaming and prompt caching included. Underneath it runs a full delivery engine: per-account health tracking, quota and cooldown enforcement, automatic failover across pools and providers, usage accounting, and telemetry. Point OpenCode, Cline, Claude Code, Codex, or any compatible client at Cartethyia and it keeps its native protocol while the gateway absorbs every provider-specific difference behind it. Runs on embedded SQLite or external PostgreSQL with optional Redis coordination, ships as a global CLI with a system-tray console, and needs only Bun 1.4+ alongside Node.js/npm.",
5
+ "license": "GPL-3.0-only",
6
+ "type": "module",
7
+ "packageManager": "bun@1.4.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/risunCodex/Cartethyia.git"
11
+ },
12
+ "homepage": "https://github.com/risunCodex/Cartethyia#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/risunCodex/Cartethyia/issues"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "gateway",
19
+ "llm",
20
+ "bun",
21
+ "self-hosted"
22
+ ],
23
+ "bin": {
24
+ "cartethyia": "scripts/cli/bin/cartethyia.cjs",
25
+ "carte": "scripts/cli/bin/carte.cjs"
26
+ },
27
+ "files": [
28
+ "scripts/cli/",
29
+ "src/",
30
+ "migrations/",
31
+ "dist/dashboard/",
32
+ ".env.example",
33
+ "README.md"
34
+ ],
35
+ "workspaces": [
36
+ "dashboard"
37
+ ],
38
+ "engines": {
39
+ "bun": ">=1.4.0"
40
+ },
41
+ "scripts": {
42
+ "dev": "bun run dev:stack",
43
+ "dev:watch": "bun run scripts/dev/supervisor.ts",
44
+ "test": "bun run scripts/test/run.ts",
45
+ "build": "bun run scripts/build/build.ts",
46
+ "prepack": "bun run dashboard:build",
47
+ "setup": "bun run scripts/cli/setup.ts",
48
+ "start": "bun run scripts/cli/main.ts start",
49
+ "doctor": "bun run scripts/cli/doctor.ts",
50
+ "probe:egress": "bun run scripts/tools/probe-egress.ts",
51
+ "restart": "bun run scripts/build/restart.ts",
52
+ "build:aot": "bun run scripts/build/aot.ts",
53
+ "dev:stack": "concurrently -n BACKEND,DASHBOARD -c cyan,magenta --kill-others-on-fail \"bun run dev:backend\" \"bun run dashboard:dev\"",
54
+ "typecheck": "tsc --noEmit",
55
+ "test:watch": "bun run scripts/test/run.ts --watch",
56
+ "test-db:up": "docker compose -f docker-compose.test.yml up -d --wait postgres-test",
57
+ "dev:backend": "bun run --hot src/main.ts",
58
+ "dev:console-auth": "bun run scripts/test/console-session.ts",
59
+ "dev:visual-baseline": "bun run scripts/test/visual-baseline.ts",
60
+ "build:binary": "bun run scripts/build/compile.ts",
61
+ "test:backend": "bun run scripts/test/run.ts --scope backend",
62
+ "test:integration": "bun run scripts/test/run.ts --scope integration",
63
+ "test-db:down": "docker compose -f docker-compose.test.yml down",
64
+ "dashboard:dev": "bun run --cwd dashboard dev",
65
+ "test-db:check": "bun run scripts/test/check-db.ts",
66
+ "dashboard:test": "bun run scripts/test/run.ts --scope dashboard",
67
+ "test-db:reset": "docker compose -f docker-compose.test.yml down -v && docker compose -f docker-compose.test.yml up -d --wait postgres-test",
68
+ "dashboard:build": "bun run --cwd dashboard build",
69
+ "dashboard:typecheck": "bun run --cwd dashboard typecheck",
70
+ "check:migrations": "bun run scripts/test/check-migrations.ts"
71
+ },
72
+ "dependencies": {
73
+ "drizzle-orm": "0.45.2",
74
+ "elysia": "2.0.0-beta.16",
75
+ "enquirer": "^2.4.1",
76
+ "ioredis": "6.0.0",
77
+ "jose": "^6.2.12",
78
+ "jsonrepair": "^3.15.0",
79
+ "pg": "8.23.0",
80
+ "pino": "^10.3.1",
81
+ "pino-pretty": "^13.1.3",
82
+ "protobufjs": "^7.6.5",
83
+ "socks-proxy-agent": "^10.1.0",
84
+ "systray2": "2.1.4",
85
+ "typebox": "1.3.27",
86
+ "ws": "8.22.0"
87
+ },
88
+ "devDependencies": {
89
+ "@types/pg": "8.11.10",
90
+ "@types/ws": "8.18.1",
91
+ "bun-types": "1.4.0",
92
+ "concurrently": "^10.0.5",
93
+ "drizzle-kit": "^0.31.11",
94
+ "typescript": "5.9.2"
95
+ }
96
+ }
package/src/boot/app.ts CHANGED
@@ -269,7 +269,17 @@ export function createGatewayApp(deps: GatewayAppDeps) {
269
269
  const INFLIGHT_BACKSTOP_INTERVAL_MS = 30_000;
270
270
  const INFLIGHT_BACKSTOP_GRACE_MS = 30_000;
271
271
  const INFLIGHT_BACKSTOP_HARD_GRACE_MS = 120_000;
272
- if (deps.mode === "production") deps.scheduledTasks?.register({
272
+ // Registered in every mode, not just production. The gauge is the operator's
273
+ // concurrency signal, and a cancelled request that never ran a finalizer
274
+ // leaves its flight behind — gating the only thing that reaps those on
275
+ // production made a permanently wrong count depend on how the gateway was
276
+ // started. The registry exists in every mode, so there is no mode where
277
+ // running this is wrong.
278
+ // `scheduledTasks` is declared on the production shape only, so it is
279
+ // narrowed rather than reached through the union — a shell composition
280
+ // without the registry simply skips the backstop, exactly as before.
281
+ const backstopTasks = "scheduledTasks" in deps ? deps.scheduledTasks : undefined;
282
+ backstopTasks?.register({
273
283
  name: "inflight-backstop",
274
284
  intervalMs: INFLIGHT_BACKSTOP_INTERVAL_MS,
275
285
  run: () => {
@@ -509,6 +509,13 @@ export interface ProviderAccountResponse {
509
509
  credentialKind: CredentialKind;
510
510
  credentialMode?: CredentialMode;
511
511
  tokenExpiresAt?: string;
512
+ /**
513
+ * Who issued this credential, read from the JWT `iss` claim at registration
514
+ * time. Present only for static JWTs, and only when the payload carried the
515
+ * claim: the secret is never readable after registration, so this is the one
516
+ * record of where the token came from.
517
+ */
518
+ jwtIssuer?: string;
512
519
  status: string;
513
520
  /** Live routing admission count for this request path; absent when unavailable. */
514
521
  inflight?: number;
@@ -59,11 +59,26 @@ function parseExpiry(value: unknown): Date | undefined {
59
59
  }
60
60
  return undefined;
61
61
  }
62
- function parseJwtCredential(raw: string): { readonly expiresAt?: Date } | undefined {
62
+ /**
63
+ * Reads the two claims a static token's own payload can answer: when it stops
64
+ * working, and who issued it.
65
+ *
66
+ * The issuer is kept because a static JWT carries no other hint of its origin:
67
+ * an operator who pasted one from the wrong surface has nothing in the console
68
+ * to check it against, and several issuers accept the same token shape.
69
+ */
70
+ function parseJwtCredential(
71
+ raw: string,
72
+ ): { readonly expiresAt?: Date; readonly issuer?: string } | undefined {
63
73
  const payload = decodeJwtPayload(raw.trim());
64
74
  if (payload === undefined) return undefined;
65
75
  const expiresAt = parseExpiry(payload["exp"]);
66
- return expiresAt === undefined ? {} : { expiresAt };
76
+ const iss = payload["iss"];
77
+ const issuer = typeof iss === "string" && iss.trim().length > 0 ? iss.trim() : undefined;
78
+ return {
79
+ ...(expiresAt === undefined ? {} : { expiresAt }),
80
+ ...(issuer === undefined ? {} : { issuer }),
81
+ };
67
82
  }
68
83
 
69
84
  /**
@@ -1124,6 +1139,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1124
1139
  const requestedCredentialMode: CredentialMode = request.credentialMode ?? "auto";
1125
1140
  let effectiveCredentialMode: CredentialMode = requestedCredentialMode;
1126
1141
  let tokenExpiresAt: Date | undefined;
1142
+ let jwtIssuer: string | undefined;
1127
1143
  let oauthRefreshCiphertext: Buffer | undefined;
1128
1144
  let oauthExpiresAt: Date | undefined;
1129
1145
  let oauthHasRefreshToken = false;
@@ -1160,6 +1176,9 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1160
1176
  effectiveCredentialKind = "api_key";
1161
1177
  effectiveCredentialMode = "jwt";
1162
1178
  tokenExpiresAt = parsedJwt.expiresAt;
1179
+ // Kept alongside the expiry: the console shows who issued a static
1180
+ // token, and the secret itself is never readable after this point.
1181
+ jwtIssuer = parsedJwt.issuer;
1163
1182
  } else if (request.credentialKind === "oauth" || hasRefreshToken) {
1164
1183
  effectiveSecret = parts.accessToken;
1165
1184
  effectiveCredentialKind = hasRefreshToken ? "oauth" : "api_key";
@@ -1185,6 +1204,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1185
1204
  ...(request.authState ?? {}),
1186
1205
  credentialMode: effectiveCredentialMode,
1187
1206
  ...(tokenExpiresAt === undefined ? {} : { tokenExpiresAt: tokenExpiresAt.toISOString() }),
1207
+ ...(jwtIssuer === undefined ? {} : { jwtIssuer }),
1188
1208
  };
1189
1209
  const nextSortIndex =
1190
1210
  sortIndexOverride ?? (await this.nextAccountSortIndex(tx, providerId, tenantId));
@@ -1409,6 +1429,8 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1409
1429
  : undefined;
1410
1430
  const tokenExpiresAt =
1411
1431
  typeof authState?.["tokenExpiresAt"] === "string" ? authState["tokenExpiresAt"] : undefined;
1432
+ const jwtIssuer =
1433
+ typeof authState?.["jwtIssuer"] === "string" ? authState["jwtIssuer"] : undefined;
1412
1434
  return {
1413
1435
  id: row.id,
1414
1436
  providerId: row.providerId,
@@ -1417,6 +1439,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1417
1439
  credentialKind: row.credentialKind,
1418
1440
  ...(credentialMode === undefined ? {} : { credentialMode }),
1419
1441
  ...(tokenExpiresAt === undefined ? {} : { tokenExpiresAt }),
1442
+ ...(jwtIssuer === undefined ? {} : { jwtIssuer }),
1420
1443
  status: row.status,
1421
1444
  ...(usage.inflight === undefined ? {} : { inflight: usage.inflight }),
1422
1445
  usageToday: usage.today,
package/src/main.ts CHANGED
@@ -52,6 +52,12 @@ const app = boot
52
52
  modelStrikes: boot.deps.modelStrikes,
53
53
  maxBodyBytes: resolveMaxBodyBytes(),
54
54
  scheduledTasks: boot.deps.scheduledTasks,
55
+ // The sampler is constructed in the dependency graph and already
56
+ // sampling on its own interval; without passing it here the app cannot
57
+ // bind its in-flight source, so `cartethyia_process_in_flight` stays
58
+ // pinned at the constructor's `() => 0` placeholder forever while the
59
+ // registry it was meant to read reports real flights.
60
+ runtimeMetricsSampler: boot.deps.runtimeMetricsSampler,
55
61
  shutdownCoordinator: boot.shutdownCoordinator,
56
62
  // Signal-free graceful stop for platforms where a catchable signal cannot
57
63
  // be delivered (Windows). Off unless `CARTETHYIA_DRAIN_TOKEN` is set.
@@ -215,7 +215,8 @@ export function eventRecordFromRow(row: {
215
215
  */
216
216
  function isPostgres(db: CartethyiaDatabase): boolean {
217
217
  const dialect = (db as unknown as { dialect?: { constructor?: { name?: string } } }).dialect;
218
- return dialect?.constructor?.name === "PostgresDialect";
218
+ const name = dialect?.constructor?.name;
219
+ return name === "PostgresDialect" || name === "PgDialect";
219
220
  }
220
221
 
221
222
  /**
@@ -508,9 +509,14 @@ export async function chartTelemetry(
508
509
  bucketSeconds: number,
509
510
  ): Promise<ChartBucket[]> {
510
511
  const width = Math.max(1, Math.trunc(bucketSeconds));
512
+ // Inlined (not parameterised) so Postgres sees the SELECT and GROUP BY
513
+ // expressions as identical: parameter numbers differing between the two
514
+ // clauses made Postgres treat them as unrelated expressions referencing
515
+ // `created_at`, which 42803-rejected the query.
516
+ const w = sql.raw(String(width));
511
517
  const bucketExpression = isPostgres(db)
512
- ? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${width}) * ${width}`
513
- : sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${width}) * ${width}`;
518
+ ? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${w}) * ${w}`
519
+ : sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${w}) * ${w}`;
514
520
  const rows = await db
515
521
  .select({
516
522
  at: bucketExpression,
@@ -521,8 +527,7 @@ export async function chartTelemetry(
521
527
  })
522
528
  .from(telemetryEvents)
523
529
  .where(scoped(tenantId, since))
524
- .groupBy(bucketExpression)
525
- .orderBy(bucketExpression);
530
+ .groupBy(bucketExpression);
526
531
  return rows.map((row) => ({
527
532
  at: numeric(row.at) * 1000,
528
533
  requests: numeric(row.requests),