fix(team): prevent double [1m] suffix on model string during re-launch

This commit is contained in:
Diego Serrano 2026-04-14 07:35:59 -04:00 committed by GitHub
parent 644b45942f
commit bda2e160f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 93 additions and 9 deletions

View file

@ -575,6 +575,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
multimodelEnabled,
storedProviderId,
storedEffort: storedEffort === null ? 'medium' : storedEffort,
storedLimitContext: localStorage.getItem('team:lastLimitContext') === 'true',
getStoredModel: getStoredTeamModel,
});
setSavedLaunchProviderId(savedProviderId);
@ -590,10 +591,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
setSelectedProviderIdRaw(launchPrefill.providerId);
setSelectedModelRaw(launchPrefill.model);
setSelectedEffortRaw(launchPrefill.effort);
setLimitContextRaw(
savedRequest?.limitContext === true ||
localStorage.getItem('team:lastLimitContext') === 'true'
);
setLimitContextRaw(launchPrefill.limitContext);
setSkipPermissionsRaw(
savedRequest?.skipPermissions ??
localStorage.getItem('team:lastSkipPermissions') !== 'false'

View file

@ -16,6 +16,7 @@ import {
GEMINI_UI_DISABLED_REASON,
isGeminiUiFrozen,
} from '@renderer/utils/geminiUiFreeze';
import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext';
import {
doesTeamModelCarryProviderBrand,
getProviderScopedTeamModelLabel,
@ -99,7 +100,7 @@ export function computeEffectiveTeamModel(
limitContext: boolean,
providerId: 'anthropic' | 'codex' | 'gemini' = 'anthropic'
): string | undefined {
const base = selectedModel || undefined;
const base = stripTrailingOneMillionSuffixes(selectedModel);
if (providerId !== 'anthropic') return base;
if (limitContext) return base;
if (base === 'haiku') return base;

View file

@ -1,4 +1,5 @@
import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext';
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability';
import { isLeadMember } from '@shared/utils/leadDetection';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
@ -9,6 +10,7 @@ interface PreviousLaunchParamsLike {
providerId?: TeamProviderId;
model?: string;
effort?: string;
limitContext?: boolean;
}
interface LaunchDialogPrefillInput {
@ -18,6 +20,7 @@ interface LaunchDialogPrefillInput {
multimodelEnabled: boolean;
storedProviderId: TeamProviderId;
storedEffort: string;
storedLimitContext: boolean;
getStoredModel: (providerId: TeamProviderId) => string;
}
@ -25,6 +28,7 @@ interface LaunchDialogPrefillResult {
providerId: TeamProviderId;
model: string;
effort: string;
limitContext: boolean;
}
function normalizeModelCandidate(model: string | undefined): string {
@ -32,7 +36,7 @@ function normalizeModelCandidate(model: string | undefined): string {
if (!trimmed || trimmed === 'default' || trimmed === '__default__') {
return '';
}
return trimmed;
return stripTrailingOneMillionSuffixes(trimmed) ?? '';
}
function canReuseModelForSelectedProvider(
@ -52,6 +56,7 @@ export function resolveLaunchDialogPrefill({
multimodelEnabled,
storedProviderId,
storedEffort,
storedLimitContext,
getStoredModel,
}: LaunchDialogPrefillInput): LaunchDialogPrefillResult {
const currentLead = members.find((member) => isLeadMember(member));
@ -88,6 +93,8 @@ export function resolveLaunchDialogPrefill({
const effort =
currentLead?.effort ?? savedRequest?.effort ?? previousLaunchParams?.effort ?? storedEffort;
const limitContext =
previousLaunchParams?.limitContext ?? savedRequest?.limitContext ?? storedLimitContext;
return {
providerId,
@ -95,5 +102,6 @@ export function resolveLaunchDialogPrefill({
? normalizeTeamModelForUi(providerId, matchingModel)
: getStoredModel(providerId),
effort,
limitContext,
};
}

View file

@ -6,6 +6,7 @@ import {
canDisplayTaskChangesForOptions,
type TaskChangeRequestOptions,
} from '@renderer/utils/taskChangeRequest';
import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext';
import { IpcError, unwrapIpc } from '@renderer/utils/unwrapIpc';
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { createLogger } from '@shared/utils/logger';
@ -1220,8 +1221,7 @@ function saveLaunchParams(teamName: string, params: TeamLaunchParams): void {
* E.g. 'opus[1m]' 'opus', 'sonnet' 'sonnet', undefined undefined.
*/
function extractBaseModel(raw?: string): string | undefined {
if (!raw) return undefined;
return raw.replace(/\[1m\]$/, '') || undefined;
return stripTrailingOneMillionSuffixes(raw);
}
const TOOL_APPROVAL_PREFIX = 'team:toolApprovalSettings:';

View file

@ -0,0 +1,8 @@
export function stripTrailingOneMillionSuffixes(model: string | undefined): string | undefined {
const trimmed = model?.trim();
if (!trimmed) {
return undefined;
}
return trimmed.replace(/(?:\[1m\])+$/, '') || undefined;
}

View file

@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest';
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
import {
computeEffectiveTeamModel,
formatTeamModelSummary,
} from '@renderer/components/team/dialogs/TeamModelSelector';
import {
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
@ -36,3 +39,34 @@ describe('formatTeamModelSummary', () => {
expect(normalizeTeamModelForUi('codex', 'gpt-5.4-mini')).toBe('gpt-5.4-mini');
});
});
describe('computeEffectiveTeamModel', () => {
it('appends [1m] for anthropic models', () => {
expect(computeEffectiveTeamModel('opus', false, 'anthropic')).toBe('opus[1m]');
expect(computeEffectiveTeamModel('sonnet', false, 'anthropic')).toBe('sonnet[1m]');
});
it('does not double-append [1m] when input already has it', () => {
expect(computeEffectiveTeamModel('opus[1m]', false, 'anthropic')).toBe('opus[1m]');
expect(computeEffectiveTeamModel('sonnet[1m]', false, 'anthropic')).toBe('sonnet[1m]');
expect(computeEffectiveTeamModel('opus[1m][1m]', false, 'anthropic')).toBe('opus[1m]');
});
it('defaults to opus[1m] when no model selected', () => {
expect(computeEffectiveTeamModel('', false, 'anthropic')).toBe('opus[1m]');
});
it('returns base model without [1m] when limitContext is true', () => {
expect(computeEffectiveTeamModel('opus', true, 'anthropic')).toBe('opus');
expect(computeEffectiveTeamModel('opus[1m]', true, 'anthropic')).toBe('opus');
expect(computeEffectiveTeamModel('opus[1m][1m]', true, 'anthropic')).toBe('opus');
});
it('returns haiku as-is', () => {
expect(computeEffectiveTeamModel('haiku', false, 'anthropic')).toBe('haiku');
});
it('returns non-anthropic models as-is', () => {
expect(computeEffectiveTeamModel('gpt-5.4', false, 'codex')).toBe('gpt-5.4');
});
});

View file

@ -38,6 +38,7 @@ describe('resolveLaunchDialogPrefill', () => {
multimodelEnabled: true,
storedProviderId: 'anthropic',
storedEffort: 'medium',
storedLimitContext: false,
getStoredModel: createStoredModelGetter({
anthropic: 'haiku',
codex: 'gpt-5.4',
@ -48,6 +49,7 @@ describe('resolveLaunchDialogPrefill', () => {
providerId: 'codex',
model: 'gpt-5.4',
effort: 'medium',
limitContext: false,
});
});
@ -78,6 +80,7 @@ describe('resolveLaunchDialogPrefill', () => {
multimodelEnabled: true,
storedProviderId: 'anthropic',
storedEffort: 'medium',
storedLimitContext: false,
getStoredModel: createStoredModelGetter({
anthropic: 'haiku',
codex: 'gpt-5.4',
@ -88,6 +91,7 @@ describe('resolveLaunchDialogPrefill', () => {
providerId: 'codex',
model: 'gpt-5.4',
effort: 'medium',
limitContext: false,
});
});
@ -103,6 +107,7 @@ describe('resolveLaunchDialogPrefill', () => {
multimodelEnabled: true,
storedProviderId: 'anthropic',
storedEffort: 'medium',
storedLimitContext: false,
getStoredModel: createStoredModelGetter({
anthropic: 'haiku',
codex: 'gpt-5.4',
@ -113,6 +118,7 @@ describe('resolveLaunchDialogPrefill', () => {
providerId: 'codex',
model: 'gpt-5.3-codex',
effort: 'high',
limitContext: false,
});
});
@ -134,6 +140,7 @@ describe('resolveLaunchDialogPrefill', () => {
multimodelEnabled: true,
storedProviderId: 'anthropic',
storedEffort: 'medium',
storedLimitContext: false,
getStoredModel: createStoredModelGetter({
anthropic: 'haiku',
codex: 'gpt-5.4',
@ -144,6 +151,34 @@ describe('resolveLaunchDialogPrefill', () => {
providerId: 'anthropic',
model: 'haiku',
effort: 'medium',
limitContext: false,
});
});
it('prefers per-team launch params for limitContext over stale global storage', () => {
const result = resolveLaunchDialogPrefill({
members: [],
savedRequest: null,
previousLaunchParams: {
providerId: 'anthropic',
model: 'opus[1m][1m]',
effort: 'high',
limitContext: true,
},
multimodelEnabled: true,
storedProviderId: 'anthropic',
storedEffort: 'medium',
storedLimitContext: false,
getStoredModel: createStoredModelGetter({
anthropic: 'haiku',
}),
});
expect(result).toEqual({
providerId: 'anthropic',
model: 'opus',
effort: 'high',
limitContext: true,
});
});
});