import { useEffect, useState } from "react"; import { useSecretField, withoutUntouchedSecrets } from "../../../../hooks/useSecretField"; import type { Backend, BedrockAuthMethod, BedrockConfig, LlamaCppConfig, OllamaConfig, OpenAiCompatibleConfig, Project, } from "../../../../lib/types"; import Field, { ConfigGroup, SwitchRow, monoInputClass, selectClass, } from "../../../ui/Field"; import Toggle from "../../../ui/Toggle"; /** Bedrock fields held in the OS keychain, never serialized back to us. */ const BEDROCK_SECRET_KEYS = [ "aws_access_key_id", "aws_secret_access_key", "aws_session_token", "aws_bearer_token", ] as const; /** The same, for the OpenAI-compatible backend. */ const OPENAI_SECRET_KEYS = ["api_key"] as const; export const DEFAULT_BEDROCK_CONFIG: BedrockConfig = { auth_method: "static_credentials", aws_region: "us-east-1", aws_access_key_id: null, aws_secret_access_key: null, aws_session_token: null, aws_profile: null, aws_bearer_token: null, model_id: null, disable_prompt_caching: false, service_tier: null, }; export const DEFAULT_OLLAMA_CONFIG: OllamaConfig = { base_url: "http://host.docker.internal:11434", model_id: null, haiku_model_id: null, }; /** `llama-server` listens on port 8080 unless `--port` says otherwise. */ export const DEFAULT_LLAMACPP_CONFIG: LlamaCppConfig = { base_url: "http://host.docker.internal:8080", model_id: null, haiku_model_id: null, }; export const DEFAULT_OPENAI_COMPATIBLE_CONFIG: OpenAiCompatibleConfig = { base_url: "http://host.docker.internal:4000", api_key: null, model_id: null, haiku_model_id: null, }; /** Shown under the optional per-backend Haiku override. Kept in one place so * all three custom-endpoint backends explain it identically. */ const HAIKU_HINT = "Optional. Claude Code resolves the `haiku` alias to this, and uses it for background work such as conversation titles. Leave blank to reuse the model above — that is what stops background calls failing against a server that only serves one model."; interface Props { project: Project; save: (patch: Partial) => Promise; disabled: boolean; } export default function ModelSection({ project, save, disabled }: Props) { const bedrock = project.bedrock_config ?? DEFAULT_BEDROCK_CONFIG; // Local text state — saved on blur, not on every keystroke. const [bedrockRegion, setBedrockRegion] = useState(bedrock.aws_region); // Secrets are never seeded from `project` — see `useSecretField`. const accessKeyId = useSecretField(project.id); const secretKey = useSecretField(project.id); const sessionToken = useSecretField(project.id); const [profile, setProfile] = useState(bedrock.aws_profile ?? ""); const bearerToken = useSecretField(project.id); const [bedrockModelId, setBedrockModelId] = useState(bedrock.model_id ?? ""); const [serviceTier, setServiceTier] = useState(bedrock.service_tier ?? ""); const [ollamaBaseUrl, setOllamaBaseUrl] = useState( project.ollama_config?.base_url ?? DEFAULT_OLLAMA_CONFIG.base_url, ); const [ollamaModelId, setOllamaModelId] = useState( project.ollama_config?.model_id ?? "", ); const [ollamaHaikuModelId, setOllamaHaikuModelId] = useState( project.ollama_config?.haiku_model_id ?? "", ); const [llamaCppBaseUrl, setLlamaCppBaseUrl] = useState( project.llamacpp_config?.base_url ?? DEFAULT_LLAMACPP_CONFIG.base_url, ); const [llamaCppModelId, setLlamaCppModelId] = useState( project.llamacpp_config?.model_id ?? "", ); const [llamaCppHaikuModelId, setLlamaCppHaikuModelId] = useState( project.llamacpp_config?.haiku_model_id ?? "", ); const [oaiBaseUrl, setOaiBaseUrl] = useState( project.openai_compatible_config?.base_url ?? DEFAULT_OPENAI_COMPATIBLE_CONFIG.base_url, ); const oaiApiKey = useSecretField(project.id); const [oaiModelId, setOaiModelId] = useState( project.openai_compatible_config?.model_id ?? "", ); const [oaiHaikuModelId, setOaiHaikuModelId] = useState( project.openai_compatible_config?.haiku_model_id ?? "", ); // Secret fields are deliberately absent here: `useSecretField` owns its own // reset, and re-seeding one from `project` would write an empty string over // whatever the user had half-typed on any unrelated project update. useEffect(() => { const bc = project.bedrock_config ?? DEFAULT_BEDROCK_CONFIG; setBedrockRegion(bc.aws_region); setProfile(bc.aws_profile ?? ""); setBedrockModelId(bc.model_id ?? ""); setServiceTier(bc.service_tier ?? ""); setOllamaBaseUrl(project.ollama_config?.base_url ?? DEFAULT_OLLAMA_CONFIG.base_url); setOllamaModelId(project.ollama_config?.model_id ?? ""); setOllamaHaikuModelId(project.ollama_config?.haiku_model_id ?? ""); setLlamaCppBaseUrl( project.llamacpp_config?.base_url ?? DEFAULT_LLAMACPP_CONFIG.base_url, ); setLlamaCppModelId(project.llamacpp_config?.model_id ?? ""); setLlamaCppHaikuModelId(project.llamacpp_config?.haiku_model_id ?? ""); setOaiBaseUrl( project.openai_compatible_config?.base_url ?? DEFAULT_OPENAI_COMPATIBLE_CONFIG.base_url, ); setOaiModelId(project.openai_compatible_config?.model_id ?? ""); setOaiHaikuModelId(project.openai_compatible_config?.haiku_model_id ?? ""); }, [project]); const saveBedrock = (patch: Partial) => save({ bedrock_config: withoutUntouchedSecrets( { ...bedrock, ...patch }, patch, BEDROCK_SECRET_KEYS, ), }); const saveOllama = (patch: Partial) => save({ ollama_config: { ...(project.ollama_config ?? DEFAULT_OLLAMA_CONFIG), ...patch }, }); const saveLlamaCpp = (patch: Partial) => save({ llamacpp_config: { ...(project.llamacpp_config ?? DEFAULT_LLAMACPP_CONFIG), ...patch, }, }); const saveOpenAi = (patch: Partial) => save({ openai_compatible_config: withoutUntouchedSecrets( { ...(project.openai_compatible_config ?? DEFAULT_OPENAI_COMPATIBLE_CONFIG), ...patch, }, patch, OPENAI_SECRET_KEYS, ), }); // Defaults to on: projects created before the field existed, and any data // that predates it, should still pick the shared token up. const useSharedToken = project.use_shared_auth_token !== false; const handleBackendChange = (mode: Backend) => { const patch: Partial = { backend: mode }; if (mode === "bedrock" && !project.bedrock_config) patch.bedrock_config = DEFAULT_BEDROCK_CONFIG; if (mode === "ollama" && !project.ollama_config) patch.ollama_config = DEFAULT_OLLAMA_CONFIG; if (mode === "llama_cpp" && !project.llamacpp_config) patch.llamacpp_config = DEFAULT_LLAMACPP_CONFIG; if (mode === "open_ai_compatible" && !project.openai_compatible_config) patch.openai_compatible_config = DEFAULT_OPENAI_COMPATIBLE_CONFIG; save(patch); }; return ( {(id) => ( )} {/* Only Anthropic reads CLAUDE_CODE_OAUTH_TOKEN; the other backends authenticate through their own credentials entirely. */} {project.backend === "anthropic" && (
save({ use_shared_auth_token: value })} disabled={disabled} /> } />
)} {project.backend === "bedrock" && (
{(id) => ( )} {(id) => ( setBedrockRegion(e.target.value)} onBlur={() => saveBedrock({ aws_region: bedrockRegion })} placeholder="us-east-1" disabled={disabled} className={monoInputClass} /> )} {bedrock.auth_method === "static_credentials" && ( <> {(id) => ( accessKeyId.setValue(e.target.value)} onBlur={() => saveBedrock(accessKeyId.patch("aws_access_key_id"))} placeholder="AKIA…" disabled={disabled} className={monoInputClass} /> )} {(id) => ( secretKey.setValue(e.target.value)} onBlur={() => saveBedrock(secretKey.patch("aws_secret_access_key")) } disabled={disabled} className={monoInputClass} /> )} {(id) => ( sessionToken.setValue(e.target.value)} onBlur={() => saveBedrock(sessionToken.patch("aws_session_token")) } disabled={disabled} className={monoInputClass} /> )} )} {bedrock.auth_method === "profile" && ( {(id) => ( setProfile(e.target.value)} onBlur={() => saveBedrock({ aws_profile: profile || null })} placeholder="default" disabled={disabled} className={monoInputClass} /> )} )} {bedrock.auth_method === "bearer_token" && ( {(id) => ( bearerToken.setValue(e.target.value)} onBlur={() => saveBedrock(bearerToken.patch("aws_bearer_token"))} disabled={disabled} className={monoInputClass} /> )} )} {(id) => ( setBedrockModelId(e.target.value)} onBlur={() => saveBedrock({ model_id: bedrockModelId || null })} placeholder="anthropic.claude-sonnet-4-20250514-v1:0" disabled={disabled} className={monoInputClass} /> )} {(id) => ( setServiceTier(e.target.value)} onBlur={() => saveBedrock({ service_tier: serviceTier.trim() || null })} placeholder="(account default)" disabled={disabled} className={monoInputClass} /> )}
)} {project.backend === "ollama" && (
{(id) => ( setOllamaBaseUrl(e.target.value)} onBlur={() => saveOllama({ base_url: ollamaBaseUrl })} placeholder="http://host.docker.internal:11434" disabled={disabled} className={monoInputClass} /> )} {(id) => ( setOllamaModelId(e.target.value)} onBlur={() => saveOllama({ model_id: ollamaModelId || null })} placeholder="qwen3.5:27b" disabled={disabled} className={monoInputClass} /> )} {(id) => ( setOllamaHaikuModelId(e.target.value)} onBlur={() => saveOllama({ haiku_model_id: ollamaHaikuModelId.trim() || null }) } placeholder="(same as the model above)" disabled={disabled} className={monoInputClass} /> )}
)} {project.backend === "llama_cpp" && (
{(id) => ( setLlamaCppBaseUrl(e.target.value)} onBlur={() => saveLlamaCpp({ base_url: llamaCppBaseUrl })} placeholder="http://host.docker.internal:8080" disabled={disabled} className={monoInputClass} /> )} {(id) => ( setLlamaCppModelId(e.target.value)} onBlur={() => saveLlamaCpp({ model_id: llamaCppModelId || null })} placeholder="qwen3.5-coder-30b" disabled={disabled} className={monoInputClass} /> )} {(id) => ( setLlamaCppHaikuModelId(e.target.value)} onBlur={() => saveLlamaCpp({ haiku_model_id: llamaCppHaikuModelId.trim() || null }) } placeholder="(same as the model above)" disabled={disabled} className={monoInputClass} /> )}
)} {project.backend === "open_ai_compatible" && (
{(id) => ( setOaiBaseUrl(e.target.value)} onBlur={() => saveOpenAi({ base_url: oaiBaseUrl })} placeholder="http://host.docker.internal:4000" disabled={disabled} className={monoInputClass} /> )} {(id) => ( oaiApiKey.setValue(e.target.value)} onBlur={() => saveOpenAi(oaiApiKey.patch("api_key"))} placeholder="sk-…" disabled={disabled} className={monoInputClass} /> )} {(id) => ( setOaiModelId(e.target.value)} onBlur={() => saveOpenAi({ model_id: oaiModelId || null })} placeholder="gpt-4o / gemini-pro / …" disabled={disabled} className={monoInputClass} /> )} {(id) => ( setOaiHaikuModelId(e.target.value)} onBlur={() => saveOpenAi({ haiku_model_id: oaiHaikuModelId.trim() || null }) } placeholder="(same as the model above)" disabled={disabled} className={monoInputClass} /> )}
)}
); }