| author | |
| committer | |
| log | 9de85ea16e5228ae68ceb3b52b03ab66f8f1b17c |
| tree | f216e130564597ecc9115e8b3c3089a7cb7bac61 |
| parent | 10dde452f90fc3d4d1aea0add67d13cdfb2567ab |
Co-authored-by: codex <codex@users.noreply.github.com>3 files changed, 64 insertions(+), 24 deletions(-)
apps/web/src/appSettings.test.ts+18-1| ... | ... | @@ -1,6 +1,11 @@ |
| 1 | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | |
| 3 | import { getAppModelOptions, getSlashModelOptions, normalizeCustomModelSlugs } from "./appSettings"; | |
| 3 | import { | |
| 4 | getAppModelOptions, | |
| 5 | getSlashModelOptions, | |
| 6 | normalizeCustomModelSlugs, | |
| 7 | resolveAppModelSelection, | |
| 8 | } from "./appSettings"; | |
| 4 | 9 | |
| 5 | 10 | describe("normalizeCustomModelSlugs", () => { |
| 6 | 11 | it("normalizes aliases, removes built-ins, and deduplicates values", () => { |
| ... | ... | @@ -42,6 +47,18 @@ describe("getAppModelOptions", () => { |
| 42 | 47 | }); |
| 43 | 48 | }); |
| 44 | 49 | |
| 50 | describe("resolveAppModelSelection", () => { | |
| 51 | it("preserves saved custom model slugs instead of falling back to the default", () => { | |
| 52 | expect(resolveAppModelSelection("codex", ["galapagos-alpha"], "galapagos-alpha")).toBe( | |
| 53 | "galapagos-alpha", | |
| 54 | ); | |
| 55 | }); | |
| 56 | ||
| 57 | it("falls back to the provider default when no model is selected", () => { | |
| 58 | expect(resolveAppModelSelection("codex", [], "")).toBe("gpt-5.4"); | |
| 59 | }); | |
| 60 | }); | |
| 61 | ||
| 45 | 62 | describe("getSlashModelOptions", () => { |
| 46 | 63 | it("includes saved custom model slugs for /model command suggestions", () => { |
| 47 | 64 | const options = getSlashModelOptions( |
apps/web/src/appSettings.ts+33-1| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import { useCallback, useSyncExternalStore } from "react"; |
| 2 | 2 | import { Option, Schema } from "effect"; |
| 3 | 3 | import { type ProviderKind } from "@t3tools/contracts"; |
| 4 | import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; | |
| 4 | import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; | |
| 5 | 5 | |
| 6 | 6 | const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1"; |
| 7 | 7 | const MAX_CUSTOM_MODEL_COUNT = 32; |
| ... | ... | @@ -111,6 +111,38 @@ export function getAppModelOptions( |
| 111 | 111 | return options; |
| 112 | 112 | } |
| 113 | 113 | |
| 114 | export function resolveAppModelSelection( | |
| 115 | provider: ProviderKind, | |
| 116 | customModels: readonly string[], | |
| 117 | selectedModel: string | null | undefined, | |
| 118 | ): string { | |
| 119 | const options = getAppModelOptions(provider, customModels, selectedModel); | |
| 120 | const trimmedSelectedModel = selectedModel?.trim(); | |
| 121 | if (trimmedSelectedModel) { | |
| 122 | const direct = options.find((option) => option.slug === trimmedSelectedModel); | |
| 123 | if (direct) { | |
| 124 | return direct.slug; | |
| 125 | } | |
| 126 | ||
| 127 | const byName = options.find( | |
| 128 | (option) => option.name.toLowerCase() === trimmedSelectedModel.toLowerCase(), | |
| 129 | ); | |
| 130 | if (byName) { | |
| 131 | return byName.slug; | |
| 132 | } | |
| 133 | } | |
| 134 | ||
| 135 | const normalizedSelectedModel = normalizeModelSlug(selectedModel, provider); | |
| 136 | if (!normalizedSelectedModel) { | |
| 137 | return getDefaultModel(provider); | |
| 138 | } | |
| 139 | ||
| 140 | return ( | |
| 141 | options.find((option) => option.slug === normalizedSelectedModel)?.slug ?? | |
| 142 | getDefaultModel(provider) | |
| 143 | ); | |
| 144 | } | |
| 145 | ||
| 114 | 146 | export function getSlashModelOptions( |
| 115 | 147 | provider: ProviderKind, |
| 116 | 148 | customModels: readonly string[], |
apps/web/src/components/ChatView.tsx+13-22| ... | ... | @@ -196,7 +196,7 @@ import { Toggle } from "./ui/toggle"; |
| 196 | 196 | import { SidebarTrigger } from "./ui/sidebar"; |
| 197 | 197 | import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; |
| 198 | 198 | import { readNativeApi } from "~/nativeApi"; |
| 199 | import { getAppModelOptions, useAppSettings } from "../appSettings"; | |
| 199 | import { getAppModelOptions, resolveAppModelSelection, useAppSettings } from "../appSettings"; | |
| 200 | 200 | import { |
| 201 | 201 | type ComposerImageAttachment, |
| 202 | 202 | type DraftThreadEnvMode, |
| ... | ... | @@ -762,30 +762,18 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 762 | 762 | selectedProvider, |
| 763 | 763 | activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider), |
| 764 | 764 | ); |
| 765 | const customModelsForSelectedProvider = settings.customCodexModels; | |
| 765 | 766 | const selectedModel = useMemo(() => { |
| 766 | 767 | const draftModel = composerDraft.model; |
| 767 | 768 | if (!draftModel) { |
| 768 | 769 | return baseThreadModel; |
| 769 | 770 | } |
| 770 | ||
| 771 | const providerOptions = getCustomModelOptionsByProvider(settings)[selectedProvider]; | |
| 772 | const directMatch = providerOptions.find((option) => option.slug === draftModel); | |
| 773 | if (directMatch) { | |
| 774 | return directMatch.slug as ModelSlug; | |
| 775 | } | |
| 776 | ||
| 777 | const normalizedDraftModel = normalizeModelSlug(draftModel, selectedProvider); | |
| 778 | if (normalizedDraftModel) { | |
| 779 | const normalizedMatch = providerOptions.find( | |
| 780 | (option) => option.slug === normalizedDraftModel, | |
| 781 | ); | |
| 782 | if (normalizedMatch) { | |
| 783 | return normalizedMatch.slug as ModelSlug; | |
| 784 | } | |
| 785 | } | |
| 786 | ||
| 787 | return resolveModelSlugForProvider(selectedProvider, draftModel); | |
| 788 | }, [baseThreadModel, composerDraft.model, selectedProvider, settings]); | |
| 771 | return resolveAppModelSelection( | |
| 772 | selectedProvider, | |
| 773 | customModelsForSelectedProvider, | |
| 774 | draftModel, | |
| 775 | ) as ModelSlug; | |
| 776 | }, [baseThreadModel, composerDraft.model, customModelsForSelectedProvider, selectedProvider]); | |
| 789 | 777 | const reasoningOptions = getReasoningEffortOptions(selectedProvider); |
| 790 | 778 | const supportsReasoningEffort = reasoningOptions.length > 0; |
| 791 | 779 | const selectedEffort = composerDraft.effort ?? getDefaultReasoningEffort(selectedProvider); |
| ... | ... | @@ -2979,9 +2967,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2979 | 2967 | scheduleComposerFocus(); |
| 2980 | 2968 | return; |
| 2981 | 2969 | } |
| 2982 | const resolvedModel = resolveModelSlugForProvider(provider, model); | |
| 2983 | 2970 | setComposerDraftProvider(activeThread.id, provider); |
| 2984 | setComposerDraftModel(activeThread.id, resolvedModel); | |
| 2971 | setComposerDraftModel( | |
| 2972 | activeThread.id, | |
| 2973 | resolveAppModelSelection(provider, settings.customCodexModels, model), | |
| 2974 | ); | |
| 2985 | 2975 | scheduleComposerFocus(); |
| 2986 | 2976 | }, |
| 2987 | 2977 | [ |
| ... | ... | @@ -2990,6 +2980,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2990 | 2980 | scheduleComposerFocus, |
| 2991 | 2981 | setComposerDraftModel, |
| 2992 | 2982 | setComposerDraftProvider, |
| 2983 | settings.customCodexModels, | |
| 2993 | 2984 | ], |
| 2994 | 2985 | ); |
| 2995 | 2986 | const onEffortSelect = useCallback( |