authorgravatar for 312235+adambuchweitz@users.noreply.github.comAdam Buchweitz <312235+adambuchweitz@users.noreply.github.com> 2026-05-22 09:02:35-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-22 07:02:35-07:00
log4f0f24f055fe5f5346f7e73372e8cdc167e052f9
tree59ee6d9de976ebca4b9e573939ae61b082d16d78
parentd1e85c4e8fdef82fbaded9539532b754080419e0
signature Signed by PGP key B5690EEEBB952194

fix: maintain reasoning selections for multiple providers (#2760)


5 files changed, 78 insertions(+), 9 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+13-4
...@@ -727,9 +727,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -727,9 +727,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
727 model: selectedModel,727 model: selectedModel,
728 models: selectedProviderModels,728 models: selectedProviderModels,
729 prompt,729 prompt,
730 modelOptions: composerModelOptions?.[selectedProvider],730 modelOptions: composerModelOptions?.[selectedInstanceId],
731 }),731 }),
732 [composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels],732 [
733 composerModelOptions,
734 prompt,
735 selectedInstanceId,
736 selectedModel,
737 selectedProvider,
738 selectedProviderModels,
739 ],
733 );740 );
734741
735 const selectedPromptEffort = composerProviderState.promptEffort;742 const selectedPromptEffort = composerProviderState.promptEffort;
...@@ -1018,21 +1025,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1018,21 +1025,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
10181025
1019 const providerTraitsMenuContent = renderProviderTraitsMenuContent({1026 const providerTraitsMenuContent = renderProviderTraitsMenuContent({
1020 provider: selectedProvider,1027 provider: selectedProvider,
1028 instanceId: selectedInstanceId,
1021 ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),1029 ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
1022 ...(routeKind === "draft" && draftId ? { draftId } : {}),1030 ...(routeKind === "draft" && draftId ? { draftId } : {}),
1023 model: selectedModel,1031 model: selectedModel,
1024 models: selectedProviderModels,1032 models: selectedProviderModels,
1025 modelOptions: composerModelOptions?.[selectedProvider],1033 modelOptions: composerModelOptions?.[selectedInstanceId],
1026 prompt,1034 prompt,
1027 onPromptChange: setPromptFromTraits,1035 onPromptChange: setPromptFromTraits,
1028 });1036 });
1029 const providerTraitsPicker = renderProviderTraitsPicker({1037 const providerTraitsPicker = renderProviderTraitsPicker({
1030 provider: selectedProvider,1038 provider: selectedProvider,
1039 instanceId: selectedInstanceId,
1031 ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),1040 ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
1032 ...(routeKind === "draft" && draftId ? { draftId } : {}),1041 ...(routeKind === "draft" && draftId ? { draftId } : {}),
1033 model: selectedModel,1042 model: selectedModel,
1034 models: selectedProviderModels,1043 models: selectedProviderModels,
1035 modelOptions: composerModelOptions?.[selectedProvider],1044 modelOptions: composerModelOptions?.[selectedInstanceId],
1036 prompt,1045 prompt,
1037 onPromptChange: setPromptFromTraits,1046 onPromptChange: setPromptFromTraits,
1038 });1047 });
apps/web/src/components/chat/TraitsPicker.tsx+7-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import {1import {
2 type ProviderDriverKind,2 type ProviderDriverKind,
3 type ProviderInstanceId,
3 type ProviderOptionDescriptor,4 type ProviderOptionDescriptor,
4 type ProviderOptionSelection,5 type ProviderOptionSelection,
5 type ScopedThreadRef,6 type ScopedThreadRef,
...@@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: {...@@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: {
196197
197export interface TraitsMenuContentProps {198export interface TraitsMenuContentProps {
198 provider: ProviderDriverKind;199 provider: ProviderDriverKind;
200 instanceId?: ProviderInstanceId;
199 models: ReadonlyArray<ServerProviderModel>;201 models: ReadonlyArray<ServerProviderModel>;
200 model: string | null | undefined;202 model: string | null | undefined;
201 prompt: string;203 prompt: string;
...@@ -208,6 +210,7 @@ export interface TraitsMenuContentProps {...@@ -208,6 +210,7 @@ export interface TraitsMenuContentProps {
208210
209export const TraitsMenuContent = memo(function TraitsMenuContentImpl({211export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
210 provider,212 provider,
213 instanceId,
211 models,214 models,
212 model,215 model,
213 prompt,216 prompt,
...@@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({...@@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
228 return;231 return;
229 }232 }
230 setProviderModelOptions(threadTarget, provider, nextOptions, {233 setProviderModelOptions(threadTarget, provider, nextOptions, {
234 ...(instanceId ? { instanceId } : {}),
231 model,235 model,
232 persistSticky: true,236 persistSticky: true,
233 });237 });
234 },238 },
235 [model, persistence, provider, setProviderModelOptions],239 [instanceId, model, persistence, provider, setProviderModelOptions],
236 );240 );
237 const {241 const {
238 descriptors,242 descriptors,
...@@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({...@@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
343347
344export const TraitsPicker = memo(function TraitsPicker({348export const TraitsPicker = memo(function TraitsPicker({
345 provider,349 provider,
350 instanceId,
346 models,351 models,
347 model,352 model,
348 prompt,353 prompt,
...@@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({...@@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({
431 <MenuPopup align="start">436 <MenuPopup align="start">
432 <TraitsMenuContent437 <TraitsMenuContent
433 provider={provider}438 provider={provider}
439 {...(instanceId ? { instanceId } : {})}
434 models={models}440 models={models}
435 model={model}441 model={model}
436 prompt={prompt}442 prompt={prompt}
apps/web/src/components/chat/composerProviderState.tsx+14-2
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import {1import {
2 type ProviderDriverKind,2 type ProviderDriverKind,
3 type ProviderInstanceId,
3 type ProviderOptionSelection,4 type ProviderOptionSelection,
4 type ScopedThreadRef,5 type ScopedThreadRef,
5 type ServerProviderModel,6 type ServerProviderModel,
...@@ -35,6 +36,7 @@ export type ComposerProviderState = {...@@ -35,6 +36,7 @@ export type ComposerProviderState = {
3536
36type TraitsRenderInput = {37type TraitsRenderInput = {
37 provider: ProviderDriverKind;38 provider: ProviderDriverKind;
39 instanceId?: ProviderInstanceId;
38 threadRef?: ScopedThreadRef;40 threadRef?: ScopedThreadRef;
39 draftId?: DraftId;41 draftId?: DraftId;
40 model: string;42 model: string;
...@@ -76,8 +78,17 @@ function renderTraitsControl(...@@ -76,8 +78,17 @@ function renderTraitsControl(
76 Component: typeof TraitsMenuContent | typeof TraitsPicker,78 Component: typeof TraitsMenuContent | typeof TraitsPicker,
77 input: TraitsRenderInput,79 input: TraitsRenderInput,
78): ReactNode {80): ReactNode {
79 const { provider, threadRef, draftId, model, models, modelOptions, prompt, onPromptChange } =81 const {
80 input;82 provider,
83 instanceId,
84 threadRef,
85 draftId,
86 model,
87 models,
88 modelOptions,
89 prompt,
90 onPromptChange,
91 } = input;
81 const hasTarget = threadRef !== undefined || draftId !== undefined;92 const hasTarget = threadRef !== undefined || draftId !== undefined;
82 if (93 if (
83 !hasTarget ||94 !hasTarget ||
...@@ -88,6 +99,7 @@ function renderTraitsControl(...@@ -88,6 +99,7 @@ function renderTraitsControl(
88 return (99 return (
89 <Component100 <Component
90 provider={provider}101 provider={provider}
102 {...(instanceId ? { instanceId } : {})}
91 models={models}103 models={models}
92 {...(threadRef ? { threadRef } : {})}104 {...(threadRef ? { threadRef } : {})}
93 {...(draftId ? { draftId } : {})}105 {...(draftId ? { draftId } : {})}
apps/web/src/composerDraftStore.test.ts+38
...@@ -21,6 +21,7 @@ import { createModelSelection } from "@t3tools/shared/model";...@@ -21,6 +21,7 @@ import { createModelSelection } from "@t3tools/shared/model";
21// `stickyModelSelectionByProvider` maps are keyed by `ProviderInstanceId`21// `stickyModelSelectionByProvider` maps are keyed by `ProviderInstanceId`
22// in production; these aliases keep the legacy-key migration tests concise.22// in production; these aliases keep the legacy-key migration tests concise.
23const CODEX_INSTANCE = ProviderInstanceId.make("codex");23const CODEX_INSTANCE = ProviderInstanceId.make("codex");
24const CODEX_SECONDARY_INSTANCE = ProviderInstanceId.make("codex_secondary");
24const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent");25const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent");
25const CURSOR_INSTANCE = ProviderInstanceId.make("cursor");26const CURSOR_INSTANCE = ProviderInstanceId.make("cursor");
26const CODEX_DRIVER = ProviderDriverKind.make("codex");27const CODEX_DRIVER = ProviderDriverKind.make("codex");
...@@ -1201,6 +1202,43 @@ describe("composerDraftStore modelSelection", () => {...@@ -1201,6 +1202,43 @@ describe("composerDraftStore modelSelection", () => {
1201 );1202 );
1202 });1203 });
12031204
1205 it("stores provider option changes on a selected custom instance", () => {
1206 const store = useComposerDraftStore.getState();
1207
1208 store.setProviderModelOptions(
1209 threadRef,
1210 CODEX_DRIVER,
1211 toSelections({ reasoningEffort: "low" }),
1212 {
1213 instanceId: CODEX_SECONDARY_INSTANCE,
1214 model: "gpt-5-codex",
1215 persistSticky: true,
1216 },
1217 );
1218
1219 expect(
1220 draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[CODEX_SECONDARY_INSTANCE],
1221 ).toEqual(
1222 expect.objectContaining({
1223 instanceId: CODEX_SECONDARY_INSTANCE,
1224 options: [{ id: "reasoningEffort", value: "low" }],
1225 }),
1226 );
1227 expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.activeProvider).toBe(CODEX_SECONDARY_INSTANCE);
1228 expect(useComposerDraftStore.getState().stickyActiveProvider).toBe(CODEX_SECONDARY_INSTANCE);
1229 expect(useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_INSTANCE]).toBe(
1230 undefined,
1231 );
1232 expect(
1233 useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_SECONDARY_INSTANCE],
1234 ).toEqual(
1235 expect.objectContaining({
1236 instanceId: CODEX_SECONDARY_INSTANCE,
1237 options: [{ id: "reasoningEffort", value: "low" }],
1238 }),
1239 );
1240 });
1241
1204 it("updates only the draft when sticky persistence is disabled", () => {1242 it("updates only the draft when sticky persistence is disabled", () => {
1205 const store = useComposerDraftStore.getState();1243 const store = useComposerDraftStore.getState();
12061244
apps/web/src/composerDraftStore.ts+6-2
...@@ -371,6 +371,7 @@ interface ComposerDraftStoreState {...@@ -371,6 +371,7 @@ interface ComposerDraftStoreState {
371 provider: ProviderDriverKind,371 provider: ProviderDriverKind,
372 nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,372 nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,
373 options?: {373 options?: {
374 instanceId?: ProviderInstanceId | null | undefined;
374 model?: string | null | undefined;375 model?: string | null | undefined;
375 persistSticky?: boolean;376 persistSticky?: boolean;
376 },377 },
...@@ -2456,7 +2457,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2456,7 +2457,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
2456 if (normalizedProvider === null) {2457 if (normalizedProvider === null) {
2457 return;2458 return;
2458 }2459 }
2459 const instanceKey = defaultInstanceIdForDriver(normalizedProvider);2460 const instanceKey = options?.instanceId ?? defaultInstanceIdForDriver(normalizedProvider);
2460 const fallbackModel =2461 const fallbackModel =
2461 normalizeModelSlug(options?.model, normalizedProvider) ??2462 normalizeModelSlug(options?.model, normalizedProvider) ??
2462 DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ??2463 DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ??
...@@ -2501,7 +2502,9 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2501,7 +2502,9 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
2501 const { options: _, ...rest } = stickyBase;2502 const { options: _, ...rest } = stickyBase;
2502 nextStickyMap[instanceKey] = rest as ModelSelection;2503 nextStickyMap[instanceKey] = rest as ModelSelection;
2503 }2504 }
2504 nextStickyActiveProvider = base.activeProvider ?? instanceKey;2505 nextStickyActiveProvider = options.instanceId
2506 ? instanceKey
2507 : (base.activeProvider ?? instanceKey);
2505 }2508 }
25062509
2507 if (2510 if (
...@@ -2514,6 +2517,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2514,6 +2517,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
25142517
2515 const nextDraft: ComposerThreadDraftState = {2518 const nextDraft: ComposerThreadDraftState = {
2516 ...base,2519 ...base,
2520 ...(options?.instanceId ? { activeProvider: instanceKey } : {}),
2517 modelSelectionByProvider: nextMap,2521 modelSelectionByProvider: nextMap,
2518 };2522 };
2519 const nextDraftsByThreadKey = { ...state.draftsByThreadKey };2523 const nextDraftsByThreadKey = { ...state.draftsByThreadKey };