mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
The Anthropic vision check listed the Claude 3 and 4 families by prefix, so a key pinned to claude-sonnet-5, claude-opus-5 or a Fable model was told the provider cannot read images, and lab and document OCR refused the upload even though the model handles image and PDF blocks. Replace the per-release prefix list with a rule: any claude-<tier>-<major> slug from the 4 family upward, plus every claude-fable-* model, counts as vision-capable. Claude 3 handling is unchanged. The Settings preset list gains claude-sonnet-5 and claude-opus-5 alongside the existing entries.
175 lines
5.8 KiB
TypeScript
175 lines
5.8 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-sonnet-5",
|
|
"claude-opus-5",
|
|
"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);
|
|
}
|