Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] e0caa17ef9 Bump node in the docker-dependencies group across 1 directory
Bumps the docker-dependencies group with 1 update in the / directory: node.


Updates `node` from 24.18.1-alpine to 26.7.0-alpine

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26.7.0-alpine
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: docker-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-19 15:29:28 +00:00
32 changed files with 140 additions and 248 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24.18.1-alpine FROM node:26.7.0-alpine
WORKDIR /app WORKDIR /app
+1 -3
View File
@@ -1785,6 +1785,7 @@
"aiClientConfigDescriptionClaude": "Anthropic's agentic coding tool for the terminal.", "aiClientConfigDescriptionClaude": "Anthropic's agentic coding tool for the terminal.",
"aiClientConfigDescriptionCodex": "OpenAI's agentic coding tool for the terminal.", "aiClientConfigDescriptionCodex": "OpenAI's agentic coding tool for the terminal.",
"aiClientConfigDescriptionOpencode": "Open source terminal coding agent.", "aiClientConfigDescriptionOpencode": "Open source terminal coding agent.",
"aiClientConfigDescriptionCursor": "AI code editor built on VS Code.",
"aiClientConfigSetup": "Setup", "aiClientConfigSetup": "Setup",
"aiClientConfigTabCli": "Automatic (CLI)", "aiClientConfigTabCli": "Automatic (CLI)",
"aiClientConfigTabManual": "Manual Configuration", "aiClientConfigTabManual": "Manual Configuration",
@@ -3547,9 +3548,6 @@
"sidebarLogsAction": "Admin Action Logs", "sidebarLogsAction": "Admin Action Logs",
"logRetention": "Log Retention", "logRetention": "Log Retention",
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them", "logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
"logRetentionDisabledWarningTitle": "Log Retention Disabled",
"logRetentionDisabledWarningDescription": "{logType} are not being retained for this organization, so new activity will not appear here. Enable retention in security settings to start collecting these logs.",
"logRetentionDisabledWarningButton": "Go to Security Settings",
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization", "requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
"aiSessionLogs": "AI Gateway Session Logs", "aiSessionLogs": "AI Gateway Session Logs",
"aiSessionLogsDescription": "View prompt and response transcripts for AI gateway requests in this organization", "aiSessionLogsDescription": "View prompt and response transcripts for AI gateway requests in this organization",
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 466.73 532.09">
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
<defs>
<style>
.st0 {
fill: #26251e;
}
</style>
</defs>
<path class="st0" d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/>
</svg>

After

Width:  |  Height:  |  Size: 793 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 466.73 532.09">
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
<defs>
<style>
.st0 {
fill: #edecec;
}
</style>
</defs>
<path class="st0" d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/>
</svg>

After

Width:  |  Height:  |  Size: 793 B

