Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435import { LoadingState } from "../components/loading-state";import { useEffect, useRef, useState } from "octane";import { Button } from "octane-kumo/components/button";import { Input } from "octane-kumo/components/input";import { Select } from "octane-kumo/components/select";import { ModelEffortSelect } from "../components/model-effort-select";import type { ModelConfiguration, ModelSettings as SavedModels,} from "../../worker/model-settings";import { useMutation } from "@octanejs/tanstack-query";import { useCustomerQuerySession } from "../runtime/customer-query-provider";import { callOwner, type SettingsConnectionProps } from "../runtime/owner-rpc";import { useModelCatalogQuery } from "../runtime/queries/use-model-catalog-query";import { modelSettingsOptions, useModelSettingsQuery,} from "../runtime/queries/use-model-settings-query";
import { MODEL_PROVIDERS, CREDENTIAL_PROVIDERS, isModelProvider, isCredentialProvider, modelLabel, type CredentialProvider,} from "../../shared/model-providers";
type Provider = ModelConfiguration["provider"];
export function ModelSettings({ connection, connected, connecting,}: SettingsConnectionProps) { const cache = useCustomerQuerySession(); const catalogQuery = useModelCatalogQuery(connection); const options = modelSettingsOptions(cache, connection); const savedQuery = useModelSettingsQuery(connection); const catalog = catalogQuery.data; const saved = savedQuery.data; const [draft, setDraft] = useState<ModelConfiguration | null>( saved?.configuration ?? null, ); const [key, setKey] = useState(""); const [keyProvider, setKeyProvider] = useState<CredentialProvider>( saved && isCredentialProvider(saved.configuration.provider) ? saved.configuration.provider : "anthropic", ); const [keyBusy, setKeyBusy] = useState(false); const [keyError, setKeyError] = useState(""); const [notice, setNotice] = useState(""); const generation = useRef(0); const dirty = useRef(false); const current = useRef({ connection, connected }); current.current = { connection, connected }; const valid = (token: number) => generation.current === token && current.current.connected && current.current.connection === connection && !!connection?.isCurrent(); useEffect(() => { if (!saved || dirty.current) return; setDraft(saved.configuration); if (isCredentialProvider(saved.configuration.provider)) setKeyProvider(saved.configuration.provider); }, [saved]); const mutation = useMutation({ mutationFn: async (configuration: ModelConfiguration) => { const token = generation.current; if (!valid(token)) throw new Error("Connection unavailable"); await cache.client.cancelQueries({ queryKey: options.queryKey }); await cache.client.invalidateQueries({ queryKey: options.queryKey, refetchType: "none", }); const value = await callOwner<SavedModels>( connection, "updateModelSettings", [configuration], ); if (!valid(token)) return; void cache.client.cancelQueries({ queryKey: options.queryKey }); cache.client.setQueryData(options.queryKey, value); setDraft(value.configuration); dirty.current = false; setNotice("Model saved. Applies from the next turn."); }, }); useEffect(() => { generation.current++; setKey(""); setKeyBusy(false); setKeyError(""); setNotice(""); mutation.reset(); return () => { generation.current++; }; }, [connection, connected]); const busy = keyBusy || mutation.isPending; const loading = catalogQuery.isPending || savedQuery.isPending; const loadFailed = catalogQuery.isError || savedQuery.isError; const error = keyError || (mutation.isError ? "Could not save the model. Your selection is still here. Try again." : loadFailed ? "Could not load model settings. Try loading them again." : ""); async function save(kind: "model" | "key" | "remove") { if (!connection?.isReady() || !connected || busy || loading || loadFailed) return; if ( (kind === "model" && draft?.provider === "openrouter" && saved?.providers?.openrouter !== true) || (kind === "key" && keyProvider === "openrouter" && saved?.providers?.openrouter !== true) ) return; setKeyError(""); setNotice(""); if (kind === "model") { if (draft) mutation.mutate(draft); return; } // Secrets stay on this explicit action path, outside mutation variables/cache. const token = generation.current; setKeyBusy(true); try { await cache.client.cancelQueries({ queryKey: options.queryKey }); await cache.client.invalidateQueries({ queryKey: options.queryKey, refetchType: "none", }); if (!valid(token)) return; const value = await callOwner<SavedModels>(connection, "setProviderKey", [ keyProvider, kind === "remove" ? null : key, ]); if (!valid(token)) return; void cache.client.cancelQueries({ queryKey: options.queryKey }); cache.client.setQueryData(options.queryKey, value); setNotice( kind === "remove" ? `${MODEL_PROVIDERS[keyProvider].name} key removed.` : `${MODEL_PROVIDERS[keyProvider].name} key saved. The provider has not been contacted to validate it.`, ); } catch { if (valid(token)) setKeyError( `Could not update the ${MODEL_PROVIDERS[keyProvider].name} key. Re-enter the key to retry, or try removing it again.`, ); } finally { if (valid(token)) { setKey(""); setKeyBusy(false); } } }
const locked = !connected || loading || loadFailed || busy; const changed = saved && draft && (saved.configuration.provider !== draft.provider || saved.configuration.model !== draft.model || saved.configuration.effort !== draft.effort); const openRouterEnabled = saved?.providers?.openrouter === true; const draftProviderEnabled = draft?.provider !== "openrouter" || openRouterEnabled; const keyProviderEnabled = keyProvider !== "openrouter" || openRouterEnabled; return ( <section className="settings-section" aria-labelledby="model-heading"> <div className="settings-intro"> <h3 id="model-heading">Model and provider</h3> <p> Choose the installation default for new chats, conversations using the default, and scheduled tasks. Use the chat composer to override the model and effort for one conversation. </p> </div> <div className="settings-section-body"> {loading && (connected || connecting) && !saved && !error && ( <LoadingState label="Loading model settings…" rows={3} field /> )} {(catalogQuery.isFetching || savedQuery.isFetching) && connected && saved && <p role="status">Refreshing model settings…</p>} {error && <p role="alert">{error}</p>} {loadFailed && connected && ( <Button onClick={() => { void catalogQuery.refetch(); void savedQuery.refetch(); }} > Reload model settings </Button> )} {catalog && draft && saved && ( <> <form className="settings-fields" onSubmit={(event) => { event.preventDefault(); void save("model"); }} > <Select label="Provider" items={Object.keys(catalog).map((provider) => ({ value: provider, label: MODEL_PROVIDERS[provider as Provider].name, }))} value={draft.provider} disabled={locked} onValueChange={(value) => { if (!isModelProvider(value)) return; dirty.current = true; setDraft({ provider: value, model: catalog[value][0], } as ModelConfiguration); setKey(""); if (isCredentialProvider(value)) setKeyProvider(value); setNotice(""); }} /> <Select label="Model" items={catalog[draft.provider].map((model) => ({ value: model, label: modelLabel(draft.provider, model), }))} value={draft.model} disabled={locked} onValueChange={(value) => { if (!value) return; dirty.current = true; setDraft({ provider: draft.provider, model: value, } as ModelConfiguration); setNotice(""); }} /> <ModelEffortSelect configuration={draft} disabled={locked} onChange={(value) => { dirty.current = true; setDraft(value); setNotice(""); }} /> <p>{MODEL_PROVIDERS[draft.provider].description}</p> {draft.provider === "openrouter" && openRouterEnabled && ( <p role="status"> OpenRouter is enabled for this installation. </p> )} {draft.provider === "openrouter" && !openRouterEnabled && ( <p role="status"> {saved.providerSetupUrl ? ( <> OpenRouter is not enabled for this installation. Enable it in the control plane before saving an OpenRouter model or key.{" "} <a href={saved.providerSetupUrl} onClick={() => setKey("")} > Enable OpenRouter </a> </> ) : ( "OpenRouter is unavailable because this installation is not connected to the control plane. Reconnect the installation, then return here to enable OpenRouter." )} </p> )} {isCredentialProvider(draft.provider) && saved.credentials[draft.provider] === "missing" && ( <p role="status"> Add an {MODEL_PROVIDERS[draft.provider].name} key before using this model. </p> )} <div className="settings-actions"> <Button type="submit" variant="primary" disabled={ locked || !changed || !draftProviderEnabled || (isCredentialProvider(draft.provider) && saved.credentials[draft.provider] === "missing") } > Save model </Button> </div> {changed && <p role="status">Unsaved model selection</p>} </form> <details className="settings-help"> <summary>Usage and billing</summary> <div> <p> Free allowance models share Workers AI’s 10,000 Neurons per day; additional usage requires payment. Paid Cloudflare models require a paid billing method. Gateway unified billing uses prepaid credits instead of the free allowance. OpenRouter and Anthropic bill separately. </p> <p> Need more Cloudflare credits?{" "} <a href="https://dash.cloudflare.com/5a5b06e36c9a6728a0e835de574c9945/ai/ai-gateway/credits" target="_blank" rel="noopener noreferrer" > Add AI Gateway credits </a>{" "} (opens in a new tab). </p> </div> </details> <form className="settings-fields provider-key" onSubmit={(event) => { event.preventDefault(); void save("key"); }} > <h4>{MODEL_PROVIDERS[keyProvider].name} API key</h4> <Select label="Credential provider" items={CREDENTIAL_PROVIDERS.map((value) => ({ value, label: MODEL_PROVIDERS[value].name, }))} value={keyProvider} disabled={locked} onValueChange={(value) => { if (!isCredentialProvider(value)) return; setKey(""); setNotice(""); setKeyProvider(value); }} /> <p role="status"> {saved.credentials[keyProvider] === "configured" ? "Key configured. This indicates storage only, not provider validation." : `No key configured. Add a key to use ${MODEL_PROVIDERS[keyProvider].name}.`} </p> {!keyProviderEnabled && draft.provider !== "openrouter" && ( <p role="status"> {saved.providerSetupUrl ? ( <> OpenRouter must be enabled before you can save a key.{" "} <a href={saved.providerSetupUrl} onClick={() => setKey("")} > Enable OpenRouter </a> </> ) : ( "OpenRouter setup is unavailable because this installation is not connected to the control plane. Reconnect the installation, then return here." )} </p> )} <Input label={ saved.credentials[keyProvider] === "configured" ? "Replacement API key" : "API key" } type="password" autoComplete="new-password" spellCheck={false} value={key} onValueChange={setKey} minLength={20} maxLength={512} pattern="[!-~]{20,512}" disabled={locked || !keyProviderEnabled} description="Stored encrypted in your installation. Saved keys are never displayed. Use 20–512 characters without spaces." /> <div className="settings-actions"> <Button type="submit" disabled={ locked || !keyProviderEnabled || !/^[\x21-\x7e]{20,512}$/.test(key) } > {saved.credentials[keyProvider] === "configured" ? "Replace key" : "Save key"} </Button> <Button disabled={ locked || saved.credentials[keyProvider] === "missing" } onClick={() => void save("remove")} > Remove key </Button> </div> {saved.configuration.provider === keyProvider && ( <p> Removing the key stops future{" "} {MODEL_PROVIDERS[keyProvider].name} turns until you add a key or save a Workers AI model. </p> )} <p className="muted"> Key drafts clear after a save attempt or lost connection. Re-enter the key to retry. </p> </form> </> )} {notice && <p role="status">{notice}</p>} </div> </section> );}