feat(teams): introduce fast mode configuration for Anthropic provider and enhance related UI components
This commit is contained in:
parent
331166216e
commit
1db7e501a0
43 changed files with 5450 additions and 65 deletions
|
|
@ -25,6 +25,7 @@ Electron 40.x, React 19.x, TypeScript 5.x, Tailwind CSS 3.x, Zustand 4.x
|
|||
|
||||
## Commands
|
||||
Always use pnpm (not npm/yarn) for this project.
|
||||
Workspace membership is canonical in `pnpm-workspace.yaml`; do not re-add root `package.json.workspaces`, because npm subproject installs in Codex Cloud must treat nested packages as standalone projects.
|
||||
Do NOT run `pnpm lint:fix` unless the user explicitly asks for it — it interferes with agents running in parallel.
|
||||
When running build/typecheck/test commands, pipe through `tail -20` to avoid flooding the context window (e.g. `pnpm typecheck 2>&1 | tail -20`).
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
| [kanban-design.md](./kanban-design.md) | Kanban flow, колонки, review mechanism, kanban-state.json |
|
||||
| [implementation.md](./implementation.md) | Техплан: файлы, шаги, verification |
|
||||
| [research-worktrees.md](./research-worktrees.md) | Git worktrees + teams, запуск Claude процессов из UI (Phase 2) |
|
||||
| [task-queue-derived-agenda-plan.md](./task-queue-derived-agenda-plan.md) | Подробный rollout-plan по разделению queue/inventory, derived actionOwner и phased agenda/delta sync |
|
||||
|
||||
## Ключевые решения
|
||||
|
||||
|
|
|
|||
3517
docs/team-management/task-queue-derived-agenda-plan.md
Normal file
3517
docs/team-management/task-queue-derived-agenda-plan.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,212 @@
|
|||
import { resolveAnthropicLaunchModel } from '@shared/utils/anthropicLaunchModel';
|
||||
|
||||
import type {
|
||||
CliProviderModelCatalog,
|
||||
CliProviderModelCatalogItem,
|
||||
CliProviderRuntimeCapabilities,
|
||||
EffortLevel,
|
||||
TeamFastMode,
|
||||
} from '@shared/types';
|
||||
|
||||
export interface AnthropicRuntimeProfileSource {
|
||||
modelCatalog?: CliProviderModelCatalog | null;
|
||||
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
|
||||
}
|
||||
|
||||
export interface AnthropicRuntimeSelection {
|
||||
resolvedLaunchModel: string | null;
|
||||
catalogModel: CliProviderModelCatalogItem | null;
|
||||
displayName: string | null;
|
||||
catalogSource: CliProviderModelCatalog['source'] | 'unavailable';
|
||||
catalogStatus: CliProviderModelCatalog['status'] | 'unavailable';
|
||||
catalogFetchedAt: string | null;
|
||||
supportedEfforts: EffortLevel[];
|
||||
defaultEffort: EffortLevel | null;
|
||||
supportsFastMode: boolean;
|
||||
providerFastModeSupported: boolean;
|
||||
providerFastModeAvailable: boolean;
|
||||
providerFastModeReason: string | null;
|
||||
}
|
||||
|
||||
export interface AnthropicFastModeResolution {
|
||||
selectedFastMode: TeamFastMode;
|
||||
requestedFastMode: boolean;
|
||||
resolvedFastMode: boolean;
|
||||
showFastModeControl: boolean;
|
||||
selectable: boolean;
|
||||
disabledReason: string | null;
|
||||
}
|
||||
|
||||
export interface AnthropicRuntimeReconciliation {
|
||||
nextEffort: EffortLevel | '';
|
||||
effortResetReason: string | null;
|
||||
nextFastMode: TeamFastMode;
|
||||
fastModeResetReason: string | null;
|
||||
}
|
||||
|
||||
function getAnthropicCatalog(
|
||||
source: AnthropicRuntimeProfileSource
|
||||
): CliProviderModelCatalog | null {
|
||||
return source.modelCatalog?.providerId === 'anthropic' ? source.modelCatalog : null;
|
||||
}
|
||||
|
||||
function normalizeEffortLevel(value: string | null | undefined): EffortLevel | null {
|
||||
return value === 'none' ||
|
||||
value === 'minimal' ||
|
||||
value === 'low' ||
|
||||
value === 'medium' ||
|
||||
value === 'high' ||
|
||||
value === 'xhigh' ||
|
||||
value === 'max'
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function normalizeEffortLevels(values: readonly string[] | undefined): EffortLevel[] {
|
||||
const normalized = new Set<EffortLevel>();
|
||||
for (const value of values ?? []) {
|
||||
const effort = normalizeEffortLevel(value);
|
||||
if (effort) {
|
||||
normalized.add(effort);
|
||||
}
|
||||
}
|
||||
return Array.from(normalized);
|
||||
}
|
||||
|
||||
function hasCatalogTruth(selection: AnthropicRuntimeSelection): boolean {
|
||||
return selection.catalogSource !== 'unavailable' && selection.catalogStatus !== 'unavailable';
|
||||
}
|
||||
|
||||
export function resolveAnthropicRuntimeSelection(params: {
|
||||
source: AnthropicRuntimeProfileSource;
|
||||
selectedModel?: string | null;
|
||||
limitContext: boolean;
|
||||
}): AnthropicRuntimeSelection {
|
||||
const catalog = getAnthropicCatalog(params.source);
|
||||
const resolvedLaunchModel =
|
||||
resolveAnthropicLaunchModel({
|
||||
selectedModel: params.selectedModel,
|
||||
limitContext: params.limitContext,
|
||||
availableLaunchModels: catalog?.models.map((model) => model.launchModel),
|
||||
defaultLaunchModel: catalog?.defaultLaunchModel ?? null,
|
||||
}) ?? null;
|
||||
|
||||
const catalogModel =
|
||||
resolvedLaunchModel && catalog
|
||||
? (catalog.models.find(
|
||||
(model) =>
|
||||
model.launchModel.trim() === resolvedLaunchModel ||
|
||||
model.id.trim() === resolvedLaunchModel
|
||||
) ?? null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
resolvedLaunchModel,
|
||||
catalogModel,
|
||||
displayName: catalogModel?.displayName?.trim() ?? null,
|
||||
catalogSource: catalog?.source ?? 'unavailable',
|
||||
catalogStatus: catalog?.status ?? 'unavailable',
|
||||
catalogFetchedAt: catalog?.fetchedAt ?? null,
|
||||
supportedEfforts: normalizeEffortLevels(catalogModel?.supportedReasoningEfforts),
|
||||
defaultEffort: normalizeEffortLevel(catalogModel?.defaultReasoningEffort ?? null),
|
||||
supportsFastMode: catalogModel?.supportsFastMode === true,
|
||||
providerFastModeSupported: params.source.runtimeCapabilities?.fastMode?.supported === true,
|
||||
providerFastModeAvailable: params.source.runtimeCapabilities?.fastMode?.available === true,
|
||||
providerFastModeReason: params.source.runtimeCapabilities?.fastMode?.reason ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAnthropicFastMode(params: {
|
||||
selection: AnthropicRuntimeSelection;
|
||||
selectedFastMode?: TeamFastMode | null;
|
||||
providerFastModeDefault?: boolean;
|
||||
}): AnthropicFastModeResolution {
|
||||
const selectedFastMode = params.selectedFastMode ?? 'inherit';
|
||||
const requestedFastMode =
|
||||
selectedFastMode === 'on'
|
||||
? true
|
||||
: selectedFastMode === 'off'
|
||||
? false
|
||||
: params.providerFastModeDefault === true;
|
||||
|
||||
const selectable =
|
||||
params.selection.providerFastModeSupported &&
|
||||
params.selection.providerFastModeAvailable &&
|
||||
params.selection.supportsFastMode;
|
||||
|
||||
let disabledReason: string | null = null;
|
||||
if (!hasCatalogTruth(params.selection) && !params.selection.providerFastModeSupported) {
|
||||
disabledReason = 'Anthropic runtime capability data is still loading.';
|
||||
} else if (!params.selection.providerFastModeSupported) {
|
||||
disabledReason =
|
||||
params.selection.providerFastModeReason ??
|
||||
'Fast mode is not supported by this Anthropic runtime.';
|
||||
} else if (!params.selection.supportsFastMode) {
|
||||
disabledReason = params.selection.displayName
|
||||
? `Fast mode is available only for Opus 4.6. Selected model resolves to ${params.selection.displayName}.`
|
||||
: 'Fast mode is available only for Opus 4.6.';
|
||||
} else if (!params.selection.providerFastModeAvailable) {
|
||||
disabledReason =
|
||||
params.selection.providerFastModeReason ?? 'Fast mode is currently unavailable.';
|
||||
}
|
||||
|
||||
return {
|
||||
selectedFastMode,
|
||||
requestedFastMode,
|
||||
resolvedFastMode: requestedFastMode && selectable,
|
||||
showFastModeControl:
|
||||
params.selection.providerFastModeSupported ||
|
||||
selectedFastMode !== 'inherit' ||
|
||||
params.providerFastModeDefault === true,
|
||||
selectable,
|
||||
disabledReason,
|
||||
};
|
||||
}
|
||||
|
||||
export function reconcileAnthropicRuntimeSelections(params: {
|
||||
selection: AnthropicRuntimeSelection;
|
||||
selectedEffort?: string | null;
|
||||
selectedFastMode?: TeamFastMode | null;
|
||||
providerFastModeDefault?: boolean;
|
||||
}): AnthropicRuntimeReconciliation {
|
||||
const selectedEffort = normalizeEffortLevel(params.selectedEffort ?? null);
|
||||
if (!hasCatalogTruth(params.selection)) {
|
||||
return {
|
||||
nextEffort: selectedEffort ?? '',
|
||||
effortResetReason: null,
|
||||
nextFastMode: params.selectedFastMode ?? 'inherit',
|
||||
fastModeResetReason: null,
|
||||
};
|
||||
}
|
||||
|
||||
const nextEffort =
|
||||
selectedEffort && !params.selection.supportedEfforts.includes(selectedEffort)
|
||||
? ''
|
||||
: (selectedEffort ?? '');
|
||||
const effortResetReason =
|
||||
selectedEffort && nextEffort === ''
|
||||
? `${selectedEffort} effort is not available for the currently selected Anthropic model. Reset to Default.`
|
||||
: null;
|
||||
|
||||
const fastResolution = resolveAnthropicFastMode({
|
||||
selection: params.selection,
|
||||
selectedFastMode: params.selectedFastMode,
|
||||
providerFastModeDefault: params.providerFastModeDefault,
|
||||
});
|
||||
const nextFastMode =
|
||||
fastResolution.selectedFastMode === 'on' && !fastResolution.selectable
|
||||
? 'inherit'
|
||||
: fastResolution.selectedFastMode;
|
||||
const fastModeResetReason =
|
||||
fastResolution.selectedFastMode === 'on' && nextFastMode !== 'on'
|
||||
? (fastResolution.disabledReason ??
|
||||
'Fast mode is not available for the currently selected Anthropic model. Reset to Default.')
|
||||
: null;
|
||||
|
||||
return {
|
||||
nextEffort,
|
||||
effortResetReason,
|
||||
nextFastMode,
|
||||
fastModeResetReason,
|
||||
};
|
||||
}
|
||||
12
src/features/anthropic-runtime-profile/main/index.ts
Normal file
12
src/features/anthropic-runtime-profile/main/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export {
|
||||
reconcileAnthropicRuntimeSelections,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
|
||||
export type {
|
||||
AnthropicFastModeResolution,
|
||||
AnthropicRuntimeProfileSource,
|
||||
AnthropicRuntimeReconciliation,
|
||||
AnthropicRuntimeSelection,
|
||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
12
src/features/anthropic-runtime-profile/renderer/index.ts
Normal file
12
src/features/anthropic-runtime-profile/renderer/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export {
|
||||
reconcileAnthropicRuntimeSelections,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
|
||||
export type {
|
||||
AnthropicFastModeResolution,
|
||||
AnthropicRuntimeProfileSource,
|
||||
AnthropicRuntimeReconciliation,
|
||||
AnthropicRuntimeSelection,
|
||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
|
|
@ -9,7 +9,7 @@ import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
|||
import { isAbsolute } from 'path';
|
||||
|
||||
import type { HttpServices } from './index';
|
||||
import type { EffortLevel, TeamLaunchRequest } from '@shared/types/team';
|
||||
import type { EffortLevel, TeamFastMode, TeamLaunchRequest } from '@shared/types/team';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
const logger = createLogger('HTTP:teams');
|
||||
|
|
@ -95,6 +95,18 @@ function assertOptionalEffort(
|
|||
return value;
|
||||
}
|
||||
|
||||
function assertOptionalFastMode(value: unknown): TeamFastMode | undefined {
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value !== 'inherit' && value !== 'on' && value !== 'off') {
|
||||
throw new HttpBadRequestError('fastMode must be one of: inherit, on, off');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest {
|
||||
const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {};
|
||||
const providerId =
|
||||
|
|
@ -117,6 +129,7 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
|
|||
}
|
||||
const model = assertOptionalString(payload.model, 'model');
|
||||
const effort = assertOptionalEffort(payload.effort, providerId);
|
||||
const fastMode = assertOptionalFastMode(payload.fastMode);
|
||||
const clearContext = assertOptionalBoolean(payload.clearContext, 'clearContext');
|
||||
const skipPermissions = assertOptionalBoolean(payload.skipPermissions, 'skipPermissions');
|
||||
const worktree = assertOptionalString(payload.worktree, 'worktree');
|
||||
|
|
@ -138,6 +151,9 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
|
|||
...(effort && {
|
||||
effort,
|
||||
}),
|
||||
...(fastMode && {
|
||||
fastMode,
|
||||
}),
|
||||
...(clearContext !== undefined && {
|
||||
clearContext,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -497,25 +497,37 @@ function validateProviderConnectionsSection(
|
|||
const anthropicUpdate: Partial<ProviderConnectionsConfig['anthropic']> = {};
|
||||
|
||||
for (const [connectionKey, connectionValue] of Object.entries(value)) {
|
||||
if (connectionKey !== 'authMode') {
|
||||
if (connectionKey !== 'authMode' && connectionKey !== 'fastModeDefault') {
|
||||
return {
|
||||
valid: false,
|
||||
error: `providerConnections.anthropic.${connectionKey} is not a valid setting`,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
connectionValue !== 'auto' &&
|
||||
connectionValue !== 'oauth' &&
|
||||
connectionValue !== 'api_key'
|
||||
) {
|
||||
if (connectionKey === 'authMode') {
|
||||
if (
|
||||
connectionValue !== 'auto' &&
|
||||
connectionValue !== 'oauth' &&
|
||||
connectionValue !== 'api_key'
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'providerConnections.anthropic.authMode must be one of: auto, oauth, api_key',
|
||||
};
|
||||
}
|
||||
|
||||
anthropicUpdate.authMode = connectionValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof connectionValue !== 'boolean') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'providerConnections.anthropic.authMode must be one of: auto, oauth, api_key',
|
||||
error: 'providerConnections.anthropic.fastModeDefault must be a boolean',
|
||||
};
|
||||
}
|
||||
|
||||
anthropicUpdate.authMode = connectionValue;
|
||||
anthropicUpdate.fastModeDefault = connectionValue;
|
||||
}
|
||||
|
||||
result.anthropic = anthropicUpdate as ProviderConnectionsConfig['anthropic'];
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ import type {
|
|||
TeamLaunchResponse,
|
||||
TeamMemberActivityMeta,
|
||||
TeamMessageNotificationData,
|
||||
TeamFastMode,
|
||||
TeamProviderBackendId,
|
||||
TeamProviderId,
|
||||
TeamProvisioningPrepareResult,
|
||||
|
|
@ -1198,6 +1199,21 @@ function parseOptionalTeamEffort(
|
|||
};
|
||||
}
|
||||
|
||||
function parseOptionalTeamFastMode(
|
||||
value: unknown
|
||||
): { valid: true; value: TeamFastMode | undefined } | { valid: false; error: string } {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return { valid: true, value: undefined };
|
||||
}
|
||||
if (value === 'inherit' || value === 'on' || value === 'off') {
|
||||
return { valid: true, value };
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
error: 'fastMode must be one of inherit, on, or off',
|
||||
};
|
||||
}
|
||||
|
||||
async function validateProvisioningRequest(
|
||||
request: unknown
|
||||
): Promise<{ valid: true; value: TeamCreateRequest } | { valid: false; error: string }> {
|
||||
|
|
@ -1224,12 +1240,15 @@ async function validateProvisioningRequest(
|
|||
if (!Array.isArray(payload.members)) {
|
||||
return { valid: false, error: 'members must be an array' };
|
||||
}
|
||||
const providerId =
|
||||
const explicitProviderId =
|
||||
payload.providerId === 'codex'
|
||||
? 'codex'
|
||||
: payload.providerId === 'gemini'
|
||||
? 'gemini'
|
||||
: 'anthropic';
|
||||
: payload.providerId === 'anthropic'
|
||||
? 'anthropic'
|
||||
: undefined;
|
||||
const providerId = explicitProviderId ?? 'anthropic';
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
const members: TeamCreateRequest['members'] = [];
|
||||
|
|
@ -1304,6 +1323,10 @@ async function validateProvisioningRequest(
|
|||
if (!effortValidation.valid) {
|
||||
return { valid: false, error: effortValidation.error };
|
||||
}
|
||||
const fastModeValidation = parseOptionalTeamFastMode(payload.fastMode);
|
||||
if (!fastModeValidation.valid) {
|
||||
return { valid: false, error: fastModeValidation.error };
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(cwd, { recursive: true });
|
||||
|
|
@ -1359,6 +1382,7 @@ async function validateProvisioningRequest(
|
|||
providerBackendId: providerBackendValidation.value,
|
||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||
effort: effortValidation.value,
|
||||
fastMode: fastModeValidation.value,
|
||||
skipPermissions:
|
||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
||||
worktree:
|
||||
|
|
@ -1512,6 +1536,10 @@ async function handleLaunchTeam(
|
|||
if (!effortValidation.valid) {
|
||||
return { success: false, error: effortValidation.error };
|
||||
}
|
||||
const fastModeValidation = parseOptionalTeamFastMode(payload.fastMode);
|
||||
if (!fastModeValidation.valid) {
|
||||
return { success: false, error: fastModeValidation.error };
|
||||
}
|
||||
|
||||
const createRequest: TeamCreateRequest = {
|
||||
teamName: tn,
|
||||
|
|
@ -1527,6 +1555,7 @@ async function handleLaunchTeam(
|
|||
),
|
||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||
effort: effortValidation.value,
|
||||
fastMode: fastModeValidation.value ?? meta?.fastMode,
|
||||
limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined,
|
||||
skipPermissions:
|
||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
||||
|
|
@ -1558,10 +1587,44 @@ async function handleLaunchTeam(
|
|||
);
|
||||
}
|
||||
|
||||
const effortValidation = parseOptionalTeamEffort(payload.effort, providerId);
|
||||
const persistedMeta = await teamMetaStore.getMeta(tn).catch(() => null);
|
||||
const launchProviderId = explicitProviderId ?? persistedMeta?.providerId ?? providerId;
|
||||
const rawLaunchProviderBackendId =
|
||||
payload.providerBackendId ??
|
||||
persistedMeta?.providerBackendId ??
|
||||
persistedMeta?.launchIdentity?.providerBackendId ??
|
||||
undefined;
|
||||
const launchProviderBackendValidation = parseOptionalProviderBackendId(
|
||||
rawLaunchProviderBackendId,
|
||||
launchProviderId
|
||||
);
|
||||
if (!launchProviderBackendValidation.valid) {
|
||||
return { success: false, error: launchProviderBackendValidation.error };
|
||||
}
|
||||
const rawLaunchEffort =
|
||||
payload.effort ??
|
||||
persistedMeta?.effort ??
|
||||
persistedMeta?.launchIdentity?.selectedEffort ??
|
||||
undefined;
|
||||
const effortValidation = parseOptionalTeamEffort(rawLaunchEffort, launchProviderId);
|
||||
if (!effortValidation.valid) {
|
||||
return { success: false, error: effortValidation.error };
|
||||
}
|
||||
const rawLaunchFastMode =
|
||||
payload.fastMode ??
|
||||
persistedMeta?.fastMode ??
|
||||
persistedMeta?.launchIdentity?.selectedFastMode ??
|
||||
undefined;
|
||||
const fastModeValidation = parseOptionalTeamFastMode(rawLaunchFastMode);
|
||||
if (!fastModeValidation.valid) {
|
||||
return { success: false, error: fastModeValidation.error };
|
||||
}
|
||||
const rawLaunchModel =
|
||||
typeof payload.model === 'string' && payload.model.trim().length > 0
|
||||
? payload.model.trim()
|
||||
: (persistedMeta?.model ?? persistedMeta?.launchIdentity?.selectedModel ?? undefined);
|
||||
const launchLimitContext =
|
||||
typeof payload.limitContext === 'boolean' ? payload.limitContext : persistedMeta?.limitContext;
|
||||
|
||||
return wrapTeamHandler('launch', () => {
|
||||
addMainBreadcrumb('team', 'launch', { teamName: validatedTeamName.value! });
|
||||
|
|
@ -1570,10 +1633,12 @@ async function handleLaunchTeam(
|
|||
teamName: validatedTeamName.value!,
|
||||
cwd,
|
||||
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
||||
providerId,
|
||||
providerBackendId: providerBackendValidation.value,
|
||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||
providerId: launchProviderId,
|
||||
providerBackendId: launchProviderBackendValidation.value,
|
||||
model: rawLaunchModel,
|
||||
effort: effortValidation.value,
|
||||
fastMode: fastModeValidation.value,
|
||||
limitContext: launchLimitContext,
|
||||
clearContext: payload.clearContext === true ? true : undefined,
|
||||
skipPermissions:
|
||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
||||
|
|
@ -2660,6 +2725,10 @@ async function handleCreateConfig(
|
|||
if (!providerBackendValidation.valid) {
|
||||
return { success: false, error: providerBackendValidation.error };
|
||||
}
|
||||
const fastModeValidation = parseOptionalTeamFastMode(payload.fastMode);
|
||||
if (!fastModeValidation.valid) {
|
||||
return { success: false, error: fastModeValidation.error };
|
||||
}
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
const members: TeamCreateConfigRequest['members'] = [];
|
||||
|
|
@ -2721,6 +2790,7 @@ async function handleCreateConfig(
|
|||
members,
|
||||
cwd: typeof payload.cwd === 'string' ? payload.cwd.trim() || undefined : undefined,
|
||||
providerBackendId: providerBackendValidation.value,
|
||||
fastMode: fastModeValidation.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -4023,6 +4093,7 @@ async function handleGetSavedRequest(
|
|||
),
|
||||
model: meta.model,
|
||||
effort: meta.effort as TeamCreateRequest['effort'],
|
||||
fastMode: meta.fastMode as TeamCreateRequest['fastMode'],
|
||||
skipPermissions: meta.skipPermissions,
|
||||
worktree: meta.worktree,
|
||||
extraCliArgs: meta.extraCliArgs,
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ export type ProviderConnectionAuthMode = 'auto' | 'oauth' | 'api_key';
|
|||
export interface ProviderConnectionsConfig {
|
||||
anthropic: {
|
||||
authMode: ProviderConnectionAuthMode;
|
||||
fastModeDefault: boolean;
|
||||
};
|
||||
codex: {
|
||||
preferredAuthMode: CodexAccountAuthMode;
|
||||
|
|
@ -333,6 +334,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||
providerConnections: {
|
||||
anthropic: {
|
||||
authMode: 'auto',
|
||||
fastModeDefault: false,
|
||||
},
|
||||
codex: {
|
||||
preferredAuthMode: 'auto',
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ interface RuntimeProviderCapabilitiesResponse {
|
|||
values?: string[];
|
||||
configPassthrough?: boolean;
|
||||
};
|
||||
fastMode?: {
|
||||
supported?: boolean;
|
||||
available?: boolean;
|
||||
reason?: string | null;
|
||||
source?: 'runtime';
|
||||
};
|
||||
}
|
||||
|
||||
interface RuntimeProviderModelCatalogItemResponse {
|
||||
|
|
@ -49,6 +55,7 @@ interface RuntimeProviderModelCatalogItemResponse {
|
|||
hidden?: boolean;
|
||||
supportedReasoningEfforts?: string[];
|
||||
defaultReasoningEffort?: string | null;
|
||||
supportsFastMode?: boolean;
|
||||
inputModalities?: string[];
|
||||
supportsPersonality?: boolean;
|
||||
isDefault?: boolean;
|
||||
|
|
@ -279,7 +286,8 @@ function normalizeRuntimeReasoningEffort(
|
|||
value === 'low' ||
|
||||
value === 'medium' ||
|
||||
value === 'high' ||
|
||||
value === 'xhigh'
|
||||
value === 'xhigh' ||
|
||||
value === 'max'
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
|
@ -347,6 +355,7 @@ function mapRuntimeProviderModelCatalog(
|
|||
hidden: model.hidden === true,
|
||||
supportedReasoningEfforts,
|
||||
defaultReasoningEffort,
|
||||
supportsFastMode: model.supportsFastMode === true,
|
||||
inputModalities: model.inputModalities?.filter((value) => value.trim().length > 0) ?? [],
|
||||
supportsPersonality: model.supportsPersonality === true,
|
||||
isDefault: model.isDefault === true,
|
||||
|
|
@ -477,6 +486,14 @@ export class ClaudeMultimodelBridgeService {
|
|||
runtimeStatus.runtimeCapabilities.reasoningEffort.configPassthrough === true,
|
||||
}
|
||||
: undefined,
|
||||
fastMode: runtimeStatus.runtimeCapabilities.fastMode
|
||||
? {
|
||||
supported: runtimeStatus.runtimeCapabilities.fastMode.supported === true,
|
||||
available: runtimeStatus.runtimeCapabilities.fastMode.available === true,
|
||||
reason: runtimeStatus.runtimeCapabilities.fastMode.reason ?? null,
|
||||
source: 'runtime',
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2384,6 +2384,7 @@ export class TeamDataService {
|
|||
color: request.color,
|
||||
cwd: request.cwd?.trim() || '',
|
||||
providerBackendId: request.providerBackendId,
|
||||
fastMode: request.fastMode,
|
||||
createdAt: joinedAt,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,13 @@ const QUOTA_EXHAUSTED_TOKENS = [
|
|||
'quota exceeded',
|
||||
'quota exhausted',
|
||||
];
|
||||
const RATE_LIMITED_TOKENS = ['rate limit', 'too many requests', '429'];
|
||||
const RATE_LIMITED_TOKENS = [
|
||||
'rate limit',
|
||||
'too many requests',
|
||||
'429',
|
||||
'model cooldown',
|
||||
'cooling down',
|
||||
];
|
||||
const AUTH_ERROR_TOKENS = [
|
||||
'unauthorized',
|
||||
'forbidden',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as path from 'path';
|
|||
|
||||
import { atomicWriteAsync } from './atomicWrite';
|
||||
|
||||
import type { ProviderModelLaunchIdentity, TeamProviderId } from '@shared/types';
|
||||
import type { ProviderModelLaunchIdentity, TeamFastMode, TeamProviderId } from '@shared/types';
|
||||
|
||||
/**
|
||||
* Persisted team-level metadata saved by the UI before CLI provisioning.
|
||||
|
|
@ -25,6 +25,7 @@ export interface TeamMetaFile {
|
|||
providerBackendId?: string;
|
||||
model?: string;
|
||||
effort?: string;
|
||||
fastMode?: TeamFastMode;
|
||||
skipPermissions?: boolean;
|
||||
worktree?: string;
|
||||
extraCliArgs?: string;
|
||||
|
|
@ -51,6 +52,10 @@ function normalizeOptionalString(value: unknown): string | null {
|
|||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function normalizeFastMode(value: unknown): TeamFastMode | null {
|
||||
return value === 'inherit' || value === 'on' || value === 'off' ? value : null;
|
||||
}
|
||||
|
||||
function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity | undefined {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return undefined;
|
||||
|
|
@ -80,7 +85,8 @@ function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity |
|
|||
raw.selectedEffort === 'low' ||
|
||||
raw.selectedEffort === 'medium' ||
|
||||
raw.selectedEffort === 'high' ||
|
||||
raw.selectedEffort === 'xhigh'
|
||||
raw.selectedEffort === 'xhigh' ||
|
||||
raw.selectedEffort === 'max'
|
||||
? raw.selectedEffort
|
||||
: null;
|
||||
const resolvedEffort =
|
||||
|
|
@ -89,7 +95,8 @@ function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity |
|
|||
raw.resolvedEffort === 'low' ||
|
||||
raw.resolvedEffort === 'medium' ||
|
||||
raw.resolvedEffort === 'high' ||
|
||||
raw.resolvedEffort === 'xhigh'
|
||||
raw.resolvedEffort === 'xhigh' ||
|
||||
raw.resolvedEffort === 'max'
|
||||
? raw.resolvedEffort
|
||||
: null;
|
||||
|
||||
|
|
@ -105,6 +112,9 @@ function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity |
|
|||
catalogFetchedAt: normalizeOptionalString(raw.catalogFetchedAt),
|
||||
selectedEffort,
|
||||
resolvedEffort,
|
||||
selectedFastMode: normalizeFastMode(raw.selectedFastMode),
|
||||
resolvedFastMode: typeof raw.resolvedFastMode === 'boolean' ? raw.resolvedFastMode : null,
|
||||
fastResolutionReason: normalizeOptionalString(raw.fastResolutionReason),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +183,7 @@ export class TeamMetaStore {
|
|||
),
|
||||
model: typeof file.model === 'string' ? file.model.trim() || undefined : undefined,
|
||||
effort: typeof file.effort === 'string' ? file.effort.trim() || undefined : undefined,
|
||||
fastMode: normalizeFastMode(file.fastMode) ?? undefined,
|
||||
skipPermissions: typeof file.skipPermissions === 'boolean' ? file.skipPermissions : undefined,
|
||||
worktree: typeof file.worktree === 'string' ? file.worktree.trim() || undefined : undefined,
|
||||
extraCliArgs:
|
||||
|
|
@ -198,6 +209,7 @@ export class TeamMetaStore {
|
|||
),
|
||||
model: data.model?.trim() || undefined,
|
||||
effort: data.effort?.trim() || undefined,
|
||||
fastMode: normalizeFastMode(data.fastMode) ?? undefined,
|
||||
skipPermissions: data.skipPermissions,
|
||||
worktree: data.worktree?.trim() || undefined,
|
||||
extraCliArgs: data.extraCliArgs?.trim() || undefined,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import {
|
|||
killTmuxPaneForCurrentPlatformSync,
|
||||
listTmuxPanePidsForCurrentPlatform,
|
||||
} from '@features/tmux-installer/main';
|
||||
import {
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '@features/anthropic-runtime-profile/main';
|
||||
import { ConfigManager } from '@main/services/infrastructure/ConfigManager';
|
||||
import { NotificationManager } from '@main/services/infrastructure/NotificationManager';
|
||||
import { getAppIconPath } from '@main/utils/appIcon';
|
||||
|
|
@ -170,6 +174,7 @@ interface RelayInboxMessageView {
|
|||
}
|
||||
|
||||
import type {
|
||||
CliProviderModelCatalog,
|
||||
ActiveToolCall,
|
||||
CliProviderRuntimeCapabilities,
|
||||
CrossTeamSendResult,
|
||||
|
|
@ -190,6 +195,7 @@ import type {
|
|||
TeamConfig,
|
||||
TeamCreateRequest,
|
||||
TeamCreateResponse,
|
||||
TeamFastMode,
|
||||
TeamLaunchAggregateState,
|
||||
TeamLaunchRequest,
|
||||
TeamLaunchResponse,
|
||||
|
|
@ -328,6 +334,7 @@ interface RuntimeStatusCommandResponse {
|
|||
providers?: Record<
|
||||
string,
|
||||
{
|
||||
modelCatalog?: CliProviderModelCatalog | null;
|
||||
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
|
||||
}
|
||||
>;
|
||||
|
|
@ -336,6 +343,7 @@ interface RuntimeStatusCommandResponse {
|
|||
interface RuntimeProviderLaunchFacts {
|
||||
defaultModel: string | null;
|
||||
modelIds: Set<string>;
|
||||
modelCatalog: CliProviderModelCatalog | null;
|
||||
runtimeCapabilities: CliProviderRuntimeCapabilities | null;
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +424,47 @@ function isCodexEffortRuntimeSupported(
|
|||
return reasoning?.configPassthrough === true && reasoning.values.includes(effort);
|
||||
}
|
||||
|
||||
function getAnthropicFastModeDefault(): boolean {
|
||||
return (
|
||||
ConfigManager.getInstance().getConfig().providerConnections.anthropic.fastModeDefault === true
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAnthropicSelectionFromFacts(params: {
|
||||
selectedModel?: string;
|
||||
limitContext?: boolean;
|
||||
facts: Pick<RuntimeProviderLaunchFacts, 'modelCatalog' | 'runtimeCapabilities'>;
|
||||
}) {
|
||||
return resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: params.facts.modelCatalog,
|
||||
runtimeCapabilities: params.facts.runtimeCapabilities,
|
||||
},
|
||||
selectedModel: params.selectedModel,
|
||||
limitContext: params.limitContext === true,
|
||||
});
|
||||
}
|
||||
|
||||
function buildAnthropicSettingsArgs(
|
||||
providerId: TeamProviderId,
|
||||
launchIdentity?: ProviderModelLaunchIdentity | null
|
||||
): string[] {
|
||||
if (providerId !== 'anthropic' || typeof launchIdentity?.resolvedFastMode !== 'boolean') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const settings = launchIdentity.resolvedFastMode
|
||||
? {
|
||||
fastMode: true,
|
||||
fastModePerSessionOptIn: false,
|
||||
}
|
||||
: {
|
||||
fastMode: false,
|
||||
};
|
||||
|
||||
return ['--settings', JSON.stringify(settings)];
|
||||
}
|
||||
|
||||
function isProbeTimeoutMessage(message: string): boolean {
|
||||
const lower = message.toLowerCase();
|
||||
return (
|
||||
|
|
@ -522,7 +571,10 @@ function mergeProvisioningWarnings(
|
|||
}
|
||||
|
||||
function buildRuntimeLaunchWarning(
|
||||
request: Pick<TeamCreateRequest, 'providerId' | 'providerBackendId' | 'model' | 'effort'>,
|
||||
request: Pick<
|
||||
TeamCreateRequest,
|
||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode'
|
||||
>,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options?: {
|
||||
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
||||
|
|
@ -534,6 +586,10 @@ function buildRuntimeLaunchWarning(
|
|||
const providerLabel = getTeamProviderLabel(providerId);
|
||||
const modelLabel = request.model?.trim() || 'default';
|
||||
const effortLabel = request.effort ?? 'default';
|
||||
const fastLabel =
|
||||
providerId === 'anthropic'
|
||||
? `, fast ${request.fastMode ?? (getAnthropicFastModeDefault() ? 'inherit:on' : 'inherit:off')}`
|
||||
: '';
|
||||
const backend =
|
||||
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
|
||||
getConfiguredRuntimeBackend(providerId);
|
||||
|
|
@ -564,14 +620,17 @@ function buildRuntimeLaunchWarning(
|
|||
typeof options?.expectedMembersCount === 'number'
|
||||
? `, members ${options.expectedMembersCount}`
|
||||
: '';
|
||||
return `Launch runtime: ${providerLabel} · ${modelLabel} · ${effortLabel}${backendPart}${authPart}${promptPart}${membersPart}${flagsPart}`;
|
||||
return `Launch runtime: ${providerLabel} · ${modelLabel} · ${effortLabel}${fastLabel}${backendPart}${authPart}${promptPart}${membersPart}${flagsPart}`;
|
||||
}
|
||||
|
||||
function logRuntimeLaunchSnapshot(
|
||||
teamName: string,
|
||||
claudePath: string,
|
||||
args: string[],
|
||||
request: Pick<TeamCreateRequest, 'providerId' | 'providerBackendId' | 'model' | 'effort'>,
|
||||
request: Pick<
|
||||
TeamCreateRequest,
|
||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode'
|
||||
>,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options?: {
|
||||
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
||||
|
|
@ -586,6 +645,7 @@ function logRuntimeLaunchSnapshot(
|
|||
providerBackendId: migrateProviderBackendId(providerId, request.providerBackendId) ?? null,
|
||||
model: request.model ?? null,
|
||||
effort: request.effort ?? null,
|
||||
fastMode: request.fastMode ?? null,
|
||||
configuredBackend:
|
||||
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
|
||||
getConfiguredRuntimeBackend(providerId),
|
||||
|
|
@ -2984,12 +3044,16 @@ export class TeamProvisioningService {
|
|||
}
|
||||
);
|
||||
const runtimeStatusPromise =
|
||||
params.providerId === 'codex'
|
||||
? execCli(params.claudePath, ['runtime', 'status', '--json', '--provider', 'codex'], {
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
timeout: 8_000,
|
||||
})
|
||||
params.providerId === 'codex' || params.providerId === 'anthropic'
|
||||
? execCli(
|
||||
params.claudePath,
|
||||
['runtime', 'status', '--json', '--provider', params.providerId],
|
||||
{
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
timeout: 8_000,
|
||||
}
|
||||
)
|
||||
: null;
|
||||
|
||||
const [modelListResult, runtimeStatusResult] = await Promise.allSettled([
|
||||
|
|
@ -3020,6 +3084,7 @@ export class TeamProvisioningService {
|
|||
}
|
||||
|
||||
let runtimeCapabilities: CliProviderRuntimeCapabilities | null = null;
|
||||
let modelCatalog: CliProviderModelCatalog | null = null;
|
||||
if (
|
||||
runtimeStatusResult.status === 'fulfilled' &&
|
||||
runtimeStatusResult.value &&
|
||||
|
|
@ -3029,7 +3094,12 @@ export class TeamProvisioningService {
|
|||
const parsed = extractJsonObjectFromCli<RuntimeStatusCommandResponse>(
|
||||
runtimeStatusResult.value.stdout
|
||||
);
|
||||
runtimeCapabilities = parsed.providers?.[params.providerId]?.runtimeCapabilities ?? null;
|
||||
const providerStatus = parsed.providers?.[params.providerId];
|
||||
runtimeCapabilities = providerStatus?.runtimeCapabilities ?? null;
|
||||
modelCatalog =
|
||||
providerStatus?.modelCatalog?.providerId === params.providerId
|
||||
? providerStatus.modelCatalog
|
||||
: null;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[${params.providerId}] Failed to parse runtime capabilities for launch validation: ${
|
||||
|
|
@ -3039,16 +3109,32 @@ export class TeamProvisioningService {
|
|||
}
|
||||
}
|
||||
|
||||
if (modelCatalog) {
|
||||
for (const model of modelCatalog.models ?? []) {
|
||||
const launchModel = model.launchModel?.trim();
|
||||
if (launchModel) {
|
||||
modelIds.add(launchModel);
|
||||
}
|
||||
const catalogId = model.id?.trim();
|
||||
if (catalogId) {
|
||||
modelIds.add(catalogId);
|
||||
}
|
||||
}
|
||||
defaultModel = modelCatalog.defaultLaunchModel?.trim() || defaultModel;
|
||||
}
|
||||
|
||||
return {
|
||||
defaultModel:
|
||||
params.providerId === 'anthropic'
|
||||
? resolveAnthropicLaunchModel({
|
||||
limitContext: params.limitContext === true,
|
||||
availableLaunchModels: modelIds,
|
||||
availableLaunchModels:
|
||||
modelCatalog?.models.map((model) => model.launchModel) ?? modelIds,
|
||||
defaultLaunchModel: defaultModel,
|
||||
})
|
||||
: defaultModel,
|
||||
modelIds,
|
||||
modelCatalog,
|
||||
runtimeCapabilities,
|
||||
};
|
||||
}
|
||||
|
|
@ -3056,7 +3142,7 @@ export class TeamProvisioningService {
|
|||
private buildProviderModelLaunchIdentity(params: {
|
||||
request: Pick<
|
||||
TeamCreateRequest,
|
||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'limitContext'
|
||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode' | 'limitContext'
|
||||
>;
|
||||
facts: RuntimeProviderLaunchFacts;
|
||||
}): ProviderModelLaunchIdentity {
|
||||
|
|
@ -3068,6 +3154,39 @@ export class TeamProvisioningService {
|
|||
limitContext: params.request.limitContext,
|
||||
facts: params.facts,
|
||||
});
|
||||
if (providerId === 'anthropic') {
|
||||
const selection = resolveAnthropicSelectionFromFacts({
|
||||
selectedModel: params.request.model,
|
||||
limitContext: params.request.limitContext,
|
||||
facts: params.facts,
|
||||
});
|
||||
const fastResolution = resolveAnthropicFastMode({
|
||||
selection,
|
||||
selectedFastMode: params.request.fastMode,
|
||||
providerFastModeDefault: getAnthropicFastModeDefault(),
|
||||
});
|
||||
|
||||
return {
|
||||
providerId,
|
||||
providerBackendId:
|
||||
migrateProviderBackendId(providerId, params.request.providerBackendId) ?? null,
|
||||
selectedModel: explicitModel ?? null,
|
||||
selectedModelKind: explicitModel ? 'explicit' : 'default',
|
||||
resolvedLaunchModel: selection.resolvedLaunchModel ?? resolvedLaunchModel,
|
||||
catalogId:
|
||||
selection.catalogModel?.id?.trim() ||
|
||||
selection.resolvedLaunchModel ||
|
||||
resolvedLaunchModel,
|
||||
catalogSource: selection.catalogSource,
|
||||
catalogFetchedAt: selection.catalogFetchedAt,
|
||||
selectedEffort: params.request.effort ?? null,
|
||||
resolvedEffort: params.request.effort ?? selection.defaultEffort ?? null,
|
||||
selectedFastMode: params.request.fastMode ?? 'inherit',
|
||||
resolvedFastMode: fastResolution.resolvedFastMode,
|
||||
fastResolutionReason: fastResolution.disabledReason,
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedEffort = params.request.effort ?? null;
|
||||
|
||||
return {
|
||||
|
|
@ -3090,10 +3209,51 @@ export class TeamProvisioningService {
|
|||
providerId: TeamProviderId;
|
||||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
limitContext?: boolean;
|
||||
facts: RuntimeProviderLaunchFacts;
|
||||
}): void {
|
||||
const explicitModel = getExplicitLaunchModelSelection(params.model);
|
||||
|
||||
if (params.providerId === 'anthropic') {
|
||||
const selection = resolveAnthropicSelectionFromFacts({
|
||||
selectedModel: params.model,
|
||||
limitContext: params.limitContext,
|
||||
facts: params.facts,
|
||||
});
|
||||
const resolvedLaunchModel = selection.resolvedLaunchModel?.trim() || null;
|
||||
if (!resolvedLaunchModel) {
|
||||
throw new Error(
|
||||
`${params.actorLabel} could not resolve the selected Anthropic model against the current runtime catalog.`
|
||||
);
|
||||
}
|
||||
if (params.facts.modelIds.size > 0 && !params.facts.modelIds.has(resolvedLaunchModel)) {
|
||||
throw new Error(
|
||||
`${params.actorLabel} resolves to Anthropic model "${resolvedLaunchModel}", but the current runtime does not list it as launchable.`
|
||||
);
|
||||
}
|
||||
if (params.effort && !selection.supportedEfforts.includes(params.effort)) {
|
||||
const modelLabel = selection.displayName ?? resolvedLaunchModel;
|
||||
throw new Error(
|
||||
`${params.actorLabel} uses Anthropic effort "${params.effort}", but ${modelLabel} does not support it in the current runtime.`
|
||||
);
|
||||
}
|
||||
|
||||
const fastResolution = resolveAnthropicFastMode({
|
||||
selection,
|
||||
selectedFastMode: params.fastMode,
|
||||
providerFastModeDefault: getAnthropicFastModeDefault(),
|
||||
});
|
||||
if ((params.fastMode ?? 'inherit') === 'on' && !fastResolution.selectable) {
|
||||
throw new Error(
|
||||
`${params.actorLabel} enables Anthropic Fast mode, but ${
|
||||
fastResolution.disabledReason ?? 'it is unavailable for the selected runtime or model.'
|
||||
}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.providerId !== 'codex') {
|
||||
if (params.effort && !isLegacySafeEffort(params.effort)) {
|
||||
throw new Error(
|
||||
|
|
@ -3133,7 +3293,7 @@ export class TeamProvisioningService {
|
|||
env: NodeJS.ProcessEnv;
|
||||
request: Pick<
|
||||
TeamCreateRequest,
|
||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'limitContext'
|
||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode' | 'limitContext'
|
||||
>;
|
||||
effectiveMembers: TeamCreateRequest['members'];
|
||||
}): Promise<ProviderModelLaunchIdentity> {
|
||||
|
|
@ -3161,6 +3321,8 @@ export class TeamProvisioningService {
|
|||
providerId: leadProviderId,
|
||||
model: params.request.model,
|
||||
effort: params.request.effort,
|
||||
fastMode: params.request.fastMode,
|
||||
limitContext: params.request.limitContext,
|
||||
facts: leadFacts,
|
||||
});
|
||||
|
||||
|
|
@ -3172,6 +3334,7 @@ export class TeamProvisioningService {
|
|||
providerId: memberProviderId,
|
||||
model: member.model,
|
||||
effort: member.effort,
|
||||
limitContext: params.request.limitContext,
|
||||
facts: memberFacts,
|
||||
});
|
||||
}
|
||||
|
|
@ -4867,6 +5030,7 @@ export class TeamProvisioningService {
|
|||
run?.request.providerId ?? persistedTeamMeta?.providerId,
|
||||
run?.request.providerBackendId ?? persistedTeamMeta?.providerBackendId
|
||||
),
|
||||
fastMode: run?.request.fastMode ?? persistedTeamMeta?.fastMode,
|
||||
members: snapshotMembers,
|
||||
};
|
||||
|
||||
|
|
@ -6728,6 +6892,8 @@ export class TeamProvisioningService {
|
|||
request.model,
|
||||
launchIdentity
|
||||
);
|
||||
const resolvedProviderId = resolveTeamProviderId(request.providerId);
|
||||
const anthropicSettingsArgs = buildAnthropicSettingsArgs(resolvedProviderId, launchIdentity);
|
||||
const spawnArgs = [
|
||||
'--input-format',
|
||||
'stream-json',
|
||||
|
|
@ -6751,7 +6917,8 @@ export class TeamProvisioningService {
|
|||
? ['--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions']
|
||||
: ['--permission-prompt-tool', 'stdio', '--permission-mode', 'default']),
|
||||
...(launchModelArg ? ['--model', launchModelArg] : []),
|
||||
...(request.effort ? ['--effort', request.effort] : []),
|
||||
...(launchIdentity.resolvedEffort ? ['--effort', launchIdentity.resolvedEffort] : []),
|
||||
...anthropicSettingsArgs,
|
||||
...(request.worktree ? ['--worktree', request.worktree] : []),
|
||||
...parseCliArgs(request.extraCliArgs),
|
||||
...providerArgs,
|
||||
|
|
@ -6784,6 +6951,7 @@ export class TeamProvisioningService {
|
|||
providerBackendId: request.providerBackendId,
|
||||
model: request.model,
|
||||
effort: request.effort,
|
||||
fastMode: request.fastMode,
|
||||
skipPermissions: request.skipPermissions,
|
||||
worktree: request.worktree,
|
||||
extraCliArgs: request.extraCliArgs,
|
||||
|
|
@ -7185,6 +7353,7 @@ export class TeamProvisioningService {
|
|||
providerBackendId: request.providerBackendId,
|
||||
model: request.model,
|
||||
effort: request.effort,
|
||||
fastMode: request.fastMode,
|
||||
skipPermissions: request.skipPermissions,
|
||||
};
|
||||
|
||||
|
|
@ -7389,12 +7558,15 @@ export class TeamProvisioningService {
|
|||
request.model,
|
||||
launchIdentity
|
||||
);
|
||||
const resolvedProviderId = resolveTeamProviderId(request.providerId);
|
||||
const anthropicSettingsArgs = buildAnthropicSettingsArgs(resolvedProviderId, launchIdentity);
|
||||
if (launchModelArg) {
|
||||
launchArgs.push('--model', launchModelArg);
|
||||
}
|
||||
if (request.effort) {
|
||||
launchArgs.push('--effort', request.effort);
|
||||
if (launchIdentity.resolvedEffort) {
|
||||
launchArgs.push('--effort', launchIdentity.resolvedEffort);
|
||||
}
|
||||
launchArgs.push(...anthropicSettingsArgs);
|
||||
if (request.worktree) {
|
||||
launchArgs.push('--worktree', request.worktree);
|
||||
}
|
||||
|
|
@ -7423,6 +7595,7 @@ export class TeamProvisioningService {
|
|||
providerBackendId: request.providerBackendId,
|
||||
model: request.model,
|
||||
effort: request.effort,
|
||||
fastMode: request.fastMode,
|
||||
skipPermissions: request.skipPermissions,
|
||||
worktree: request.worktree,
|
||||
extraCliArgs: request.extraCliArgs,
|
||||
|
|
|
|||
|
|
@ -722,6 +722,19 @@ export const ProviderRuntimeSettingsDialog = ({
|
|||
const codexActionBusy =
|
||||
disabled || selectedProviderLoading || connectionSaving || codexAccount.loading;
|
||||
const runtimeBusy = disabled || selectedProviderLoading || runtimeSaving;
|
||||
const anthropicFastModeCapability =
|
||||
selectedProvider?.providerId === 'anthropic'
|
||||
? (selectedProvider.runtimeCapabilities?.fastMode ?? null)
|
||||
: null;
|
||||
const anthropicFastModeEnabled =
|
||||
appConfig?.providerConnections?.anthropic.fastModeDefault === true;
|
||||
const anthropicFastModeSupported = anthropicFastModeCapability?.supported === true;
|
||||
const anthropicFastModeAvailable = anthropicFastModeCapability?.available === true;
|
||||
const anthropicFastModeDisabledReason =
|
||||
anthropicFastModeCapability?.reason ??
|
||||
(anthropicFastModeSupported
|
||||
? 'Fast mode is currently unavailable for this Anthropic runtime.'
|
||||
: 'This Anthropic runtime does not expose Fast mode.');
|
||||
const connectionMethodCardsHint = selectedProvider
|
||||
? getConnectionMethodCardsHint(selectedProvider)
|
||||
: null;
|
||||
|
|
@ -969,6 +982,29 @@ export const ProviderRuntimeSettingsDialog = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleAnthropicFastModeDefaultChange = async (enabled: boolean): Promise<void> => {
|
||||
if (selectedProvider?.providerId !== 'anthropic' || anthropicFastModeEnabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setConnectionSaving(true);
|
||||
setConnectionError(null);
|
||||
try {
|
||||
await updateConfig('providerConnections', {
|
||||
anthropic: {
|
||||
fastModeDefault: enabled,
|
||||
},
|
||||
});
|
||||
await onRefreshProvider?.('anthropic');
|
||||
} catch (error) {
|
||||
setConnectionError(
|
||||
error instanceof Error ? error.message : 'Failed to update Anthropic Fast mode'
|
||||
);
|
||||
} finally {
|
||||
setConnectionSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
|
|
@ -1188,6 +1224,50 @@ export const ProviderRuntimeSettingsDialog = ({
|
|||
) : null}
|
||||
</div>
|
||||
|
||||
{selectedProvider.providerId === 'anthropic' ? (
|
||||
<div
|
||||
className="space-y-2 rounded-md border p-3"
|
||||
style={{ borderColor: 'var(--color-border-subtle)' }}
|
||||
>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
||||
Fast mode default
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
||||
Apply Claude Code Fast mode by default for new Anthropic team launches when the
|
||||
resolved model and runtime allow it.
|
||||
</div>
|
||||
{anthropicFastModeSupported ? (
|
||||
<div className="inline-flex rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
|
||||
{[
|
||||
{ enabled: false, label: 'Default Off' },
|
||||
{ enabled: true, label: 'Prefer Fast' },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.label}
|
||||
type="button"
|
||||
className={`rounded-[3px] px-3 py-1 text-xs font-medium transition-colors ${
|
||||
anthropicFastModeEnabled === option.enabled
|
||||
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)] shadow-sm'
|
||||
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
|
||||
}`}
|
||||
disabled={connectionBusy || !anthropicFastModeAvailable}
|
||||
onClick={() => void handleAnthropicFastModeDefaultChange(option.enabled)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
||||
{anthropicFastModeSupported && anthropicFastModeAvailable
|
||||
? anthropicFastModeEnabled
|
||||
? 'New Anthropic launches will request Fast mode by default when the resolved model supports it.'
|
||||
: 'New Anthropic launches stay on normal speed unless a team explicitly enables Fast mode.'
|
||||
: anthropicFastModeDisabledReason}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{selectedProvider.providerId === 'codex' ? (
|
||||
<div
|
||||
className="space-y-3 rounded-md border p-3"
|
||||
|
|
|
|||
|
|
@ -331,6 +331,7 @@ export function useSettingsHandlers({
|
|||
providerConnections: {
|
||||
anthropic: {
|
||||
authMode: 'auto',
|
||||
fastModeDefault: false,
|
||||
},
|
||||
codex: {
|
||||
preferredAuthMode: 'auto',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '@features/anthropic-runtime-profile/renderer';
|
||||
import { Label } from '@renderer/components/ui/label';
|
||||
import { useEffectiveCliProviderStatus } from '@renderer/hooks/useEffectiveCliProviderStatus';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { Zap } from 'lucide-react';
|
||||
|
||||
import type { TeamFastMode } from '@shared/types';
|
||||
|
||||
export interface AnthropicFastModeSelectorProps {
|
||||
value: TeamFastMode;
|
||||
onValueChange: (value: TeamFastMode) => void;
|
||||
providerFastModeDefault: boolean;
|
||||
model?: string;
|
||||
limitContext: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export const AnthropicFastModeSelector: React.FC<AnthropicFastModeSelectorProps> = ({
|
||||
value,
|
||||
onValueChange,
|
||||
providerFastModeDefault,
|
||||
model,
|
||||
limitContext,
|
||||
id,
|
||||
}) => {
|
||||
const { providerStatus } = useEffectiveCliProviderStatus('anthropic');
|
||||
|
||||
const selection = useMemo(
|
||||
() =>
|
||||
resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: providerStatus?.modelCatalog,
|
||||
runtimeCapabilities: providerStatus?.runtimeCapabilities,
|
||||
},
|
||||
selectedModel: model,
|
||||
limitContext,
|
||||
}),
|
||||
[limitContext, model, providerStatus?.modelCatalog, providerStatus?.runtimeCapabilities]
|
||||
);
|
||||
|
||||
const resolution = useMemo(
|
||||
() =>
|
||||
resolveAnthropicFastMode({
|
||||
selection,
|
||||
selectedFastMode: value,
|
||||
providerFastModeDefault,
|
||||
}),
|
||||
[providerFastModeDefault, selection, value]
|
||||
);
|
||||
|
||||
if (!resolution.showFastModeControl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultLabel = providerFastModeDefault ? 'Default (Fast)' : 'Default (Off)';
|
||||
const helperText =
|
||||
value === 'inherit'
|
||||
? `Default currently resolves to ${resolution.resolvedFastMode ? 'Fast' : 'Off'}.`
|
||||
: (resolution.disabledReason ??
|
||||
'Fast mode is runtime-backed and only unlocks when the resolved Anthropic launch model supports it.');
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<Label htmlFor={id} className="label-optional mb-1.5 block">
|
||||
Fast mode (optional)
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap size={16} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||
<div className="inline-flex flex-wrap rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
|
||||
{[
|
||||
{ value: 'inherit' as const, label: defaultLabel, disabled: false },
|
||||
{ value: 'on' as const, label: 'Fast', disabled: !resolution.selectable },
|
||||
{ value: 'off' as const, label: 'Off', disabled: false },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
id={option.value === value ? id : undefined}
|
||||
className={cn(
|
||||
'rounded-[3px] px-3 py-1 text-xs font-medium transition-colors',
|
||||
value === option.value
|
||||
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)] shadow-sm'
|
||||
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]',
|
||||
option.disabled &&
|
||||
'cursor-not-allowed opacity-50 hover:text-[var(--color-text-muted)]'
|
||||
)}
|
||||
disabled={option.disabled}
|
||||
onClick={() => onValueChange(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[var(--color-text-muted)]">{helperText}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
reconcileAnthropicRuntimeSelections,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '@features/anthropic-runtime-profile/renderer';
|
||||
import {
|
||||
mergeCodexCliStatusWithSnapshot,
|
||||
useCodexAccountSnapshot,
|
||||
|
|
@ -100,6 +105,7 @@ import type {
|
|||
EffortLevel,
|
||||
Project,
|
||||
TeamCreateRequest,
|
||||
TeamFastMode,
|
||||
TeamProviderId,
|
||||
TeamProvisioningMemberInput,
|
||||
} from '@shared/types';
|
||||
|
|
@ -121,6 +127,11 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
|
|||
return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
|
||||
}
|
||||
|
||||
function getStoredTeamFastMode(): TeamFastMode {
|
||||
const stored = localStorage.getItem('team:lastSelectedFastMode');
|
||||
return stored === 'on' || stored === 'off' || stored === 'inherit' ? stored : 'inherit';
|
||||
}
|
||||
|
||||
function isEphemeralRenderedProjectPath(projectPath: string | null | undefined): boolean {
|
||||
const normalized = normalizePath(projectPath ?? '').toLowerCase();
|
||||
return (
|
||||
|
|
@ -313,6 +324,9 @@ export const CreateTeamDialog = ({
|
|||
}: CreateTeamDialogProps): React.JSX.Element => {
|
||||
const { isLight } = useTheme();
|
||||
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
||||
const anthropicProviderFastModeDefault = useStore(
|
||||
(s) => s.appConfig?.providerConnections?.anthropic.fastModeDefault ?? false
|
||||
);
|
||||
const cliStatus = useStore((s) => s.cliStatus);
|
||||
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
|
||||
const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus);
|
||||
|
|
@ -396,6 +410,8 @@ export const CreateTeamDialog = ({
|
|||
const stored = localStorage.getItem('team:lastSelectedEffort');
|
||||
return stored === null ? 'medium' : stored;
|
||||
});
|
||||
const [selectedFastMode, setSelectedFastModeRaw] = useState<TeamFastMode>(getStoredTeamFastMode);
|
||||
const [anthropicRuntimeNotice, setAnthropicRuntimeNotice] = useState<string | null>(null);
|
||||
|
||||
// Advanced CLI section state (use teamName-derived key for localStorage)
|
||||
const advancedKey = sanitizeTeamName(teamName.trim()) || '_new_';
|
||||
|
|
@ -456,6 +472,11 @@ export const CreateTeamDialog = ({
|
|||
localStorage.setItem('team:lastSelectedEffort', value);
|
||||
};
|
||||
|
||||
const setSelectedFastMode = (value: TeamFastMode): void => {
|
||||
setSelectedFastModeRaw(value);
|
||||
localStorage.setItem('team:lastSelectedFastMode', value);
|
||||
};
|
||||
|
||||
const setWorktreeEnabled = (value: boolean): void => {
|
||||
setWorktreeEnabledRaw(value);
|
||||
localStorage.setItem(`team:lastWorktreeEnabled:${advancedKey}`, String(value));
|
||||
|
|
@ -1003,6 +1024,84 @@ export const CreateTeamDialog = ({
|
|||
),
|
||||
[limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId]
|
||||
);
|
||||
const anthropicRuntimeSelection = useMemo(
|
||||
() =>
|
||||
selectedProviderId === 'anthropic'
|
||||
? resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: runtimeProviderStatusById.get('anthropic')?.modelCatalog,
|
||||
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
|
||||
},
|
||||
selectedModel,
|
||||
limitContext,
|
||||
})
|
||||
: null,
|
||||
[limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId]
|
||||
);
|
||||
const anthropicFastModeResolution = useMemo(
|
||||
() =>
|
||||
selectedProviderId === 'anthropic' && anthropicRuntimeSelection
|
||||
? resolveAnthropicFastMode({
|
||||
selection: anthropicRuntimeSelection,
|
||||
selectedFastMode,
|
||||
providerFastModeDefault: anthropicProviderFastModeDefault,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
anthropicProviderFastModeDefault,
|
||||
anthropicRuntimeSelection,
|
||||
selectedFastMode,
|
||||
selectedProviderId,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProviderId !== 'anthropic') {
|
||||
setAnthropicRuntimeNotice(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const reconciliation = reconcileAnthropicRuntimeSelections({
|
||||
selection:
|
||||
anthropicRuntimeSelection ??
|
||||
resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: null,
|
||||
runtimeCapabilities: null,
|
||||
},
|
||||
selectedModel,
|
||||
limitContext,
|
||||
}),
|
||||
selectedEffort,
|
||||
selectedFastMode,
|
||||
providerFastModeDefault: anthropicProviderFastModeDefault,
|
||||
});
|
||||
|
||||
const notices: string[] = [];
|
||||
if (reconciliation.nextEffort !== selectedEffort) {
|
||||
setSelectedEffortRaw(reconciliation.nextEffort);
|
||||
localStorage.setItem('team:lastSelectedEffort', reconciliation.nextEffort);
|
||||
if (reconciliation.effortResetReason) {
|
||||
notices.push(reconciliation.effortResetReason);
|
||||
}
|
||||
}
|
||||
if (reconciliation.nextFastMode !== selectedFastMode) {
|
||||
setSelectedFastModeRaw(reconciliation.nextFastMode);
|
||||
localStorage.setItem('team:lastSelectedFastMode', reconciliation.nextFastMode);
|
||||
if (reconciliation.fastModeResetReason) {
|
||||
notices.push(reconciliation.fastModeResetReason);
|
||||
}
|
||||
}
|
||||
setAnthropicRuntimeNotice(notices.length > 0 ? notices.join(' ') : null);
|
||||
}, [
|
||||
anthropicProviderFastModeDefault,
|
||||
anthropicRuntimeSelection,
|
||||
limitContext,
|
||||
selectedEffort,
|
||||
selectedFastMode,
|
||||
selectedModel,
|
||||
selectedProviderId,
|
||||
]);
|
||||
|
||||
const sanitizedTeamName = sanitizeTeamName(teamName.trim());
|
||||
const teamNameInlineError = validateTeamNameInline(teamName);
|
||||
|
|
@ -1026,6 +1125,7 @@ export const CreateTeamDialog = ({
|
|||
) ?? undefined,
|
||||
model: effectiveModel,
|
||||
effort: (selectedEffort as EffortLevel) || undefined,
|
||||
fastMode: selectedProviderId === 'anthropic' ? selectedFastMode : undefined,
|
||||
limitContext,
|
||||
skipPermissions,
|
||||
worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined,
|
||||
|
|
@ -1043,6 +1143,7 @@ export const CreateTeamDialog = ({
|
|||
runtimeProviderStatusById,
|
||||
effectiveModel,
|
||||
selectedEffort,
|
||||
selectedFastMode,
|
||||
limitContext,
|
||||
skipPermissions,
|
||||
worktreeEnabled,
|
||||
|
|
@ -1132,18 +1233,49 @@ export const CreateTeamDialog = ({
|
|||
args.push('--mcp-config', '<auto>', '--disallowedTools', APP_TEAM_RUNTIME_DISALLOWED_TOOLS);
|
||||
if (skipPermissions) args.push('--dangerously-skip-permissions');
|
||||
if (effectiveModel) args.push('--model', effectiveModel);
|
||||
if (selectedEffort) args.push('--effort', selectedEffort);
|
||||
const effectiveEffort =
|
||||
selectedProviderId === 'anthropic'
|
||||
? selectedEffort || anthropicRuntimeSelection?.defaultEffort || ''
|
||||
: selectedEffort;
|
||||
if (effectiveEffort) args.push('--effort', effectiveEffort);
|
||||
if (selectedProviderId === 'anthropic') {
|
||||
const fastSettings = anthropicFastModeResolution?.resolvedFastMode
|
||||
? { fastMode: true, fastModePerSessionOptIn: false }
|
||||
: { fastMode: false };
|
||||
args.push('--settings', JSON.stringify(fastSettings));
|
||||
}
|
||||
return args;
|
||||
}, [skipPermissions, effectiveModel, selectedEffort]);
|
||||
}, [
|
||||
anthropicFastModeResolution?.resolvedFastMode,
|
||||
anthropicRuntimeSelection?.defaultEffort,
|
||||
effectiveModel,
|
||||
selectedEffort,
|
||||
selectedProviderId,
|
||||
skipPermissions,
|
||||
]);
|
||||
|
||||
const launchOptionalSummary = useMemo(() => {
|
||||
const summary: string[] = [];
|
||||
if (prompt.trim()) summary.push('Lead prompt');
|
||||
if (skipPermissions) summary.push('Auto-approve tools');
|
||||
if (selectedProviderId === 'anthropic') {
|
||||
if (selectedFastMode === 'on') summary.push('Fast mode');
|
||||
else if (selectedFastMode === 'off') summary.push('Fast disabled');
|
||||
else if (anthropicProviderFastModeDefault) summary.push('Fast default');
|
||||
}
|
||||
if (worktreeEnabled && worktreeName.trim()) summary.push(`Worktree: ${worktreeName.trim()}`);
|
||||
if (customArgs.trim()) summary.push('Custom CLI args');
|
||||
return summary;
|
||||
}, [prompt, skipPermissions, worktreeEnabled, worktreeName, customArgs]);
|
||||
}, [
|
||||
anthropicProviderFastModeDefault,
|
||||
customArgs,
|
||||
prompt,
|
||||
selectedFastMode,
|
||||
selectedProviderId,
|
||||
skipPermissions,
|
||||
worktreeEnabled,
|
||||
worktreeName,
|
||||
]);
|
||||
|
||||
const teamDetailsSummary = useMemo(() => {
|
||||
const summary: string[] = [];
|
||||
|
|
@ -1212,6 +1344,8 @@ export const CreateTeamDialog = ({
|
|||
color: request.color,
|
||||
members: request.members,
|
||||
cwd: effectiveCwd || undefined,
|
||||
providerBackendId: request.providerBackendId,
|
||||
fastMode: request.fastMode,
|
||||
});
|
||||
onOpenTeam(request.teamName, effectiveCwd || undefined);
|
||||
resetFormState();
|
||||
|
|
@ -1389,11 +1523,15 @@ export const CreateTeamDialog = ({
|
|||
onProviderChange={setSelectedProviderId}
|
||||
onModelChange={setSelectedModel}
|
||||
onEffortChange={setSelectedEffort}
|
||||
fastMode={selectedFastMode}
|
||||
providerFastModeDefault={anthropicProviderFastModeDefault}
|
||||
onFastModeChange={setSelectedFastMode}
|
||||
onLimitContextChange={setLimitContext}
|
||||
syncModelsWithTeammates={syncModelsWithLead}
|
||||
onSyncModelsWithTeammatesChange={handleSyncModelsWithLeadChange}
|
||||
disableGeminiOption={isGeminiUiFrozen()}
|
||||
leadModelIssueText={leadModelIssueText}
|
||||
leadFastModeNotice={anthropicRuntimeNotice}
|
||||
memberModelIssueById={memberModelIssueById}
|
||||
headerTop={
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface EffortLevelSelectorProps {
|
|||
id?: string;
|
||||
providerId?: TeamProviderId;
|
||||
model?: string;
|
||||
limitContext?: boolean;
|
||||
}
|
||||
|
||||
export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
|
||||
|
|
@ -22,9 +23,12 @@ export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
|
|||
id,
|
||||
providerId,
|
||||
model,
|
||||
limitContext,
|
||||
}) => {
|
||||
const { providerStatus } = useEffectiveCliProviderStatus(providerId);
|
||||
const effortOptions = getTeamEffortOptions({ providerId, model, providerStatus });
|
||||
const effortOptions = getTeamEffortOptions({ providerId, model, limitContext, providerStatus });
|
||||
const showsAnthropicMax =
|
||||
providerId === 'anthropic' && effortOptions.some((option) => option.value === 'max');
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
|
|
@ -56,6 +60,12 @@ export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
|
|||
Controls how much reasoning the selected provider invests before responding. Default uses
|
||||
the provider's standard behavior for the selected model.
|
||||
</p>
|
||||
{showsAnthropicMax ? (
|
||||
<p className="mt-1 text-[11px] text-[var(--color-text-muted)]">
|
||||
Max is Anthropic's heavier reasoning mode and only appears when the resolved launch
|
||||
model supports it.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
reconcileAnthropicRuntimeSelections,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '@features/anthropic-runtime-profile/renderer';
|
||||
import {
|
||||
mergeCodexCliStatusWithSnapshot,
|
||||
useCodexAccountSnapshot,
|
||||
|
|
@ -115,6 +120,7 @@ import type {
|
|||
Schedule,
|
||||
ScheduleLaunchConfig,
|
||||
TeamCreateRequest,
|
||||
TeamFastMode,
|
||||
TeamLaunchRequest,
|
||||
TeamProviderId,
|
||||
UpdateSchedulePatch,
|
||||
|
|
@ -216,6 +222,11 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
|
|||
return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
|
||||
}
|
||||
|
||||
function getStoredTeamFastMode(): TeamFastMode {
|
||||
const stored = localStorage.getItem('team:lastSelectedFastMode');
|
||||
return stored === 'on' || stored === 'off' || stored === 'inherit' ? stored : 'inherit';
|
||||
}
|
||||
|
||||
function getProviderLabel(providerId: TeamProviderId): string {
|
||||
return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic';
|
||||
}
|
||||
|
|
@ -254,6 +265,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
const { open, onClose } = props;
|
||||
const { isLight } = useTheme();
|
||||
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
||||
const anthropicProviderFastModeDefault = useStore(
|
||||
(s) => s.appConfig?.providerConnections?.anthropic.fastModeDefault ?? false
|
||||
);
|
||||
const cliStatus = useStore((s) => s.cliStatus);
|
||||
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
|
||||
const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus);
|
||||
|
|
@ -341,6 +355,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
const stored = localStorage.getItem('team:lastSelectedEffort');
|
||||
return stored === null ? 'medium' : stored;
|
||||
});
|
||||
const [selectedFastMode, setSelectedFastModeRaw] = useState<TeamFastMode>(getStoredTeamFastMode);
|
||||
const [anthropicRuntimeNotice, setAnthropicRuntimeNotice] = useState<string | null>(null);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Launch-only state
|
||||
|
|
@ -535,6 +551,11 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
localStorage.setItem('team:lastSelectedEffort', value);
|
||||
};
|
||||
|
||||
const setSelectedFastMode = (value: TeamFastMode): void => {
|
||||
setSelectedFastModeRaw(value);
|
||||
localStorage.setItem('team:lastSelectedFastMode', value);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// localStorage migration: schedule → team namespace (one-time)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -625,6 +646,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
);
|
||||
setSkipPermissionsRaw(schedule.launchConfig.skipPermissions !== false);
|
||||
setSelectedEffortRaw(schedule.launchConfig.effort ?? '');
|
||||
setSelectedFastModeRaw(getStoredTeamFastMode());
|
||||
} else {
|
||||
// Create mode — reset to defaults
|
||||
setSchedLabel('');
|
||||
|
|
@ -641,6 +663,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
setSelectedProviderIdRaw(storedProviderId);
|
||||
setSelectedModelRaw(getStoredTeamModel(storedProviderId));
|
||||
setSelectedEffortRaw('medium');
|
||||
setSelectedFastModeRaw(getStoredTeamFastMode());
|
||||
}
|
||||
|
||||
setLocalError(null);
|
||||
|
|
@ -690,6 +713,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
multimodelEnabled,
|
||||
storedProviderId,
|
||||
storedEffort: storedEffort === null ? 'medium' : storedEffort,
|
||||
storedFastMode: getStoredTeamFastMode(),
|
||||
storedLimitContext: localStorage.getItem('team:lastLimitContext') === 'true',
|
||||
getStoredModel: getStoredTeamModel,
|
||||
});
|
||||
|
|
@ -709,6 +733,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
setSelectedProviderIdRaw(launchPrefill.providerId);
|
||||
setSelectedModelRaw(launchPrefill.model);
|
||||
setSelectedEffortRaw(launchPrefill.effort);
|
||||
setSelectedFastModeRaw(launchPrefill.fastMode);
|
||||
setLimitContextRaw(launchPrefill.limitContext);
|
||||
setSkipPermissionsRaw(
|
||||
savedRequest?.skipPermissions ??
|
||||
|
|
@ -753,6 +778,85 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
) ?? '',
|
||||
[limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId]
|
||||
);
|
||||
const anthropicRuntimeSelection = useMemo(
|
||||
() =>
|
||||
selectedProviderId === 'anthropic'
|
||||
? resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: runtimeProviderStatusById.get('anthropic')?.modelCatalog,
|
||||
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
|
||||
},
|
||||
selectedModel,
|
||||
limitContext,
|
||||
})
|
||||
: null,
|
||||
[limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId]
|
||||
);
|
||||
const anthropicFastModeResolution = useMemo(
|
||||
() =>
|
||||
selectedProviderId === 'anthropic' && anthropicRuntimeSelection
|
||||
? resolveAnthropicFastMode({
|
||||
selection: anthropicRuntimeSelection,
|
||||
selectedFastMode,
|
||||
providerFastModeDefault: anthropicProviderFastModeDefault,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
anthropicProviderFastModeDefault,
|
||||
anthropicRuntimeSelection,
|
||||
selectedFastMode,
|
||||
selectedProviderId,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProviderId !== 'anthropic') {
|
||||
setAnthropicRuntimeNotice(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const reconciliation = reconcileAnthropicRuntimeSelections({
|
||||
selection:
|
||||
anthropicRuntimeSelection ??
|
||||
resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: null,
|
||||
runtimeCapabilities: null,
|
||||
},
|
||||
selectedModel,
|
||||
limitContext,
|
||||
}),
|
||||
selectedEffort,
|
||||
selectedFastMode,
|
||||
providerFastModeDefault: anthropicProviderFastModeDefault,
|
||||
});
|
||||
|
||||
const notices: string[] = [];
|
||||
if (reconciliation.nextEffort !== selectedEffort) {
|
||||
setSelectedEffortRaw(reconciliation.nextEffort);
|
||||
localStorage.setItem('team:lastSelectedEffort', reconciliation.nextEffort);
|
||||
if (reconciliation.effortResetReason) {
|
||||
notices.push(reconciliation.effortResetReason);
|
||||
}
|
||||
}
|
||||
if (reconciliation.nextFastMode !== selectedFastMode) {
|
||||
setSelectedFastModeRaw(reconciliation.nextFastMode);
|
||||
localStorage.setItem('team:lastSelectedFastMode', reconciliation.nextFastMode);
|
||||
if (reconciliation.fastModeResetReason) {
|
||||
notices.push(reconciliation.fastModeResetReason);
|
||||
}
|
||||
}
|
||||
setAnthropicRuntimeNotice(notices.length > 0 ? notices.join(' ') : null);
|
||||
}, [
|
||||
anthropicProviderFastModeDefault,
|
||||
anthropicRuntimeSelection,
|
||||
limitContext,
|
||||
selectedEffort,
|
||||
selectedFastMode,
|
||||
selectedModel,
|
||||
selectedProviderId,
|
||||
]);
|
||||
|
||||
const selectedModelChecksByProvider = useMemo(() => {
|
||||
const modelsByProvider = new Map<TeamProviderId, string[]>();
|
||||
const defaultSelectionByProvider = new Map<TeamProviderId, boolean>();
|
||||
|
|
@ -1237,17 +1341,30 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
runtimeProviderStatusById.get(selectedProviderId)
|
||||
);
|
||||
if (model) args.push('--model', model);
|
||||
if (selectedEffort) args.push('--effort', selectedEffort);
|
||||
const effectiveEffort =
|
||||
selectedProviderId === 'anthropic'
|
||||
? selectedEffort || anthropicRuntimeSelection?.defaultEffort || ''
|
||||
: selectedEffort;
|
||||
if (effectiveEffort) args.push('--effort', effectiveEffort);
|
||||
if (selectedProviderId === 'anthropic') {
|
||||
const fastSettings = anthropicFastModeResolution?.resolvedFastMode
|
||||
? { fastMode: true, fastModePerSessionOptIn: false }
|
||||
: { fastMode: false };
|
||||
args.push('--settings', JSON.stringify(fastSettings));
|
||||
}
|
||||
if (!clearContext) args.push('--resume', '<previous>');
|
||||
return args;
|
||||
}, [
|
||||
anthropicFastModeResolution?.resolvedFastMode,
|
||||
anthropicRuntimeSelection?.defaultEffort,
|
||||
isLaunchMode,
|
||||
skipPermissions,
|
||||
selectedModel,
|
||||
limitContext,
|
||||
selectedEffort,
|
||||
clearContext,
|
||||
selectedProviderId,
|
||||
clearContext,
|
||||
runtimeProviderStatusById,
|
||||
]);
|
||||
|
||||
const launchOptionalSummary = useMemo(() => {
|
||||
|
|
@ -1258,6 +1375,11 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
summary.push(`Provider: ${getProviderLabel(selectedProviderId)}`);
|
||||
if (selectedModel) summary.push(`Model: ${selectedModel}`);
|
||||
if (selectedEffort) summary.push(`Effort: ${selectedEffort}`);
|
||||
if (selectedProviderId === 'anthropic') {
|
||||
if (selectedFastMode === 'on') summary.push('Fast mode');
|
||||
else if (selectedFastMode === 'off') summary.push('Fast disabled');
|
||||
else if (anthropicProviderFastModeDefault) summary.push('Fast default');
|
||||
}
|
||||
if (selectedProviderId === 'anthropic' && limitContext) summary.push('Limited to 200K context');
|
||||
if (skipPermissions) summary.push('Auto-approve tools');
|
||||
if (clearContext) summary.push('Fresh session');
|
||||
|
|
@ -1270,6 +1392,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
selectedModel,
|
||||
selectedProviderId,
|
||||
selectedEffort,
|
||||
selectedFastMode,
|
||||
anthropicProviderFastModeDefault,
|
||||
limitContext,
|
||||
skipPermissions,
|
||||
clearContext,
|
||||
|
|
@ -1478,6 +1602,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
runtimeProviderStatusById.get(selectedProviderId)
|
||||
),
|
||||
effort: (selectedEffort as EffortLevel) || undefined,
|
||||
fastMode: selectedProviderId === 'anthropic' ? selectedFastMode : undefined,
|
||||
limitContext,
|
||||
clearContext: clearContext || undefined,
|
||||
skipPermissions,
|
||||
|
|
@ -1869,14 +1994,18 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
providerId={selectedProviderId}
|
||||
model={selectedModel}
|
||||
effort={(selectedEffort as EffortLevel) || undefined}
|
||||
fastMode={selectedFastMode}
|
||||
providerFastModeDefault={anthropicProviderFastModeDefault}
|
||||
limitContext={limitContext}
|
||||
onProviderChange={setSelectedProviderId}
|
||||
onModelChange={setSelectedModel}
|
||||
onEffortChange={setSelectedEffort}
|
||||
onFastModeChange={setSelectedFastMode}
|
||||
onLimitContextChange={setLimitContext}
|
||||
syncModelsWithTeammates={syncModelsWithLead}
|
||||
onSyncModelsWithTeammatesChange={setSyncModelsWithLead}
|
||||
leadWarningText={leadRuntimeWarningText}
|
||||
leadFastModeNotice={anthropicRuntimeNotice}
|
||||
memberWarningById={memberRuntimeWarningById}
|
||||
leadModelIssueText={leadModelIssueText}
|
||||
memberModelIssueById={memberModelIssueById}
|
||||
|
|
@ -2029,6 +2158,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
id="dialog-effort"
|
||||
providerId={selectedProviderId}
|
||||
model={selectedModel}
|
||||
limitContext={false}
|
||||
/>
|
||||
<SkipPermissionsCheckbox
|
||||
id="dialog-skip-permissions"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ interface PreviousLaunchParamsLike {
|
|||
providerBackendId?: string;
|
||||
model?: string;
|
||||
effort?: string;
|
||||
fastMode?: 'inherit' | 'on' | 'off';
|
||||
limitContext?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ interface LaunchDialogPrefillInput {
|
|||
multimodelEnabled: boolean;
|
||||
storedProviderId: TeamProviderId;
|
||||
storedEffort: string;
|
||||
storedFastMode: 'inherit' | 'on' | 'off';
|
||||
storedLimitContext: boolean;
|
||||
getStoredModel: (providerId: TeamProviderId) => string;
|
||||
}
|
||||
|
|
@ -31,6 +33,7 @@ interface LaunchDialogPrefillResult {
|
|||
providerBackendId?: string;
|
||||
model: string;
|
||||
effort: string;
|
||||
fastMode: 'inherit' | 'on' | 'off';
|
||||
limitContext: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +65,7 @@ export function resolveLaunchDialogPrefill({
|
|||
multimodelEnabled,
|
||||
storedProviderId,
|
||||
storedEffort,
|
||||
storedFastMode,
|
||||
storedLimitContext,
|
||||
getStoredModel,
|
||||
}: LaunchDialogPrefillInput): LaunchDialogPrefillResult {
|
||||
|
|
@ -99,6 +103,8 @@ export function resolveLaunchDialogPrefill({
|
|||
|
||||
const effort =
|
||||
currentLead?.effort ?? savedRequest?.effort ?? previousLaunchParams?.effort ?? storedEffort;
|
||||
const fastMode =
|
||||
savedRequest?.fastMode ?? previousLaunchParams?.fastMode ?? storedFastMode ?? 'inherit';
|
||||
const limitContext =
|
||||
previousLaunchParams?.limitContext ?? savedRequest?.limitContext ?? storedLimitContext;
|
||||
|
||||
|
|
@ -113,6 +119,7 @@ export function resolveLaunchDialogPrefill({
|
|||
? normalizeExplicitTeamModelForUi(providerId, matchingModel)
|
||||
: getStoredModel(providerId),
|
||||
effort,
|
||||
fastMode,
|
||||
limitContext,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { AnthropicFastModeSelector } from '@renderer/components/team/dialogs/AnthropicFastModeSelector';
|
||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||
import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector';
|
||||
import { LimitContextCheckbox } from '@renderer/components/team/dialogs/LimitContextCheckbox';
|
||||
|
|
@ -20,20 +21,24 @@ import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react';
|
|||
|
||||
import { Button } from '../../ui/button';
|
||||
|
||||
import type { EffortLevel, TeamProviderId } from '@shared/types';
|
||||
import type { EffortLevel, TeamFastMode, TeamProviderId } from '@shared/types';
|
||||
|
||||
interface LeadModelRowProps {
|
||||
providerId: TeamProviderId;
|
||||
model: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
providerFastModeDefault?: boolean;
|
||||
limitContext: boolean;
|
||||
onProviderChange: (providerId: TeamProviderId) => void;
|
||||
onModelChange: (model: string) => void;
|
||||
onEffortChange: (effort: string) => void;
|
||||
onFastModeChange?: (fastMode: TeamFastMode) => void;
|
||||
onLimitContextChange: (value: boolean) => void;
|
||||
syncModelsWithTeammates: boolean;
|
||||
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
||||
warningText?: string | null;
|
||||
fastModeNotice?: string | null;
|
||||
disableGeminiOption?: boolean;
|
||||
modelIssueText?: string | null;
|
||||
}
|
||||
|
|
@ -42,14 +47,18 @@ export const LeadModelRow = ({
|
|||
providerId,
|
||||
model,
|
||||
effort,
|
||||
fastMode = 'inherit',
|
||||
providerFastModeDefault = false,
|
||||
limitContext,
|
||||
onProviderChange,
|
||||
onModelChange,
|
||||
onEffortChange,
|
||||
onFastModeChange,
|
||||
onLimitContextChange,
|
||||
syncModelsWithTeammates,
|
||||
onSyncModelsWithTeammatesChange,
|
||||
warningText,
|
||||
fastModeNotice,
|
||||
disableGeminiOption = false,
|
||||
modelIssueText,
|
||||
}: LeadModelRowProps): React.JSX.Element => {
|
||||
|
|
@ -157,7 +166,18 @@ export const LeadModelRow = ({
|
|||
id="lead-effort"
|
||||
providerId={providerId}
|
||||
model={model}
|
||||
limitContext={limitContext}
|
||||
/>
|
||||
{providerId === 'anthropic' && onFastModeChange ? (
|
||||
<AnthropicFastModeSelector
|
||||
value={fastMode}
|
||||
onValueChange={onFastModeChange}
|
||||
providerFastModeDefault={providerFastModeDefault}
|
||||
model={model}
|
||||
limitContext={limitContext}
|
||||
id="lead-fast-mode"
|
||||
/>
|
||||
) : null}
|
||||
{providerId === 'anthropic' ? (
|
||||
<LimitContextCheckbox
|
||||
id="lead-limit-context"
|
||||
|
|
@ -166,6 +186,12 @@ export const LeadModelRow = ({
|
|||
disabled={isAnthropicHaikuTeamModel(model)}
|
||||
/>
|
||||
) : null}
|
||||
{fastModeNotice ? (
|
||||
<div className="bg-amber-500/8 flex items-start gap-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
|
||||
<Info className="mt-0.5 size-3.5 shrink-0 text-amber-300" />
|
||||
<p>{fastModeNotice}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-start gap-2 rounded-md border border-sky-500/20 bg-sky-500/5 px-3 py-2">
|
||||
<Info className="mt-0.5 size-3.5 shrink-0 text-sky-400" />
|
||||
<p className="text-[11px] leading-relaxed text-sky-300">
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ interface MemberDraftRowProps {
|
|||
inheritedProviderId?: TeamProviderId;
|
||||
inheritedModel?: string;
|
||||
inheritedEffort?: EffortLevel;
|
||||
limitContext?: boolean;
|
||||
draftKeyPrefix?: string;
|
||||
projectPath?: string | null;
|
||||
mentionSuggestions?: MentionSuggestion[];
|
||||
|
|
@ -91,6 +92,7 @@ export const MemberDraftRow = ({
|
|||
inheritedProviderId = 'anthropic',
|
||||
inheritedModel = '',
|
||||
inheritedEffort,
|
||||
limitContext = false,
|
||||
draftKeyPrefix,
|
||||
projectPath,
|
||||
mentionSuggestions = [],
|
||||
|
|
@ -448,6 +450,7 @@ export const MemberDraftRow = ({
|
|||
id={`member-${member.id}-effort`}
|
||||
providerId={effectiveProviderId}
|
||||
model={effectiveModel}
|
||||
limitContext={limitContext}
|
||||
/>
|
||||
{lockProviderModel && (
|
||||
<p className="text-[11px] text-amber-300">
|
||||
|
|
|
|||
|
|
@ -168,7 +168,9 @@ function areLaunchParamsEquivalent(
|
|||
left.providerId === right.providerId &&
|
||||
left.providerBackendId === right.providerBackendId &&
|
||||
left.model === right.model &&
|
||||
left.effort === right.effort
|
||||
left.effort === right.effort &&
|
||||
left.fastMode === right.fastMode &&
|
||||
left.limitContext === right.limitContext
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export interface MembersEditorSectionProps {
|
|||
inheritedProviderId?: TeamProviderId;
|
||||
inheritedModel?: string;
|
||||
inheritedEffort?: EffortLevel;
|
||||
limitContext?: boolean;
|
||||
inheritModelSettingsByDefault?: boolean;
|
||||
forceInheritedModelSettings?: boolean;
|
||||
modelLockReason?: string;
|
||||
|
|
@ -134,6 +135,7 @@ export const MembersEditorSection = ({
|
|||
inheritedProviderId,
|
||||
inheritedModel,
|
||||
inheritedEffort,
|
||||
limitContext = false,
|
||||
inheritModelSettingsByDefault = false,
|
||||
forceInheritedModelSettings = false,
|
||||
modelLockReason,
|
||||
|
|
@ -331,6 +333,7 @@ export const MembersEditorSection = ({
|
|||
inheritedProviderId={inheritedProviderId}
|
||||
inheritedModel={inheritedModel}
|
||||
inheritedEffort={inheritedEffort}
|
||||
limitContext={limitContext}
|
||||
forceInheritedModelSettings={forceInheritedModelSettings}
|
||||
draftKeyPrefix={draftKeyPrefix}
|
||||
projectPath={projectPath}
|
||||
|
|
@ -374,6 +377,7 @@ export const MembersEditorSection = ({
|
|||
inheritedProviderId={inheritedProviderId}
|
||||
inheritedModel={inheritedModel}
|
||||
inheritedEffort={inheritedEffort}
|
||||
limitContext={limitContext}
|
||||
forceInheritedModelSettings={forceInheritedModelSettings}
|
||||
draftKeyPrefix={draftKeyPrefix}
|
||||
projectPath={projectPath}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { MembersEditorSection } from './MembersEditorSection';
|
|||
|
||||
import type { MemberDraft } from './membersEditorTypes';
|
||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||
import type { EffortLevel, TeamProviderId } from '@shared/types';
|
||||
import type { EffortLevel, TeamFastMode, TeamProviderId } from '@shared/types';
|
||||
|
||||
interface TeamRosterEditorSectionProps {
|
||||
members: MemberDraft[];
|
||||
|
|
@ -31,10 +31,13 @@ interface TeamRosterEditorSectionProps {
|
|||
providerId: TeamProviderId;
|
||||
model: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
providerFastModeDefault?: boolean;
|
||||
limitContext: boolean;
|
||||
onProviderChange: (providerId: TeamProviderId) => void;
|
||||
onModelChange: (model: string) => void;
|
||||
onEffortChange: (effort: string) => void;
|
||||
onFastModeChange?: (fastMode: TeamFastMode) => void;
|
||||
onLimitContextChange: (value: boolean) => void;
|
||||
syncModelsWithTeammates: boolean;
|
||||
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
||||
|
|
@ -42,6 +45,7 @@ interface TeamRosterEditorSectionProps {
|
|||
headerBottom?: React.ReactNode;
|
||||
softDeleteMembers?: boolean;
|
||||
leadWarningText?: string | null;
|
||||
leadFastModeNotice?: string | null;
|
||||
memberWarningById?: Record<string, string | null | undefined>;
|
||||
disableGeminiOption?: boolean;
|
||||
leadModelIssueText?: string | null;
|
||||
|
|
@ -72,10 +76,13 @@ export const TeamRosterEditorSection = ({
|
|||
providerId,
|
||||
model,
|
||||
effort,
|
||||
fastMode,
|
||||
providerFastModeDefault,
|
||||
limitContext,
|
||||
onProviderChange,
|
||||
onModelChange,
|
||||
onEffortChange,
|
||||
onFastModeChange,
|
||||
onLimitContextChange,
|
||||
syncModelsWithTeammates,
|
||||
onSyncModelsWithTeammatesChange,
|
||||
|
|
@ -83,6 +90,7 @@ export const TeamRosterEditorSection = ({
|
|||
headerBottom,
|
||||
softDeleteMembers = false,
|
||||
leadWarningText,
|
||||
leadFastModeNotice,
|
||||
memberWarningById,
|
||||
disableGeminiOption = false,
|
||||
leadModelIssueText,
|
||||
|
|
@ -106,6 +114,7 @@ export const TeamRosterEditorSection = ({
|
|||
inheritedProviderId={inheritedProviderId}
|
||||
inheritedModel={inheritedModel}
|
||||
inheritedEffort={inheritedEffort}
|
||||
limitContext={limitContext}
|
||||
inheritModelSettingsByDefault={inheritModelSettingsByDefault}
|
||||
lockProviderModel={lockProviderModel}
|
||||
forceInheritedModelSettings={forceInheritedModelSettings}
|
||||
|
|
@ -120,14 +129,18 @@ export const TeamRosterEditorSection = ({
|
|||
providerId={providerId}
|
||||
model={model}
|
||||
effort={effort}
|
||||
fastMode={fastMode}
|
||||
providerFastModeDefault={providerFastModeDefault}
|
||||
limitContext={limitContext}
|
||||
onProviderChange={onProviderChange}
|
||||
onModelChange={onModelChange}
|
||||
onEffortChange={onEffortChange}
|
||||
onFastModeChange={onFastModeChange}
|
||||
onLimitContextChange={onLimitContextChange}
|
||||
syncModelsWithTeammates={syncModelsWithTeammates}
|
||||
onSyncModelsWithTeammatesChange={onSyncModelsWithTeammatesChange}
|
||||
warningText={leadWarningText}
|
||||
fastModeNotice={leadFastModeNotice}
|
||||
disableGeminiOption={disableGeminiOption}
|
||||
modelIssueText={leadModelIssueText}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1526,6 +1526,7 @@ export interface TeamLaunchParams {
|
|||
providerBackendId?: string;
|
||||
model?: string; // 'opus' | 'sonnet' | 'haiku'
|
||||
effort?: EffortLevel;
|
||||
fastMode?: 'inherit' | 'on' | 'off';
|
||||
limitContext?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -4426,6 +4427,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
providerBackendId: request.providerBackendId,
|
||||
model: baseModel || 'default',
|
||||
effort: request.effort,
|
||||
fastMode: request.fastMode,
|
||||
limitContext: request.limitContext ?? false,
|
||||
};
|
||||
saveLaunchParams(request.teamName, params);
|
||||
|
|
@ -4598,6 +4600,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
providerBackendId: request.providerBackendId,
|
||||
model: baseModel || 'default',
|
||||
effort: request.effort,
|
||||
fastMode: request.fastMode,
|
||||
limitContext: request.limitContext ?? false,
|
||||
};
|
||||
saveLaunchParams(request.teamName, params);
|
||||
|
|
|
|||
|
|
@ -93,15 +93,16 @@ describe('team effort options', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('shows only supported low/medium/high efforts for Anthropic and never leaks max', () => {
|
||||
it('keeps Anthropic aliases conservative when the resolved runtime model does not support effort', () => {
|
||||
const providerStatus = createProviderStatus('anthropic', {
|
||||
id: 'opus',
|
||||
launchModel: 'opus',
|
||||
displayName: 'Opus 4.7',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
||||
id: 'opus[1m]',
|
||||
launchModel: 'opus[1m]',
|
||||
displayName: 'Opus 4.7 (1M)',
|
||||
hidden: true,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: false,
|
||||
supportsPersonality: false,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
|
|
@ -110,11 +111,83 @@ describe('team effort options', () => {
|
|||
|
||||
expect(
|
||||
getTeamEffortOptions({ providerId: 'anthropic', model: 'opus', providerStatus })
|
||||
).toEqual([{ value: '', label: 'Default' }]);
|
||||
});
|
||||
|
||||
it('shows Anthropic max only for the exact resolved model that supports it', () => {
|
||||
const providerStatus = {
|
||||
...createProviderStatus('anthropic', {
|
||||
id: 'claude-opus-4-6',
|
||||
launchModel: 'claude-opus-4-6',
|
||||
displayName: 'Opus 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||
defaultReasoningEffort: 'medium',
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: true,
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
}),
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'anthropic' as const,
|
||||
source: 'anthropic-models-api' as const,
|
||||
status: 'ready' as const,
|
||||
fetchedAt: '2026-04-21T00:00:00.000Z',
|
||||
staleAt: '2026-04-21T00:10:00.000Z',
|
||||
defaultModelId: 'opus[1m]',
|
||||
defaultLaunchModel: 'opus[1m]',
|
||||
models: [
|
||||
{
|
||||
id: 'opus[1m]',
|
||||
launchModel: 'opus[1m]',
|
||||
displayName: 'Opus 4.7 (1M)',
|
||||
hidden: true,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: false,
|
||||
supportsPersonality: false,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api' as const,
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
launchModel: 'claude-opus-4-6',
|
||||
displayName: 'Opus 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||
defaultReasoningEffort: 'medium',
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: true,
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api' as const,
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
} satisfies CliProviderStatus;
|
||||
|
||||
expect(
|
||||
getTeamEffortOptions({
|
||||
providerId: 'anthropic',
|
||||
model: 'claude-opus-4-6',
|
||||
providerStatus,
|
||||
})
|
||||
).toEqual([
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: '', label: 'Default (Medium)' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'max', label: 'Max' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import {
|
|||
getAvailableTeamProviderModelOptions,
|
||||
getAvailableTeamProviderModels,
|
||||
getTeamModelSelectionError,
|
||||
isTeamModelAvailableForUi,
|
||||
normalizeTeamModelForUi,
|
||||
} from '../teamModelAvailability';
|
||||
|
||||
import type { CliProviderStatus } from '@shared/types';
|
||||
|
|
@ -62,6 +64,58 @@ function createCodexProviderStatus(
|
|||
};
|
||||
}
|
||||
|
||||
function createAnthropicProviderStatus(
|
||||
models: NonNullable<CliProviderStatus['modelCatalog']>['models']
|
||||
): CliProviderStatus {
|
||||
return {
|
||||
providerId: 'anthropic',
|
||||
displayName: 'Anthropic',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'claude.ai',
|
||||
verificationState: 'verified',
|
||||
models: ['opus', 'claude-opus-4-6', 'sonnet', 'haiku'],
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'anthropic',
|
||||
source: 'anthropic-models-api',
|
||||
status: 'ready',
|
||||
fetchedAt: '2026-04-21T00:00:00.000Z',
|
||||
staleAt: '2026-04-21T00:10:00.000Z',
|
||||
defaultModelId: 'opus[1m]',
|
||||
defaultLaunchModel: 'opus[1m]',
|
||||
models,
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
modelAvailability: [],
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
reasoningEffort: {
|
||||
supported: true,
|
||||
values: ['low', 'medium', 'high'],
|
||||
configPassthrough: false,
|
||||
},
|
||||
},
|
||||
canLoginFromUi: true,
|
||||
capabilities: {
|
||||
teamLaunch: true,
|
||||
oneShot: true,
|
||||
extensions: {
|
||||
plugins: { status: 'supported', ownership: 'shared', reason: null },
|
||||
mcp: { status: 'supported', ownership: 'shared', reason: null },
|
||||
skills: { status: 'supported', ownership: 'shared', reason: null },
|
||||
apiKeys: { status: 'supported', ownership: 'shared', reason: null },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('team model availability Codex catalog integration', () => {
|
||||
it('uses app-server catalog models even when the static Codex list has not learned a new model yet', () => {
|
||||
const providerStatus = createCodexProviderStatus(
|
||||
|
|
@ -158,4 +212,163 @@ describe('team model availability Codex catalog integration', () => {
|
|||
|
||||
expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.4']);
|
||||
});
|
||||
|
||||
it('keeps the curated Anthropic picker surface while using runtime-backed labels', () => {
|
||||
const providerStatus = createAnthropicProviderStatus([
|
||||
{
|
||||
id: 'opus',
|
||||
launchModel: 'opus',
|
||||
displayName: 'Opus 4.8',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
badgeLabel: 'Opus 4.8',
|
||||
},
|
||||
{
|
||||
id: 'opus[1m]',
|
||||
launchModel: 'opus[1m]',
|
||||
displayName: 'Opus 4.8 (1M)',
|
||||
hidden: true,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsPersonality: false,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
launchModel: 'claude-opus-4-6',
|
||||
displayName: 'Opus 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
badgeLabel: 'Opus 4.6',
|
||||
},
|
||||
{
|
||||
id: 'sonnet',
|
||||
launchModel: 'sonnet',
|
||||
displayName: 'Sonnet 4.7',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
badgeLabel: 'Sonnet 4.7',
|
||||
},
|
||||
{
|
||||
id: 'haiku',
|
||||
launchModel: 'haiku',
|
||||
displayName: 'Haiku 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
badgeLabel: 'Haiku 4.6',
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-6[1m]',
|
||||
launchModel: 'claude-sonnet-4-6[1m]',
|
||||
displayName: 'Sonnet 4.6 (1M)',
|
||||
hidden: true,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'static-fallback',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(getAvailableTeamProviderModels('anthropic', providerStatus)).toEqual([
|
||||
'haiku',
|
||||
'opus',
|
||||
'claude-opus-4-6',
|
||||
'sonnet',
|
||||
]);
|
||||
expect(getAvailableTeamProviderModelOptions('anthropic', providerStatus)).toEqual([
|
||||
{
|
||||
value: '',
|
||||
label: 'Default',
|
||||
badgeLabel: 'Default',
|
||||
availabilityStatus: undefined,
|
||||
availabilityReason: undefined,
|
||||
},
|
||||
{
|
||||
value: 'opus',
|
||||
label: 'Opus 4.8',
|
||||
badgeLabel: 'Opus 4.8',
|
||||
availabilityStatus: 'available',
|
||||
availabilityReason: null,
|
||||
},
|
||||
{
|
||||
value: 'claude-opus-4-6',
|
||||
label: 'Opus 4.6',
|
||||
badgeLabel: 'Opus 4.6',
|
||||
availabilityStatus: 'available',
|
||||
availabilityReason: null,
|
||||
},
|
||||
{
|
||||
value: 'sonnet',
|
||||
label: 'Sonnet 4.7',
|
||||
badgeLabel: 'Sonnet 4.7',
|
||||
availabilityStatus: 'available',
|
||||
availabilityReason: null,
|
||||
},
|
||||
{
|
||||
value: 'haiku',
|
||||
label: 'Haiku 4.6',
|
||||
badgeLabel: 'Haiku 4.6',
|
||||
availabilityStatus: 'available',
|
||||
availabilityReason: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps persisted hidden Anthropic compatibility values valid when runtime catalog supplies them', () => {
|
||||
const providerStatus = createAnthropicProviderStatus([
|
||||
{
|
||||
id: 'claude-sonnet-4-6[1m]',
|
||||
launchModel: 'claude-sonnet-4-6[1m]',
|
||||
displayName: 'Sonnet 4.6 (1M)',
|
||||
hidden: true,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'static-fallback',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(isTeamModelAvailableForUi('anthropic', 'claude-sonnet-4-6[1m]', providerStatus)).toBe(
|
||||
true
|
||||
);
|
||||
expect(normalizeTeamModelForUi('anthropic', 'claude-sonnet-4-6[1m]', providerStatus)).toBe(
|
||||
'claude-sonnet-4-6[1m]'
|
||||
);
|
||||
expect(getTeamModelSelectionError('anthropic', 'claude-sonnet-4-6[1m]', providerStatus)).toBe(
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { resolveAnthropicRuntimeSelection } from '@features/anthropic-runtime-profile/renderer';
|
||||
|
||||
import type { CliProviderStatus, EffortLevel, TeamProviderId } from '@shared/types';
|
||||
|
||||
const BASE_EFFORT_OPTIONS = [{ value: '', label: 'Default' }] as const;
|
||||
|
|
@ -9,6 +11,7 @@ export const TEAM_EFFORT_LABELS: Record<EffortLevel, string> = {
|
|||
low: 'Low',
|
||||
medium: 'Medium',
|
||||
high: 'High',
|
||||
max: 'Max',
|
||||
xhigh: 'XHigh',
|
||||
};
|
||||
|
||||
|
|
@ -59,6 +62,7 @@ function normalizeEfforts(
|
|||
export function getTeamEffortOptions(params: {
|
||||
providerId?: TeamProviderId;
|
||||
model?: string;
|
||||
limitContext?: boolean;
|
||||
providerStatus?: CliProviderStatus | null;
|
||||
}): readonly TeamEffortOption[] {
|
||||
const providerId = params.providerId;
|
||||
|
|
@ -66,6 +70,27 @@ export function getTeamEffortOptions(params: {
|
|||
return BASE_EFFORT_OPTIONS;
|
||||
}
|
||||
|
||||
if (providerId === 'anthropic') {
|
||||
const selection = resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: params.providerStatus?.modelCatalog,
|
||||
runtimeCapabilities: params.providerStatus?.runtimeCapabilities,
|
||||
},
|
||||
selectedModel: params.model,
|
||||
limitContext: params.limitContext === true,
|
||||
});
|
||||
const defaultLabel = selection.defaultEffort
|
||||
? `Default (${TEAM_EFFORT_LABELS[selection.defaultEffort]})`
|
||||
: 'Default';
|
||||
return [
|
||||
{ value: '', label: defaultLabel },
|
||||
...selection.supportedEfforts.map((effort) => ({
|
||||
value: effort,
|
||||
label: TEAM_EFFORT_LABELS[effort],
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
const runtimeCapability = params.providerStatus?.runtimeCapabilities?.reasoningEffort;
|
||||
const catalogModel = getCatalogModel(providerId, params.providerStatus, params.model);
|
||||
const catalogEfforts = catalogModel?.supportedReasoningEfforts ?? [];
|
||||
|
|
@ -82,16 +107,6 @@ export function getTeamEffortOptions(params: {
|
|||
? `Default (${TEAM_EFFORT_LABELS[catalogModel.defaultReasoningEffort]})`
|
||||
: 'Default';
|
||||
|
||||
if (providerId === 'anthropic') {
|
||||
return [
|
||||
{ value: '', label: defaultLabel },
|
||||
...efforts.map((effort) => ({
|
||||
value: effort,
|
||||
label: TEAM_EFFORT_LABELS[effort],
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
if (providerId === 'codex') {
|
||||
const fallbackEfforts =
|
||||
efforts.length > 0 ? efforts : (['low', 'medium', 'high'] as EffortLevel[]);
|
||||
|
|
|
|||
|
|
@ -117,7 +117,14 @@ export interface CliProviderModelAvailability {
|
|||
checkedAt?: string | null;
|
||||
}
|
||||
|
||||
export type CliProviderReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||
export type CliProviderReasoningEffort =
|
||||
| 'none'
|
||||
| 'minimal'
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high'
|
||||
| 'xhigh'
|
||||
| 'max';
|
||||
|
||||
export type CliProviderModelCatalogSource =
|
||||
| 'anthropic-models-api'
|
||||
|
|
@ -132,6 +139,7 @@ export interface CliProviderModelCatalogItem {
|
|||
hidden: boolean;
|
||||
supportedReasoningEfforts: CliProviderReasoningEffort[];
|
||||
defaultReasoningEffort: CliProviderReasoningEffort | null;
|
||||
supportsFastMode?: boolean;
|
||||
inputModalities: string[];
|
||||
supportsPersonality: boolean;
|
||||
isDefault: boolean;
|
||||
|
|
@ -169,6 +177,12 @@ export interface CliProviderRuntimeCapabilities {
|
|||
values: CliProviderReasoningEffort[];
|
||||
configPassthrough?: boolean;
|
||||
};
|
||||
fastMode?: {
|
||||
supported: boolean;
|
||||
available: boolean;
|
||||
reason?: string | null;
|
||||
source: 'runtime';
|
||||
};
|
||||
}
|
||||
|
||||
export interface CliProviderStatus {
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ export interface AppConfig {
|
|||
providerConnections: {
|
||||
anthropic: {
|
||||
authMode: 'auto' | 'oauth' | 'api_key';
|
||||
fastModeDefault: boolean;
|
||||
};
|
||||
codex: {
|
||||
preferredAuthMode: 'auto' | 'chatgpt' | 'api_key';
|
||||
|
|
|
|||
|
|
@ -782,9 +782,10 @@ export interface TeamViewSnapshot {
|
|||
isAlive?: boolean;
|
||||
}
|
||||
|
||||
export type EffortLevel = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||
export type EffortLevel = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
||||
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini';
|
||||
export type TeamProviderBackendId = 'auto' | 'adapter' | 'api' | 'cli-sdk' | 'codex-native';
|
||||
export type TeamFastMode = 'inherit' | 'on' | 'off';
|
||||
|
||||
export interface ProviderModelLaunchIdentity {
|
||||
providerId: TeamProviderId;
|
||||
|
|
@ -802,6 +803,9 @@ export interface ProviderModelLaunchIdentity {
|
|||
catalogFetchedAt: string | null;
|
||||
selectedEffort: EffortLevel | null;
|
||||
resolvedEffort: EffortLevel | null;
|
||||
selectedFastMode?: TeamFastMode | null;
|
||||
resolvedFastMode?: boolean | null;
|
||||
fastResolutionReason?: string | null;
|
||||
}
|
||||
|
||||
export interface TeamLaunchRequest {
|
||||
|
|
@ -812,6 +816,7 @@ export interface TeamLaunchRequest {
|
|||
providerBackendId?: TeamProviderBackendId;
|
||||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
/** When true, context window is limited to 200K tokens instead of the default. */
|
||||
limitContext?: boolean;
|
||||
/** When true, skip --resume and start a fresh session (clears context memory). */
|
||||
|
|
@ -949,6 +954,7 @@ export interface TeamAgentRuntimeSnapshot {
|
|||
updatedAt: string;
|
||||
runId: string | null;
|
||||
providerBackendId?: TeamProviderBackendId;
|
||||
fastMode?: TeamFastMode;
|
||||
members: Record<string, TeamAgentRuntimeEntry>;
|
||||
}
|
||||
|
||||
|
|
@ -1061,6 +1067,7 @@ export interface TeamCreateRequest {
|
|||
providerBackendId?: TeamProviderBackendId;
|
||||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
/** When true, context window is limited to 200K tokens instead of the default. */
|
||||
limitContext?: boolean;
|
||||
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
|
||||
|
|
@ -1079,6 +1086,7 @@ export interface TeamCreateConfigRequest {
|
|||
members: TeamProvisioningMemberInput[];
|
||||
cwd?: string;
|
||||
providerBackendId?: TeamProviderBackendId;
|
||||
fastMode?: TeamFastMode;
|
||||
}
|
||||
|
||||
export interface TeamCreateResponse {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const PROTECTED_CLI_FLAGS = new Set([
|
|||
'--effort',
|
||||
'--teammate-mode',
|
||||
'--resume',
|
||||
'--settings',
|
||||
'--permission-mode',
|
||||
'--permission-prompt-tool',
|
||||
'--dangerously-skip-permissions',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const TEAM_EFFORT_LEVELS = [
|
|||
'medium',
|
||||
'high',
|
||||
'xhigh',
|
||||
'max',
|
||||
] as const satisfies readonly EffortLevel[];
|
||||
|
||||
export const LEGACY_TEAM_EFFORT_LEVELS = [
|
||||
|
|
@ -23,8 +24,16 @@ export const CODEX_TEAM_EFFORT_LEVELS = [
|
|||
'xhigh',
|
||||
] as const satisfies readonly EffortLevel[];
|
||||
|
||||
export const ANTHROPIC_TEAM_EFFORT_LEVELS = [
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'max',
|
||||
] as const satisfies readonly EffortLevel[];
|
||||
|
||||
const LEGACY_TEAM_EFFORT_LEVEL_SET = new Set<EffortLevel>(LEGACY_TEAM_EFFORT_LEVELS);
|
||||
const CODEX_TEAM_EFFORT_LEVEL_SET = new Set<EffortLevel>(CODEX_TEAM_EFFORT_LEVELS);
|
||||
const ANTHROPIC_TEAM_EFFORT_LEVEL_SET = new Set<EffortLevel>(ANTHROPIC_TEAM_EFFORT_LEVELS);
|
||||
|
||||
export function isTeamEffortLevel(value: unknown): value is EffortLevel {
|
||||
return typeof value === 'string' && TEAM_EFFORT_LEVELS.includes(value as EffortLevel);
|
||||
|
|
@ -46,6 +55,10 @@ export function isTeamEffortLevelForProvider(
|
|||
return CODEX_TEAM_EFFORT_LEVEL_SET.has(value);
|
||||
}
|
||||
|
||||
if (providerId === 'anthropic') {
|
||||
return ANTHROPIC_TEAM_EFFORT_LEVEL_SET.has(value);
|
||||
}
|
||||
|
||||
return LEGACY_TEAM_EFFORT_LEVEL_SET.has(value);
|
||||
}
|
||||
|
||||
|
|
@ -53,5 +66,8 @@ export function formatEffortLevelListForProvider(providerId?: TeamProviderId | n
|
|||
if (providerId === 'codex') {
|
||||
return CODEX_TEAM_EFFORT_LEVELS.join(', ');
|
||||
}
|
||||
if (providerId === 'anthropic') {
|
||||
return ANTHROPIC_TEAM_EFFORT_LEVELS.join(', ');
|
||||
}
|
||||
return LEGACY_TEAM_EFFORT_LEVELS.join(', ');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,24 @@
|
|||
*/
|
||||
|
||||
const RATE_LIMIT_SUBSTRING = "You've hit your limit";
|
||||
const MODEL_COOLDOWN_CODE = 'model_cooldown';
|
||||
|
||||
interface StructuredRateLimitPayload {
|
||||
code: string | null;
|
||||
message: string | null;
|
||||
resetSeconds: number | null;
|
||||
resetTime: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the message text contains the rate limit indicator.
|
||||
*/
|
||||
export function isRateLimitMessage(text: string): boolean {
|
||||
return text.includes(RATE_LIMIT_SUBSTRING);
|
||||
if (!text) return false;
|
||||
if (text.includes(RATE_LIMIT_SUBSTRING)) return true;
|
||||
|
||||
const structured = extractStructuredRateLimitPayload(text);
|
||||
return structured ? isStructuredRateLimitPayload(structured) : false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -63,6 +75,14 @@ export function parseRateLimitResetTime(text: string, now: Date = new Date()): D
|
|||
// words like "reset" (e.g. "reset the 5pm meeting").
|
||||
if (!isRateLimitMessage(text)) return null;
|
||||
|
||||
const structured = extractStructuredRateLimitPayload(text);
|
||||
if (structured && isStructuredRateLimitPayload(structured)) {
|
||||
const structuredReset = parseStructuredResetTime(structured, now);
|
||||
if (structuredReset) {
|
||||
return structuredReset;
|
||||
}
|
||||
}
|
||||
|
||||
const relative = parseRelativeResetDuration(text);
|
||||
if (relative !== null) {
|
||||
return new Date(now.getTime() + relative);
|
||||
|
|
@ -120,6 +140,79 @@ function parseRelativeResetDuration(text: string): number | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function extractStructuredRateLimitPayload(text: string): StructuredRateLimitPayload | null {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const prefixedMatch = /^(?:API Error:\s*\d+\s+|\d+\s+)?(\{[\s\S]*\})$/i.exec(trimmed);
|
||||
const jsonCandidate = prefixedMatch?.[1] ?? (trimmed.startsWith('{') ? trimmed : null);
|
||||
if (!jsonCandidate) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonCandidate) as {
|
||||
error?: {
|
||||
code?: unknown;
|
||||
message?: unknown;
|
||||
reset_seconds?: unknown;
|
||||
reset_time?: unknown;
|
||||
};
|
||||
code?: unknown;
|
||||
message?: unknown;
|
||||
reset_seconds?: unknown;
|
||||
reset_time?: unknown;
|
||||
};
|
||||
const errorPayload = parsed.error;
|
||||
|
||||
return {
|
||||
code: readStringField(errorPayload?.code) ?? readStringField(parsed.code),
|
||||
message: readStringField(errorPayload?.message) ?? readStringField(parsed.message),
|
||||
resetSeconds:
|
||||
readNumberField(errorPayload?.reset_seconds) ?? readNumberField(parsed.reset_seconds),
|
||||
resetTime: readStringField(errorPayload?.reset_time) ?? readStringField(parsed.reset_time),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isStructuredRateLimitPayload(payload: StructuredRateLimitPayload): boolean {
|
||||
const code = payload.code?.trim().toLowerCase();
|
||||
if (code === MODEL_COOLDOWN_CODE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = payload.message?.trim().toLowerCase() ?? '';
|
||||
return (
|
||||
(message.includes('cooling down') || message.includes('model cooldown')) &&
|
||||
(payload.resetSeconds !== null || payload.resetTime !== null)
|
||||
);
|
||||
}
|
||||
|
||||
function parseStructuredResetTime(payload: StructuredRateLimitPayload, now: Date): Date | null {
|
||||
if (payload.resetSeconds !== null) {
|
||||
return new Date(now.getTime() + Math.max(0, payload.resetSeconds) * 1000);
|
||||
}
|
||||
|
||||
const resetTime = payload.resetTime?.trim();
|
||||
if (!resetTime) return null;
|
||||
|
||||
const relative = parseRelativeResetDuration(`Resets in ${resetTime}`);
|
||||
if (relative !== null) {
|
||||
return new Date(now.getTime() + relative);
|
||||
}
|
||||
|
||||
const absolute = Date.parse(resetTime);
|
||||
return Number.isFinite(absolute) ? new Date(absolute) : null;
|
||||
}
|
||||
|
||||
function readStringField(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function readNumberField(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Absolute clock times: "resets at 3pm (PST)", "resets at 15:30 UTC"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -0,0 +1,307 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
reconcileAnthropicRuntimeSelections,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '@features/anthropic-runtime-profile/renderer';
|
||||
import type { CliProviderModelCatalog, CliProviderRuntimeCapabilities } from '@shared/types';
|
||||
|
||||
import type { AnthropicRuntimeProfileSource } from '@features/anthropic-runtime-profile/renderer';
|
||||
|
||||
function createAnthropicSource(options: {
|
||||
models: CliProviderModelCatalog['models'];
|
||||
defaultLaunchModel?: string;
|
||||
fastMode?: CliProviderRuntimeCapabilities['fastMode'];
|
||||
}): AnthropicRuntimeProfileSource {
|
||||
return {
|
||||
modelCatalog: {
|
||||
schemaVersion: 1 as const,
|
||||
providerId: 'anthropic' as const,
|
||||
source: 'anthropic-models-api' as const,
|
||||
status: 'ready' as const,
|
||||
fetchedAt: '2026-04-21T00:00:00.000Z',
|
||||
staleAt: '2026-04-21T00:10:00.000Z',
|
||||
defaultModelId: options.defaultLaunchModel ?? options.models[0]?.id ?? 'opus[1m]',
|
||||
defaultLaunchModel: options.defaultLaunchModel ?? options.models[0]?.launchModel ?? 'opus[1m]',
|
||||
models: options.models,
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'anthropic-models-api' as const,
|
||||
},
|
||||
reasoningEffort: {
|
||||
supported: true,
|
||||
values: ['low', 'medium', 'high', 'max'],
|
||||
configPassthrough: true,
|
||||
},
|
||||
fastMode: options.fastMode ?? {
|
||||
supported: true,
|
||||
available: true,
|
||||
reason: null,
|
||||
source: 'runtime' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('resolveAnthropicRuntimeProfile', () => {
|
||||
it('uses the resolved launch model, not the alias family, for effort and fast capability truth', () => {
|
||||
const source = createAnthropicSource({
|
||||
defaultLaunchModel: 'opus[1m]',
|
||||
models: [
|
||||
{
|
||||
id: 'opus[1m]',
|
||||
launchModel: 'opus[1m]',
|
||||
displayName: 'Opus 4.7 (1M)',
|
||||
hidden: true,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: false,
|
||||
supportsPersonality: false,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
launchModel: 'claude-opus-4-6',
|
||||
displayName: 'Opus 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||
defaultReasoningEffort: 'medium',
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: true,
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const aliasSelection = resolveAnthropicRuntimeSelection({
|
||||
source,
|
||||
selectedModel: 'opus',
|
||||
limitContext: false,
|
||||
});
|
||||
const explicit46Selection = resolveAnthropicRuntimeSelection({
|
||||
source,
|
||||
selectedModel: 'claude-opus-4-6',
|
||||
limitContext: false,
|
||||
});
|
||||
|
||||
expect(aliasSelection.resolvedLaunchModel).toBe('opus[1m]');
|
||||
expect(aliasSelection.supportedEfforts).toEqual([]);
|
||||
expect(aliasSelection.supportsFastMode).toBe(false);
|
||||
|
||||
expect(explicit46Selection.resolvedLaunchModel).toBe('claude-opus-4-6');
|
||||
expect(explicit46Selection.supportedEfforts).toEqual(['low', 'medium', 'high', 'max']);
|
||||
expect(explicit46Selection.defaultEffort).toBe('medium');
|
||||
expect(explicit46Selection.supportsFastMode).toBe(true);
|
||||
});
|
||||
|
||||
it('resolves inherited fast mode from the provider default only when the exact model supports it', () => {
|
||||
const selection = resolveAnthropicRuntimeSelection({
|
||||
source: createAnthropicSource({
|
||||
models: [
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
launchModel: 'claude-opus-4-6',
|
||||
displayName: 'Opus 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||
defaultReasoningEffort: 'medium',
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: true,
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectedModel: 'claude-opus-4-6',
|
||||
limitContext: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveAnthropicFastMode({
|
||||
selection,
|
||||
selectedFastMode: undefined,
|
||||
providerFastModeDefault: true,
|
||||
})
|
||||
).toMatchObject({
|
||||
selectedFastMode: 'inherit',
|
||||
requestedFastMode: true,
|
||||
resolvedFastMode: true,
|
||||
selectable: true,
|
||||
disabledReason: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('resets only the invalid fast selection when an alias resolves to a non-fast model', () => {
|
||||
const selection = resolveAnthropicRuntimeSelection({
|
||||
source: createAnthropicSource({
|
||||
defaultLaunchModel: 'opus[1m]',
|
||||
models: [
|
||||
{
|
||||
id: 'opus[1m]',
|
||||
launchModel: 'opus[1m]',
|
||||
displayName: 'Opus 4.7 (1M)',
|
||||
hidden: true,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: false,
|
||||
supportsPersonality: false,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectedModel: 'opus',
|
||||
limitContext: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
reconcileAnthropicRuntimeSelections({
|
||||
selection,
|
||||
selectedEffort: '',
|
||||
selectedFastMode: 'on',
|
||||
providerFastModeDefault: false,
|
||||
})
|
||||
).toEqual({
|
||||
nextEffort: '',
|
||||
effortResetReason: null,
|
||||
nextFastMode: 'inherit',
|
||||
fastModeResetReason:
|
||||
'Fast mode is available only for Opus 4.6. Selected model resolves to Opus 4.7 (1M).',
|
||||
});
|
||||
});
|
||||
|
||||
it('resets invalid max effort without mutating unrelated fast intent', () => {
|
||||
const selection = resolveAnthropicRuntimeSelection({
|
||||
source: createAnthropicSource({
|
||||
models: [
|
||||
{
|
||||
id: 'haiku',
|
||||
launchModel: 'haiku',
|
||||
displayName: 'Haiku 4.5',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: false,
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectedModel: 'haiku',
|
||||
limitContext: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
reconcileAnthropicRuntimeSelections({
|
||||
selection,
|
||||
selectedEffort: 'max',
|
||||
selectedFastMode: 'off',
|
||||
providerFastModeDefault: true,
|
||||
})
|
||||
).toEqual({
|
||||
nextEffort: '',
|
||||
effortResetReason:
|
||||
'max effort is not available for the currently selected Anthropic model. Reset to Default.',
|
||||
nextFastMode: 'off',
|
||||
fastModeResetReason: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reset explicit max or fast while runtime catalog truth is still unavailable', () => {
|
||||
const selection = resolveAnthropicRuntimeSelection({
|
||||
source: {
|
||||
modelCatalog: null,
|
||||
runtimeCapabilities: null,
|
||||
},
|
||||
selectedModel: 'claude-opus-4-6',
|
||||
limitContext: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
reconcileAnthropicRuntimeSelections({
|
||||
selection,
|
||||
selectedEffort: 'max',
|
||||
selectedFastMode: 'on',
|
||||
providerFastModeDefault: false,
|
||||
})
|
||||
).toEqual({
|
||||
nextEffort: 'max',
|
||||
effortResetReason: null,
|
||||
nextFastMode: 'on',
|
||||
fastModeResetReason: null,
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveAnthropicFastMode({
|
||||
selection,
|
||||
selectedFastMode: 'on',
|
||||
providerFastModeDefault: false,
|
||||
}).disabledReason
|
||||
).toBe('Anthropic runtime capability data is still loading.');
|
||||
});
|
||||
|
||||
it('keeps the fast control visible in degraded states and surfaces the provider reason', () => {
|
||||
const selection = resolveAnthropicRuntimeSelection({
|
||||
source: createAnthropicSource({
|
||||
models: [
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
launchModel: 'claude-opus-4-6',
|
||||
displayName: 'Opus 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||
defaultReasoningEffort: 'medium',
|
||||
inputModalities: ['text', 'image'],
|
||||
supportsFastMode: true,
|
||||
supportsPersonality: false,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
},
|
||||
],
|
||||
fastMode: {
|
||||
supported: true,
|
||||
available: false,
|
||||
reason: 'Fast mode status is degraded right now.',
|
||||
source: 'runtime',
|
||||
},
|
||||
}),
|
||||
selectedModel: 'claude-opus-4-6',
|
||||
limitContext: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveAnthropicFastMode({
|
||||
selection,
|
||||
selectedFastMode: 'inherit',
|
||||
providerFastModeDefault: true,
|
||||
})
|
||||
).toMatchObject({
|
||||
showFastModeControl: true,
|
||||
selectable: false,
|
||||
requestedFastMode: true,
|
||||
resolvedFastMode: false,
|
||||
disabledReason: 'Fast mode status is degraded right now.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,6 +6,8 @@ import type { ConfigManager } from '../../../../src/main/services/infrastructure
|
|||
|
||||
const TEAM = 'test-team';
|
||||
const RATE_LIMIT_MSG = "You've hit your limit. Resets in 5 minutes.";
|
||||
const MODEL_COOLDOWN_API_ERROR =
|
||||
'API Error: 429 {"error":{"code":"model_cooldown","message":"All credentials for model claude-opus-4-6 are cooling down via provider claude","model":"claude-opus-4-6","provider":"claude","reset_seconds":41,"reset_time":"40s"}}';
|
||||
|
||||
describe('AutoResumeService', () => {
|
||||
const mockConfig = { autoResumeOnRateLimit: false };
|
||||
|
|
@ -60,6 +62,21 @@ describe('AutoResumeService', () => {
|
|||
expect(provisioningService.sendMessageToTeam).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('schedules auto-resume from model_cooldown API errors', async () => {
|
||||
mockConfig.autoResumeOnRateLimit = true;
|
||||
provisioningService.isTeamAlive.mockReturnValue(true);
|
||||
provisioningService.sendMessageToTeam.mockResolvedValue(undefined);
|
||||
const now = new Date('2026-04-17T12:00:00Z');
|
||||
|
||||
service.handleRateLimitMessage(TEAM, MODEL_COOLDOWN_API_ERROR, now);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(41 * 1000 + 29 * 1000);
|
||||
expect(provisioningService.sendMessageToTeam).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1100);
|
||||
expect(provisioningService.sendMessageToTeam).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reschedules when a later rate-limit message changes the reset time', async () => {
|
||||
mockConfig.autoResumeOnRateLimit = true;
|
||||
provisioningService.isTeamAlive.mockReturnValue(true);
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ describe('TeamMemberRuntimeAdvisoryService', () => {
|
|||
|
||||
it.each([
|
||||
['rate_limited', 'Provider returned 429 rate limit for this request.'],
|
||||
['rate_limited', 'All credentials for model claude-opus-4-6 are cooling down via provider claude.'],
|
||||
['auth_error', 'Authentication failed due to invalid API key.'],
|
||||
['network_error', 'Fetch failed because the network connection timed out.'],
|
||||
['provider_overloaded', 'Service unavailable: provider temporarily unavailable (503).'],
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -50,6 +51,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -81,6 +83,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -93,6 +96,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -110,6 +114,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -122,6 +127,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.3-codex',
|
||||
effort: 'high',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -142,6 +148,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -154,6 +161,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -174,6 +182,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'codex',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
codex: 'gpt-5.4',
|
||||
|
|
@ -185,6 +194,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
providerBackendId: 'codex-native',
|
||||
model: 'gpt-5.4',
|
||||
effort: 'medium',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -207,6 +217,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -216,8 +227,10 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
providerId: 'anthropic',
|
||||
providerBackendId: undefined,
|
||||
model: 'haiku',
|
||||
effort: 'medium',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -235,6 +248,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -243,8 +257,10 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
providerId: 'anthropic',
|
||||
providerBackendId: undefined,
|
||||
model: 'opus',
|
||||
effort: 'high',
|
||||
fastMode: 'inherit',
|
||||
limitContext: true,
|
||||
});
|
||||
});
|
||||
|
|
@ -261,6 +277,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -273,6 +290,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
providerBackendId: 'codex-native',
|
||||
model: 'custom-model[1m]',
|
||||
effort: 'medium',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -289,6 +307,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
multimodelEnabled: true,
|
||||
storedProviderId: 'anthropic',
|
||||
storedEffort: 'medium',
|
||||
storedFastMode: 'inherit',
|
||||
storedLimitContext: false,
|
||||
getStoredModel: createStoredModelGetter({
|
||||
anthropic: 'haiku',
|
||||
|
|
@ -301,6 +320,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
|||
providerBackendId: 'codex-native',
|
||||
model: 'custom-model[1m]',
|
||||
effort: 'medium',
|
||||
fastMode: 'inherit',
|
||||
limitContext: false,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@ function makeAppConfig(multimodelEnabled: boolean): AppConfig {
|
|||
providerConnections: {
|
||||
anthropic: {
|
||||
authMode: 'auto',
|
||||
fastModeDefault: false,
|
||||
},
|
||||
codex: {
|
||||
preferredAuthMode: 'auto',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import {
|
|||
// Helper: every production rate-limit message starts with this substring.
|
||||
// Prefix test inputs so they clear the parser's rate-limit-context gate.
|
||||
const RL = "You've hit your limit. ";
|
||||
const MODEL_COOLDOWN_API_ERROR =
|
||||
'API Error: 429 {"error":{"code":"model_cooldown","message":"All credentials for model claude-opus-4-6 are cooling down via provider claude","model":"claude-opus-4-6","provider":"claude","reset_seconds":41,"reset_time":"40s"}}';
|
||||
const MODEL_COOLDOWN_NO_SECONDS_API_ERROR =
|
||||
'API Error: 429 {"error":{"code":"model_cooldown","message":"All credentials for model claude-opus-4-6 are cooling down via provider claude","model":"claude-opus-4-6","provider":"claude","reset_time":"40s"}}';
|
||||
|
||||
describe('isRateLimitMessage', () => {
|
||||
it('detects the canonical substring', () => {
|
||||
|
|
@ -22,6 +26,10 @@ describe('isRateLimitMessage', () => {
|
|||
expect(isRateLimitMessage('hit the limit')).toBe(false); // missing "You've"
|
||||
expect(isRateLimitMessage('')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects structured model_cooldown API errors as rate limits', () => {
|
||||
expect(isRateLimitMessage(MODEL_COOLDOWN_API_ERROR)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseRateLimitResetTime', () => {
|
||||
|
|
@ -41,6 +49,18 @@ describe('parseRateLimitResetTime', () => {
|
|||
expect(parseRateLimitResetTime('Resets in 2 hours.', now)).toBeNull();
|
||||
});
|
||||
|
||||
it('parses model_cooldown reset_seconds from structured API errors', () => {
|
||||
const now = new Date('2026-04-17T12:00:00Z');
|
||||
const result = parseRateLimitResetTime(MODEL_COOLDOWN_API_ERROR, now);
|
||||
expect(result?.toISOString()).toBe('2026-04-17T12:00:41.000Z');
|
||||
});
|
||||
|
||||
it('falls back to structured reset_time when reset_seconds is missing', () => {
|
||||
const now = new Date('2026-04-17T12:00:00Z');
|
||||
const result = parseRateLimitResetTime(MODEL_COOLDOWN_NO_SECONDS_API_ERROR, now);
|
||||
expect(result?.toISOString()).toBe('2026-04-17T12:00:40.000Z');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Relative durations
|
||||
// ---------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue