mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-21 03:32:33 +02:00
@@ -21,7 +21,9 @@ export function createAiGatewayServer() {
|
||||
|
||||
aiGatewayServer.use(helmet());
|
||||
aiGatewayServer.use(cors());
|
||||
aiGatewayServer.use(express.json());
|
||||
// AI requests can carry large payloads (long conversation history, tool
|
||||
// results, embedded documents), well beyond express.json()'s 100kb default.
|
||||
aiGatewayServer.use(express.json({ limit: "50mb" }));
|
||||
|
||||
aiGatewayServer.use(createAiGatewayRouter());
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export const orgs = pgTable("orgs", {
|
||||
"settingsLogRetentionDaysAISessions"
|
||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(7),
|
||||
.default(0),
|
||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||
isBillingOrg: boolean("isBillingOrg"),
|
||||
|
||||
@@ -17,7 +17,7 @@ export type AiUsage = {
|
||||
estimated: boolean;
|
||||
};
|
||||
|
||||
function emptyUsage(): AiUsage {
|
||||
export function emptyUsage(): AiUsage {
|
||||
return {
|
||||
promptTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
|
||||
@@ -443,7 +443,11 @@ export const configSchema = z
|
||||
disable_config_managed_domains: z.boolean().optional(),
|
||||
disable_product_help_banners: 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(),
|
||||
acme: z
|
||||
|
||||
@@ -48,7 +48,7 @@ export class PrivateConfig {
|
||||
|
||||
this.rawPrivateConfig = parsedPrivateConfig;
|
||||
|
||||
this.migrateDeprecatedAcmeConfig(privateEnvironment);
|
||||
this.migrateDeprecatedConfig(privateEnvironment);
|
||||
|
||||
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER =
|
||||
this.rawPrivateConfig.branding?.hide_auth_layout_footer === true
|
||||
@@ -152,12 +152,12 @@ export class PrivateConfig {
|
||||
return this.rawPrivateConfig;
|
||||
}
|
||||
|
||||
// `flags.enable_acme_cert_sync` and `acme` used to live in the private
|
||||
// config file. They now live in the public config file. If an operator
|
||||
// still has them set in the private config and hasn't moved them over to
|
||||
// the public config, pull them forward so behavior doesn't silently
|
||||
// change out from under them.
|
||||
private migrateDeprecatedAcmeConfig(privateEnvironment: any) {
|
||||
// `flags.enable_acme_cert_sync`, `flags.disable_private_http_placeholder`,
|
||||
// and `acme` used to live in the private config file. They now live in
|
||||
// the public config file. If an operator still has them set in the
|
||||
// private config and hasn't moved them over to the public config, pull
|
||||
// them forward so behavior doesn't silently change out from under them.
|
||||
private migrateDeprecatedConfig(privateEnvironment: any) {
|
||||
const publicEnvironment: any = readPublicConfigFile();
|
||||
const rawConfig: any = config.getRawConfig();
|
||||
|
||||
@@ -182,6 +182,20 @@ export class PrivateConfig {
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,10 +115,13 @@ export const privateConfigSchema = z
|
||||
// any value set here is migrated into the public config at
|
||||
// startup by PrivateConfig (server/private/lib/config.ts).
|
||||
enable_acme_cert_sync: z.boolean().optional(),
|
||||
disable_private_http_placeholder: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
// @deprecated Moved to the public config file as
|
||||
// `flags.disable_private_http_placeholder`
|
||||
// (server/lib/readConfigFile.ts). Kept here only so existing
|
||||
// private config files keep parsing; any value set here is
|
||||
// migrated into the public config at startup by PrivateConfig
|
||||
// (server/private/lib/config.ts).
|
||||
disable_private_http_placeholder: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
.prefault({}),
|
||||
|
||||
@@ -329,8 +329,7 @@ export async function getTraefikConfig(
|
||||
}[] = [];
|
||||
if (
|
||||
build == "enterprise" &&
|
||||
!privateConfig.getRawPrivateConfig().flags
|
||||
.disable_private_http_placeholder
|
||||
!config.getRawConfig().flags?.disable_private_http_placeholder
|
||||
) {
|
||||
// we dont want to do this on the cloud
|
||||
// Query siteResources in HTTP mode with SSL enabled and aliases - cert generation / HTTPS edge
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
needsStreamUsageInjection,
|
||||
withStreamUsageOption,
|
||||
extractResponseModel,
|
||||
emptyUsage,
|
||||
type AiUsage
|
||||
} from "@server/lib/aiUsageExtraction";
|
||||
import { streamAiGatewayResponse } from "@server/routers/aiGateway/streamAiGatewayResponse";
|
||||
@@ -713,19 +714,35 @@ export function recordAiGatewayCompletion(args: {
|
||||
budgets
|
||||
} = args;
|
||||
|
||||
let usage: AiUsage | null = extractUsage(
|
||||
capability,
|
||||
responseText,
|
||||
isStream,
|
||||
headers
|
||||
);
|
||||
if (!usage || isUsageEmpty(usage)) {
|
||||
usage = estimateUsage(JSON.stringify(requestBody ?? ""), responseText);
|
||||
}
|
||||
// A non-2xx status means the upstream provider rejected the request
|
||||
// (bad auth, invalid request, rate limit, 5xx, etc.) before ever running
|
||||
// the model - no tokens were actually billed, so don't estimate usage
|
||||
// off the error body text or price/charge it. We still record a
|
||||
// zeroed-out row below (rather than skipping it) so request-count
|
||||
// dashboards built on aiUsageRecords keep counting every attempt.
|
||||
const upstreamSucceeded = statusCode >= 200 && statusCode < 300;
|
||||
|
||||
const model = extractResponseModel(responseText) ?? requestedModel;
|
||||
const pricing = getModelPricing(provider.type as AiProviderType, model);
|
||||
const cost = calculateAiCost(pricing, usage);
|
||||
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;
|
||||
}
|
||||
|
||||
// Shared by the usage record and the session log so the two can be
|
||||
// joined later to show token/cost usage alongside the transcript -
|
||||
@@ -738,6 +755,7 @@ export function recordAiGatewayCompletion(args: {
|
||||
providerId: provider.providerId,
|
||||
providerType: provider.type,
|
||||
model,
|
||||
statusCode,
|
||||
estimated: usage.estimated,
|
||||
promptTokens: usage.promptTokens,
|
||||
cacheReadTokens: usage.cacheReadTokens,
|
||||
|
||||
Reference in New Issue
Block a user