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)
727727 model: selectedModel,
728728 models: selectedProviderModels,
729729 prompt,
730 modelOptions: composerModelOptions?.[selectedProvider],
730 modelOptions: composerModelOptions?.[selectedInstanceId],
731731 }),
732 [composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels],
732 [
733 composerModelOptions,
734 prompt,
735 selectedInstanceId,
736 selectedModel,
737 selectedProvider,
738 selectedProviderModels,
739 ],
733740 );
734741
735742 const selectedPromptEffort = composerProviderState.promptEffort;
......@@ -1018,21 +1025,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
10181025
10191026 const providerTraitsMenuContent = renderProviderTraitsMenuContent({
10201027 provider: selectedProvider,
1028 instanceId: selectedInstanceId,
10211029 ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
10221030 ...(routeKind === "draft" && draftId ? { draftId } : {}),
10231031 model: selectedModel,
10241032 models: selectedProviderModels,
1025 modelOptions: composerModelOptions?.[selectedProvider],
1033 modelOptions: composerModelOptions?.[selectedInstanceId],
10261034 prompt,
10271035 onPromptChange: setPromptFromTraits,
10281036 });
10291037 const providerTraitsPicker = renderProviderTraitsPicker({
10301038 provider: selectedProvider,
1039 instanceId: selectedInstanceId,
10311040 ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
10321041 ...(routeKind === "draft" && draftId ? { draftId } : {}),
10331042 model: selectedModel,
10341043 models: selectedProviderModels,
1035 modelOptions: composerModelOptions?.[selectedProvider],
1044 modelOptions: composerModelOptions?.[selectedInstanceId],
10361045 prompt,
10371046 onPromptChange: setPromptFromTraits,
10381047 });
apps/web/src/components/chat/TraitsPicker.tsx+7-1
......@@ -1,5 +1,6 @@
11import {
22 type ProviderDriverKind,
3 type ProviderInstanceId,
34 type ProviderOptionDescriptor,
45 type ProviderOptionSelection,
56 type ScopedThreadRef,
......@@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: {
196197
197198export interface TraitsMenuContentProps {
198199 provider: ProviderDriverKind;
200 instanceId?: ProviderInstanceId;
199201 models: ReadonlyArray<ServerProviderModel>;
200202 model: string | null | undefined;
201203 prompt: string;
......@@ -208,6 +210,7 @@ export interface TraitsMenuContentProps {
208210
209211export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
210212 provider,
213 instanceId,
211214 models,
212215 model,
213216 prompt,
......@@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
228231 return;
229232 }
230233 setProviderModelOptions(threadTarget, provider, nextOptions, {
234 ...(instanceId ? { instanceId } : {}),
231235 model,
232236 persistSticky: true,
233237 });
234238 },
235 [model, persistence, provider, setProviderModelOptions],
239 [instanceId, model, persistence, provider, setProviderModelOptions],
236240 );
237241 const {
238242 descriptors,
......@@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
343347
344348export const TraitsPicker = memo(function TraitsPicker({
345349 provider,
350 instanceId,
346351 models,
347352 model,
348353 prompt,
......@@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({
431436 <MenuPopup align="start">
432437 <TraitsMenuContent
433438 provider={provider}
439 {...(instanceId ? { instanceId } : {})}
434440 models={models}
435441 model={model}
436442 prompt={prompt}
apps/web/src/components/chat/composerProviderState.tsx+14-2
......@@ -1,5 +1,6 @@
11import {
22 type ProviderDriverKind,
3 type ProviderInstanceId,
34 type ProviderOptionSelection,
45 type ScopedThreadRef,
56 type ServerProviderModel,
......@@ -35,6 +36,7 @@ export type ComposerProviderState = {
3536
3637type TraitsRenderInput = {
3738 provider: ProviderDriverKind;
39 instanceId?: ProviderInstanceId;
3840 threadRef?: ScopedThreadRef;
3941 draftId?: DraftId;
4042 model: string;
......@@ -76,8 +78,17 @@ function renderTraitsControl(
7678 Component: typeof TraitsMenuContent | typeof TraitsPicker,
7779 input: TraitsRenderInput,
7880): ReactNode {
79 const { provider, threadRef, draftId, model, models, modelOptions, prompt, onPromptChange } =
80 input;
81 const {
82 provider,
83 instanceId,
84 threadRef,
85 draftId,
86 model,
87 models,
88 modelOptions,
89 prompt,
90 onPromptChange,
91 } = input;
8192 const hasTarget = threadRef !== undefined || draftId !== undefined;
8293 if (
8394 !hasTarget ||
......@@ -88,6 +99,7 @@ function renderTraitsControl(
8899 return (
89100 <Component
90101 provider={provider}
102 {...(instanceId ? { instanceId } : {})}
91103 models={models}
92104 {...(threadRef ? { threadRef } : {})}
93105 {...(draftId ? { draftId } : {})}
apps/web/src/composerDraftStore.test.ts+38
......@@ -21,6 +21,7 @@ import { createModelSelection } from "@t3tools/shared/model";
2121// `stickyModelSelectionByProvider` maps are keyed by `ProviderInstanceId`
2222// in production; these aliases keep the legacy-key migration tests concise.
2323const CODEX_INSTANCE = ProviderInstanceId.make("codex");
24const CODEX_SECONDARY_INSTANCE = ProviderInstanceId.make("codex_secondary");
2425const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent");
2526const CURSOR_INSTANCE = ProviderInstanceId.make("cursor");
2627const CODEX_DRIVER = ProviderDriverKind.make("codex");
......@@ -1201,6 +1202,43 @@ describe("composerDraftStore modelSelection", () => {
12011202 );
12021203 });
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
12041242 it("updates only the draft when sticky persistence is disabled", () => {
12051243 const store = useComposerDraftStore.getState();
12061244
apps/web/src/composerDraftStore.ts+6-2
......@@ -371,6 +371,7 @@ interface ComposerDraftStoreState {
371371 provider: ProviderDriverKind,
372372 nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,
373373 options?: {
374 instanceId?: ProviderInstanceId | null | undefined;
374375 model?: string | null | undefined;
375376 persistSticky?: boolean;
376377 },
......@@ -2456,7 +2457,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
24562457 if (normalizedProvider === null) {
24572458 return;
24582459 }
2459 const instanceKey = defaultInstanceIdForDriver(normalizedProvider);
2460 const instanceKey = options?.instanceId ?? defaultInstanceIdForDriver(normalizedProvider);
24602461 const fallbackModel =
24612462 normalizeModelSlug(options?.model, normalizedProvider) ??
24622463 DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ??
......@@ -2501,7 +2502,9 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
25012502 const { options: _, ...rest } = stickyBase;
25022503 nextStickyMap[instanceKey] = rest as ModelSelection;
25032504 }
2504 nextStickyActiveProvider = base.activeProvider ?? instanceKey;
2505 nextStickyActiveProvider = options.instanceId
2506 ? instanceKey
2507 : (base.activeProvider ?? instanceKey);
25052508 }
25062509
25072510 if (
......@@ -2514,6 +2517,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
25142517
25152518 const nextDraft: ComposerThreadDraftState = {
25162519 ...base,
2520 ...(options?.instanceId ? { activeProvider: instanceKey } : {}),
25172521 modelSelectionByProvider: nextMap,
25182522 };
25192523 const nextDraftsByThreadKey = { ...state.draftsByThreadKey };