cartethyia 2.7.2 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-2TJD37Hb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-JEbYJFxw.js} +1 -1
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-DGnGNYnM.js → ConsoleLogPage-C3xx_inM.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-C4w7vea-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BtPcAfot.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +9 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-7P2n--Fv.js → QuotaPage-Ci5O4BnH.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CxZ1fTkN.js → SettingsPage-CDZjTt87.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js +1 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-BAL5npBC.js} +1 -1
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-TP7voAQ0.js} +3 -3
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-CTkLAuRc.js → UsagePage-Cj_fEzhg.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-CUi6UXsR.js → api-keys-MbI7e7rt.js} +1 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +2 -0
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/{assert-67xXH5rf.js → assert-C0YYuZJ4.js} +1 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-B_DMpMJN.js} +1 -1
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DMrMPmth.js} +1 -1
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-Be5Uzamx.js} +1 -1
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-KvsWa1Jl.js → cli-tools-muFRphDu.js} +1 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DOfrVmsi.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-C9omdjQt.js} +1 -1
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js +1 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/{entry-_xHo5x6r.js → entry-DS90gUks.js} +1 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/{entry-ClCMCWWQ.js → entry-j40LJotO.js} +3 -3
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-q5HwZ8nN.js} +1 -1
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-BTScrQpe.js} +1 -1
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-Orjuzemr.js} +1 -1
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/{index-DFVFrnWW.js → index-cPrX5vn6.js} +2 -2
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-Dvux8w-0.js} +1 -1
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-CU1Rgf3p.js} +1 -1
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-C_7A6H55.js} +1 -1
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-CzFvogbj.js} +1 -1
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/{live-D0mm--ME.js → live-KaG15HLQ.js} +1 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-JJGQm8TD.js} +1 -1
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-BNDZnsUE.js} +1 -1
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-CLaNz8Xz.js} +1 -1
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BnIpi1IJ.js → routing-BEH32QOC.js} +1 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/{select-DBNVLdUu.js → select-B3iTetHN.js} +1 -1
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/{settings-BAFhDgX8.js → settings-Dt3Eqbnq.js} +1 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-KrOrm7JZ.js} +1 -1
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-DLS7IDMy.js} +1 -1
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-42ItvbAs.js} +1 -1
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-Dmnfh1Ue.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +117 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/package.json +1 -1
- package/src/config/network.ts +1 -1
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +423 -410
- package/src/console/api-keys/routes.ts +118 -19
- package/src/console/backup/export-endpoint.ts +8 -11
- package/src/console/backup/routes.ts +40 -8
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/domain-registration.ts +2 -0
- package/src/console/quota/account-quota-tenant.ts +8 -1
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/share/share-router.ts +39 -6
- package/src/console/share/share-stats.ts +64 -25
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/main.ts +11 -0
- package/src/observability/event-query.ts +118 -7
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/schema.ts +102 -0
- package/src/persistence/stores/api-key-store.ts +753 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +57 -1
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/providers/integrations/antigravity/antigravity.ts +20 -23
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-media.ts +5 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -1
- package/src/providers/integrations/cline/cline.ts +7 -0
- package/src/providers/integrations/github/github.ts +20 -1
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/qoder/qoder-checkin.ts +224 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +156 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
- package/src/providers/integrations/qoder/qoder-umid.ts +288 -0
- package/src/providers/integrations/qoder/qoder.ts +5 -3
- package/src/providers/operations/account-error-classification.ts +6 -8
- package/src/providers/operations/account-health-service.ts +14 -16
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/provider-contracts.ts +8 -0
- package/src/security/access-control.ts +19 -0
- package/src/security/api-key-auth.ts +14 -1
- package/src/transport/dispatch/proxy-request.ts +31 -2
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/request/preparer.ts +17 -1
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +7 -0
- package/src/transport/routing/route-model.ts +53 -0
- package/src/transport/routing/router.ts +306 -256
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-wvwCi4Eh.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-wvwCi4Eh.js.gz} +0 -0
|
@@ -155,7 +155,18 @@ export type SharedApiKeyIssueResult =
|
|
|
155
155
|
readonly keyPrefix: string;
|
|
156
156
|
readonly createdAt: Date;
|
|
157
157
|
}
|
|
158
|
-
| { readonly kind: "link_unavailable" }
|
|
158
|
+
| { readonly kind: "link_unavailable" }
|
|
159
|
+
| {
|
|
160
|
+
readonly kind: "already_issued";
|
|
161
|
+
readonly parentKeyId: string;
|
|
162
|
+
readonly tenantId: string;
|
|
163
|
+
}
|
|
164
|
+
| {
|
|
165
|
+
readonly kind: "slot_full";
|
|
166
|
+
readonly parentKeyId: string;
|
|
167
|
+
readonly tenantId: string;
|
|
168
|
+
readonly limit: number;
|
|
169
|
+
};
|
|
159
170
|
|
|
160
171
|
type ApiKeyRow = typeof apiKeys.$inferSelect;
|
|
161
172
|
type ShareLinkRow = typeof shareLinks.$inferSelect;
|
|
@@ -485,6 +496,53 @@ export class DrizzleShareLinkStore implements ShareLinkStore {
|
|
|
485
496
|
);
|
|
486
497
|
if (!activeLink) return { kind: "link_unavailable" };
|
|
487
498
|
|
|
499
|
+
// One child key per canonical client IP. The public route mints from an
|
|
500
|
+
// enrollment link, and a bot replaying the same URL from one IP was
|
|
501
|
+
// creating an unbounded number of children (observed: 331k `api_keys`
|
|
502
|
+
// rows from two Google Cloud IPs in three days). If this (parent, IP)
|
|
503
|
+
// pair already has a live child, that is the same enrollment — return it
|
|
504
|
+
// as `already_issued` instead of minting another.
|
|
505
|
+
if (material.clientIpKey.length > 0) {
|
|
506
|
+
const existing = await tx
|
|
507
|
+
.select({ id: apiKeys.id })
|
|
508
|
+
.from(apiKeys)
|
|
509
|
+
.where(
|
|
510
|
+
and(
|
|
511
|
+
eq(apiKeys.parentKeyId, parent.id),
|
|
512
|
+
eq(apiKeys.issuedClientIpKey, material.clientIpKey),
|
|
513
|
+
isNull(apiKeys.revokedAt),
|
|
514
|
+
),
|
|
515
|
+
)
|
|
516
|
+
.limit(1);
|
|
517
|
+
if (existing[0] !== undefined) {
|
|
518
|
+
return {
|
|
519
|
+
kind: "already_issued",
|
|
520
|
+
parentKeyId: parent.id,
|
|
521
|
+
tenantId: parent.tenantId,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Slot cap: a template may bound how many concurrently-active children
|
|
527
|
+
// exist. The public link is mint-able by anyone, so without this cap a
|
|
528
|
+
// bot rotating egress IPs (CDN edges, cloud egress, proxies) could mint
|
|
529
|
+
// without bound — observed: 331k children in three days. The count is
|
|
530
|
+
// live children only; revoked children free their slot for reuse.
|
|
531
|
+
if (parent.maxChildren !== null && parent.maxChildren !== undefined) {
|
|
532
|
+
const liveCount = await tx
|
|
533
|
+
.select({ value: sql<number>`count(*) filter (where ${apiKeys.revokedAt} is null)` })
|
|
534
|
+
.from(apiKeys)
|
|
535
|
+
.where(eq(apiKeys.parentKeyId, parent.id));
|
|
536
|
+
if (Number(liveCount[0]?.value ?? 0) >= parent.maxChildren) {
|
|
537
|
+
return {
|
|
538
|
+
kind: "slot_full",
|
|
539
|
+
parentKeyId: parent.id,
|
|
540
|
+
tenantId: parent.tenantId,
|
|
541
|
+
limit: parent.maxChildren,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
488
546
|
// The label is `hint + random`, capped at 12 chars — never the
|
|
489
547
|
// parent's full label plus a suffix, which grew past every table
|
|
490
548
|
// column it rendered in. Built here (not imported from the console
|
|
@@ -493,6 +551,12 @@ export class DrizzleShareLinkStore implements ShareLinkStore {
|
|
|
493
551
|
const suffix = randomInt(1000, 10_000).toString();
|
|
494
552
|
const label =
|
|
495
553
|
hint.length === 0 ? `key-${suffix}` : `${hint}-${suffix}`.slice(0, 12);
|
|
554
|
+
// Child rows carry identity + scopes only. Auth resolves every policy
|
|
555
|
+
// field from the parent (`policy = parent ?? row`), so copying notes,
|
|
556
|
+
// limits, model_list, denylist, etc. was pure write-amplification.
|
|
557
|
+
// Nullable policy columns stay NULL; `model_access_mode` takes its
|
|
558
|
+
// NOT NULL default ('whitelist'). Keep in sync with migration 0029 and
|
|
559
|
+
// `normalizeShareChildPolicy` in `src/console/backup/store.ts`.
|
|
496
560
|
const [child] = await tx
|
|
497
561
|
.insert(apiKeys)
|
|
498
562
|
.values({
|
|
@@ -506,18 +570,6 @@ export class DrizzleShareLinkStore implements ShareLinkStore {
|
|
|
506
570
|
scopes: parent.scopes,
|
|
507
571
|
keyPrefix: material.keyPrefix,
|
|
508
572
|
keyEncrypted: null,
|
|
509
|
-
notesTitle: parent.notesTitle,
|
|
510
|
-
notesSubtitle: parent.notesSubtitle,
|
|
511
|
-
notesBody: parent.notesBody,
|
|
512
|
-
requestsPerMinute: parent.requestsPerMinute,
|
|
513
|
-
dailyTokenLimit: parent.dailyTokenLimit,
|
|
514
|
-
monthlyTokenLimit: parent.monthlyTokenLimit,
|
|
515
|
-
lifetimeTokenBudget: parent.lifetimeTokenBudget,
|
|
516
|
-
maxConcurrentRequests: parent.maxConcurrentRequests,
|
|
517
|
-
modelPrefix: parent.modelPrefix,
|
|
518
|
-
modelAccessMode: parent.modelAccessMode,
|
|
519
|
-
modelList: parent.modelList,
|
|
520
|
-
clientRouterDenylist: parent.clientRouterDenylist,
|
|
521
573
|
lifetimeTokensConsumed: 0,
|
|
522
574
|
})
|
|
523
575
|
.returning({ id: apiKeys.id, tenantId: apiKeys.tenantId, createdAt: apiKeys.createdAt });
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readdir, rm } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { sql } from "drizzle-orm";
|
|
3
|
+
import { inArray, sql } from "drizzle-orm";
|
|
4
4
|
import { affectedRows, currentTimestamp, laterOf } from "../dialect";
|
|
5
5
|
import type { CartethyiaDatabase } from "../db-handle";
|
|
6
6
|
import {
|
|
7
|
+
apiKeys,
|
|
7
8
|
telemetryEvents,
|
|
8
9
|
telemetryPayloads,
|
|
9
10
|
telemetryUsageTotals,
|
|
@@ -117,6 +118,31 @@ export class DrizzleTelemetryStore {
|
|
|
117
118
|
this.eventWriter = eventWriter;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Resolves the share-template key for each distinct api key in a batch.
|
|
123
|
+
*
|
|
124
|
+
* The template is the key itself when it is a top-level template, or its
|
|
125
|
+
* parent when it is a minted child. The column is denormalized onto the
|
|
126
|
+
* event so family rollups do not depend on the key row surviving.
|
|
127
|
+
*/
|
|
128
|
+
private async resolveFamilyTemplateIds(
|
|
129
|
+
rows: readonly (typeof telemetryEvents.$inferInsert)[],
|
|
130
|
+
): Promise<Map<string, string>> {
|
|
131
|
+
const ids = [
|
|
132
|
+
...new Set(
|
|
133
|
+
rows
|
|
134
|
+
.map((row) => row.apiKeyId)
|
|
135
|
+
.filter((id): id is string => typeof id === "string" && id.length > 0),
|
|
136
|
+
),
|
|
137
|
+
];
|
|
138
|
+
if (ids.length === 0) return new Map();
|
|
139
|
+
const resolved = await this.db
|
|
140
|
+
.select({ id: apiKeys.id, parentKeyId: apiKeys.parentKeyId })
|
|
141
|
+
.from(apiKeys)
|
|
142
|
+
.where(inArray(apiKeys.id, ids));
|
|
143
|
+
return new Map(resolved.map((row) => [row.id, row.parentKeyId ?? row.id]));
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
/**
|
|
121
147
|
* Applies usage deltas inside an enclosing transaction.
|
|
122
148
|
*
|
|
@@ -137,11 +163,34 @@ export class DrizzleTelemetryStore {
|
|
|
137
163
|
left.tenantId.localeCompare(right.tenantId)
|
|
138
164
|
|| left.identityType.localeCompare(right.identityType)
|
|
139
165
|
|| left.entityId.localeCompare(right.entityId));
|
|
166
|
+
// Resolve the share-template key for each api_key identity in the batch so
|
|
167
|
+
// the durable totals row carries it — family reads then survive the key
|
|
168
|
+
// row being deleted.
|
|
169
|
+
const apiKeyIds = [
|
|
170
|
+
...new Set(
|
|
171
|
+
ordered
|
|
172
|
+
.filter((delta) => delta.identityType === "api_key")
|
|
173
|
+
.map((delta) => delta.entityId),
|
|
174
|
+
),
|
|
175
|
+
];
|
|
176
|
+
const familyTemplates = new Map<string, string>();
|
|
177
|
+
if (apiKeyIds.length > 0) {
|
|
178
|
+
const keyRows = await tx
|
|
179
|
+
.select({ id: apiKeys.id, parentKeyId: apiKeys.parentKeyId })
|
|
180
|
+
.from(apiKeys)
|
|
181
|
+
.where(inArray(apiKeys.id, apiKeyIds));
|
|
182
|
+
for (const row of keyRows) {
|
|
183
|
+
familyTemplates.set(row.id, row.parentKeyId ?? row.id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
140
186
|
const now = new Date();
|
|
141
187
|
await tx
|
|
142
188
|
.insert(telemetryUsageTotals)
|
|
143
189
|
.values(ordered.map((delta) => ({
|
|
144
190
|
...delta,
|
|
191
|
+
...(delta.identityType === "api_key" && familyTemplates.has(delta.entityId)
|
|
192
|
+
? { familyTemplateKeyId: familyTemplates.get(delta.entityId) }
|
|
193
|
+
: {}),
|
|
145
194
|
requests: delta.requests,
|
|
146
195
|
errors: delta.errors,
|
|
147
196
|
inputTokens: delta.inputTokens,
|
|
@@ -171,6 +220,13 @@ export class DrizzleTelemetryStore {
|
|
|
171
220
|
// The file accepts anything, so a malformed row would be written and then
|
|
172
221
|
// retried forever without ever failing. Rejecting it here keeps the buffer's
|
|
173
222
|
// drop-and-count contract: one bad row fails the batch, once.
|
|
223
|
+
const familyTemplates = await this.resolveFamilyTemplateIds(rows);
|
|
224
|
+
for (const row of rows) {
|
|
225
|
+
if (typeof row.apiKeyId === "string" && row.apiKeyId.length > 0) {
|
|
226
|
+
const template = familyTemplates.get(row.apiKeyId);
|
|
227
|
+
if (template !== undefined) row.familyTemplateKeyId = template;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
174
230
|
const unique: Array<typeof telemetryEvents.$inferInsert> = [];
|
|
175
231
|
const ids = new Set<string>();
|
|
176
232
|
for (const row of rows) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Persistence for tenant system-prompt injection settings.
|
|
2
|
+
//
|
|
3
|
+
// One row per tenant. Mutations bump the settings revision so the dispatch
|
|
4
|
+
// path's revision-keyed reader converges immediately (same mechanism as the
|
|
5
|
+
// runtime-preferences cache).
|
|
6
|
+
import { eq } from "drizzle-orm";
|
|
7
|
+
import type { CartethyiaDatabase } from "./db-handle";
|
|
8
|
+
import { tenantSystemPrompts } from "./schema";
|
|
9
|
+
import type {
|
|
10
|
+
ProviderPromptEntry,
|
|
11
|
+
PromptSetting,
|
|
12
|
+
TenantSystemPrompts,
|
|
13
|
+
} from "../console/system-prompt/contracts";
|
|
14
|
+
import {
|
|
15
|
+
EMPTY_TENANT_SYSTEM_PROMPTS,
|
|
16
|
+
SYSTEM_PROMPT_MODEL_LIMIT,
|
|
17
|
+
SYSTEM_PROMPT_PROVIDER_LIMIT,
|
|
18
|
+
isPromptSetting,
|
|
19
|
+
parseSystemPromptModelKey,
|
|
20
|
+
} from "../console/system-prompt/contracts";
|
|
21
|
+
import { bumpSettingsRevision } from "./settings-revision";
|
|
22
|
+
|
|
23
|
+
/** The stored row, normalized. Absent row reads as the empty configuration. */
|
|
24
|
+
export type StoredTenantSystemPrompts = TenantSystemPrompts;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads the tenant's stored configuration, defensively normalized: a
|
|
28
|
+
* hand-edited or legacy row must not poison the dispatch path, so malformed
|
|
29
|
+
* entries are dropped rather than trusted.
|
|
30
|
+
*/
|
|
31
|
+
export async function readTenantSystemPrompts(
|
|
32
|
+
db: CartethyiaDatabase,
|
|
33
|
+
tenantId: string,
|
|
34
|
+
): Promise<StoredTenantSystemPrompts> {
|
|
35
|
+
const rows = await db
|
|
36
|
+
.select({
|
|
37
|
+
global: tenantSystemPrompts.global,
|
|
38
|
+
providers: tenantSystemPrompts.providers,
|
|
39
|
+
models: tenantSystemPrompts.models,
|
|
40
|
+
})
|
|
41
|
+
.from(tenantSystemPrompts)
|
|
42
|
+
.where(eq(tenantSystemPrompts.tenantId, tenantId))
|
|
43
|
+
.limit(1);
|
|
44
|
+
const row = rows[0];
|
|
45
|
+
if (row === undefined) return EMPTY_TENANT_SYSTEM_PROMPTS;
|
|
46
|
+
|
|
47
|
+
const providers: Record<string, ProviderPromptEntry> = {};
|
|
48
|
+
for (const [providerId, entry] of Object.entries(row.providers ?? {})) {
|
|
49
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
50
|
+
const candidate = entry as Partial<ProviderPromptEntry>;
|
|
51
|
+
if (typeof candidate.followGlobal !== "boolean") continue;
|
|
52
|
+
if (candidate.followGlobal === false) {
|
|
53
|
+
if (!isPromptSetting(candidate.setting)) continue;
|
|
54
|
+
providers[providerId] = { followGlobal: false, setting: candidate.setting };
|
|
55
|
+
} else {
|
|
56
|
+
providers[providerId] = { followGlobal: true };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const models: Record<string, PromptSetting> = {};
|
|
61
|
+
for (const [key, setting] of Object.entries(row.models ?? {})) {
|
|
62
|
+
if (parseSystemPromptModelKey(key) === null) continue;
|
|
63
|
+
if (!isPromptSetting(setting)) continue;
|
|
64
|
+
models[key] = setting;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
global: row.global !== null && isPromptSetting(row.global) ? row.global : null,
|
|
69
|
+
providers,
|
|
70
|
+
models,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Upserts the tenant's configuration and bumps the settings revision. */
|
|
75
|
+
export async function writeTenantSystemPrompts(
|
|
76
|
+
db: CartethyiaDatabase,
|
|
77
|
+
tenantId: string,
|
|
78
|
+
next: TenantSystemPrompts,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
// Keep the row bounded even though the API already enforces the caps: a
|
|
81
|
+
// direct store caller must not be able to plant an unbounded map.
|
|
82
|
+
const providers = Object.entries(next.providers).slice(0, SYSTEM_PROMPT_PROVIDER_LIMIT);
|
|
83
|
+
const models = Object.entries(next.models).slice(0, SYSTEM_PROMPT_MODEL_LIMIT);
|
|
84
|
+
const values = {
|
|
85
|
+
global: next.global,
|
|
86
|
+
providers: Object.fromEntries(providers),
|
|
87
|
+
models: Object.fromEntries(models),
|
|
88
|
+
updatedAt: new Date(),
|
|
89
|
+
};
|
|
90
|
+
await db
|
|
91
|
+
.insert(tenantSystemPrompts)
|
|
92
|
+
.values({ tenantId, ...values })
|
|
93
|
+
.onConflictDoUpdate({ target: tenantSystemPrompts.tenantId, set: values });
|
|
94
|
+
bumpSettingsRevision();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Clears the tenant's configuration entirely (console "reset" path). */
|
|
98
|
+
export async function clearTenantSystemPrompts(
|
|
99
|
+
db: CartethyiaDatabase,
|
|
100
|
+
tenantId: string,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
await db.delete(tenantSystemPrompts).where(eq(tenantSystemPrompts.tenantId, tenantId));
|
|
103
|
+
bumpSettingsRevision();
|
|
104
|
+
}
|
|
@@ -50,19 +50,83 @@ export function applyDeepSeekReasoning(payload: Record<string, unknown>): void {
|
|
|
50
50
|
payload["reasoning_effort"] = "high";
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Model families whose Chat-compatible upstream enforces the DeepSeek replay
|
|
55
|
+
* rule: every prior assistant turn must carry `reasoning_content`, or the
|
|
56
|
+
* upstream refuses the whole request with
|
|
57
|
+
* `Missing required parameter: 'messages[N].reasoning_content'`. Seen on
|
|
58
|
+
* Cline's resold `reflection/beam-free` (2026-10-30) and on the buddy
|
|
59
|
+
* DeepSeek family. Cline, OpenRouter, Hermes, and Kilo share one chat wire,
|
|
60
|
+
* so the rule is stated once, keyed on the model family.
|
|
61
|
+
*/
|
|
62
|
+
const REASONING_REPLAY_MODEL_RE = /(?:deepseek|reflection\/beam)/i;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensures every assistant turn carries `reasoning_content` for model families
|
|
66
|
+
* whose upstream enforces the DeepSeek replay rule. The rule demands the field
|
|
67
|
+
* on *every* earlier assistant message — presence, not content — and the
|
|
68
|
+
* upstream rejects an absent field with
|
|
69
|
+
* `Missing required parameter: 'messages[N].reasoning_content'` even when the
|
|
70
|
+
* turn genuinely has no trace to replay. Clients that already learned the rule
|
|
71
|
+
* send `reasoning_content: ""` for exactly those turns (see the chat surface
|
|
72
|
+
* parser), so an empty string is the honest value for a turn with nothing to
|
|
73
|
+
* replay. Existing values are never overwritten.
|
|
74
|
+
*
|
|
75
|
+
* Unlike `backfillDeepSeekReasoningContent`, this runs only for the
|
|
76
|
+
* OpenAI-compatible resellers (Cline/OpenRouter/Hermes/Kilo), not the buddy
|
|
77
|
+
* family, whose upstream rejects empty reasoning as "stripped" and drops it.
|
|
78
|
+
*/
|
|
79
|
+
export function ensureChatReasoningReplayPresence(payload: Record<string, unknown>): void {
|
|
80
|
+
const model = typeof payload["model"] === "string" ? payload["model"] : "";
|
|
81
|
+
if (!REASONING_REPLAY_MODEL_RE.test(model)) return;
|
|
82
|
+
const messages = payload["messages"];
|
|
83
|
+
if (!Array.isArray(messages)) return;
|
|
84
|
+
const thinking = payload["thinking"];
|
|
85
|
+
const thinkingEnabled = isRecord(thinking) && thinking["type"] === "enabled";
|
|
86
|
+
const hasTrace = messages.some(
|
|
87
|
+
(message) =>
|
|
88
|
+
isRecord(message) &&
|
|
89
|
+
((typeof message["reasoning"] === "string" && message["reasoning"].length > 0) ||
|
|
90
|
+
"reasoning_content" in message),
|
|
91
|
+
);
|
|
92
|
+
// Same gate as the backfill: a request that never entered thinking mode and
|
|
93
|
+
// carries no reasoning anywhere has nothing the model could demand, so do
|
|
94
|
+
// not start marking its turns as thinking turns.
|
|
95
|
+
if (!thinkingEnabled && !hasTrace) return;
|
|
96
|
+
for (const message of messages) {
|
|
97
|
+
if (!isRecord(message) || message["role"] !== "assistant") continue;
|
|
98
|
+
if (typeof message["reasoning_content"] === "string") continue;
|
|
99
|
+
message["reasoning_content"] = "";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
53
103
|
/** Backfills DeepSeek assistant reasoning_content required for replayed turns. */
|
|
54
104
|
export function backfillDeepSeekReasoningContent(payload: Record<string, unknown>): void {
|
|
105
|
+
const model = typeof payload["model"] === "string" ? payload["model"] : "";
|
|
55
106
|
const thinking = payload["thinking"];
|
|
56
107
|
const thinkingEnabled = isRecord(thinking) && thinking["type"] === "enabled";
|
|
57
108
|
const messages = payload["messages"];
|
|
58
109
|
if (!Array.isArray(messages)) return;
|
|
110
|
+
const requiresReplay = REASONING_REPLAY_MODEL_RE.test(model);
|
|
59
111
|
const hasTrace = messages.some(
|
|
60
112
|
(message) =>
|
|
61
113
|
isRecord(message) &&
|
|
62
114
|
((typeof message["reasoning"] === "string" && message["reasoning"].length > 0) ||
|
|
63
115
|
"reasoning_content" in message),
|
|
64
116
|
);
|
|
65
|
-
if (!thinkingEnabled && !hasTrace) return;
|
|
117
|
+
if (!thinkingEnabled && !requiresReplay && !hasTrace) return;
|
|
118
|
+
// As an *input* (a caller replays a prior turn), the model's own `reasoning`
|
|
119
|
+
// key is the canonical record of a thinking turn. Without it there is no
|
|
120
|
+
// trace to replay, so an assistant turn stays bare even under the hard rule;
|
|
121
|
+
// fabricating `""` would be read by the upstream as "thinking mode with the
|
|
122
|
+
// reasoning stripped" and reject the request anyway.
|
|
123
|
+
const hasModelReasoning = messages.some(
|
|
124
|
+
(message) =>
|
|
125
|
+
isRecord(message) &&
|
|
126
|
+
typeof message["reasoning"] === "string" &&
|
|
127
|
+
message["reasoning"].length > 0,
|
|
128
|
+
);
|
|
129
|
+
if (requiresReplay && !thinkingEnabled && !hasModelReasoning && !hasTrace) return;
|
|
66
130
|
for (const message of messages) {
|
|
67
131
|
if (!isRecord(message) || message["role"] !== "assistant") continue;
|
|
68
132
|
if (typeof message["reasoning_content"] === "string") continue;
|
|
@@ -549,6 +549,7 @@ export function canonicalToChatPayload(
|
|
|
549
549
|
...(options.maxOutputTokens === undefined ? {} : { maxOutputTokens: options.maxOutputTokens }),
|
|
550
550
|
});
|
|
551
551
|
delete payload["max_tokens"];
|
|
552
|
+
delete payload["max_completion_tokens"];
|
|
552
553
|
if (bounded !== undefined) payload[tokenLimitField] = bounded;
|
|
553
554
|
return payload;
|
|
554
555
|
}
|
|
@@ -84,34 +84,31 @@ const antigravityModel = (
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
export const ANTIGRAVITY_MODELS: readonly ModelDefinition[] = [
|
|
87
|
-
// Claude via Antigravity (Anthropic-branded SKUs, Gemini wire)
|
|
87
|
+
// Claude via Antigravity (Anthropic-branded SKUs, Gemini wire). The 4.6
|
|
88
|
+
// rows are pinned at 64000 because Antigravity was observed rejecting
|
|
89
|
+
// anything higher on `daily-cloudcode-pa`; `out` is published metadata, not
|
|
90
|
+
// a request field (the envelope's `maxOutputTokens` comes from
|
|
91
|
+
// `ANTIGRAVITY_MODEL_WIRE_PROFILES`).
|
|
88
92
|
antigravityModel("claude-sonnet-4-6", 250_000, 64_000, true),
|
|
89
93
|
antigravityModel("claude-opus-4-6", 250_000, 64_000, true),
|
|
90
|
-
// The 5.5 generation. Anthropic's own model pages publish 1M context and
|
|
91
|
-
// 128k output for both, which is the same figure the Claude integration
|
|
92
|
-
// carries; Antigravity has no base-catalog rows of its own, so the numbers
|
|
93
|
-
// are stated here rather than resolved from a source that does not exist.
|
|
94
|
-
//
|
|
95
|
-
// `out` is published metadata, not a request field: the `maxOutputTokens` the
|
|
96
|
-
// envelope carries comes from `ANTIGRAVITY_MODEL_WIRE_PROFILES`, and an id
|
|
97
|
-
// absent from that table passes the caller's own value through. The 4.6 rows
|
|
98
|
-
// are pinned at 64000 because Antigravity was observed rejecting anything
|
|
99
|
-
// higher on `daily-cloudcode-pa`. The 5.5 cap is UNVERIFIED — no 5.5 request
|
|
100
|
-
// has been captured against that host — so if it turns out to carry the same
|
|
101
|
-
// 64000 ceiling, it needs its own profile entry; a catalog row cannot express
|
|
102
|
-
// it.
|
|
103
94
|
antigravityModel("claude-sonnet-5-5", 1_000_000, 128_000, true),
|
|
104
95
|
antigravityModel("claude-opus-5-5", 1_000_000, 128_000, true),
|
|
105
|
-
// Gemini
|
|
106
|
-
antigravityModel("gemini-2.5-flash",
|
|
107
|
-
antigravityModel("gemini-
|
|
96
|
+
// Gemini text SKUs, as the Antigravity roster publishes them.
|
|
97
|
+
antigravityModel("gemini-2.5-flash", 1_000_000, 66_000, true),
|
|
98
|
+
antigravityModel("gemini-2.5-flash-thinking", 1_000_000, 66_000, true),
|
|
99
|
+
antigravityModel("gemini-3-flash", 1_000_000, 66_000, true),
|
|
100
|
+
antigravityModel("gemini-3.1-flash-lite", 1_000_000, 66_000, true),
|
|
101
|
+
antigravityModel("gemini-2.5-flash-lite", 1_000_000, 66_000, true),
|
|
102
|
+
antigravityModel("gemini-3.5-flash-lite", 1_000_000, 66_000, true),
|
|
103
|
+
antigravityModel("gemini-3.8-flash", 1_000_000, 66_000, true),
|
|
104
|
+
antigravityModel("gemini-4-argon", 1_000_000, 65_536, true),
|
|
105
|
+
antigravityModel("gemini-4-carbon", 1_000_000, 65_536, true),
|
|
106
|
+
// Tab (laptop-class) preview SKU.
|
|
107
|
+
antigravityModel("tab_flash_lite_preview", 16_000, 4_000, true),
|
|
108
|
+
// Gemini image generation. `gemini-3-pro-image` uses the same catalog
|
|
109
|
+
// limits/capabilities as the existing `gemini-3.1-flash-image` row.
|
|
108
110
|
antigravityModel("gemini-3.1-flash-image", 200_000, 64_000, false, true),
|
|
109
|
-
antigravityModel("gemini-3
|
|
110
|
-
antigravityModel("gemini-3.6-flash", 1_048_576, 65_536, true),
|
|
111
|
-
antigravityModel("gemini-3.7-flash", 1_048_576, 65_536, true),
|
|
112
|
-
antigravityModel("gemini-3.8-flash", 1_048_576, 65_536, true),
|
|
113
|
-
// GPT-OSS
|
|
114
|
-
antigravityModel("gpt-oss-120b", 131_072, 32_768, true),
|
|
111
|
+
antigravityModel("gemini-3-pro-image", 200_000, 64_000, false, true),
|
|
115
112
|
];
|
|
116
113
|
|
|
117
114
|
function normalizeAntigravityGrounding(data: unknown, maxResults: number): readonly WebSearchResult[] {
|
|
@@ -21,7 +21,7 @@ import { createHash } from "node:crypto";
|
|
|
21
21
|
import { completeRequiredSchema } from "../../../protocol/codecs";
|
|
22
22
|
import type { CanonicalRequest } from "../../../protocol/canonical-model";
|
|
23
23
|
import { isRecord } from "../../../protocol/codecs";
|
|
24
|
-
import { applyDeepSeekReasoning, applyOpenAIReasoning, backfillDeepSeekReasoningContent, clampedEffortOutputFloor } from "../../../protocol/reasoning";
|
|
24
|
+
import { applyDeepSeekReasoning, applyOpenAIReasoning, backfillDeepSeekReasoningContent, clampedEffortOutputFloor, ensureChatReasoningReplayPresence } from "../../../protocol/reasoning";
|
|
25
25
|
import { BUDDY_CN_EFFORTS, BUDDY_INTL_EFFORTS, BUDDY_SHARED_RAW } from "./buddy-catalog-shared";
|
|
26
26
|
// Imported for the CN roster only: a CN-only id has no row in the shared
|
|
27
27
|
// table, and without one `raiseOutputForRaisedEffort` silently no-ops, so a
|
|
@@ -30,6 +30,7 @@ import { BUDDY_CN_EFFORTS, BUDDY_INTL_EFFORTS, BUDDY_SHARED_RAW } from "./buddy-
|
|
|
30
30
|
// cn-provider (which imports this module) resolves before any call.
|
|
31
31
|
import { CODEBUDDY_CN_RAW } from "./codebuddy/cn-catalog";
|
|
32
32
|
import { buddyAccountUid } from "./buddy-oauth-shared";
|
|
33
|
+
import { SYSTEM_PROMPT_WRAPPER_PREFIX, SYSTEM_PROMPT_WRAPPER_SUFFIX } from "../../../console/system-prompt/contracts";
|
|
33
34
|
import type { ProviderDispatchContext } from "../../provider-contracts";
|
|
34
35
|
|
|
35
36
|
const BUDDY_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
@@ -409,6 +410,21 @@ export function scrubBuddyChannelIdentity(messages: Array<Record<string, unknown
|
|
|
409
410
|
}
|
|
410
411
|
}
|
|
411
412
|
|
|
413
|
+
/**
|
|
414
|
+
* OpenAI-compatible adapters that need the DeepSeek reasoning-replay backfill
|
|
415
|
+
* on outbound chat payloads. The buddy family runs it inside
|
|
416
|
+
* `buddyPrePayloadCommon`; these adapters register here so the same rule
|
|
417
|
+
* reaches every reseller that serves a thinking model.
|
|
418
|
+
*/
|
|
419
|
+
export const clineReasoningReplayPrepares: readonly ((
|
|
420
|
+
payload: Record<string, unknown>,
|
|
421
|
+
) => void)[] = [
|
|
422
|
+
(payload) => {
|
|
423
|
+
backfillDeepSeekReasoningContent(payload);
|
|
424
|
+
ensureChatReasoningReplayPresence(payload);
|
|
425
|
+
},
|
|
426
|
+
];
|
|
427
|
+
|
|
412
428
|
/**
|
|
413
429
|
* Payload prefix shared by the buddy pre-payload hooks: force `stream=true`,
|
|
414
430
|
* apply the provider's DeepSeek thinking contract, opt `reasoning_summary` in
|
|
@@ -1152,12 +1168,24 @@ export function finalizeBuddyMessages(
|
|
|
1152
1168
|
export function applyBuddySystemPrompt(
|
|
1153
1169
|
messages: Array<Record<string, unknown>>,
|
|
1154
1170
|
systemPrompt: string,
|
|
1171
|
+
extraSystemText?: string,
|
|
1155
1172
|
): void {
|
|
1173
|
+
// Tenant injection (Inject System Prompt) rides AFTER the fixed prompt,
|
|
1174
|
+
// inside the same system turn, framed by the gateway's marker prefix/suffix:
|
|
1175
|
+
// the upstream validates the identity prompt as the channel opener, so the
|
|
1176
|
+
// fixed text must stay first and byte-identical; the wrapped tail carries
|
|
1177
|
+
// the tenant's directive without touching the identity. An override-mode
|
|
1178
|
+
// tenant prompt degrades to this wrapped append (the caller decides via
|
|
1179
|
+
// `isWrappedInjectionProvider` before passing the text).
|
|
1180
|
+
const effectivePrompt =
|
|
1181
|
+
extraSystemText === undefined || extraSystemText.length === 0
|
|
1182
|
+
? systemPrompt
|
|
1183
|
+
: `${systemPrompt}\n\n${SYSTEM_PROMPT_WRAPPER_PREFIX}\n${extraSystemText}\n${SYSTEM_PROMPT_WRAPPER_SUFFIX}`;
|
|
1156
1184
|
const source = messages.filter(
|
|
1157
1185
|
(message) => message["role"] !== "system" && message["role"] !== "developer",
|
|
1158
1186
|
);
|
|
1159
1187
|
messages.length = 0;
|
|
1160
|
-
messages.push({ role: "system", content:
|
|
1188
|
+
messages.push({ role: "system", content: effectivePrompt });
|
|
1161
1189
|
for (const message of source) {
|
|
1162
1190
|
if (message["role"] === "user" && typeof message["content"] === "string") {
|
|
1163
1191
|
messages.push({
|
|
@@ -173,7 +173,11 @@ export async function buddyGenerateImage(
|
|
|
173
173
|
buddyPrePayloadCommon(payload, undefined, "intl", context);
|
|
174
174
|
const messages = payload["messages"];
|
|
175
175
|
if (Array.isArray(messages)) {
|
|
176
|
-
applyBuddySystemPrompt(
|
|
176
|
+
applyBuddySystemPrompt(
|
|
177
|
+
messages as Array<Record<string, unknown>>,
|
|
178
|
+
config.systemPrompt,
|
|
179
|
+
context?.tenant_system_prompt,
|
|
180
|
+
);
|
|
177
181
|
}
|
|
178
182
|
const headers: Record<string, string> = {
|
|
179
183
|
"content-type": "application/json",
|
|
@@ -84,7 +84,11 @@ function codeBuddyIntlPrePayload(
|
|
|
84
84
|
if (!source) throw new GatewayError("invalid_request", 400, "CodeBuddy model is required");
|
|
85
85
|
const messages = payload["messages"];
|
|
86
86
|
if (Array.isArray(messages)) {
|
|
87
|
-
applyBuddySystemPrompt(
|
|
87
|
+
applyBuddySystemPrompt(
|
|
88
|
+
messages as Array<Record<string, unknown>>,
|
|
89
|
+
CODEBUDDY_SYSTEM_PROMPT,
|
|
90
|
+
context?.tenant_system_prompt,
|
|
91
|
+
);
|
|
88
92
|
}
|
|
89
93
|
normalizeBuddyToolNames(payload);
|
|
90
94
|
injectBuddyParallelToolCalls(payload, request);
|
|
@@ -138,7 +138,11 @@ export function workbuddyPrePayload(
|
|
|
138
138
|
const messages = payload["messages"];
|
|
139
139
|
if (Array.isArray(messages)) {
|
|
140
140
|
const normalizedMessages = messages as Array<Record<string, unknown>>;
|
|
141
|
-
applyBuddySystemPrompt(
|
|
141
|
+
applyBuddySystemPrompt(
|
|
142
|
+
normalizedMessages,
|
|
143
|
+
WORKBUDDY_SYSTEM_PROMPT,
|
|
144
|
+
context?.tenant_system_prompt,
|
|
145
|
+
);
|
|
142
146
|
restoreHistoricalToolDefinitions(payload, normalizedMessages);
|
|
143
147
|
}
|
|
144
148
|
normalizeBuddyToolNames(payload);
|
|
@@ -8,6 +8,7 @@ import { completeRequiredSchema, isRecord } from "../../../protocol/codecs";
|
|
|
8
8
|
import { providerBaseUrl } from "../../provider-metadata";
|
|
9
9
|
import { getCachedModelDiscovery } from "../../operations/model-discovery-cache";
|
|
10
10
|
import { defineModel } from "../../provider-model-definition";
|
|
11
|
+
import { clineReasoningReplayPrepares } from "../buddy/buddy-chat-shared";
|
|
11
12
|
import { clinePlatform, clinePlatformVersion } from "../../operations/cli-platform";
|
|
12
13
|
import {
|
|
13
14
|
getClineCliVersion,
|
|
@@ -133,6 +134,12 @@ function clinePrePayload(
|
|
|
133
134
|
? messages
|
|
134
135
|
: [{ role: "system", content: "You are a helpful assistant." }, ...messages];
|
|
135
136
|
|
|
137
|
+
// 1.5) DeepSeek-replay backfill: Cline resells thinking models (deepseek,
|
|
138
|
+
// reflection/beam) whose upstream rejects the whole history when any prior
|
|
139
|
+
// assistant turn lacks `reasoning_content`. Same wire as OpenRouter/Hermes/
|
|
140
|
+
// Kilo, so the shared helper runs here too — it only mirrors a real trace.
|
|
141
|
+
for (const prepare of clineReasoningReplayPrepares) prepare(payload);
|
|
142
|
+
|
|
136
143
|
// 2) Materialize deepseek-v4-flash non-stream quirk
|
|
137
144
|
const model = typeof payload["model"] === "string" ? (payload["model"] as string) : candidate.model_id;
|
|
138
145
|
const needsMaterialize = !request.stream && model.includes("deepseek-v4-flash");
|
|
@@ -288,6 +288,23 @@ interface GithubAutoDecision {
|
|
|
288
288
|
readonly sessionToken: string;
|
|
289
289
|
readonly expiresAt: number;
|
|
290
290
|
readonly hasImage: boolean;
|
|
291
|
+
readonly route?: GithubModelRoute;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function routeFromAutoModel(value: unknown): GithubModelRoute | undefined {
|
|
295
|
+
if (!isRecord(value) || !Array.isArray(value["supported_endpoints"])) return undefined;
|
|
296
|
+
const endpoints = new Set(
|
|
297
|
+
value["supported_endpoints"]
|
|
298
|
+
.filter((endpoint): endpoint is string => typeof endpoint === "string")
|
|
299
|
+
.map((endpoint) => endpoint.replace(/^\/v1(?=\/)/, "")),
|
|
300
|
+
);
|
|
301
|
+
if (endpoints.has("/chat/completions")) {
|
|
302
|
+
return { wireFamily: "chat", endpointPath: GITHUB_ENDPOINT_PATH };
|
|
303
|
+
}
|
|
304
|
+
if (endpoints.has("/responses")) {
|
|
305
|
+
return { wireFamily: "responses", endpointPath: GITHUB_RESPONSES_ENDPOINT_PATH };
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
291
308
|
}
|
|
292
309
|
|
|
293
310
|
interface CopilotModelOptions {
|
|
@@ -420,7 +437,7 @@ class GithubAdapter extends OpenAICompatibleAdapter {
|
|
|
420
437
|
if (cacheKey !== undefined) this.#cacheAutoDecision(cacheKey, autoDecision);
|
|
421
438
|
}
|
|
422
439
|
const selectedModelId = autoDecision.modelId;
|
|
423
|
-
let route = await githubModelRoute(selectedModelId, credential);
|
|
440
|
+
let route = autoDecision.route ?? await githubModelRoute(selectedModelId, credential);
|
|
424
441
|
if (route === undefined) {
|
|
425
442
|
// dynamic-import: discovery module is only loaded when the first
|
|
426
443
|
// request routes to a model id not in the bundled roster; off the hot
|
|
@@ -539,11 +556,13 @@ class GithubAdapter extends OpenAICompatibleAdapter {
|
|
|
539
556
|
Number.isFinite(responsePayload.expires_at)
|
|
540
557
|
? responsePayload.expires_at
|
|
541
558
|
: 0;
|
|
559
|
+
const route = routeFromAutoModel(responsePayload.selected_model);
|
|
542
560
|
return {
|
|
543
561
|
modelId: responsePayload.selected_model.id.trim(),
|
|
544
562
|
sessionToken: responsePayload.session_token.trim(),
|
|
545
563
|
expiresAt,
|
|
546
564
|
hasImage,
|
|
565
|
+
...(route === undefined ? {} : { route }),
|
|
547
566
|
};
|
|
548
567
|
}
|
|
549
568
|
|
|
@@ -4,6 +4,7 @@ import type { ApiKeyProviderSpec } from "../configured-provider";
|
|
|
4
4
|
import { isRecord } from "../../protocol/codecs";
|
|
5
5
|
import { boundedUpstreamArray, boundedUpstreamNumber, sanitizeUpstreamLabel } from "../provider-sanitize";
|
|
6
6
|
import { getHermesVersion } from "../operations/client-versions";
|
|
7
|
+
import { clineReasoningReplayPrepares } from "./buddy/buddy-chat-shared";
|
|
7
8
|
|
|
8
9
|
const HERMES_PROVIDER_ID = "hermes" as const;
|
|
9
10
|
|
|
@@ -110,7 +111,10 @@ export const HERMES_MODELS: readonly ModelDefinition[] = [
|
|
|
110
111
|
export const HERMES_SPEC: ApiKeyProviderSpec = {
|
|
111
112
|
provider_id: HERMES_PROVIDER_ID,
|
|
112
113
|
endpoint_paths_by_wire_family: {},
|
|
113
|
-
prePayload:
|
|
114
|
+
prePayload: (payload, request, candidate) => {
|
|
115
|
+
injectHermesTags(payload, request, candidate);
|
|
116
|
+
for (const prepare of clineReasoningReplayPrepares) prepare(payload);
|
|
117
|
+
},
|
|
114
118
|
// The origin sits behind Cloudflare, which answers a request carrying no
|
|
115
119
|
// `user-agent` with 403 "Attention Required!" while the same request with any
|
|
116
120
|
// non-empty UA answers 200 — measured directly against the API: an empty UA
|
|
@@ -24,6 +24,7 @@ import type { CanonicalRequest } from "../../../protocol/canonical-model";
|
|
|
24
24
|
import { OpenAICompatibleAdapter, withBearerAuthentication } from "../../adapters/compatible-adapter";
|
|
25
25
|
import { providerBaseUrl } from "../../provider-metadata";
|
|
26
26
|
import { decodeKiloCredential } from "../../../security/credential-codec";
|
|
27
|
+
import { clineReasoningReplayPrepares } from "../buddy/buddy-chat-shared";
|
|
27
28
|
|
|
28
29
|
export const KILO_PROVIDER_ID = "kilo" as const;
|
|
29
30
|
export const KILO_BASE_URL = providerBaseUrl("kilo");
|
|
@@ -117,6 +118,9 @@ class KiloAdapter extends OpenAICompatibleAdapter {
|
|
|
117
118
|
provider_id: KILO_PROVIDER_ID,
|
|
118
119
|
base_url: KILO_BASE_URL,
|
|
119
120
|
endpoint_paths_by_wire_family: { chat: KILO_ENDPOINT_PATH },
|
|
121
|
+
prePayload: (payload) => {
|
|
122
|
+
for (const prepare of clineReasoningReplayPrepares) prepare(payload);
|
|
123
|
+
},
|
|
120
124
|
...(fetchImpl ? { fetchImpl } : {}),
|
|
121
125
|
}),
|
|
122
126
|
);
|