mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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.
This commit is contained in:
1 parent
0c7059804b
commit
ee620b2707
11 files changed
+268
-6
No files matched your search
+23
-4
@@ -4477,10 +4477,10 @@ paths:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Read the calling user's AI-provider configuration
|
||||
description: "Returns the user's selected provider/model/baseUrl plus the effective availability: `aiAvailable` is true
|
||||
when any provider can serve the user (BYOK, self-hosted, or the operator's server-managed key), and `managedBy`
|
||||
reports which. iOS gates Coach visibility off these two fields. Key material is reported as presence + a 4-char
|
||||
masked preview only."
|
||||
description: "Returns the user's selected provider/model/baseUrl (plus the OpenAI-compatible gateway's own base URL and
|
||||
model) and the effective availability: `aiAvailable` is true when any provider can serve the user (BYOK,
|
||||
self-hosted, or the operator's server-managed key), and `managedBy` reports which. iOS gates Coach visibility
|
||||
off these two fields. Key material is reported as presence + a 4-char masked preview only."
|
||||
responses:
|
||||
"200":
|
||||
description: Resolved AI-provider configuration.
|
||||
@@ -21942,6 +21942,7 @@ components:
|
||||
- OPENAI
|
||||
- ANTHROPIC
|
||||
- LOCAL
|
||||
- OPENAI_COMPATIBLE
|
||||
- CHATGPT_OAUTH
|
||||
- type: "null"
|
||||
description: The user's selected provider, or null when none is set.
|
||||
@@ -21984,6 +21985,21 @@ components:
|
||||
- type: string
|
||||
- type: "null"
|
||||
description: "`...` + last 4 chars of the stored OpenAI key, or null."
|
||||
compatBaseUrl:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
description: "v1.33.1 (#470) — base URL of the OpenAI-compatible gateway (LiteLLM / OpenRouter / vLLM). Its own column:
|
||||
the OpenAI provider stays pinned to api.openai.com and never reads it."
|
||||
compatModel:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
description: Model the gateway should route. Null falls back to `model`; with neither set the gateway entry does not
|
||||
resolve.
|
||||
hasCompatKey:
|
||||
type: boolean
|
||||
description: Whether a bearer is stored for the gateway. The bearer is optional — a gateway without auth needs none.
|
||||
responseTimeoutSeconds:
|
||||
anyOf:
|
||||
- type: integer
|
||||
@@ -22003,6 +22019,9 @@ components:
|
||||
- hasLocalKey
|
||||
- hasOpenaiKey
|
||||
- openaiKeyPreview
|
||||
- compatBaseUrl
|
||||
- compatModel
|
||||
- hasCompatKey
|
||||
- responseTimeoutSeconds
|
||||
additionalProperties: false
|
||||
description: The calling user's AI-provider configuration plus the effective availability (`aiAvailable` + `managedBy`).
|
||||
|
||||
@@ -4937,6 +4937,7 @@
|
||||
"openai": "OpenAI (eigener Key)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Lokales Modell",
|
||||
"openai-compatible": "OpenAI-kompatibles Gateway",
|
||||
"admin-openai": "Admin-OpenAI"
|
||||
},
|
||||
"title": "Fallback-Kette",
|
||||
@@ -4963,8 +4964,14 @@
|
||||
"openai": "OpenAI (eigener API-Key)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Lokales Modell (OpenAI-kompatibel)",
|
||||
"openai-compatible": "OpenAI-kompatibles Gateway",
|
||||
"admin-openai": "Admin-OpenAI"
|
||||
},
|
||||
"compat": {
|
||||
"description": "Ein vollwertiger Anbieter für jeden Endpunkt, der die OpenAI-API /v1/chat/completions spricht: LiteLLM, OpenRouter, vLLM oder ein anderes Gateway. Die Basis-URL endet auf /v1, der API-Key ist optional, und das Modell ist das, was dein Gateway weiterleitet. Dein OpenAI-Key erreicht diesen Endpunkt nie — dieser Anbieter hat einen eigenen.",
|
||||
"keyLabel": "API-Key (optional)",
|
||||
"modelHint": "Der Modellname, den dein Gateway erwartet. Bleibt das Feld leer, wird das für deine anderen Anbieter gespeicherte Modell verwendet; ist auch das leer, wird dieser Anbieter übersprungen."
|
||||
},
|
||||
"openai": {
|
||||
"modelSelect": "Modell",
|
||||
"modelOptionCustom": "Eigener Slug…",
|
||||
|
||||
@@ -4937,6 +4937,7 @@
|
||||
"openai": "OpenAI (your key)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Local model",
|
||||
"openai-compatible": "OpenAI-compatible gateway",
|
||||
"admin-openai": "Admin OpenAI"
|
||||
},
|
||||
"title": "Fallback chain",
|
||||
@@ -4963,8 +4964,14 @@
|
||||
"openai": "OpenAI (your API key)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Local model (OpenAI-compatible)",
|
||||
"openai-compatible": "OpenAI-compatible gateway",
|
||||
"admin-openai": "Admin-provided OpenAI"
|
||||
},
|
||||
"compat": {
|
||||
"description": "A first-class provider for any endpoint that speaks the OpenAI /v1/chat/completions API: LiteLLM, OpenRouter, vLLM or another gateway. The base URL ends with /v1, the API key is optional, and the model is whatever your gateway routes. Your OpenAI key never reaches this endpoint — this provider has its own.",
|
||||
"keyLabel": "API key (optional)",
|
||||
"modelHint": "The model name your gateway expects. Left blank, the model saved for your other providers is used; with neither set, this provider is skipped."
|
||||
},
|
||||
"openai": {
|
||||
"modelSelect": "Model",
|
||||
"modelOptionCustom": "Custom slug…",
|
||||
|
||||
@@ -4937,6 +4937,7 @@
|
||||
"openai": "OpenAI (tu clave)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Modelo local",
|
||||
"openai-compatible": "Pasarela compatible con OpenAI",
|
||||
"admin-openai": "OpenAI de administrador"
|
||||
},
|
||||
"title": "Cadena de respaldo",
|
||||
@@ -4963,8 +4964,14 @@
|
||||
"openai": "OpenAI (tu clave de API)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Modelo local (compatible con OpenAI)",
|
||||
"openai-compatible": "Pasarela compatible con OpenAI",
|
||||
"admin-openai": "OpenAI proporcionado por el administrador"
|
||||
},
|
||||
"compat": {
|
||||
"description": "Un proveedor de pleno derecho para cualquier endpoint que hable la API /v1/chat/completions de OpenAI: LiteLLM, OpenRouter, vLLM u otra pasarela. La URL base termina en /v1, la clave de API es opcional y el modelo es el que enrute tu pasarela. Tu clave de OpenAI nunca llega a este endpoint: este proveedor tiene la suya.",
|
||||
"keyLabel": "Clave de API (opcional)",
|
||||
"modelHint": "El nombre de modelo que espera tu pasarela. Si lo dejas vacío, se usa el modelo guardado para tus otros proveedores; si tampoco hay uno, este proveedor se omite."
|
||||
},
|
||||
"openai": {
|
||||
"modelSelect": "Modelo",
|
||||
"modelOptionCustom": "Slug personalizado…",
|
||||
|
||||
@@ -4937,6 +4937,7 @@
|
||||
"openai": "OpenAI (votre clé)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Modèle local",
|
||||
"openai-compatible": "Passerelle compatible OpenAI",
|
||||
"admin-openai": "OpenAI administrateur"
|
||||
},
|
||||
"title": "Chaîne de secours",
|
||||
@@ -4963,8 +4964,14 @@
|
||||
"openai": "OpenAI (votre clé API)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Modèle local (compatible OpenAI)",
|
||||
"openai-compatible": "Passerelle compatible OpenAI",
|
||||
"admin-openai": "OpenAI fourni par l’administrateur"
|
||||
},
|
||||
"compat": {
|
||||
"description": "Un fournisseur à part entière pour tout point de terminaison qui parle l'API /v1/chat/completions d'OpenAI : LiteLLM, OpenRouter, vLLM ou une autre passerelle. L'URL de base se termine par /v1, la clé API est facultative et le modèle est celui que votre passerelle achemine. Votre clé OpenAI n'atteint jamais ce point de terminaison — ce fournisseur a la sienne.",
|
||||
"keyLabel": "Clé API (facultative)",
|
||||
"modelHint": "Le nom de modèle attendu par votre passerelle. Laissé vide, le modèle enregistré pour vos autres fournisseurs est utilisé ; si aucun n'est défini, ce fournisseur est ignoré."
|
||||
},
|
||||
"openai": {
|
||||
"modelSelect": "Modèle",
|
||||
"modelOptionCustom": "Slug personnalisé…",
|
||||
|
||||
@@ -4937,6 +4937,7 @@
|
||||
"openai": "OpenAI (la tua chiave)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Modello locale",
|
||||
"openai-compatible": "Gateway compatibile con OpenAI",
|
||||
"admin-openai": "OpenAI amministratore"
|
||||
},
|
||||
"title": "Catena di fallback",
|
||||
@@ -4963,8 +4964,14 @@
|
||||
"openai": "OpenAI (la tua chiave API)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Modello locale (compatibile con OpenAI)",
|
||||
"openai-compatible": "Gateway compatibile con OpenAI",
|
||||
"admin-openai": "OpenAI fornito dall’amministratore"
|
||||
},
|
||||
"compat": {
|
||||
"description": "Un provider a pieno titolo per qualsiasi endpoint che parli l'API /v1/chat/completions di OpenAI: LiteLLM, OpenRouter, vLLM o un altro gateway. L'URL di base termina con /v1, la chiave API è facoltativa e il modello è quello instradato dal gateway. La tua chiave OpenAI non raggiunge mai questo endpoint: questo provider ha la propria.",
|
||||
"keyLabel": "Chiave API (facoltativa)",
|
||||
"modelHint": "Il nome del modello che il gateway si aspetta. Se lasci il campo vuoto viene usato il modello salvato per gli altri provider; se non è impostato nessuno dei due, questo provider viene saltato."
|
||||
},
|
||||
"openai": {
|
||||
"modelSelect": "Modello",
|
||||
"modelOptionCustom": "Slug personalizzato…",
|
||||
|
||||
@@ -4937,6 +4937,7 @@
|
||||
"openai": "OpenAI (Twój klucz)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Model lokalny",
|
||||
"openai-compatible": "Brama zgodna z OpenAI",
|
||||
"admin-openai": "OpenAI administratora"
|
||||
},
|
||||
"title": "Łańcuch zapasowy",
|
||||
@@ -4963,8 +4964,14 @@
|
||||
"openai": "OpenAI (Twój klucz API)",
|
||||
"anthropic": "Anthropic (Claude)",
|
||||
"local": "Model lokalny (zgodny z OpenAI)",
|
||||
"openai-compatible": "Brama zgodna z OpenAI",
|
||||
"admin-openai": "OpenAI udostępniony przez administratora"
|
||||
},
|
||||
"compat": {
|
||||
"description": "Pełnoprawny dostawca dla każdego punktu końcowego mówiącego API /v1/chat/completions OpenAI: LiteLLM, OpenRouter, vLLM lub inna brama. Adres bazowy kończy się na /v1, klucz API jest opcjonalny, a model to ten, który kieruje twoja brama. Twój klucz OpenAI nigdy nie trafia do tego punktu końcowego — ten dostawca ma własny.",
|
||||
"keyLabel": "Klucz API (opcjonalny)",
|
||||
"modelHint": "Nazwa modelu, której oczekuje twoja brama. Pozostawione puste, użyty zostanie model zapisany dla pozostałych dostawców; gdy nie ustawiono żadnego, ten dostawca jest pomijany."
|
||||
},
|
||||
"openai": {
|
||||
"modelSelect": "Model",
|
||||
"modelOptionCustom": "Niestandardowy slug…",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { AnthropicProviderForm } from "./anthropic-provider-form";
|
||||
import { AutoReadCard } from "./auto-read-card";
|
||||
import { CentralCodexSwitch } from "./central-codex-switch";
|
||||
import { CodexProviderForm } from "./codex-provider-form";
|
||||
import { CompatProviderForm } from "./compat-provider-form";
|
||||
import { FallbackChainCard } from "./fallback-chain-card";
|
||||
import { LocalProviderForm } from "./local-provider-form";
|
||||
import { OpenAIProviderForm } from "./openai-provider-form";
|
||||
@@ -344,6 +345,9 @@ function ProviderConfigCard({
|
||||
{provider === "local" && (
|
||||
<LocalProviderForm userProvider={userProvider} />
|
||||
)}
|
||||
{provider === "openai-compatible" && (
|
||||
<CompatProviderForm userProvider={userProvider} />
|
||||
)}
|
||||
{provider === "admin-openai" && (
|
||||
<AdminOpenAIProviderForm hasAdminKey={insightsSettings?.hasAdminKey} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
* OpenAI-compatible gateway form (#470) — base URL + optional key +
|
||||
* model. Three fields of its own, written to three columns of its own:
|
||||
* the OpenAI provider stays pinned to api.openai.com and cannot see any
|
||||
* of them, and the gateway never sees the OpenAI key.
|
||||
* ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
import { useRef, useState, type FormEvent } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Loader2, Save } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { apiPatch } from "@/lib/api/api-fetch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
import { uiToLegacyProviderEnum, type UserAIProvider } from "./shared";
|
||||
|
||||
export function CompatProviderForm({
|
||||
userProvider,
|
||||
}: {
|
||||
userProvider: UserAIProvider | null | undefined;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [model, setModel] = useState("");
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [ok, setOk] = useState(false);
|
||||
const submitInFlightRef = useRef(false);
|
||||
|
||||
// Seed from the server once per distinct persisted value, the same
|
||||
// render-time pattern the sibling forms use (no setState-in-effect).
|
||||
const seededKey =
|
||||
userProvider != null
|
||||
? `${userProvider.compatBaseUrl ?? ""}|${userProvider.compatModel ?? ""}`
|
||||
: null;
|
||||
const [previousSeed, setPreviousSeed] = useState<string | null>(null);
|
||||
if (seededKey && seededKey !== previousSeed) {
|
||||
setPreviousSeed(seededKey);
|
||||
setBaseUrl(userProvider?.compatBaseUrl ?? "");
|
||||
setModel(userProvider?.compatModel ?? "");
|
||||
}
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const body: Record<string, unknown> = {
|
||||
provider: uiToLegacyProviderEnum("openai-compatible"),
|
||||
compatBaseUrl: baseUrl.trim() || null,
|
||||
compatModel: model.trim() || null,
|
||||
};
|
||||
if (apiKey.trim()) body.compatKey = apiKey.trim();
|
||||
await apiPatch("/api/user/ai-provider", body);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setOk(true);
|
||||
setMsg(t("settings.ai.saved"));
|
||||
setApiKey("");
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.userAiProvider() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.insightsRoot() });
|
||||
},
|
||||
onError: (e) => {
|
||||
setOk(false);
|
||||
setMsg(e instanceof Error ? e.message : t("settings.ai.errorGeneric"));
|
||||
},
|
||||
onSettled: () => {
|
||||
submitInFlightRef.current = false;
|
||||
},
|
||||
});
|
||||
|
||||
function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (submitInFlightRef.current || saveMutation.isPending) return;
|
||||
submitInFlightRef.current = true;
|
||||
saveMutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
data-testid="ai-provider-config-openai-compatible"
|
||||
className="space-y-4"
|
||||
onSubmit={submit}
|
||||
noValidate
|
||||
>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.ai.compat.description")}
|
||||
</p>
|
||||
<div>
|
||||
<Label htmlFor="ai-compat-base-url">
|
||||
{t("settings.ai.baseUrlLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-compat-base-url"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder="https://litellm.example.com/v1"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="ai-compat-key">
|
||||
{t("settings.ai.compat.keyLabel")}
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="ai-compat-key"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={
|
||||
userProvider?.hasCompatKey ? t("settings.ai.savedShort") : ""
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="ai-compat-model">{t("settings.ai.modelLabel")}</Label>
|
||||
<Input
|
||||
id="ai-compat-model"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder="anthropic/claude-sonnet-4-6"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{t("settings.ai.compat.modelHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
aria-busy={saveMutation.isPending || undefined}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
{t("settings.ai.saveCta")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{msg && (
|
||||
<p className={`text-xs ${ok ? "text-success" : "text-destructive"}`}>
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,12 @@ export interface UserAIProvider {
|
||||
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;
|
||||
}
|
||||
@@ -46,6 +52,7 @@ export const PROVIDER_TYPES = [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"local",
|
||||
"openai-compatible",
|
||||
"admin-openai",
|
||||
] as const;
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
@@ -65,6 +72,12 @@ export interface ProviderChainData {
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 },
|
||||
@@ -114,6 +127,8 @@ export function uiToLegacyProviderEnum(p: ProviderType): string | null {
|
||||
return "ANTHROPIC";
|
||||
case "local":
|
||||
return "LOCAL";
|
||||
case "openai-compatible":
|
||||
return "OPENAI_COMPATIBLE";
|
||||
case "codex":
|
||||
case "admin-openai":
|
||||
default:
|
||||
|
||||
@@ -346,7 +346,13 @@ const profileUpdateResponse = z
|
||||
const aiProviderResponse = z
|
||||
.object({
|
||||
provider: z
|
||||
.enum(["OPENAI", "ANTHROPIC", "LOCAL", "CHATGPT_OAUTH"])
|
||||
.enum([
|
||||
"OPENAI",
|
||||
"ANTHROPIC",
|
||||
"LOCAL",
|
||||
"OPENAI_COMPATIBLE",
|
||||
"CHATGPT_OAUTH",
|
||||
])
|
||||
.nullable()
|
||||
.describe("The user's selected provider, or null when none is set."),
|
||||
model: z.string().nullable(),
|
||||
@@ -376,6 +382,23 @@ const aiProviderResponse = z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe("`...` + last 4 chars of the stored OpenAI key, or null."),
|
||||
compatBaseUrl: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
"v1.33.1 (#470) — base URL of the OpenAI-compatible gateway (LiteLLM / OpenRouter / vLLM). Its own column: the OpenAI provider stays pinned to api.openai.com and never reads it.",
|
||||
),
|
||||
compatModel: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
"Model the gateway should route. Null falls back to `model`; with neither set the gateway entry does not resolve.",
|
||||
),
|
||||
hasCompatKey: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"Whether a bearer is stored for the gateway. The bearer is optional — a gateway without auth needs none.",
|
||||
),
|
||||
responseTimeoutSeconds: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -396,7 +419,7 @@ export const profilePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Auth"],
|
||||
summary: "Read the calling user's AI-provider configuration",
|
||||
description:
|
||||
"Returns the user's selected provider/model/baseUrl plus the effective availability: `aiAvailable` is true when any provider can serve the user (BYOK, self-hosted, or the operator's server-managed key), and `managedBy` reports which. iOS gates Coach visibility off these two fields. Key material is reported as presence + a 4-char masked preview only.",
|
||||
"Returns the user's selected provider/model/baseUrl (plus the OpenAI-compatible gateway's own base URL and model) and the effective availability: `aiAvailable` is true when any provider can serve the user (BYOK, self-hosted, or the operator's server-managed key), and `managedBy` reports which. iOS gates Coach visibility off these two fields. Key material is reported as presence + a 4-char masked preview only.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Resolved AI-provider configuration.",
|
||||
|
||||
Reference in new issue
Block a user