fix(team): prevent double [1m] suffix on model string during re-launch
This commit is contained in:
parent
644b45942f
commit
bda2e160f7
7 changed files with 93 additions and 9 deletions
|
|
@ -575,6 +575,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
multimodelEnabled,
|
multimodelEnabled,
|
||||||
storedProviderId,
|
storedProviderId,
|
||||||
storedEffort: storedEffort === null ? 'medium' : storedEffort,
|
storedEffort: storedEffort === null ? 'medium' : storedEffort,
|
||||||
|
storedLimitContext: localStorage.getItem('team:lastLimitContext') === 'true',
|
||||||
getStoredModel: getStoredTeamModel,
|
getStoredModel: getStoredTeamModel,
|
||||||
});
|
});
|
||||||
setSavedLaunchProviderId(savedProviderId);
|
setSavedLaunchProviderId(savedProviderId);
|
||||||
|
|
@ -590,10 +591,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
setSelectedProviderIdRaw(launchPrefill.providerId);
|
setSelectedProviderIdRaw(launchPrefill.providerId);
|
||||||
setSelectedModelRaw(launchPrefill.model);
|
setSelectedModelRaw(launchPrefill.model);
|
||||||
setSelectedEffortRaw(launchPrefill.effort);
|
setSelectedEffortRaw(launchPrefill.effort);
|
||||||
setLimitContextRaw(
|
setLimitContextRaw(launchPrefill.limitContext);
|
||||||
savedRequest?.limitContext === true ||
|
|
||||||
localStorage.getItem('team:lastLimitContext') === 'true'
|
|
||||||
);
|
|
||||||
setSkipPermissionsRaw(
|
setSkipPermissionsRaw(
|
||||||
savedRequest?.skipPermissions ??
|
savedRequest?.skipPermissions ??
|
||||||
localStorage.getItem('team:lastSkipPermissions') !== 'false'
|
localStorage.getItem('team:lastSkipPermissions') !== 'false'
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
GEMINI_UI_DISABLED_REASON,
|
GEMINI_UI_DISABLED_REASON,
|
||||||
isGeminiUiFrozen,
|
isGeminiUiFrozen,
|
||||||
} from '@renderer/utils/geminiUiFreeze';
|
} from '@renderer/utils/geminiUiFreeze';
|
||||||
|
import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext';
|
||||||
import {
|
import {
|
||||||
doesTeamModelCarryProviderBrand,
|
doesTeamModelCarryProviderBrand,
|
||||||
getProviderScopedTeamModelLabel,
|
getProviderScopedTeamModelLabel,
|
||||||
|
|
@ -99,7 +100,7 @@ export function computeEffectiveTeamModel(
|
||||||
limitContext: boolean,
|
limitContext: boolean,
|
||||||
providerId: 'anthropic' | 'codex' | 'gemini' = 'anthropic'
|
providerId: 'anthropic' | 'codex' | 'gemini' = 'anthropic'
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const base = selectedModel || undefined;
|
const base = stripTrailingOneMillionSuffixes(selectedModel);
|
||||||
if (providerId !== 'anthropic') return base;
|
if (providerId !== 'anthropic') return base;
|
||||||
if (limitContext) return base;
|
if (limitContext) return base;
|
||||||
if (base === 'haiku') return base;
|
if (base === 'haiku') return base;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
|
import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
|
||||||
|
import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext';
|
||||||
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability';
|
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||||
|
|
@ -9,6 +10,7 @@ interface PreviousLaunchParamsLike {
|
||||||
providerId?: TeamProviderId;
|
providerId?: TeamProviderId;
|
||||||
model?: string;
|
model?: string;
|
||||||
effort?: string;
|
effort?: string;
|
||||||
|
limitContext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LaunchDialogPrefillInput {
|
interface LaunchDialogPrefillInput {
|
||||||
|
|
@ -18,6 +20,7 @@ interface LaunchDialogPrefillInput {
|
||||||
multimodelEnabled: boolean;
|
multimodelEnabled: boolean;
|
||||||
storedProviderId: TeamProviderId;
|
storedProviderId: TeamProviderId;
|
||||||
storedEffort: string;
|
storedEffort: string;
|
||||||
|
storedLimitContext: boolean;
|
||||||
getStoredModel: (providerId: TeamProviderId) => string;
|
getStoredModel: (providerId: TeamProviderId) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,6 +28,7 @@ interface LaunchDialogPrefillResult {
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
model: string;
|
model: string;
|
||||||
effort: string;
|
effort: string;
|
||||||
|
limitContext: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeModelCandidate(model: string | undefined): string {
|
function normalizeModelCandidate(model: string | undefined): string {
|
||||||
|
|
@ -32,7 +36,7 @@ function normalizeModelCandidate(model: string | undefined): string {
|
||||||
if (!trimmed || trimmed === 'default' || trimmed === '__default__') {
|
if (!trimmed || trimmed === 'default' || trimmed === '__default__') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return trimmed;
|
return stripTrailingOneMillionSuffixes(trimmed) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function canReuseModelForSelectedProvider(
|
function canReuseModelForSelectedProvider(
|
||||||
|
|
@ -52,6 +56,7 @@ export function resolveLaunchDialogPrefill({
|
||||||
multimodelEnabled,
|
multimodelEnabled,
|
||||||
storedProviderId,
|
storedProviderId,
|
||||||
storedEffort,
|
storedEffort,
|
||||||
|
storedLimitContext,
|
||||||
getStoredModel,
|
getStoredModel,
|
||||||
}: LaunchDialogPrefillInput): LaunchDialogPrefillResult {
|
}: LaunchDialogPrefillInput): LaunchDialogPrefillResult {
|
||||||
const currentLead = members.find((member) => isLeadMember(member));
|
const currentLead = members.find((member) => isLeadMember(member));
|
||||||
|
|
@ -88,6 +93,8 @@ export function resolveLaunchDialogPrefill({
|
||||||
|
|
||||||
const effort =
|
const effort =
|
||||||
currentLead?.effort ?? savedRequest?.effort ?? previousLaunchParams?.effort ?? storedEffort;
|
currentLead?.effort ?? savedRequest?.effort ?? previousLaunchParams?.effort ?? storedEffort;
|
||||||
|
const limitContext =
|
||||||
|
previousLaunchParams?.limitContext ?? savedRequest?.limitContext ?? storedLimitContext;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
providerId,
|
providerId,
|
||||||
|
|
@ -95,5 +102,6 @@ export function resolveLaunchDialogPrefill({
|
||||||
? normalizeTeamModelForUi(providerId, matchingModel)
|
? normalizeTeamModelForUi(providerId, matchingModel)
|
||||||
: getStoredModel(providerId),
|
: getStoredModel(providerId),
|
||||||
effort,
|
effort,
|
||||||
|
limitContext,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
canDisplayTaskChangesForOptions,
|
canDisplayTaskChangesForOptions,
|
||||||
type TaskChangeRequestOptions,
|
type TaskChangeRequestOptions,
|
||||||
} from '@renderer/utils/taskChangeRequest';
|
} from '@renderer/utils/taskChangeRequest';
|
||||||
|
import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext';
|
||||||
import { IpcError, unwrapIpc } from '@renderer/utils/unwrapIpc';
|
import { IpcError, unwrapIpc } from '@renderer/utils/unwrapIpc';
|
||||||
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
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.
|
* E.g. 'opus[1m]' → 'opus', 'sonnet' → 'sonnet', undefined → undefined.
|
||||||
*/
|
*/
|
||||||
function extractBaseModel(raw?: string): string | undefined {
|
function extractBaseModel(raw?: string): string | undefined {
|
||||||
if (!raw) return undefined;
|
return stripTrailingOneMillionSuffixes(raw);
|
||||||
return raw.replace(/\[1m\]$/, '') || undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_APPROVAL_PREFIX = 'team:toolApprovalSettings:';
|
const TOOL_APPROVAL_PREFIX = 'team:toolApprovalSettings:';
|
||||||
|
|
|
||||||
8
src/renderer/utils/teamModelContext.ts
Normal file
8
src/renderer/utils/teamModelContext.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
|
import {
|
||||||
|
computeEffectiveTeamModel,
|
||||||
|
formatTeamModelSummary,
|
||||||
|
} from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||||
import {
|
import {
|
||||||
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
|
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
|
||||||
GPT_5_3_CODEX_SPARK_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');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
codex: 'gpt-5.4',
|
codex: 'gpt-5.4',
|
||||||
|
|
@ -48,6 +49,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -78,6 +80,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
codex: 'gpt-5.4',
|
codex: 'gpt-5.4',
|
||||||
|
|
@ -88,6 +91,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -103,6 +107,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
codex: 'gpt-5.4',
|
codex: 'gpt-5.4',
|
||||||
|
|
@ -113,6 +118,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
model: 'gpt-5.3-codex',
|
model: 'gpt-5.3-codex',
|
||||||
effort: 'high',
|
effort: 'high',
|
||||||
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -134,6 +140,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
codex: 'gpt-5.4',
|
codex: 'gpt-5.4',
|
||||||
|
|
@ -144,6 +151,34 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
model: 'haiku',
|
model: 'haiku',
|
||||||
effort: 'medium',
|
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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue