Files
HealthLog/src/components/settings/ai/shared.ts
T
Marc-André Bombeck ee620b2707 feat(settings): configure the OpenAI-compatible gateway from Settings
Its own form with its own three fields, its own entry in the provider
dropdown and in the fallback-chain editor, and copy in six locales that
says plainly where the OpenAI key does not go.

The chain editor offers the entry but the default chain does not carry
it: it resolves only against a base URL somebody saved.

The read endpoint reports the gateway's base URL and model, and the
bearer as presence only. OpenAPI regenerated.
2026-07-26 22:27:48 +02:00

173 lines
5.7 KiB
TypeScript

/**
* Shared types, constants, and pure helpers for the Settings → AI
* provider components. Extracted from the former 2k-LOC
* `ai-section.tsx` monolith; one provider form per file lives next to
* this module under `src/components/settings/ai/`.
*/
export interface InsightsSettings {
codexStatus: string;
codexConnectedAt: string | null;
hasAdminKey: boolean;
/** True when the operator has set `CODEX_OAUTH_CLIENT_ID` on this
* instance. The UI hides the "Connect with ChatGPT" button when
* false to avoid the v1.4.2 dead-end where the click bounced the
* user to chatgpt.com without any OAuth flow. */
codexOauthConfigured?: boolean;
/** True when the operator has connected the shared central Codex. Drives
* whether the per-user "use the server's shared AI access" switch shows. */
centralCodexAvailable?: boolean;
/** The user's own opt-in to the operator's shared central Codex. */
useCentralCodex?: boolean;
privacyMode: string;
lastInsightAt: string | null;
}
export interface UserAIProvider {
provider: string | null;
model: string | null;
baseUrl: string | null;
hasAnthropicKey: boolean;
anthropicKeyPreview: string | null;
hasLocalKey: boolean;
hasOpenaiKey: boolean;
openaiKeyPreview: string | null;
// v1.33.1 (#470) — the OpenAI-compatible gateway's own configuration. Its
// own fields, because its own columns: the OpenAI provider is pinned to
// api.openai.com and shares nothing with a user-supplied host.
compatBaseUrl: string | null;
compatModel: string | null;
hasCompatKey: boolean;
// v1.22 (#89) — per-user response timeout, in seconds (null = default).
responseTimeoutSeconds: number | null;
}
/**
* v1.4.16 phase B2 — provider tags exposed to the UI. Mirrors
* `PROVIDER_CHAIN_TYPES` server-side; kept as a const-array here to
* avoid pulling a server-only module into the client bundle.
*/
export const PROVIDER_TYPES = [
"codex",
"openai",
"anthropic",
"local",
"openai-compatible",
"admin-openai",
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];
export interface ChainEntry {
providerType: ProviderType;
enabled: boolean;
}
export interface ProviderChainData {
activeProvider: ProviderType | null;
cachedActiveProvider: ProviderType | null;
configuredChain: {
providerType: ProviderType;
enabled: boolean;
available: boolean;
}[];
}
/**
* The chain a user starts with and returns to on reset. `openai-compatible`
* is deliberately absent: it resolves only against a base URL somebody
* configured, so a default entry would be dead weight for everyone else.
* Users add it from the chain editor once their gateway is saved.
*/
export const DEFAULT_CHAIN: readonly ChainEntry[] = [
{ providerType: "codex", enabled: true },
{ providerType: "openai", enabled: true },
{ providerType: "anthropic", enabled: true },
{ providerType: "local", enabled: true },
{ providerType: "admin-openai", enabled: true },
];
/**
* OpenAI model presets — the wire-up keeps users out of the freetext
* box for the common case. The `__custom__` sentinel surfaces a
* freetext input so power users can target preview models or
* fine-tunes (e.g. `gpt-5`, which is admitted via the env override on
* the Codex backend but not yet a documented OpenAI model id).
*/
export const OPENAI_MODEL_PRESETS = [
"gpt-4o",
"gpt-4o-mini",
"gpt-4-turbo",
] as const;
export const ANTHROPIC_MODEL_PRESETS = [
"claude-sonnet-4-6",
"claude-opus-4-7",
"claude-haiku-4-5",
"claude-3-5-sonnet-latest",
] as const;
export const LOCAL_MODEL_PRESETS = [
"llama3.1:8b",
"llama3.1:70b",
"mistral",
"qwen2.5",
] as const;
export const CUSTOM_MODEL_SENTINEL = "__custom__";
/**
* Map UI provider tag → the `aiProvider` enum understood by the
* legacy single-result `resolveProvider()` resolver. `codex` and
* `admin-openai` aren't user-level provider columns (they're handled
* by Codex OAuth + admin-key fallback), so they map to null and the
* legacy column is left blank.
*/
export function uiToLegacyProviderEnum(p: ProviderType): string | null {
switch (p) {
case "openai":
return "OPENAI";
case "anthropic":
return "ANTHROPIC";
case "local":
return "LOCAL";
case "openai-compatible":
return "OPENAI_COMPATIBLE";
case "codex":
case "admin-openai":
default:
return null;
}
}
// Map the connection-test failure category (a stable, secret-free code from
// /api/ai/test) to a localised message. Falls back to the server's plain
// English `reason` for any unmapped / legacy code so a German operator never
// sees an untranslated sentence in the otherwise-localised Settings panel.
export function localiseTestReason(
t: (key: string, params?: Record<string, string | number>) => string,
reasonCode: string | undefined,
reason: string | undefined,
httpStatus?: number | null,
): string | undefined {
switch (reasonCode) {
case "credentials":
return t("settings.ai.testReasonCredentials");
case "rate_limited":
return t("settings.ai.testReasonRateLimited");
case "server_error":
return t("settings.ai.testReasonServerError");
// v1.28.28 (#470) — the endpoint answered with a 4xx: a request-shape /
// model-name problem, not connectivity. Falls back to the server's plain
// reason when the status did not arrive (legacy response shape).
case "bad_request":
return httpStatus != null
? t("settings.ai.testReasonBadRequest", { status: httpStatus })
: reason;
case "unreachable":
return t("settings.ai.testReasonUnreachable");
default:
return reason;
}
}
export function isProviderType(v: string | null): v is ProviderType {
return v != null && (PROVIDER_TYPES as readonly string[]).includes(v);
}