feat(models): wire /api/models dynamic discovery to model selector dialog
Fetches live Claude model list from /api/models on mount; falls back to hardcoded CLAUDE_MODELS.OPTIONS when the endpoint is unavailable. Closes #1
This commit is contained in:
parent
a1517e9f26
commit
18516fd488
1 changed files with 43 additions and 17 deletions
|
|
@ -11,6 +11,7 @@ import {
|
||||||
GEMINI_MODELS,
|
GEMINI_MODELS,
|
||||||
PROVIDERS,
|
PROVIDERS,
|
||||||
} from "../../../../../shared/modelConstants";
|
} from "../../../../../shared/modelConstants";
|
||||||
|
import { authenticatedFetch } from "../../../../utils/api";
|
||||||
import type { ProjectSession, LLMProvider } from "../../../../types/app";
|
import type { ProjectSession, LLMProvider } from "../../../../types/app";
|
||||||
import { NextTaskBanner } from "../../../task-master";
|
import { NextTaskBanner } from "../../../task-master";
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,6 +31,8 @@ import {
|
||||||
const MOD_KEY =
|
const MOD_KEY =
|
||||||
typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl";
|
typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl";
|
||||||
|
|
||||||
|
type ModelOption = { value: string; label: string };
|
||||||
|
|
||||||
type ProviderSelectionEmptyStateProps = {
|
type ProviderSelectionEmptyStateProps = {
|
||||||
selectedSession: ProjectSession | null;
|
selectedSession: ProjectSession | null;
|
||||||
currentSessionId: string | null;
|
currentSessionId: string | null;
|
||||||
|
|
@ -53,22 +56,15 @@ type ProviderSelectionEmptyStateProps = {
|
||||||
type ProviderGroup = {
|
type ProviderGroup = {
|
||||||
id: LLMProvider;
|
id: LLMProvider;
|
||||||
name: string;
|
name: string;
|
||||||
models: { value: string; label: string }[];
|
models: ModelOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROVIDER_GROUPS: ProviderGroup[] = PROVIDERS.map((p) => ({
|
const STATIC_PROVIDER_GROUPS: ProviderGroup[] = PROVIDERS.map((p) => ({
|
||||||
id: p.id as LLMProvider,
|
id: p.id as LLMProvider,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
models: p.models.OPTIONS,
|
models: p.models.OPTIONS,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function getModelConfig(p: LLMProvider) {
|
|
||||||
if (p === "claude") return CLAUDE_MODELS;
|
|
||||||
if (p === "codex") return CODEX_MODELS;
|
|
||||||
if (p === "gemini") return GEMINI_MODELS;
|
|
||||||
return CURSOR_MODELS;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentModel(
|
function getCurrentModel(
|
||||||
p: LLMProvider,
|
p: LLMProvider,
|
||||||
c: string,
|
c: string,
|
||||||
|
|
@ -111,10 +107,36 @@ export default function ProviderSelectionEmptyState({
|
||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
const { isWindowsServer } = useServerPlatform();
|
const { isWindowsServer } = useServerPlatform();
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [claudeModelOptions, setClaudeModelOptions] = useState<ModelOption[]>(CLAUDE_MODELS.OPTIONS);
|
||||||
|
|
||||||
|
// Fetch live Claude model list from the server; fall back to static constants on failure.
|
||||||
|
useEffect(() => {
|
||||||
|
authenticatedFetch("/api/models")
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) return;
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (Array.isArray(data?.claude) && data.claude.length > 0) {
|
||||||
|
setClaudeModelOptions(data.claude);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Static fallback already set as initial state.
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const providerGroups = useMemo<ProviderGroup[]>(
|
||||||
|
() =>
|
||||||
|
STATIC_PROVIDER_GROUPS.map((g) =>
|
||||||
|
g.id === "claude" ? { ...g, models: claudeModelOptions } : g,
|
||||||
|
),
|
||||||
|
[claudeModelOptions],
|
||||||
|
);
|
||||||
|
|
||||||
const visibleProviderGroups = useMemo(
|
const visibleProviderGroups = useMemo(
|
||||||
() => (isWindowsServer ? PROVIDER_GROUPS.filter((p) => p.id !== "cursor") : PROVIDER_GROUPS),
|
() => (isWindowsServer ? providerGroups.filter((p) => p.id !== "cursor") : providerGroups),
|
||||||
[isWindowsServer],
|
[isWindowsServer, providerGroups],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -137,12 +159,16 @@ export default function ProviderSelectionEmptyState({
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentModelLabel = useMemo(() => {
|
const currentModelLabel = useMemo(() => {
|
||||||
const config = getModelConfig(provider);
|
const options: ModelOption[] =
|
||||||
const found = config.OPTIONS.find(
|
provider === "claude"
|
||||||
(o: { value: string; label: string }) => o.value === currentModel,
|
? claudeModelOptions
|
||||||
);
|
: provider === "codex"
|
||||||
return found?.label || currentModel;
|
? CODEX_MODELS.OPTIONS
|
||||||
}, [provider, currentModel]);
|
: provider === "gemini"
|
||||||
|
? GEMINI_MODELS.OPTIONS
|
||||||
|
: CURSOR_MODELS.OPTIONS;
|
||||||
|
return options.find((o) => o.value === currentModel)?.label ?? currentModel;
|
||||||
|
}, [provider, currentModel, claudeModelOptions]);
|
||||||
|
|
||||||
const setModelForProvider = useCallback(
|
const setModelForProvider = useCallback(
|
||||||
(providerId: LLMProvider, modelValue: string) => {
|
(providerId: LLMProvider, modelValue: string) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue