From 1f80bdd3616e220ef728fab4a0e8433f6888d3cc Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 19 Aug 2026 14:25:59 -0400 Subject: [PATCH 1/7] change default model for cursor config --- src/lib/aiClientConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/aiClientConfig.ts b/src/lib/aiClientConfig.ts index 76d750213..0ae0517ae 100644 --- a/src/lib/aiClientConfig.ts +++ b/src/lib/aiClientConfig.ts @@ -347,7 +347,7 @@ function buildCursorGuide(endpoint: string, auth: AiClientAuth): AiClientGuide { 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)." + "5. Add a custom model matching the model your Pangolin AI Gateway serves (e.g. gpt-5.6-sol)." ].join("\n"), auth, "steps", From ef8985b0af73161a919780289041241fa27d14da Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 19 Aug 2026 15:07:29 -0400 Subject: [PATCH 2/7] Set the parser limit for the gateway because AI messages can be pretty big --- server/aiGatewayServer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/aiGatewayServer.ts b/server/aiGatewayServer.ts index 87d57b86b..3283abe5b 100644 --- a/server/aiGatewayServer.ts +++ b/server/aiGatewayServer.ts @@ -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()); From acd64a049fdf368b5afefe02a2383435ae917955 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 19 Aug 2026 15:45:40 -0400 Subject: [PATCH 3/7] Show that opencode has providers warning --- src/lib/aiClientConfig.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/aiClientConfig.ts b/src/lib/aiClientConfig.ts index 0ae0517ae..c3dbf96f5 100644 --- a/src/lib/aiClientConfig.ts +++ b/src/lib/aiClientConfig.ts @@ -293,7 +293,7 @@ function buildOpencodeGuide( ' "options": {', ` "baseURL": "${endpoint}/v1"`, " }", - " }", + " },", ' "openai": {', ' "options": {', ` "baseURL": "${endpoint}/v1"`, @@ -314,12 +314,26 @@ function buildOpencodeGuide( ' "anthropic": {', ' "type": "api",', ` "key": "${key}"`, + " },", + ' "openai": {', + ' "type": "api",', + ` "key": "${key}"`, " }", "}" ].join("\n"), 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 { id: "opencode", name: AI_CLIENT_NAMES.opencode, @@ -329,7 +343,10 @@ function buildOpencodeGuide( id: "default", label: "Default", relation: "steps", - blocks: [config, authFile] + blocks: + auth.mode === "keyed" + ? [config, authFile, moreProviders] + : [config, moreProviders] } ] }; From 5c6da72ec12335d7c2706dd17120d34824243672 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 19 Aug 2026 15:46:49 -0400 Subject: [PATCH 4/7] remove cursor --- messages/en-US.json | 1 - public/third-party/cursor-dark.svg | 12 ------ public/third-party/cursor-light.svg | 12 ------ .../AiClientConfigSection.tsx | 7 +--- src/lib/aiClientConfig.ts | 42 ++----------------- 5 files changed, 4 insertions(+), 70 deletions(-) delete mode 100644 public/third-party/cursor-dark.svg delete mode 100644 public/third-party/cursor-light.svg diff --git a/messages/en-US.json b/messages/en-US.json index f08126a94..89d8b1c4b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1785,7 +1785,6 @@ "aiClientConfigDescriptionClaude": "Anthropic's agentic coding tool for the terminal.", "aiClientConfigDescriptionCodex": "OpenAI's agentic coding tool for the terminal.", "aiClientConfigDescriptionOpencode": "Open source terminal coding agent.", - "aiClientConfigDescriptionCursor": "AI code editor built on VS Code.", "aiClientConfigSetup": "Setup", "aiClientConfigTabCli": "Automatic (CLI)", "aiClientConfigTabManual": "Manual Configuration", diff --git a/public/third-party/cursor-dark.svg b/public/third-party/cursor-dark.svg deleted file mode 100644 index 635d3ccdc..000000000 --- a/public/third-party/cursor-dark.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/public/third-party/cursor-light.svg b/public/third-party/cursor-light.svg deleted file mode 100644 index 10d50ca84..000000000 --- a/public/third-party/cursor-light.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/components/ai-client-config/AiClientConfigSection.tsx b/src/components/ai-client-config/AiClientConfigSection.tsx index c82f71368..955dfb4a9 100644 --- a/src/components/ai-client-config/AiClientConfigSection.tsx +++ b/src/components/ai-client-config/AiClientConfigSection.tsx @@ -49,10 +49,6 @@ const CLIENT_LOGOS = { opencode: { light: "/third-party/opencode-dark.svg", dark: "/third-party/opencode-light.svg" - }, - cursor: { - light: "/third-party/cursor-dark.svg", - dark: "/third-party/cursor-light.svg" } } as const; @@ -69,8 +65,7 @@ export function AiClientConfigSection({ const descriptions: Record = { claude: t("aiClientConfigDescriptionClaude"), codex: t("aiClientConfigDescriptionCodex"), - opencode: t("aiClientConfigDescriptionOpencode"), - cursor: t("aiClientConfigDescriptionCursor") + opencode: t("aiClientConfigDescriptionOpencode") }; return ( diff --git a/src/lib/aiClientConfig.ts b/src/lib/aiClientConfig.ts index c3dbf96f5..37fe7cd16 100644 --- a/src/lib/aiClientConfig.ts +++ b/src/lib/aiClientConfig.ts @@ -1,11 +1,10 @@ -export const AI_CLIENT_IDS = ["claude", "codex", "opencode", "cursor"] as const; +export const AI_CLIENT_IDS = ["claude", "codex", "opencode"] as const; export type AiClientId = (typeof AI_CLIENT_IDS)[number]; export const AI_CLIENT_NAMES: Record = { claude: "Claude Code", codex: "Codex", - opencode: "OpenCode", - cursor: "Cursor" + opencode: "OpenCode" }; /** Auth as supplied by callers: the real key isn't fetched yet. */ @@ -352,40 +351,6 @@ function buildOpencodeGuide( }; } -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. gpt-5.6-sol)." - ].join("\n"), - auth, - "steps", - true - ); - - return { - id: "cursor", - name: AI_CLIENT_NAMES.cursor, - cli: null, - presets: [ - { - id: "default", - label: "Default", - relation: "steps", - blocks: [steps] - } - ] - }; -} - const GUIDE_BUILDERS: Record< AiClientId, ( @@ -396,8 +361,7 @@ const GUIDE_BUILDERS: Record< > = { claude: buildClaudeGuide, codex: buildCodexGuide, - opencode: buildOpencodeGuide, - cursor: buildCursorGuide + opencode: buildOpencodeGuide }; export function buildAiClientGuide( From ef051c2f8cb7a43cde7630531b862fa42f2c73ab Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 19 Aug 2026 15:58:52 -0400 Subject: [PATCH 5/7] Dont count usage when the response was not successful --- server/lib/aiUsageExtraction.ts | 2 +- server/routers/aiGateway/pipeline.ts | 42 ++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/server/lib/aiUsageExtraction.ts b/server/lib/aiUsageExtraction.ts index 48f19f308..fcec807e1 100644 --- a/server/lib/aiUsageExtraction.ts +++ b/server/lib/aiUsageExtraction.ts @@ -17,7 +17,7 @@ export type AiUsage = { estimated: boolean; }; -function emptyUsage(): AiUsage { +export function emptyUsage(): AiUsage { return { promptTokens: 0, cacheReadTokens: 0, diff --git a/server/routers/aiGateway/pipeline.ts b/server/routers/aiGateway/pipeline.ts index 50d5e98eb..e11e14a62 100644 --- a/server/routers/aiGateway/pipeline.ts +++ b/server/routers/aiGateway/pipeline.ts @@ -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 = null; + let cost: ReturnType = 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, From 114592add89833f51674c48d177d18916bf2cabc Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 19 Aug 2026 16:16:05 -0400 Subject: [PATCH 6/7] Show a warning if the logs are disabled to reduce confusion --- messages/en-US.json | 3 ++ server/db/pg/schema/schema.ts | 2 +- server/lib/readConfigFile.ts | 6 ++- server/private/lib/config.ts | 28 +++++++++---- server/private/lib/readConfigFile.ts | 11 +++-- .../private/lib/traefik/getTraefikConfig.ts | 3 +- src/app/[orgId]/settings/logs/access/page.tsx | 10 +++++ src/app/[orgId]/settings/logs/action/page.tsx | 10 +++++ src/app/[orgId]/settings/logs/ai/page.tsx | 11 +++++ .../[orgId]/settings/logs/connection/page.tsx | 10 +++++ src/app/[orgId]/settings/logs/layout.tsx | 14 ++++++- .../[orgId]/settings/logs/request/page.tsx | 11 +++++ src/components/LogRetentionWarning.tsx | 40 +++++++++++++++++++ 13 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 src/components/LogRetentionWarning.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 89d8b1c4b..02c022e35 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3547,6 +3547,9 @@ "sidebarLogsAction": "Admin Action Logs", "logRetention": "Log Retention", "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", "aiSessionLogs": "AI Gateway Session Logs", "aiSessionLogsDescription": "View prompt and response transcripts for AI gateway requests in this organization", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index f9863c154..1a4013f0f 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -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"), diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 5f6939675..68dc7cea7 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -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 diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts index 278d9d636..e819b1281 100644 --- a/server/private/lib/config.ts +++ b/server/private/lib/config.ts @@ -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; + } } } diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index 8be68672d..363c9b782 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -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({}), diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index ecbf1d68d..0515b8265 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -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 diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index a6fa601e9..48c5d11d1 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -19,6 +19,8 @@ import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHr import axios from "axios"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; 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 { tierMatrix } from "@server/lib/billing/tierMatrix"; import { logQueries } from "@app/lib/queries"; @@ -32,6 +34,7 @@ export default function GeneralPage() { const t = useTranslations(); const { orgId } = useParams(); + const { org } = useOrgContext(); const { isPaidUser } = usePaidStatus(); const [isExporting, startTransition] = useTransition(); @@ -529,6 +532,13 @@ export default function GeneralPage() { + {org.org.settingsLogRetentionDaysAccess === 0 && ( + + )} + + {org.org.settingsLogRetentionDaysAction === 0 && ( + + )} + (0); @@ -641,6 +645,13 @@ export default function AiSessionLogsPage() { description={t("aiSessionLogsDescription")} /> + {org.org.settingsLogRetentionDaysAISessions === 0 && ( + + )} + + {org.org.settingsLogRetentionDaysConnection === 0 && ( + + )} + {children}; } diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index 732bad3ad..d5ecdbfab 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -2,9 +2,11 @@ import { ColumnFilter } from "@app/components/ColumnFilter"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { LogDataTable } from "@app/components/LogDataTable"; +import LogRetentionWarning from "@app/components/LogRetentionWarning"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Button } from "@app/components/ui/button"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { useTranslations } from "next-intl"; @@ -29,6 +31,8 @@ export default function GeneralPage() { const { orgId } = useParams(); const searchParams = useSearchParams(); + const { org } = useOrgContext(); + const [isExporting, startTransition] = useTransition(); const [currentPage, setCurrentPage] = useState(0); @@ -714,6 +718,13 @@ export default function GeneralPage() { description={t("requestLogsDescription")} /> + {org.org.settingsLogRetentionDaysRequest === 0 && ( + + )} + } + description={t("logRetentionDisabledWarningDescription", { + logType: logTypeLabel + })} + actions={ + + + + } + /> + ); +} + +export default LogRetentionWarning; From 887e13888d1892b3b7bce0ae06d21dcd35b3da0b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 19 Aug 2026 16:26:44 -0400 Subject: [PATCH 7/7] redirect differently for org auth domain --- src/app/auth/resource/[resourceGuid]/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index e3e93ce9d..c3cb00615 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -201,7 +201,11 @@ export default async function ResourceAuthPage(props: { // Inference resources never establish a resource session on the inference // host. Authenticated users retrieve their virtual API key on the dashboard. if (isInference && user) { - redirect(keysPath); + if (host !== expectedHost) { + redirect(`/auth/org?redirect=${encodeURIComponent(keysPath)}`); + } else { + redirect(keysPath); + } } // After password/pincode/SSO, do not send the browser back to the