From dbf80803cc27d130b0b5901ad149d995a6cbb72d Mon Sep 17 00:00:00 2001 From: Daniel Buckner Date: Sat, 24 Oct 2026 10:31:48 -0700 Subject: [PATCH] refactor: trim custom-endpoint comments and share synthetic provider const Review pass: shorten verbose doc comments across the custom-endpoint code, merge duplicate imports, share one synthetic provider entry between the keys section and setup dialog, collapse the duplicated connect-editor block, revert an unrelated settings-route drive-by. --- app/api/custom-provider/route.ts | 27 +++----- app/api/settings/[section]/route.ts | 4 +- components/settings/keys-section.tsx | 41 +++--------- components/setup-cta.tsx | 3 +- components/setup-dialog.tsx | 17 ++--- src/core/auth-keys.ts | 15 ++--- src/core/settings-resolution.ts | 5 +- src/core/settings.ts | 66 ++++++------------- src/server/custom-provider.ts | 98 +++++++--------------------- src/server/index.ts | 5 +- src/server/keys.ts | 6 +- src/server/model-catalog.ts | 21 +++--- src/server/settings-body.ts | 6 +- src/server/stores.ts | 4 +- 14 files changed, 92 insertions(+), 226 deletions(-) diff --git a/app/api/custom-provider/route.ts b/app/api/custom-provider/route.ts index e14bcd8..5439fb9 100644 --- a/app/api/custom-provider/route.ts +++ b/app/api/custom-provider/route.ts @@ -1,19 +1,10 @@ /** - * The custom OpenAI-compatible endpoint's connect surface (custom-endpoint - * wave). It is ONE provider in the Keys list, connected exactly like the - * built-ins are connected by a key — the only extra input is a base URL. - * - * GET → the resolved non-secret config { baseUrl, models } + whether a key - * is present (masked status only; the key is a secret in the auth - * store and never enters settings.json). - * POST → connect. Body { baseUrl, key? }: validate + normalize the base URL, - * PROBE the endpoint's /models (fail fast — a broken URL never - * persists), then store the key (empty string clears it, absent - * leaves it), persist { baseUrl, models } to the settings record, and - * re-register the provider into piModels() with no restart. - * - * Discovery lives here (not a separate route) because connecting and discovering - * are one user action: paste URL + key, get a working provider. + * The custom endpoint's connect surface (one provider in the Keys list). + * GET → non-secret config { baseUrl, models } + keyPresent (masked). + * POST → connect: validate + normalize the base URL, probe /models (fail + * fast), store the key, persist { baseUrl, models }, re-sync the + * provider into piModels(). Discovery lives here because connecting + * and discovering are one user action. */ import { deleteKey, @@ -43,13 +34,11 @@ export async function POST(request: Request) { const base = normalizeBaseUrl(typeof body?.baseUrl === "string" ? body.baseUrl : ""); if (!base) return Response.json({ error: "Base URL must be a valid http(s) URL" }, { status: 400 }); - // A provided key is trimmed; an empty string means "clear it", an absent - // field means "leave whatever is stored". Only a real key rides the probe. + // Empty key string clears it, absent leaves it stored; only a real key rides the probe. const keyProvided = typeof body?.key === "string"; const key = keyProvided ? (body!.key as string).trim() : undefined; - // Probe first: a URL that doesn't answer with a model list never persists, - // so a typo can't leave a half-configured provider behind. + // Probe first: a URL that doesn't answer with a model list never persists. const found = await discoverModels(base, key ? key : undefined); if (!found.ok) return Response.json({ error: found.error }, { status: 502 }); diff --git a/app/api/settings/[section]/route.ts b/app/api/settings/[section]/route.ts index 460b593..2c955cf 100644 --- a/app/api/settings/[section]/route.ts +++ b/app/api/settings/[section]/route.ts @@ -128,6 +128,6 @@ export async function PUT(request: Request, ctx: { params: Promise<{ section: st // Read-modify-write ONE section of the record; everything else rides // along untouched (other sections, unknown keys — sanitize preserves). const next = { ...record, [section]: sectionOut }; - const saved = writeSettingsRecord(next); - return Response.json({ settings: saved }); + writeSettingsRecord(next); + return Response.json({ settings: next }); } diff --git a/components/settings/keys-section.tsx b/components/settings/keys-section.tsx index cd80df5..fb8e7f2 100644 --- a/components/settings/keys-section.tsx +++ b/components/settings/keys-section.tsx @@ -58,10 +58,8 @@ interface CustomStatus { * section can never drift from core/auth-keys.ts again. */ const TOOL_IDS = AUTH_KEYS.map((k) => k.id) as readonly string[]; -/** The custom endpoint as it appears in the Add-provider dropdown — a synthetic - * entry (it is not one of pi-ai's 33), offered only while unconfigured; once - * connected it graduates to its own row like any stored provider. */ -const CUSTOM_PROVIDER_INFO: ProviderKeyInfo = { +/** The custom endpoint's synthetic Add-provider entry (offered only while unconfigured). */ +export const CUSTOM_PROVIDER_INFO: ProviderKeyInfo = { id: CUSTOM_PROVIDER.id, envVar: CUSTOM_PROVIDER.envVar, label: CUSTOM_PROVIDER.label, @@ -489,10 +487,10 @@ export function KeysSection({ onToggle={() => setAddingCustom((v) => !v)} /> )} - {custom?.baseUrl && addingCustom && ( + {addingCustom && ( { setAddingCustom(false); await reload(); @@ -516,18 +514,6 @@ export function KeysSection({ }} /> )} - {!custom?.baseUrl && addingCustom && ( - { - setAddingCustom(false); - await reload(); - notifySettingsSaved("keys"); - }} - onCancel={() => setAddingCustom(false)} - /> - )} {adding && (
{adding.label} @@ -637,9 +623,7 @@ function AddProviderEditor({ ); } -/** The custom endpoint's status row in the provider list — one more provider, - * shown only once it's configured (exactly like a built-in appears only once - * its key is stored). Clicking re-opens the connect editor. */ +/** The custom endpoint's status row (shown once configured); clicking re-opens the editor. */ function CustomEndpointRow({ status, expanded, @@ -669,12 +653,8 @@ function CustomEndpointRow({ ); } -/** The custom endpoint's connect editor — base URL + optional key, exactly the - * built-ins' key-paste flow plus the one extra URL field. Connect POSTs to - * /api/custom-provider (validate + discover + register); on success the parent - * reloads so the row appears and the catalog widens with no restart. Image - * capability is detected automatically from the endpoint — there is nothing to - * tick here. Keyless local servers (Ollama / LM Studio) connect with no key. */ +/** The custom endpoint's connect editor: base URL + key → POST /api/custom-provider + * (validate + discover + register). Image capability is detected automatically. */ function CustomEndpointEditor({ baseUrl0, keyPresent, @@ -691,9 +671,8 @@ function CustomEndpointEditor({ const [keyTouched, setKeyTouched] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); - // A key is required to enable chatting (the send gate needs a provider key); - // local servers accept any value. When one is already stored, leaving the - // field blank keeps it. + // A key is required to enable chatting; local servers accept any value. A + // stored key is kept when the field is left blank. const needsKey = !keyPresent; const canConnect = Boolean(baseUrl.trim()) && (!needsKey || Boolean(key.trim())); diff --git a/components/setup-cta.tsx b/components/setup-cta.tsx index 2e34caa..347dd9f 100644 --- a/components/setup-cta.tsx +++ b/components/setup-cta.tsx @@ -16,10 +16,9 @@ */ import { useEffect, useState } from "react"; -import { CUSTOM_PROVIDER, MODEL_SLOTS, type AuthKeyStatus } from "@/src/core"; +import { CUSTOM_PROVIDER, MODEL_SLOTS, type AuthKeyStatus, type ModelCatalogResponse } from "@/src/core"; import { classifyKeys, type ProviderKeyInfo } from "@/components/settings/keys-section"; import { useModelCatalog } from "@/components/model-catalog"; -import type { ModelCatalogResponse } from "@/src/core"; /** Pure: does a MODEL-PROVIDER key exist among the statuses? Reuses * classifyKeys (the keys section's own classification — no drift): diff --git a/components/setup-dialog.tsx b/components/setup-dialog.tsx index 3face30..3aee5ca 100644 --- a/components/setup-dialog.tsx +++ b/components/setup-dialog.tsx @@ -48,7 +48,7 @@ import { notifySettingsSaved, useSettingsSaved } from "@/components/settings-sig import { useModelCatalog } from "@/components/model-catalog"; import { savedSlotsConfigured } from "@/components/setup-cta"; import { Hint } from "@/components/ui/hint"; -import { type ProviderKeyInfo } from "@/components/settings/keys-section"; +import { type ProviderKeyInfo, CUSTOM_PROVIDER_INFO } from "@/components/settings/keys-section"; import { ProviderMenuList } from "@/components/settings/model-menu"; import { ChatGPTBackgroundAccess } from "@/components/settings/chatgpt-connection"; import { NativeSubscriptionBackgroundAccess, SubscriptionConnections } from "@/components/settings/subscription-connections"; @@ -65,11 +65,6 @@ const SLOT_META: Record = { }; const SLOT_ORDER: readonly ModelSlotName[] = ["strong", "cheap", "vision"]; -/** The custom endpoint's synthetic picker entry — one more provider in the - * Stage 1 roster, connected by a base URL + key (POST - * /api/custom-provider). Not part of the /api/keys plain-key roster. */ -const CUSTOM_PICKER: ProviderKeyInfo = { id: CUSTOM_PROVIDER.id, envVar: CUSTOM_PROVIDER.envVar, label: CUSTOM_PROVIDER.label }; - /** The GET /api/models response fields the flow consumes. */ interface ModelsBody { models: ModelCatalogEntry[]; @@ -159,9 +154,8 @@ export function SetupKeysBody({ onSave: () => void; /** The MenuShell anchor — wired by the wrapper (its triggerRef). */ menuAnchor: React.RefObject; - /** The chosen provider is the custom endpoint: a base URL field joins the - * key field (a key is required — local servers accept any value). All three - * optional so the plain-key callers (and tests) stay unchanged. */ + /** Chosen provider is the custom endpoint: a base URL field joins the key + * field. All three optional so plain-key callers (and tests) stay unchanged. */ isCustom?: boolean; baseUrl?: string; onBaseUrl?: (v: string) => void; @@ -408,8 +402,7 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) { if (!keyValue.trim()) return; setKeySaving(true); setKeyError(null); - // The custom endpoint connects through its own route (base URL + key + - // discovery in one action); every other provider is a plain key PUT. + // Custom endpoint connects through its own route; every other provider is a plain key PUT. const res = isCustom ? await fetch("/api/custom-provider", { method: "POST", @@ -460,7 +453,7 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) { } const keysBody = ; - /** The single custom OpenAI-compatible endpoint (custom-endpoint wave): - * resolved concrete — baseUrl defaults "", models default []. Per-model - * defaults (context window, cost) apply at provider-build time in the - * server (custom-provider.ts), not here. */ + /** Custom endpoint, resolved concrete: baseUrl defaults "", models default []. */ customProvider: { baseUrl: string; models: CustomModel[] }; } diff --git a/src/core/settings.ts b/src/core/settings.ts index 560e8d9..d69103b 100644 --- a/src/core/settings.ts +++ b/src/core/settings.ts @@ -206,29 +206,19 @@ export interface ModelsSettings { vision?: ModelSlot; } -// ─── Custom OpenAI-compatible endpoint (custom-endpoint wave) ──────────────── -// ONE user-configured endpoint speaking the OpenAI /chat/completions API -// (self-hosted vLLM / Ollama / llama.cpp, a corporate proxy). It is NOT a -// special concept: it is one more provider in the Keys list, connected by a -// base URL + (optional) key exactly as the built-ins are connected by a key. -// NON-SECRET config only — base URL + the model ids discovered from it. The -// API key is a SECRET and lives in the auth store under CUSTOM_PROVIDER.envVar -// (core/auth-keys.ts), never in this record, exactly like every other key. The -// endpoint is registered into pi-ai as a normal provider -// (server/custom-provider.ts) so its models fill the same slots/roles. +// ─── Custom OpenAI-compatible endpoint ─────────────────────────────────────── +// ONE user-configured OpenAI-compatible server (vLLM / Ollama / a proxy). It is +// one more provider in the Keys list, keyed by a base URL instead of an env var. +// NON-SECRET config only (base URL + discovered model ids); the API key is a +// secret in the auth store, never here. Registered into pi-ai as a normal +// provider (server/custom-provider.ts). -/** Default facts for a custom model. The endpoint's /models reports ids (and - * sometimes modality); these fill the rest: self-hosted endpoints are free. */ +/** Defaults for a custom model — self-hosted endpoints are free. */ export const CUSTOM_MODEL_DEFAULT_CONTEXT_WINDOW = 8192; export const CUSTOM_MODEL_DEFAULT_MAX_TOKENS = 4096; -/** One discovered custom model. `image` marks it as accepting image input so - * it is selectable for the Vision slot (PDFs, image tasks). Discovery sets it - * from the endpoint's /models metadata when reported; the Keys row exposes a - * checkbox for endpoints that don't report it. `context` / `maxTokens` carry - * the endpoint's reported token limits (max_input_tokens / max_output_tokens). - * Anything the endpoint omits (cost, and any unreported window) lands on the - * CUSTOM_MODEL_DEFAULT_* values at provider-build time. */ +/** One discovered custom model. `image`/`context`/`maxTokens` are detected from + * the endpoint's /models metadata; anything it omits falls back to a default. */ export interface CustomModel { id: string; /** Accepts image input (Vision-capable). Default false (text-only). */ @@ -239,26 +229,16 @@ export interface CustomModel { maxTokens?: number; } -/** The single custom endpoint's stored config (delta section). `models` are - * discovered from the endpoint's /models on connect; the only user-editable - * fact per model is the `image` capability toggle. */ +/** The single custom endpoint's stored config (delta section). */ export interface CustomProviderSettings { - /** Base URL of the OpenAI-compatible server, NORMALIZED (see - * normalizeBaseUrl): no trailing slash, absolute http(s). Empty/absent = - * not configured. */ + /** Normalized base URL (see normalizeBaseUrl). Empty/absent = not configured. */ baseUrl?: string; - /** Models discovered from the endpoint (refreshed on connect), each with - * its image-capability flag. */ + /** Models discovered from the endpoint (refreshed on connect). */ models?: CustomModel[]; } -/** THE one place the base-URL canonical form is defined (single source — the - * sanitizer stores the normalized value, so no consumer re-derives it and - * the trailing-slash edge case is defined out of existence). Trims; parses - * as an absolute URL (scheme + host lowercased by the parser); drops any - * trailing slashes (the OpenAI SDK appends /chat/completions itself). - * Returns "" for anything that is not a valid absolute http(s) URL — callers - * treat "" as "not configured". */ +/** Canonical base-URL form (single source): trims, requires absolute http(s), + * drops trailing slashes. Returns "" for anything invalid ("not configured"). */ export function normalizeBaseUrl(raw: string): string { const value = raw.trim(); if (!value) return ""; @@ -272,11 +252,8 @@ export function normalizeBaseUrl(raw: string): string { return url.toString().replace(/\/+$/, ""); } -/** THE single source of "is the custom endpoint usable?" (custom-endpoint - * wave): a non-empty base URL. The API key is OPTIONAL (keyless local - * servers), so key presence is deliberately NOT part of this — the picker - * federation, the sync, and the roster all consult this one predicate so the - * rule can never drift between them. */ +/** THE single source of "is the custom endpoint usable?": a non-empty base URL. + * Key presence is deliberately excluded (keyless local servers are allowed). */ export function customProviderUsable( cp: CustomProviderSettings | undefined, ): cp is CustomProviderSettings & { baseUrl: string } { @@ -448,8 +425,7 @@ export interface UserSettings { agent?: AgentSettings; /** The strong/cheap/vision model slots (model slots wave, 2026-09-20). */ models?: ModelsSettings; - /** The single custom OpenAI-compatible endpoint (custom-endpoint wave). - * Non-secret config only; the API key lives in the auth store. */ + /** The custom OpenAI-compatible endpoint (non-secret config only). */ customProvider?: CustomProviderSettings; appearance?: AppearanceSettings; graph?: GraphSettings; @@ -577,16 +553,14 @@ function sanitizeModels(raw: Record): ModelsSettings { return out; } -/** The customProvider section: a normalized base URL (invalid → dropped, i.e. - * treated as unset) and the discovered models (each `{ id, image? }`; ids - * trimmed, empties and duplicate ids dropped, `image` kept only when true; - * a non-array drops the whole list). Clamp-never-reject, like every - * sanitizer here. */ /** A positive integer, or undefined — keeps only meaningful token counts. */ function positiveInt(value: unknown): number | undefined { return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined; } +/** The customProvider section: normalized base URL (invalid → dropped) and + * discovered models (ids trimmed, dupes/empties dropped; image kept only when + * true; token counts only as positive ints). Clamp-never-reject. */ function sanitizeCustomProvider(raw: Record): CustomProviderSettings { const out: CustomProviderSettings = {}; if (typeof raw.baseUrl === "string") { diff --git a/src/server/custom-provider.ts b/src/server/custom-provider.ts index 6c0cf3f..3d42a67 100644 --- a/src/server/custom-provider.ts +++ b/src/server/custom-provider.ts @@ -1,30 +1,8 @@ /** - * THE CUSTOM OPENAI-COMPATIBLE ENDPOINT (custom-endpoint wave): the deep - * module that turns the user's non-secret config (base URL + discovered model - * ids, core/settings.ts `customProvider`) into a real pi-ai provider registered - * in the process-wide `piModels()` collection — so its models fill the same - * strong/cheap/vision slots and background roles as any built-in. - * - * WHY A MODULE OWNS THIS (information hiding): the fact "a custom endpoint is - * an OpenAI-completions provider at a user URL with an optional key" would - * otherwise leak into the catalog, the keys seam, and the settings route. It - * lives here instead; those surfaces consult the pure `customProviderUsable` - * predicate (core) and call `syncCustomProvider`. The single-endpoint scope is - * special-purpose, but the builder is written general (`buildOpenAiCompatible - * Provider`) so a future multi-endpoint wave reuses it unchanged. - * - * LIFECYCLE (no restart, mirroring the keys seam): `syncCustomProvider` is - * idempotent and cheap — called at boot (stores.ts, the composition root) and - * after every connect (POST /api/custom-provider). It replaces - * or removes the provider in `piModels()` by id. The base URL is fixed at - * provider construction, so a base-URL change is a rebuild — which is exactly - * what a re-sync does. - * - * AUTH: pi-ai resolves a provider's key at request time. We read the reserved - * env var through the keys seam (`keyValue` — env-wins, file fallback, honors - * the fresh-user switch), and fall back to a placeholder when none is set so - * KEYLESS local servers (Ollama / LM Studio / llama.cpp) work. The placeholder - * is an internal detail of `resolve()` — no caller ever sees it. + * The custom OpenAI-compatible endpoint: turns the user's non-secret config + * (base URL + discovered models) into a pi-ai provider registered in + * `piModels()`, so its models fill the same slots as any built-in. Keyless + * local servers work via a placeholder key resolved at request time. */ import { createProvider, type ApiKeyAuth, type Api, type Model, type Provider } from "@earendil-works/pi-ai"; @@ -41,35 +19,24 @@ import { } from "../core/settings.ts"; import { keyValue } from "./keys.ts"; -/** Placeholder bearer sent to keyless endpoints. Many local OpenAI-compatible - * servers ignore the Authorization header entirely; the OpenAI client still - * requires *a* key, so we hand it this. Never shown to the user. */ +/** Placeholder bearer for keyless endpoints (the OpenAI client needs *a* key). */ const KEYLESS_PLACEHOLDER = "not-needed"; /** Narrow an unknown to a plain object (not null, not an array). */ const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); -/** The custom endpoint's auth: resolve the reserved key through the keys - * seam, else the keyless placeholder. `resolve` is the only method pi-ai - * needs to treat the provider as configured. */ +/** Auth: the reserved key via the keys seam, else the keyless placeholder. */ const customEndpointAuth: ApiKeyAuth = { name: `${CUSTOM_PROVIDER.label} API key`, resolve: async () => { const key = keyValue(CUSTOM_PROVIDER.envVar)?.trim(); - // Keyless local servers ignore the header; the OpenAI client still - // needs *a* key, so hand it the placeholder. return { auth: { apiKey: key || KEYLESS_PLACEHOLDER }, source: key ? CUSTOM_PROVIDER.envVar : "keyless" }; }, }; -/** Map one discovered custom model to a pi-ai chat model entry. The endpoint's - * /models reports ids plus whatever metadata it exposes; `image` sets `input` - * so a Vision-capable model is selectable for the Vision slot (PDFs, images), - * and `context` / `maxTokens` carry the endpoint's reported token limits. - * Anything the endpoint omits falls back to the CUSTOM_MODEL_DEFAULT_* values - * (self-hosted = free, modest window). `baseUrl` is stamped per-model (pi reads - * it from the model, not the provider). */ +/** Map a discovered model to a pi-ai entry: image→input, context/maxTokens→ + * limits (else CUSTOM_MODEL_DEFAULT_*). baseUrl is stamped per-model. */ function toPiModel(model: CustomModel, baseUrl: string): Model { return { id: model.id, @@ -86,8 +53,7 @@ function toPiModel(model: CustomModel, baseUrl: string): Model { }; } -/** Build a general OpenAI-compatible provider from parts (single-use today, - * general by design). `baseUrl` must already be normalized. */ +/** Build an OpenAI-compatible provider from parts. baseUrl must be normalized. */ export function buildOpenAiCompatibleProvider(input: { baseUrl: string; models: readonly CustomModel[]; @@ -102,21 +68,14 @@ export function buildOpenAiCompatibleProvider(input: { }); } -/** - * Re-sync the custom provider into `piModels()` from the resolved section - * (idempotent). Usable → (re)build and `setProvider` (replaces by id, so a - * base-URL/model change lands with no restart). Not usable → `deleteProvider` - * so it stops federating and any slot pinning one of its models falls back to - * the honest models_not_configured refusal. Never throws: a bad config simply - * leaves the provider absent. - */ +/** Re-sync the provider into piModels() (idempotent): usable → rebuild; + * not usable → remove. Never throws. */ export function syncCustomProvider(cp: CustomProviderSettings | undefined): void { const models = piModels(); if (!customProviderUsable(cp)) { models.deleteProvider(CUSTOM_PROVIDER.id); return; } - // `customProviderUsable` narrowed cp to a defined section with a baseUrl. models.setProvider( buildOpenAiCompatibleProvider({ baseUrl: normalizeBaseUrl(cp.baseUrl), models: cp.models ?? [] }), ); @@ -125,14 +84,9 @@ export function syncCustomProvider(cp: CustomProviderSettings | undefined): void /** Outcome of a model-discovery probe. */ export type DiscoverResult = { ok: true; models: CustomModel[] } | { ok: false; error: string }; -/** - * Probe an endpoint's `GET {baseUrl}/models` (the OpenAI list-models route) - * and return the model ids. The scheme is validated via normalizeBaseUrl - * (http/https only — the SSRF guard: this is a local app, but a user paste of - * `file://` or a stray string must never reach fetch). The key rides as a - * bearer when one is configured; keyless endpoints answer without it. A - * non-2xx or unparseable body is an honest error, never a silent empty list. - */ +/** Probe GET {baseUrl}/models for model ids. Scheme validated (http/https + * only); keyless endpoints answer without a bearer. Non-2xx or unparseable + * body → honest error, never a silent empty list. */ export async function discoverModels(baseUrl: string, apiKey?: string): Promise { const base = normalizeBaseUrl(baseUrl); if (!base) return { ok: false, error: "Base URL must be a valid http(s) URL" }; @@ -157,9 +111,8 @@ export async function discoverModels(baseUrl: string, apiKey?: string): Promise< } } -/** The model rows in a list response, or null when the shape is unrecognized. - * Accepts the OpenAI `{data:[…]}`, the Ollama-style `{models:[…]}`, or a - * bare array. */ +/** Model rows from a list response ({data:[…]} | {models:[…]} | bare array), + * or null if unrecognized. */ function modelRows(body: unknown): unknown[] | null { if (Array.isArray(body)) return body; if (isRecord(body)) { @@ -169,12 +122,8 @@ function modelRows(body: unknown): unknown[] | null { return null; } -/** Does a /models row advertise image input? Endpoints are inconsistent, so - * scan the common modality/capability fields (OpenRouter's - * `architecture.input_modalities`, plus `input_modalities` / `modalities` / - * `supported_modalities` / `capabilities`) for a value mentioning image or - * vision. Absent/unrecognized -> false (text-only); the Keys row exposes a - * manual toggle for endpoints that report nothing. */ +/** Does a /models row advertise image input? Scans the common modality/ + * capability fields; absent or unrecognized → false (text-only). */ function detectImage(row: Record): boolean { const architecture = isRecord(row.architecture) ? row.architecture : undefined; const candidates = [ @@ -190,9 +139,8 @@ function detectImage(row: Record): boolean { return false; } -/** First positive integer among `keys` on a /models row, else undefined. - * Endpoints name the token limits inconsistently, so the caller passes an - * ordered list of candidate field names. */ +/** First positive integer among `keys` on a row (endpoints name limits + * inconsistently). */ function firstPositive(row: Record, keys: readonly string[]): number | undefined { for (const key of keys) { const value = row[key]; @@ -205,10 +153,8 @@ function firstPositive(row: Record, keys: readonly string[]): n const CONTEXT_KEYS = ["max_input_tokens", "context_length", "context_window", "max_context", "n_ctx", "max_seq_len", "max_position_embeddings"] as const; const MAX_TOKEN_KEYS = ["max_output_tokens"] as const; -/** Pull models out of a list response (entries are `{id, ...}` objects or bare - * strings), each carrying its detected image capability and token limits. - * Returns null when the shape is unrecognized - the caller reports an honest - * error rather than a misleading empty "no models". */ +/** Pull models from a list response (objects with `id` or bare strings), each + * with detected image + token limits. Null if the shape is unrecognized. */ function extractModels(body: unknown): CustomModel[] | null { const rows = modelRows(body); if (rows === null) return null; diff --git a/src/server/index.ts b/src/server/index.ts index 591054c..eb5af54 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -45,7 +45,6 @@ export * from "./observability.ts"; export * from "./model-catalog.ts"; export { subscriptionControlsResponse, subscriptionBrowserResponse } from "./web-subscription-http.ts"; export { sessionCost } from "./pricing.ts"; -// The custom OpenAI-compatible endpoint (custom-endpoint wave): the deep -// module that registers the user's endpoint into piModels() and probes it for -// models. Importing it has no side effects; the boot sync lives in stores.ts. +// The custom endpoint module (registers into piModels() + probes for models). +// No import side effects; the boot sync lives in stores.ts. export * from "./custom-provider.ts"; diff --git a/src/server/keys.ts b/src/server/keys.ts index ce05359..286256b 100644 --- a/src/server/keys.ts +++ b/src/server/keys.ts @@ -73,10 +73,8 @@ function boot(): AuthBoot { return g[BOOT_KEY]; } -/** Is `id` a WRITABLE key name — curated catalog, model-provider env - * var (provider-keys interlude), or the custom endpoint's reserved key - * (custom-endpoint wave: writable through the same seam, generic shape - * guard, but NOT part of the test-pinned AUTH_PROVIDER_KEYS table). */ +/** Is `id` a WRITABLE key name — curated catalog, model-provider env var, or + * the custom endpoint's reserved key (writable through the same seam). */ export function isWritableAuthKey(id: string): boolean { return isKnownAuthKey(id) || isProviderEnvVar(id) || id === CUSTOM_PROVIDER.envVar; } diff --git a/src/server/model-catalog.ts b/src/server/model-catalog.ts index 2fa8819..5209e76 100644 --- a/src/server/model-catalog.ts +++ b/src/server/model-catalog.ts @@ -47,10 +47,15 @@ import type { SlotPins, } from "../core/contracts/models.ts"; import type { EffectiveModelSpec, EffectiveSlot } from "../core/settings-resolution.ts"; -import { AUTH_PROVIDER_KEYS } from "../core/auth-keys.ts"; -import { CUSTOM_PROVIDER } from "../core/auth-keys.ts"; -import { customProviderUsable } from "../core/settings.ts"; -import { BACKGROUND_MODEL_ROLES, MODEL_SLOTS, type BackgroundModelRole, type ModelSlotName, type ModelsSettings } from "../core/settings.ts"; +import { AUTH_PROVIDER_KEYS, CUSTOM_PROVIDER } from "../core/auth-keys.ts"; +import { + BACKGROUND_MODEL_ROLES, + MODEL_SLOTS, + customProviderUsable, + type BackgroundModelRole, + type ModelSlotName, + type ModelsSettings, +} from "../core/settings.ts"; /** The picker's client-safe model entry (contract: core/contracts/models.ts). */ export type ModelDto = ModelCatalogEntry; @@ -144,12 +149,8 @@ export function catalogProviders(): CatalogProvider[] { })).concat([ { id: "chatgpt", envVar: "", keyPresent: chatGPTConnected() }, ...NATIVE_SUBSCRIPTIONS.map((p) => ({ id: p.logicalId, envVar: "", keyPresent: nativeSubscriptionConnected(p.providerId) })), - // The custom endpoint (custom-endpoint wave): a row only when usable (a - // base URL is set). Its key is OPTIONAL (keyless local servers), so - // keyPresent here reflects USABILITY, not key presence — the picker - // federates exactly the usable providers. `customProviderUsable` is the - // single source of that rule, so this row and the sync in custom-provider.ts - // can never disagree. + // The custom endpoint: a row only when usable (base URL set). keyPresent + // reflects usability, not key presence (keyless local servers are allowed). ...(customProviderUsable(effectiveSettings().customProvider) ? [{ id: CUSTOM_PROVIDER.id, envVar: CUSTOM_PROVIDER.envVar, keyPresent: true }] : []), diff --git a/src/server/settings-body.ts b/src/server/settings-body.ts index 595067e..a733003 100644 --- a/src/server/settings-body.ts +++ b/src/server/settings-body.ts @@ -386,10 +386,8 @@ export function parseDictationSettingsBody(body: unknown): SettingsBodyResult