+1 -3
View File
@@ -21,9 +21,7 @@ export function createAiGatewayServer() {
aiGatewayServer.use(helmet()); aiGatewayServer.use(helmet());
aiGatewayServer.use(cors()); aiGatewayServer.use(cors());
// AI requests can carry large payloads (long conversation history, tool aiGatewayServer.use(express.json());
// results, embedded documents), well beyond express.json()'s 100kb default.
aiGatewayServer.use(express.json({ limit: "50mb" }));
aiGatewayServer.use(createAiGatewayRouter()); aiGatewayServer.use(createAiGatewayRouter());
+1 -1
View File
@@ -69,7 +69,7 @@ export const orgs = pgTable("orgs", {
"settingsLogRetentionDaysAISessions" "settingsLogRetentionDaysAISessions"
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year ) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull() .notNull()
.default(0), .default(7),
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format) sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format) sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: boolean("isBillingOrg"), isBillingOrg: boolean("isBillingOrg"),
+3 -4
View File
@@ -16,7 +16,6 @@ import {
aiModels, aiModels,
aiUsageRecords, aiUsageRecords,
db, db,
logsDb,
userOrgRoles userOrgRoles
} from "@server/db"; } from "@server/db";
import { modelKeyMatches } from "@server/lib/aiModelKeyMatch"; import { modelKeyMatches } from "@server/lib/aiModelKeyMatch";
@@ -169,7 +168,7 @@ async function sumUsageAmount(
): Promise<number> { ): Promise<number> {
const column = const column =
unit === "usd" ? aiUsageRecords.costUsd : aiUsageRecords.totalTokens; unit === "usd" ? aiUsageRecords.costUsd : aiUsageRecords.totalTokens;
const [row] = await logsDb const [row] = await db
.select({ total: sql<number>`coalesce(sum(${column}), 0)` }) .select({ total: sql<number>`coalesce(sum(${column}), 0)` })
.from(aiUsageRecords) .from(aiUsageRecords)
.where(where); .where(where);
@@ -201,7 +200,7 @@ export async function sumUsageForBudget(
if (!model) { if (!model) {
return 0; return 0;
} }
const rows = await logsDb const rows = await db
.select({ .select({
requestedModel: aiUsageRecords.requestedModel, requestedModel: aiUsageRecords.requestedModel,
costUsd: aiUsageRecords.costUsd, costUsd: aiUsageRecords.costUsd,
@@ -497,7 +496,7 @@ async function flushUsageRecords() {
try { try {
// Use a transaction to ensure all inserts succeed or fail together // Use a transaction to ensure all inserts succeed or fail together
await logsDb.transaction(async (tx) => { await db.transaction(async (tx) => {
// Batch insert in groups to avoid overwhelming the database // Batch insert in groups to avoid overwhelming the database
const DB_BATCH_SIZE = 25; const DB_BATCH_SIZE = 25;
for (let i = 0; i < recordsToWrite.length; i += DB_BATCH_SIZE) { for (let i = 0; i < recordsToWrite.length; i += DB_BATCH_SIZE) {
+1 -1
View File
@@ -17,7 +17,7 @@ export type AiUsage = {
estimated: boolean; estimated: boolean;
}; };
export function emptyUsage(): AiUsage { function emptyUsage(): AiUsage {
return { return {
promptTokens: 0, promptTokens: 0,
cacheReadTokens: 0, cacheReadTokens: 0,
+1 -5
View File
@@ -443,11 +443,7 @@ export const configSchema = z
disable_config_managed_domains: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(),
disable_product_help_banners: z.boolean().optional(), disable_product_help_banners: z.boolean().optional(),
disable_enterprise_features: z.boolean().optional(), disable_enterprise_features: z.boolean().optional(),
enable_acme_cert_sync: z.boolean().optional().default(true), enable_acme_cert_sync: z.boolean().optional().default(true)
disable_private_http_placeholder: z
.boolean()
.optional()
.default(false)
}) })
.optional(), .optional(),
acme: z acme: z
+7 -21
View File
@@ -48,7 +48,7 @@ export class PrivateConfig {
this.rawPrivateConfig = parsedPrivateConfig; this.rawPrivateConfig = parsedPrivateConfig;
this.migrateDeprecatedConfig(privateEnvironment); this.migrateDeprecatedAcmeConfig(privateEnvironment);
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER = process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER =
this.rawPrivateConfig.branding?.hide_auth_layout_footer === true this.rawPrivateConfig.branding?.hide_auth_layout_footer === true
@@ -152,12 +152,12 @@ export class PrivateConfig {
return this.rawPrivateConfig; return this.rawPrivateConfig;
} }
// `flags.enable_acme_cert_sync`, `flags.disable_private_http_placeholder`, // `flags.enable_acme_cert_sync` and `acme` used to live in the private
// and `acme` used to live in the private config file. They now live in // config file. They now live in the public config file. If an operator
// the public config file. If an operator still has them set in the // still has them set in the private config and hasn't moved them over to
// private config and hasn't moved them over to the public config, pull // the public config, pull them forward so behavior doesn't silently
// them forward so behavior doesn't silently change out from under them. // change out from under them.
private migrateDeprecatedConfig(privateEnvironment: any) { private migrateDeprecatedAcmeConfig(privateEnvironment: any) {
const publicEnvironment: any = readPublicConfigFile(); const publicEnvironment: any = readPublicConfigFile();
const rawConfig: any = config.getRawConfig(); const rawConfig: any = config.getRawConfig();
@@ -182,20 +182,6 @@ export class PrivateConfig {
); );
rawConfig.acme = this.rawPrivateConfig.acme; rawConfig.acme = this.rawPrivateConfig.acme;
} }
if (
privateEnvironment?.flags?.disable_private_http_placeholder !==
undefined &&
publicEnvironment?.flags?.disable_private_http_placeholder ===
undefined
) {
logger.warn(
"`flags.disable_private_http_placeholder` is deprecated in the private config file and has moved to the public config file. Using the value from the private config file for now, but please move it to the public config."
);
rawConfig.flags = rawConfig.flags ?? {};
rawConfig.flags.disable_private_http_placeholder =
this.rawPrivateConfig.flags.disable_private_http_placeholder;
}
} }
} }
+4 -7
View File
@@ -115,13 +115,10 @@ export const privateConfigSchema = z
// any value set here is migrated into the public config at // any value set here is migrated into the public config at
// startup by PrivateConfig (server/private/lib/config.ts). // startup by PrivateConfig (server/private/lib/config.ts).
enable_acme_cert_sync: z.boolean().optional(), enable_acme_cert_sync: z.boolean().optional(),
// @deprecated Moved to the public config file as disable_private_http_placeholder: z
// `flags.disable_private_http_placeholder` .boolean()
// (server/lib/readConfigFile.ts). Kept here only so existing .optional()
// private config files keep parsing; any value set here is .default(false)
// migrated into the public config at startup by PrivateConfig
// (server/private/lib/config.ts).
disable_private_http_placeholder: z.boolean().optional()
}) })
.optional() .optional()
.prefault({}), .prefault({}),
@@ -329,7 +329,8 @@ export async function getTraefikConfig(
}[] = []; }[] = [];
if ( if (
build == "enterprise" && build == "enterprise" &&
!config.getRawConfig().flags?.disable_private_http_placeholder !privateConfig.getRawPrivateConfig().flags
.disable_private_http_placeholder
) { ) {
// we dont want to do this on the cloud // we dont want to do this on the cloud
// Query siteResources in HTTP mode with SSL enabled and aliases - cert generation / HTTPS edge // Query siteResources in HTTP mode with SSL enabled and aliases - cert generation / HTTPS edge
+12 -30
View File
@@ -82,7 +82,6 @@ import {
needsStreamUsageInjection, needsStreamUsageInjection,
withStreamUsageOption, withStreamUsageOption,
extractResponseModel, extractResponseModel,
emptyUsage,
type AiUsage type AiUsage
} from "@server/lib/aiUsageExtraction"; } from "@server/lib/aiUsageExtraction";
import { streamAiGatewayResponse } from "@server/routers/aiGateway/streamAiGatewayResponse"; import { streamAiGatewayResponse } from "@server/routers/aiGateway/streamAiGatewayResponse";
@@ -714,36 +713,20 @@ export function recordAiGatewayCompletion(args: {
budgets budgets
} = args; } = args;
// A non-2xx status means the upstream provider rejected the request let usage: AiUsage | null = extractUsage(
// (bad auth, invalid request, rate limit, 5xx, etc.) before ever running capability,
// the model - no tokens were actually billed, so don't estimate usage responseText,
// off the error body text or price/charge it. We still record a isStream,
// zeroed-out row below (rather than skipping it) so request-count headers
// dashboards built on aiUsageRecords keep counting every attempt. );
const upstreamSucceeded = statusCode >= 200 && statusCode < 300; if (!usage || isUsageEmpty(usage)) {
usage = estimateUsage(JSON.stringify(requestBody ?? ""), responseText);
let usage: AiUsage;
let model: string | undefined;
let pricing: ReturnType<typeof getModelPricing> = null;
let cost: ReturnType<typeof calculateAiCost> = null;
if (upstreamSucceeded) {
usage = extractUsage(capability, responseText, isStream, headers) ?? emptyUsage();
if (isUsageEmpty(usage)) {
usage = estimateUsage(
JSON.stringify(requestBody ?? ""),
responseText
);
}
model = extractResponseModel(responseText) ?? requestedModel;
pricing = getModelPricing(provider.type as AiProviderType, model);
cost = calculateAiCost(pricing, usage);
} else {
usage = emptyUsage();
model = requestedModel;
} }
const model = extractResponseModel(responseText) ?? requestedModel;
const pricing = getModelPricing(provider.type as AiProviderType, model);
const cost = calculateAiCost(pricing, usage);
// Shared by the usage record and the session log so the two can be // Shared by the usage record and the session log so the two can be
// joined later to show token/cost usage alongside the transcript - // joined later to show token/cost usage alongside the transcript -
// generated up front since neither buffered insert's row id is known // generated up front since neither buffered insert's row id is known
@@ -755,7 +738,6 @@ export function recordAiGatewayCompletion(args: {
providerId: provider.providerId, providerId: provider.providerId,
providerType: provider.type, providerType: provider.type,
model, model,
statusCode,
estimated: usage.estimated, estimated: usage.estimated,
promptTokens: usage.promptTokens, promptTokens: usage.promptTokens,
cacheReadTokens: usage.cacheReadTokens, cacheReadTokens: usage.cacheReadTokens,
@@ -312,7 +312,7 @@ async function enrichWithDetails(
>(); >();
const sessionIds = logs.map((log) => log.sessionId); const sessionIds = logs.map((log) => log.sessionId);
if (sessionIds.length > 0) { if (sessionIds.length > 0) {
const usageDetails = await logsDb const usageDetails = await primaryDb
.select({ .select({
sessionId: aiUsageRecords.sessionId, sessionId: aiUsageRecords.sessionId,
promptTokens: aiUsageRecords.promptTokens, promptTokens: aiUsageRecords.promptTokens,
@@ -1,6 +1,5 @@
import { import {
db, db,
logsDb,
aiUsageRecords, aiUsageRecords,
aiProviders, aiProviders,
resources, resources,
@@ -79,22 +78,22 @@ async function query(data: Q) {
uniqueUsers, uniqueUsers,
uniqueVirtualApiKeys uniqueVirtualApiKeys
] = await Promise.all([ ] = await Promise.all([
logsDb db
.selectDistinct({ id: aiUsageRecords.providerId }) .selectDistinct({ id: aiUsageRecords.providerId })
.from(aiUsageRecords) .from(aiUsageRecords)
.where(baseConditions) .where(baseConditions)
.limit(DISTINCT_LIMIT + 1), .limit(DISTINCT_LIMIT + 1),
logsDb db
.selectDistinct({ model: aiUsageRecords.requestedModel }) .selectDistinct({ model: aiUsageRecords.requestedModel })
.from(aiUsageRecords) .from(aiUsageRecords)
.where(baseConditions) .where(baseConditions)
.limit(DISTINCT_LIMIT + 1), .limit(DISTINCT_LIMIT + 1),
logsDb db
.selectDistinct({ id: aiUsageRecords.resourceId }) .selectDistinct({ id: aiUsageRecords.resourceId })
.from(aiUsageRecords) .from(aiUsageRecords)
.where(and(baseConditions, not(isNull(aiUsageRecords.resourceId)))) .where(and(baseConditions, not(isNull(aiUsageRecords.resourceId))))
.limit(DISTINCT_LIMIT + 1), .limit(DISTINCT_LIMIT + 1),
logsDb db
.selectDistinct({ id: aiUsageRecords.siteResourceId }) .selectDistinct({ id: aiUsageRecords.siteResourceId })
.from(aiUsageRecords) .from(aiUsageRecords)
.where( .where(
@@ -105,12 +104,12 @@ async function query(data: Q) {
) )
) )
.limit(DISTINCT_LIMIT + 1), .limit(DISTINCT_LIMIT + 1),
logsDb db
.selectDistinct({ userId: aiUsageRecords.userId }) .selectDistinct({ userId: aiUsageRecords.userId })
.from(aiUsageRecords) .from(aiUsageRecords)
.where(and(baseConditions, not(isNull(aiUsageRecords.userId)))) .where(and(baseConditions, not(isNull(aiUsageRecords.userId))))
.limit(DISTINCT_LIMIT + 1), .limit(DISTINCT_LIMIT + 1),
logsDb db
.selectDistinct({ id: aiUsageRecords.virtualApiKeyId }) .selectDistinct({ id: aiUsageRecords.virtualApiKeyId })
.from(aiUsageRecords) .from(aiUsageRecords)
.where( .where(
@@ -1,4 +1,4 @@
import { logsDb, aiUsageRecords } from "@server/db"; import { db, aiUsageRecords } from "@server/db";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
import { Request, Response } from "express"; import { Request, Response } from "express";
@@ -29,7 +29,7 @@ async function query(data: Q) {
const roleUserIds = await resolveRoleUserIds(data.orgId, data.roleId); const roleUserIds = await resolveRoleUserIds(data.orgId, data.roleId);
const baseConditions = buildAiUsageWhere(data, roleUserIds); const baseConditions = buildAiUsageWhere(data, roleUserIds);
const [totalsRow] = await logsDb const [totalsRow] = await db
.select({ .select({
requests: count(), requests: count(),
promptTokens: sql<number>`COALESCE(SUM(${aiUsageRecords.promptTokens}), 0)`, promptTokens: sql<number>`COALESCE(SUM(${aiUsageRecords.promptTokens}), 0)`,
@@ -46,7 +46,7 @@ async function query(data: Q) {
const dayExpr = dayBucketExpr(); const dayExpr = dayBucketExpr();
const requestsPerDay = await logsDb const requestsPerDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
requests: count() requests: count()
@@ -56,7 +56,7 @@ async function query(data: Q) {
.groupBy(dayExpr) .groupBy(dayExpr)
.orderBy(dayExpr); .orderBy(dayExpr);
const tokensPerDay = await logsDb const tokensPerDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
promptTokens: sql<number>`COALESCE(SUM(${aiUsageRecords.promptTokens}), 0)`, promptTokens: sql<number>`COALESCE(SUM(${aiUsageRecords.promptTokens}), 0)`,
@@ -70,7 +70,7 @@ async function query(data: Q) {
.groupBy(dayExpr) .groupBy(dayExpr)
.orderBy(dayExpr); .orderBy(dayExpr);
const costPerDay = await logsDb const costPerDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
cost: sql<number>`COALESCE(SUM(${aiUsageRecords.costUsd}), 0)` cost: sql<number>`COALESCE(SUM(${aiUsageRecords.costUsd}), 0)`
@@ -80,7 +80,7 @@ async function query(data: Q) {
.groupBy(dayExpr) .groupBy(dayExpr)
.orderBy(dayExpr); .orderBy(dayExpr);
const modelByDay = await logsDb const modelByDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
model: aiUsageRecords.requestedModel, model: aiUsageRecords.requestedModel,
@@ -117,7 +117,7 @@ async function query(data: Q) {
topModelsByTokens topModelsByTokens
); );
const topModelsRaw = await logsDb const topModelsRaw = await db
.select({ .select({
model: aiUsageRecords.requestedModel, model: aiUsageRecords.requestedModel,
requests: count(), requests: count(),
@@ -1,4 +1,4 @@
import { db, logsDb, aiUsageRecords, aiProviders } from "@server/db"; import { db, aiUsageRecords, aiProviders } from "@server/db";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
import { Request, Response } from "express"; import { Request, Response } from "express";
@@ -29,7 +29,7 @@ async function query(data: Q) {
const baseConditions = buildAiUsageWhere(data, roleUserIds); const baseConditions = buildAiUsageWhere(data, roleUserIds);
const dayExpr = dayBucketExpr(); const dayExpr = dayBucketExpr();
const providerByDay = await logsDb const providerByDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
providerId: aiUsageRecords.providerId, providerId: aiUsageRecords.providerId,
@@ -69,7 +69,7 @@ async function query(data: Q) {
topByTokens topByTokens
); );
const topProvidersRaw = await logsDb const topProvidersRaw = await db
.select({ .select({
providerId: aiUsageRecords.providerId, providerId: aiUsageRecords.providerId,
requests: count(), requests: count(),
@@ -1,4 +1,4 @@
import { db, logsDb, aiUsageRecords, resources, siteResources } from "@server/db"; import { db, aiUsageRecords, resources, siteResources } from "@server/db";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
import { Request, Response } from "express"; import { Request, Response } from "express";
@@ -39,7 +39,7 @@ async function query(data: Q) {
const baseConditions = buildAiUsageWhere(data, roleUserIds); const baseConditions = buildAiUsageWhere(data, roleUserIds);
const dayExpr = dayBucketExpr(); const dayExpr = dayBucketExpr();
const resourceByDay = await logsDb const resourceByDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
resourceId: aiUsageRecords.resourceId, resourceId: aiUsageRecords.resourceId,
@@ -80,7 +80,7 @@ async function query(data: Q) {
topByTokens topByTokens
); );
const topResourcesRaw = await logsDb const topResourcesRaw = await db
.select({ .select({
resourceId: aiUsageRecords.resourceId, resourceId: aiUsageRecords.resourceId,
siteResourceId: aiUsageRecords.siteResourceId, siteResourceId: aiUsageRecords.siteResourceId,
@@ -1,11 +1,4 @@
import { import { db, aiUsageRecords, users, roles, userOrgRoles } from "@server/db";
db,
logsDb,
aiUsageRecords,
users,
roles,
userOrgRoles
} from "@server/db";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
import { Request, Response } from "express"; import { Request, Response } from "express";
@@ -43,7 +36,7 @@ async function query(data: Q) {
// userId, so role totals are derived by expanding each user's usage into // userId, so role totals are derived by expanding each user's usage into
// every role they hold in the org (per-role double counting for // every role they hold in the org (per-role double counting for
// multi-role users is expected/accepted). // multi-role users is expected/accepted).
const userByDay = await logsDb const userByDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
userId: aiUsageRecords.userId, userId: aiUsageRecords.userId,
@@ -55,7 +48,7 @@ async function query(data: Q) {
.groupBy(dayExpr, aiUsageRecords.userId) .groupBy(dayExpr, aiUsageRecords.userId)
.orderBy(dayExpr); .orderBy(dayExpr);
const userTotalsRaw = await logsDb const userTotalsRaw = await db
.select({ .select({
userId: aiUsageRecords.userId, userId: aiUsageRecords.userId,
requests: count(), requests: count(),
@@ -1,4 +1,4 @@
import { db, logsDb, aiUsageRecords, virtualApiKeys } from "@server/db"; import { db, aiUsageRecords, virtualApiKeys } from "@server/db";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
import { Request, Response } from "express"; import { Request, Response } from "express";
@@ -31,7 +31,7 @@ async function query(data: Q) {
const baseConditions = buildAiUsageWhere(data, roleUserIds); const baseConditions = buildAiUsageWhere(data, roleUserIds);
const dayExpr = dayBucketExpr(); const dayExpr = dayBucketExpr();
const virtualApiKeyByDay = await logsDb const virtualApiKeyByDay = await db
.select({ .select({
day: dayExpr.as("day"), day: dayExpr.as("day"),
virtualApiKeyId: aiUsageRecords.virtualApiKeyId, virtualApiKeyId: aiUsageRecords.virtualApiKeyId,
@@ -43,7 +43,7 @@ async function query(data: Q) {
.groupBy(dayExpr, aiUsageRecords.virtualApiKeyId) .groupBy(dayExpr, aiUsageRecords.virtualApiKeyId)
.orderBy(dayExpr); .orderBy(dayExpr);
const virtualApiKeyTotalsRaw = await logsDb const virtualApiKeyTotalsRaw = await db
.select({ .select({
virtualApiKeyId: aiUsageRecords.virtualApiKeyId, virtualApiKeyId: aiUsageRecords.virtualApiKeyId,
requests: count(), requests: count(),
+5 -2
View File
@@ -18,6 +18,9 @@ export default async function migration() {
try { try {
await db.execute(sql`BEGIN`); await db.execute(sql`BEGIN`);
await db.execute(sql`
`);
await db.execute(sql` await db.execute(sql`
CREATE TABLE "aiBudgetBreachEvents" ( CREATE TABLE "aiBudgetBreachEvents" (
"id" serial PRIMARY KEY NOT NULL, "id" serial PRIMARY KEY NOT NULL,
@@ -451,14 +454,14 @@ export default async function migration() {
throw new Error(fromZodError(parsedConfig.error).toString()); throw new Error(fromZodError(parsedConfig.error).toString());
} }
traefikConfig.experimental.plugins.badger.version = "v1.6.1"; traefikConfig.experimental.plugins.badger.version = "v1.6.0";
const updatedTraefikYaml = yaml.dump(traefikConfig); const updatedTraefikYaml = yaml.dump(traefikConfig);
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8"); fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
console.log( console.log(
"Updated the version of Badger in your Traefik configuration to v1.6.1" "Updated the version of Badger in your Traefik configuration to v1.6.0"
); );
} catch (e) { } catch (e) {
console.log( console.log(
+2 -2
View File
@@ -456,14 +456,14 @@ export default async function migration() {
throw new Error(fromZodError(parsedConfig.error).toString()); throw new Error(fromZodError(parsedConfig.error).toString());
} }
traefikConfig.experimental.plugins.badger.version = "v1.6.1"; traefikConfig.experimental.plugins.badger.version = "v1.6.0";
const updatedTraefikYaml = yaml.dump(traefikConfig); const updatedTraefikYaml = yaml.dump(traefikConfig);
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8"); fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
console.log( console.log(
"Updated the version of Badger in your Traefik configuration to v1.6.1" "Updated the version of Badger in your Traefik configuration to v1.6.0"
); );
} catch (e) { } catch (e) {
console.log( console.log(
@@ -19,8 +19,6 @@ import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHr
import axios from "axios"; import axios from "axios";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import LogRetentionWarning from "@app/components/LogRetentionWarning";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { logQueries } from "@app/lib/queries"; import { logQueries } from "@app/lib/queries";
@@ -34,7 +32,6 @@ export default function GeneralPage() {
const t = useTranslations(); const t = useTranslations();
const { orgId } = useParams(); const { orgId } = useParams();
const { org } = useOrgContext();
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const [isExporting, startTransition] = useTransition(); const [isExporting, startTransition] = useTransition();
@@ -532,13 +529,6 @@ export default function GeneralPage() {
<PaidFeaturesAlert tiers={tierMatrix.accessLogs} /> <PaidFeaturesAlert tiers={tierMatrix.accessLogs} />
{org.org.settingsLogRetentionDaysAccess === 0 && (
<LogRetentionWarning
orgId={orgId as string}
logTypeLabel={t("accessLogs")}
/>
)}
<LogDataTable <LogDataTable
columns={columns} columns={columns}
data={rows} data={rows}
@@ -3,10 +3,8 @@ import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
import { DateTimeValue } from "@app/components/DateTimePicker"; import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable"; import { LogDataTable } from "@app/components/LogDataTable";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import LogRetentionWarning from "@app/components/LogRetentionWarning";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
@@ -31,7 +29,6 @@ export default function GeneralPage() {
const { orgId } = useParams(); const { orgId } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { org } = useOrgContext();
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const [isExporting, startTransition] = useTransition(); const [isExporting, startTransition] = useTransition();
@@ -362,13 +359,6 @@ export default function GeneralPage() {
<PaidFeaturesAlert tiers={tierMatrix.actionLogs} /> <PaidFeaturesAlert tiers={tierMatrix.actionLogs} />
{org.org.settingsLogRetentionDaysAction === 0 && (
<LogRetentionWarning
orgId={orgId as string}
logTypeLabel={t("actionLogs")}
/>
)}
<LogDataTable <LogDataTable
columns={columns} columns={columns}
data={rows} data={rows}
-11
View File
@@ -3,11 +3,9 @@ import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
import { DateTimeValue } from "@app/components/DateTimePicker"; import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable"; import { LogDataTable } from "@app/components/LogDataTable";
import { AiSessionChatView } from "@app/components/AiSessionChatView"; import { AiSessionChatView } from "@app/components/AiSessionChatView";
import LogRetentionWarning from "@app/components/LogRetentionWarning";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -43,8 +41,6 @@ export default function AiSessionLogsPage() {
const { orgId } = useParams(); const { orgId } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { org } = useOrgContext();
const [isExporting, startTransition] = useTransition(); const [isExporting, startTransition] = useTransition();
const [currentPage, setCurrentPage] = useState<number>(0); const [currentPage, setCurrentPage] = useState<number>(0);
@@ -645,13 +641,6 @@ export default function AiSessionLogsPage() {
description={t("aiSessionLogsDescription")} description={t("aiSessionLogsDescription")}
/> />
{org.org.settingsLogRetentionDaysAISessions === 0 && (
<LogRetentionWarning
orgId={orgId as string}
logTypeLabel={t("aiSessionLogs")}
/>
)}
<LogDataTable <LogDataTable
columns={columns} columns={columns}
data={rows} data={rows}
@@ -4,10 +4,8 @@ import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
import { DateTimeValue } from "@app/components/DateTimePicker"; import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable"; import { LogDataTable } from "@app/components/LogDataTable";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import LogRetentionWarning from "@app/components/LogRetentionWarning";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
@@ -49,7 +47,6 @@ export default function ConnectionLogsPage() {
const { orgId } = useParams(); const { orgId } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { org } = useOrgContext();
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const [isExporting, startTransition] = useTransition(); const [isExporting, startTransition] = useTransition();
@@ -585,13 +582,6 @@ export default function ConnectionLogsPage() {
<PaidFeaturesAlert tiers={tierMatrix.connectionLogs} /> <PaidFeaturesAlert tiers={tierMatrix.connectionLogs} />
{org.org.settingsLogRetentionDaysConnection === 0 && (
<LogRetentionWarning
orgId={orgId as string}
logTypeLabel={t("connectionLogs")}
/>
)}
<LogDataTable <LogDataTable
columns={columns} columns={columns}
data={rows} data={rows}
+1 -13
View File
@@ -1,8 +1,6 @@
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { cache } from "react"; import { cache } from "react";
import OrgProvider from "@app/providers/OrgProvider";
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
type GeneralSettingsProps = { type GeneralSettingsProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -13,8 +11,6 @@ export default async function GeneralSettingsPage({
children, children,
params params
}: GeneralSettingsProps) { }: GeneralSettingsProps) {
const { orgId } = await params;
const getUser = cache(verifySession); const getUser = cache(verifySession);
const user = await getUser(); const user = await getUser();
@@ -22,13 +18,5 @@ export default async function GeneralSettingsPage({
redirect(`/`); redirect(`/`);
} }
let org = null; return children;
try {
const res = await getCachedOrg(orgId);
org = res.data.data;
} catch {
redirect(`/${orgId}`);
}
return <OrgProvider org={org}>{children}</OrgProvider>;
} }
@@ -2,11 +2,9 @@
import { ColumnFilter } from "@app/components/ColumnFilter"; import { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker"; import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable"; import { LogDataTable } from "@app/components/LogDataTable";
import LogRetentionWarning from "@app/components/LogRetentionWarning";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -31,8 +29,6 @@ export default function GeneralPage() {
const { orgId } = useParams(); const { orgId } = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { org } = useOrgContext();
const [isExporting, startTransition] = useTransition(); const [isExporting, startTransition] = useTransition();
const [currentPage, setCurrentPage] = useState<number>(0); const [currentPage, setCurrentPage] = useState<number>(0);
@@ -718,13 +714,6 @@ export default function GeneralPage() {
description={t("requestLogsDescription")} description={t("requestLogsDescription")}
/> />
{org.org.settingsLogRetentionDaysRequest === 0 && (
<LogRetentionWarning
orgId={orgId as string}
logTypeLabel={t("requestLogs")}
/>
)}
<LogDataTable <LogDataTable
columns={columns} columns={columns}
data={rows} data={rows}
@@ -201,11 +201,7 @@ export default async function ResourceAuthPage(props: {
// Inference resources never establish a resource session on the inference // Inference resources never establish a resource session on the inference
// host. Authenticated users retrieve their virtual API key on the dashboard. // host. Authenticated users retrieve their virtual API key on the dashboard.
if (isInference && user) { if (isInference && user) {
if (host !== expectedHost) { redirect(keysPath);
redirect(`/auth/org?redirect=${encodeURIComponent(keysPath)}`);
} else {
redirect(keysPath);
}
} }
// After password/pincode/SSO, do not send the browser back to the // After password/pincode/SSO, do not send the browser back to the
-40
View File
@@ -1,40 +0,0 @@
"use client";
import ActionBanner from "@app/components/ActionBanner";
import { Button } from "@app/components/ui/button";
import { ArrowRight, ShieldAlert } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
type LogRetentionWarningProps = {
orgId: string;
logTypeLabel: string;
};
export function LogRetentionWarning({
orgId,
logTypeLabel
}: LogRetentionWarningProps) {
const t = useTranslations();
return (
<ActionBanner
variant="warning"
title={t("logRetentionDisabledWarningTitle")}
titleIcon={<ShieldAlert className="w-5 h-5" />}
description={t("logRetentionDisabledWarningDescription", {
logType: logTypeLabel
})}
actions={
<Link href={`/${orgId}/settings/general/security`}>
<Button variant="outline" className="gap-2">
{t("logRetentionDisabledWarningButton")}
<ArrowRight className="size-4" />
</Button>
</Link>
}
/>
);
}
export default LogRetentionWarning;
@@ -49,6 +49,10 @@ const CLIENT_LOGOS = {
opencode: { opencode: {
light: "/third-party/opencode-dark.svg", light: "/third-party/opencode-dark.svg",
dark: "/third-party/opencode-light.svg" dark: "/third-party/opencode-light.svg"
},
cursor: {
light: "/third-party/cursor-dark.svg",
dark: "/third-party/cursor-light.svg"
} }
} as const; } as const;
@@ -65,7 +69,8 @@ export function AiClientConfigSection({
const descriptions: Record<string, string> = { const descriptions: Record<string, string> = {
claude: t("aiClientConfigDescriptionClaude"), claude: t("aiClientConfigDescriptionClaude"),
codex: t("aiClientConfigDescriptionCodex"), codex: t("aiClientConfigDescriptionCodex"),
opencode: t("aiClientConfigDescriptionOpencode") opencode: t("aiClientConfigDescriptionOpencode"),
cursor: t("aiClientConfigDescriptionCursor")
}; };
return ( return (
+41 -22
View File
@@ -1,10 +1,11 @@
export const AI_CLIENT_IDS = ["claude", "codex", "opencode"] as const; export const AI_CLIENT_IDS = ["claude", "codex", "opencode", "cursor"] as const;
export type AiClientId = (typeof AI_CLIENT_IDS)[number]; export type AiClientId = (typeof AI_CLIENT_IDS)[number];
export const AI_CLIENT_NAMES: Record<AiClientId, string> = { export const AI_CLIENT_NAMES: Record<AiClientId, string> = {
claude: "Claude Code", claude: "Claude Code",
codex: "Codex", codex: "Codex",
opencode: "OpenCode" opencode: "OpenCode",
cursor: "Cursor"
}; };
/** Auth as supplied by callers: the real key isn't fetched yet. */ /** Auth as supplied by callers: the real key isn't fetched yet. */
@@ -292,7 +293,7 @@ function buildOpencodeGuide(
' "options": {', ' "options": {',
` "baseURL": "${endpoint}/v1"`, ` "baseURL": "${endpoint}/v1"`,
" }", " }",
" },", " }",
' "openai": {', ' "openai": {',
' "options": {', ' "options": {',
` "baseURL": "${endpoint}/v1"`, ` "baseURL": "${endpoint}/v1"`,
@@ -313,26 +314,12 @@ function buildOpencodeGuide(
' "anthropic": {', ' "anthropic": {',
' "type": "api",', ' "type": "api",',
` "key": "${key}"`, ` "key": "${key}"`,
" },",
' "openai": {',
' "type": "api",',
` "key": "${key}"`,
" }", " }",
"}" "}"
].join("\n"), ].join("\n"),
auth auth
); );
const moreProviders = block(
"opencode-more-providers",
"More providers",
() =>
"OpenCode configures providers individually, so Anthropic and OpenAI are just the ones set up above. " +
'You can point any other OpenCode-supported provider (e.g. "openrouter", "google", "groq") at this gateway the same way: add a matching entry under "provider" in opencode.json, and under auth.json if it needs an API key.',
auth,
"steps"
);
return { return {
id: "opencode", id: "opencode",
name: AI_CLIENT_NAMES.opencode, name: AI_CLIENT_NAMES.opencode,
@@ -342,10 +329,41 @@ function buildOpencodeGuide(
id: "default", id: "default",
label: "Default", label: "Default",
relation: "steps", relation: "steps",
blocks: blocks: [config, authFile]
auth.mode === "keyed" }
? [config, authFile, moreProviders] ]
: [config, moreProviders] };
}
function buildCursorGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
const steps = block(
"cursor-steps",
"Cursor Settings",
(key) =>
[
"1. Open Cursor Settings -> Models.",
'2. Enable "Override OpenAI Base URL".',
`3. Set the base URL to: ${endpoint}/v1`,
auth.mode === "keyed"
? `4. Paste your API key into the OpenAI API Key field: ${key}`
: '4. Leave the OpenAI API Key field set to a placeholder (e.g. "-"). Pangolin authenticates the request over your Newt/Olm connection automatically.',
"5. Add a custom model matching the model your Pangolin AI Gateway serves (e.g. claude-sonnet-4-6)."
].join("\n"),
auth,
"steps",
true
);
return {
id: "cursor",
name: AI_CLIENT_NAMES.cursor,
cli: null,
presets: [
{
id: "default",
label: "Default",
relation: "steps",
blocks: [steps]
} }
] ]
}; };
@@ -361,7 +379,8 @@ const GUIDE_BUILDERS: Record<
> = { > = {
claude: buildClaudeGuide, claude: buildClaudeGuide,
codex: buildCodexGuide, codex: buildCodexGuide,
opencode: buildOpencodeGuide opencode: buildOpencodeGuide,
cursor: buildCursorGuide
}; };
export function buildAiClientGuide( export function buildAiClientGuide(