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
|
## Commands
|
||||||
Always use pnpm (not npm/yarn) for this project.
|
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.
|
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`).
|
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 |
|
| [kanban-design.md](./kanban-design.md) | Kanban flow, колонки, review mechanism, kanban-state.json |
|
||||||
| [implementation.md](./implementation.md) | Техплан: файлы, шаги, verification |
|
| [implementation.md](./implementation.md) | Техплан: файлы, шаги, verification |
|
||||||
| [research-worktrees.md](./research-worktrees.md) | Git worktrees + teams, запуск Claude процессов из UI (Phase 2) |
|
| [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 { isAbsolute } from 'path';
|
||||||
|
|
||||||
import type { HttpServices } from './index';
|
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';
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
const logger = createLogger('HTTP:teams');
|
const logger = createLogger('HTTP:teams');
|
||||||
|
|
@ -95,6 +95,18 @@ function assertOptionalEffort(
|
||||||
return value;
|
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 {
|
function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest {
|
||||||
const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {};
|
const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {};
|
||||||
const providerId =
|
const providerId =
|
||||||
|
|
@ -117,6 +129,7 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
|
||||||
}
|
}
|
||||||
const model = assertOptionalString(payload.model, 'model');
|
const model = assertOptionalString(payload.model, 'model');
|
||||||
const effort = assertOptionalEffort(payload.effort, providerId);
|
const effort = assertOptionalEffort(payload.effort, providerId);
|
||||||
|
const fastMode = assertOptionalFastMode(payload.fastMode);
|
||||||
const clearContext = assertOptionalBoolean(payload.clearContext, 'clearContext');
|
const clearContext = assertOptionalBoolean(payload.clearContext, 'clearContext');
|
||||||
const skipPermissions = assertOptionalBoolean(payload.skipPermissions, 'skipPermissions');
|
const skipPermissions = assertOptionalBoolean(payload.skipPermissions, 'skipPermissions');
|
||||||
const worktree = assertOptionalString(payload.worktree, 'worktree');
|
const worktree = assertOptionalString(payload.worktree, 'worktree');
|
||||||
|
|
@ -138,6 +151,9 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
|
||||||
...(effort && {
|
...(effort && {
|
||||||
effort,
|
effort,
|
||||||
}),
|
}),
|
||||||
|
...(fastMode && {
|
||||||
|
fastMode,
|
||||||
|
}),
|
||||||
...(clearContext !== undefined && {
|
...(clearContext !== undefined && {
|
||||||
clearContext,
|
clearContext,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -497,25 +497,37 @@ function validateProviderConnectionsSection(
|
||||||
const anthropicUpdate: Partial<ProviderConnectionsConfig['anthropic']> = {};
|
const anthropicUpdate: Partial<ProviderConnectionsConfig['anthropic']> = {};
|
||||||
|
|
||||||
for (const [connectionKey, connectionValue] of Object.entries(value)) {
|
for (const [connectionKey, connectionValue] of Object.entries(value)) {
|
||||||
if (connectionKey !== 'authMode') {
|
if (connectionKey !== 'authMode' && connectionKey !== 'fastModeDefault') {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: `providerConnections.anthropic.${connectionKey} is not a valid setting`,
|
error: `providerConnections.anthropic.${connectionKey} is not a valid setting`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (connectionKey === 'authMode') {
|
||||||
connectionValue !== 'auto' &&
|
if (
|
||||||
connectionValue !== 'oauth' &&
|
connectionValue !== 'auto' &&
|
||||||
connectionValue !== 'api_key'
|
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 {
|
return {
|
||||||
valid: false,
|
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'];
|
result.anthropic = anthropicUpdate as ProviderConnectionsConfig['anthropic'];
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ import type {
|
||||||
TeamLaunchResponse,
|
TeamLaunchResponse,
|
||||||
TeamMemberActivityMeta,
|
TeamMemberActivityMeta,
|
||||||
TeamMessageNotificationData,
|
TeamMessageNotificationData,
|
||||||
|
TeamFastMode,
|
||||||
TeamProviderBackendId,
|
TeamProviderBackendId,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
TeamProvisioningPrepareResult,
|
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(
|
async function validateProvisioningRequest(
|
||||||
request: unknown
|
request: unknown
|
||||||
): Promise<{ valid: true; value: TeamCreateRequest } | { valid: false; error: string }> {
|
): Promise<{ valid: true; value: TeamCreateRequest } | { valid: false; error: string }> {
|
||||||
|
|
@ -1224,12 +1240,15 @@ async function validateProvisioningRequest(
|
||||||
if (!Array.isArray(payload.members)) {
|
if (!Array.isArray(payload.members)) {
|
||||||
return { valid: false, error: 'members must be an array' };
|
return { valid: false, error: 'members must be an array' };
|
||||||
}
|
}
|
||||||
const providerId =
|
const explicitProviderId =
|
||||||
payload.providerId === 'codex'
|
payload.providerId === 'codex'
|
||||||
? 'codex'
|
? 'codex'
|
||||||
: payload.providerId === 'gemini'
|
: payload.providerId === 'gemini'
|
||||||
? 'gemini'
|
? 'gemini'
|
||||||
: 'anthropic';
|
: payload.providerId === 'anthropic'
|
||||||
|
? 'anthropic'
|
||||||
|
: undefined;
|
||||||
|
const providerId = explicitProviderId ?? 'anthropic';
|
||||||
|
|
||||||
const seenNames = new Set<string>();
|
const seenNames = new Set<string>();
|
||||||
const members: TeamCreateRequest['members'] = [];
|
const members: TeamCreateRequest['members'] = [];
|
||||||
|
|
@ -1304,6 +1323,10 @@ async function validateProvisioningRequest(
|
||||||
if (!effortValidation.valid) {
|
if (!effortValidation.valid) {
|
||||||
return { valid: false, error: effortValidation.error };
|
return { valid: false, error: effortValidation.error };
|
||||||
}
|
}
|
||||||
|
const fastModeValidation = parseOptionalTeamFastMode(payload.fastMode);
|
||||||
|
if (!fastModeValidation.valid) {
|
||||||
|
return { valid: false, error: fastModeValidation.error };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.mkdir(cwd, { recursive: true });
|
await fs.promises.mkdir(cwd, { recursive: true });
|
||||||
|
|
@ -1359,6 +1382,7 @@ async function validateProvisioningRequest(
|
||||||
providerBackendId: providerBackendValidation.value,
|
providerBackendId: providerBackendValidation.value,
|
||||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||||
effort: effortValidation.value,
|
effort: effortValidation.value,
|
||||||
|
fastMode: fastModeValidation.value,
|
||||||
skipPermissions:
|
skipPermissions:
|
||||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
||||||
worktree:
|
worktree:
|
||||||
|
|
@ -1512,6 +1536,10 @@ async function handleLaunchTeam(
|
||||||
if (!effortValidation.valid) {
|
if (!effortValidation.valid) {
|
||||||
return { success: false, error: effortValidation.error };
|
return { success: false, error: effortValidation.error };
|
||||||
}
|
}
|
||||||
|
const fastModeValidation = parseOptionalTeamFastMode(payload.fastMode);
|
||||||
|
if (!fastModeValidation.valid) {
|
||||||
|
return { success: false, error: fastModeValidation.error };
|
||||||
|
}
|
||||||
|
|
||||||
const createRequest: TeamCreateRequest = {
|
const createRequest: TeamCreateRequest = {
|
||||||
teamName: tn,
|
teamName: tn,
|
||||||
|
|
@ -1527,6 +1555,7 @@ async function handleLaunchTeam(
|
||||||
),
|
),
|
||||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||||
effort: effortValidation.value,
|
effort: effortValidation.value,
|
||||||
|
fastMode: fastModeValidation.value ?? meta?.fastMode,
|
||||||
limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined,
|
limitContext: typeof payload.limitContext === 'boolean' ? payload.limitContext : undefined,
|
||||||
skipPermissions:
|
skipPermissions:
|
||||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
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) {
|
if (!effortValidation.valid) {
|
||||||
return { success: false, error: effortValidation.error };
|
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', () => {
|
return wrapTeamHandler('launch', () => {
|
||||||
addMainBreadcrumb('team', 'launch', { teamName: validatedTeamName.value! });
|
addMainBreadcrumb('team', 'launch', { teamName: validatedTeamName.value! });
|
||||||
|
|
@ -1570,10 +1633,12 @@ async function handleLaunchTeam(
|
||||||
teamName: validatedTeamName.value!,
|
teamName: validatedTeamName.value!,
|
||||||
cwd,
|
cwd,
|
||||||
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
||||||
providerId,
|
providerId: launchProviderId,
|
||||||
providerBackendId: providerBackendValidation.value,
|
providerBackendId: launchProviderBackendValidation.value,
|
||||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
model: rawLaunchModel,
|
||||||
effort: effortValidation.value,
|
effort: effortValidation.value,
|
||||||
|
fastMode: fastModeValidation.value,
|
||||||
|
limitContext: launchLimitContext,
|
||||||
clearContext: payload.clearContext === true ? true : undefined,
|
clearContext: payload.clearContext === true ? true : undefined,
|
||||||
skipPermissions:
|
skipPermissions:
|
||||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
||||||
|
|
@ -2660,6 +2725,10 @@ async function handleCreateConfig(
|
||||||
if (!providerBackendValidation.valid) {
|
if (!providerBackendValidation.valid) {
|
||||||
return { success: false, error: providerBackendValidation.error };
|
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 seenNames = new Set<string>();
|
||||||
const members: TeamCreateConfigRequest['members'] = [];
|
const members: TeamCreateConfigRequest['members'] = [];
|
||||||
|
|
@ -2721,6 +2790,7 @@ async function handleCreateConfig(
|
||||||
members,
|
members,
|
||||||
cwd: typeof payload.cwd === 'string' ? payload.cwd.trim() || undefined : undefined,
|
cwd: typeof payload.cwd === 'string' ? payload.cwd.trim() || undefined : undefined,
|
||||||
providerBackendId: providerBackendValidation.value,
|
providerBackendId: providerBackendValidation.value,
|
||||||
|
fastMode: fastModeValidation.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -4023,6 +4093,7 @@ async function handleGetSavedRequest(
|
||||||
),
|
),
|
||||||
model: meta.model,
|
model: meta.model,
|
||||||
effort: meta.effort as TeamCreateRequest['effort'],
|
effort: meta.effort as TeamCreateRequest['effort'],
|
||||||
|
fastMode: meta.fastMode as TeamCreateRequest['fastMode'],
|
||||||
skipPermissions: meta.skipPermissions,
|
skipPermissions: meta.skipPermissions,
|
||||||
worktree: meta.worktree,
|
worktree: meta.worktree,
|
||||||
extraCliArgs: meta.extraCliArgs,
|
extraCliArgs: meta.extraCliArgs,
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ export type ProviderConnectionAuthMode = 'auto' | 'oauth' | 'api_key';
|
||||||
export interface ProviderConnectionsConfig {
|
export interface ProviderConnectionsConfig {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
authMode: ProviderConnectionAuthMode;
|
authMode: ProviderConnectionAuthMode;
|
||||||
|
fastModeDefault: boolean;
|
||||||
};
|
};
|
||||||
codex: {
|
codex: {
|
||||||
preferredAuthMode: CodexAccountAuthMode;
|
preferredAuthMode: CodexAccountAuthMode;
|
||||||
|
|
@ -333,6 +334,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
||||||
providerConnections: {
|
providerConnections: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
authMode: 'auto',
|
authMode: 'auto',
|
||||||
|
fastModeDefault: false,
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
preferredAuthMode: 'auto',
|
preferredAuthMode: 'auto',
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ interface RuntimeProviderCapabilitiesResponse {
|
||||||
values?: string[];
|
values?: string[];
|
||||||
configPassthrough?: boolean;
|
configPassthrough?: boolean;
|
||||||
};
|
};
|
||||||
|
fastMode?: {
|
||||||
|
supported?: boolean;
|
||||||
|
available?: boolean;
|
||||||
|
reason?: string | null;
|
||||||
|
source?: 'runtime';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuntimeProviderModelCatalogItemResponse {
|
interface RuntimeProviderModelCatalogItemResponse {
|
||||||
|
|
@ -49,6 +55,7 @@ interface RuntimeProviderModelCatalogItemResponse {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
supportedReasoningEfforts?: string[];
|
supportedReasoningEfforts?: string[];
|
||||||
defaultReasoningEffort?: string | null;
|
defaultReasoningEffort?: string | null;
|
||||||
|
supportsFastMode?: boolean;
|
||||||
inputModalities?: string[];
|
inputModalities?: string[];
|
||||||
supportsPersonality?: boolean;
|
supportsPersonality?: boolean;
|
||||||
isDefault?: boolean;
|
isDefault?: boolean;
|
||||||
|
|
@ -279,7 +286,8 @@ function normalizeRuntimeReasoningEffort(
|
||||||
value === 'low' ||
|
value === 'low' ||
|
||||||
value === 'medium' ||
|
value === 'medium' ||
|
||||||
value === 'high' ||
|
value === 'high' ||
|
||||||
value === 'xhigh'
|
value === 'xhigh' ||
|
||||||
|
value === 'max'
|
||||||
? value
|
? value
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
@ -347,6 +355,7 @@ function mapRuntimeProviderModelCatalog(
|
||||||
hidden: model.hidden === true,
|
hidden: model.hidden === true,
|
||||||
supportedReasoningEfforts,
|
supportedReasoningEfforts,
|
||||||
defaultReasoningEffort,
|
defaultReasoningEffort,
|
||||||
|
supportsFastMode: model.supportsFastMode === true,
|
||||||
inputModalities: model.inputModalities?.filter((value) => value.trim().length > 0) ?? [],
|
inputModalities: model.inputModalities?.filter((value) => value.trim().length > 0) ?? [],
|
||||||
supportsPersonality: model.supportsPersonality === true,
|
supportsPersonality: model.supportsPersonality === true,
|
||||||
isDefault: model.isDefault === true,
|
isDefault: model.isDefault === true,
|
||||||
|
|
@ -477,6 +486,14 @@ export class ClaudeMultimodelBridgeService {
|
||||||
runtimeStatus.runtimeCapabilities.reasoningEffort.configPassthrough === true,
|
runtimeStatus.runtimeCapabilities.reasoningEffort.configPassthrough === true,
|
||||||
}
|
}
|
||||||
: undefined,
|
: 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,
|
: null,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2384,6 +2384,7 @@ export class TeamDataService {
|
||||||
color: request.color,
|
color: request.color,
|
||||||
cwd: request.cwd?.trim() || '',
|
cwd: request.cwd?.trim() || '',
|
||||||
providerBackendId: request.providerBackendId,
|
providerBackendId: request.providerBackendId,
|
||||||
|
fastMode: request.fastMode,
|
||||||
createdAt: joinedAt,
|
createdAt: joinedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,13 @@ const QUOTA_EXHAUSTED_TOKENS = [
|
||||||
'quota exceeded',
|
'quota exceeded',
|
||||||
'quota exhausted',
|
'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 = [
|
const AUTH_ERROR_TOKENS = [
|
||||||
'unauthorized',
|
'unauthorized',
|
||||||
'forbidden',
|
'forbidden',
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import * as path from 'path';
|
||||||
|
|
||||||
import { atomicWriteAsync } from './atomicWrite';
|
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.
|
* Persisted team-level metadata saved by the UI before CLI provisioning.
|
||||||
|
|
@ -25,6 +25,7 @@ export interface TeamMetaFile {
|
||||||
providerBackendId?: string;
|
providerBackendId?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
effort?: string;
|
effort?: string;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
skipPermissions?: boolean;
|
skipPermissions?: boolean;
|
||||||
worktree?: string;
|
worktree?: string;
|
||||||
extraCliArgs?: string;
|
extraCliArgs?: string;
|
||||||
|
|
@ -51,6 +52,10 @@ function normalizeOptionalString(value: unknown): string | null {
|
||||||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : 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 {
|
function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity | undefined {
|
||||||
if (!value || typeof value !== 'object') {
|
if (!value || typeof value !== 'object') {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -80,7 +85,8 @@ function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity |
|
||||||
raw.selectedEffort === 'low' ||
|
raw.selectedEffort === 'low' ||
|
||||||
raw.selectedEffort === 'medium' ||
|
raw.selectedEffort === 'medium' ||
|
||||||
raw.selectedEffort === 'high' ||
|
raw.selectedEffort === 'high' ||
|
||||||
raw.selectedEffort === 'xhigh'
|
raw.selectedEffort === 'xhigh' ||
|
||||||
|
raw.selectedEffort === 'max'
|
||||||
? raw.selectedEffort
|
? raw.selectedEffort
|
||||||
: null;
|
: null;
|
||||||
const resolvedEffort =
|
const resolvedEffort =
|
||||||
|
|
@ -89,7 +95,8 @@ function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity |
|
||||||
raw.resolvedEffort === 'low' ||
|
raw.resolvedEffort === 'low' ||
|
||||||
raw.resolvedEffort === 'medium' ||
|
raw.resolvedEffort === 'medium' ||
|
||||||
raw.resolvedEffort === 'high' ||
|
raw.resolvedEffort === 'high' ||
|
||||||
raw.resolvedEffort === 'xhigh'
|
raw.resolvedEffort === 'xhigh' ||
|
||||||
|
raw.resolvedEffort === 'max'
|
||||||
? raw.resolvedEffort
|
? raw.resolvedEffort
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -105,6 +112,9 @@ function normalizeLaunchIdentity(value: unknown): ProviderModelLaunchIdentity |
|
||||||
catalogFetchedAt: normalizeOptionalString(raw.catalogFetchedAt),
|
catalogFetchedAt: normalizeOptionalString(raw.catalogFetchedAt),
|
||||||
selectedEffort,
|
selectedEffort,
|
||||||
resolvedEffort,
|
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,
|
model: typeof file.model === 'string' ? file.model.trim() || undefined : undefined,
|
||||||
effort: typeof file.effort === 'string' ? file.effort.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,
|
skipPermissions: typeof file.skipPermissions === 'boolean' ? file.skipPermissions : undefined,
|
||||||
worktree: typeof file.worktree === 'string' ? file.worktree.trim() || undefined : undefined,
|
worktree: typeof file.worktree === 'string' ? file.worktree.trim() || undefined : undefined,
|
||||||
extraCliArgs:
|
extraCliArgs:
|
||||||
|
|
@ -198,6 +209,7 @@ export class TeamMetaStore {
|
||||||
),
|
),
|
||||||
model: data.model?.trim() || undefined,
|
model: data.model?.trim() || undefined,
|
||||||
effort: data.effort?.trim() || undefined,
|
effort: data.effort?.trim() || undefined,
|
||||||
|
fastMode: normalizeFastMode(data.fastMode) ?? undefined,
|
||||||
skipPermissions: data.skipPermissions,
|
skipPermissions: data.skipPermissions,
|
||||||
worktree: data.worktree?.trim() || undefined,
|
worktree: data.worktree?.trim() || undefined,
|
||||||
extraCliArgs: data.extraCliArgs?.trim() || undefined,
|
extraCliArgs: data.extraCliArgs?.trim() || undefined,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import {
|
||||||
killTmuxPaneForCurrentPlatformSync,
|
killTmuxPaneForCurrentPlatformSync,
|
||||||
listTmuxPanePidsForCurrentPlatform,
|
listTmuxPanePidsForCurrentPlatform,
|
||||||
} from '@features/tmux-installer/main';
|
} from '@features/tmux-installer/main';
|
||||||
|
import {
|
||||||
|
resolveAnthropicFastMode,
|
||||||
|
resolveAnthropicRuntimeSelection,
|
||||||
|
} from '@features/anthropic-runtime-profile/main';
|
||||||
import { ConfigManager } from '@main/services/infrastructure/ConfigManager';
|
import { ConfigManager } from '@main/services/infrastructure/ConfigManager';
|
||||||
import { NotificationManager } from '@main/services/infrastructure/NotificationManager';
|
import { NotificationManager } from '@main/services/infrastructure/NotificationManager';
|
||||||
import { getAppIconPath } from '@main/utils/appIcon';
|
import { getAppIconPath } from '@main/utils/appIcon';
|
||||||
|
|
@ -170,6 +174,7 @@ interface RelayInboxMessageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
CliProviderModelCatalog,
|
||||||
ActiveToolCall,
|
ActiveToolCall,
|
||||||
CliProviderRuntimeCapabilities,
|
CliProviderRuntimeCapabilities,
|
||||||
CrossTeamSendResult,
|
CrossTeamSendResult,
|
||||||
|
|
@ -190,6 +195,7 @@ import type {
|
||||||
TeamConfig,
|
TeamConfig,
|
||||||
TeamCreateRequest,
|
TeamCreateRequest,
|
||||||
TeamCreateResponse,
|
TeamCreateResponse,
|
||||||
|
TeamFastMode,
|
||||||
TeamLaunchAggregateState,
|
TeamLaunchAggregateState,
|
||||||
TeamLaunchRequest,
|
TeamLaunchRequest,
|
||||||
TeamLaunchResponse,
|
TeamLaunchResponse,
|
||||||
|
|
@ -328,6 +334,7 @@ interface RuntimeStatusCommandResponse {
|
||||||
providers?: Record<
|
providers?: Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
|
modelCatalog?: CliProviderModelCatalog | null;
|
||||||
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
|
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
@ -336,6 +343,7 @@ interface RuntimeStatusCommandResponse {
|
||||||
interface RuntimeProviderLaunchFacts {
|
interface RuntimeProviderLaunchFacts {
|
||||||
defaultModel: string | null;
|
defaultModel: string | null;
|
||||||
modelIds: Set<string>;
|
modelIds: Set<string>;
|
||||||
|
modelCatalog: CliProviderModelCatalog | null;
|
||||||
runtimeCapabilities: CliProviderRuntimeCapabilities | null;
|
runtimeCapabilities: CliProviderRuntimeCapabilities | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,6 +424,47 @@ function isCodexEffortRuntimeSupported(
|
||||||
return reasoning?.configPassthrough === true && reasoning.values.includes(effort);
|
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 {
|
function isProbeTimeoutMessage(message: string): boolean {
|
||||||
const lower = message.toLowerCase();
|
const lower = message.toLowerCase();
|
||||||
return (
|
return (
|
||||||
|
|
@ -522,7 +571,10 @@ function mergeProvisioningWarnings(
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRuntimeLaunchWarning(
|
function buildRuntimeLaunchWarning(
|
||||||
request: Pick<TeamCreateRequest, 'providerId' | 'providerBackendId' | 'model' | 'effort'>,
|
request: Pick<
|
||||||
|
TeamCreateRequest,
|
||||||
|
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode'
|
||||||
|
>,
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
options?: {
|
options?: {
|
||||||
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
||||||
|
|
@ -534,6 +586,10 @@ function buildRuntimeLaunchWarning(
|
||||||
const providerLabel = getTeamProviderLabel(providerId);
|
const providerLabel = getTeamProviderLabel(providerId);
|
||||||
const modelLabel = request.model?.trim() || 'default';
|
const modelLabel = request.model?.trim() || 'default';
|
||||||
const effortLabel = request.effort ?? 'default';
|
const effortLabel = request.effort ?? 'default';
|
||||||
|
const fastLabel =
|
||||||
|
providerId === 'anthropic'
|
||||||
|
? `, fast ${request.fastMode ?? (getAnthropicFastModeDefault() ? 'inherit:on' : 'inherit:off')}`
|
||||||
|
: '';
|
||||||
const backend =
|
const backend =
|
||||||
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
|
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
|
||||||
getConfiguredRuntimeBackend(providerId);
|
getConfiguredRuntimeBackend(providerId);
|
||||||
|
|
@ -564,14 +620,17 @@ function buildRuntimeLaunchWarning(
|
||||||
typeof options?.expectedMembersCount === 'number'
|
typeof options?.expectedMembersCount === 'number'
|
||||||
? `, members ${options.expectedMembersCount}`
|
? `, 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(
|
function logRuntimeLaunchSnapshot(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
claudePath: string,
|
claudePath: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
request: Pick<TeamCreateRequest, 'providerId' | 'providerBackendId' | 'model' | 'effort'>,
|
request: Pick<
|
||||||
|
TeamCreateRequest,
|
||||||
|
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode'
|
||||||
|
>,
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
options?: {
|
options?: {
|
||||||
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
geminiRuntimeAuth?: GeminiRuntimeAuthState | null;
|
||||||
|
|
@ -586,6 +645,7 @@ function logRuntimeLaunchSnapshot(
|
||||||
providerBackendId: migrateProviderBackendId(providerId, request.providerBackendId) ?? null,
|
providerBackendId: migrateProviderBackendId(providerId, request.providerBackendId) ?? null,
|
||||||
model: request.model ?? null,
|
model: request.model ?? null,
|
||||||
effort: request.effort ?? null,
|
effort: request.effort ?? null,
|
||||||
|
fastMode: request.fastMode ?? null,
|
||||||
configuredBackend:
|
configuredBackend:
|
||||||
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
|
migrateProviderBackendId(providerId, request.providerBackendId?.trim()) ||
|
||||||
getConfiguredRuntimeBackend(providerId),
|
getConfiguredRuntimeBackend(providerId),
|
||||||
|
|
@ -2984,12 +3044,16 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const runtimeStatusPromise =
|
const runtimeStatusPromise =
|
||||||
params.providerId === 'codex'
|
params.providerId === 'codex' || params.providerId === 'anthropic'
|
||||||
? execCli(params.claudePath, ['runtime', 'status', '--json', '--provider', 'codex'], {
|
? execCli(
|
||||||
cwd: params.cwd,
|
params.claudePath,
|
||||||
env: params.env,
|
['runtime', 'status', '--json', '--provider', params.providerId],
|
||||||
timeout: 8_000,
|
{
|
||||||
})
|
cwd: params.cwd,
|
||||||
|
env: params.env,
|
||||||
|
timeout: 8_000,
|
||||||
|
}
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const [modelListResult, runtimeStatusResult] = await Promise.allSettled([
|
const [modelListResult, runtimeStatusResult] = await Promise.allSettled([
|
||||||
|
|
@ -3020,6 +3084,7 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
let runtimeCapabilities: CliProviderRuntimeCapabilities | null = null;
|
let runtimeCapabilities: CliProviderRuntimeCapabilities | null = null;
|
||||||
|
let modelCatalog: CliProviderModelCatalog | null = null;
|
||||||
if (
|
if (
|
||||||
runtimeStatusResult.status === 'fulfilled' &&
|
runtimeStatusResult.status === 'fulfilled' &&
|
||||||
runtimeStatusResult.value &&
|
runtimeStatusResult.value &&
|
||||||
|
|
@ -3029,7 +3094,12 @@ export class TeamProvisioningService {
|
||||||
const parsed = extractJsonObjectFromCli<RuntimeStatusCommandResponse>(
|
const parsed = extractJsonObjectFromCli<RuntimeStatusCommandResponse>(
|
||||||
runtimeStatusResult.value.stdout
|
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) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[${params.providerId}] Failed to parse runtime capabilities for launch validation: ${
|
`[${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 {
|
return {
|
||||||
defaultModel:
|
defaultModel:
|
||||||
params.providerId === 'anthropic'
|
params.providerId === 'anthropic'
|
||||||
? resolveAnthropicLaunchModel({
|
? resolveAnthropicLaunchModel({
|
||||||
limitContext: params.limitContext === true,
|
limitContext: params.limitContext === true,
|
||||||
availableLaunchModels: modelIds,
|
availableLaunchModels:
|
||||||
|
modelCatalog?.models.map((model) => model.launchModel) ?? modelIds,
|
||||||
defaultLaunchModel: defaultModel,
|
defaultLaunchModel: defaultModel,
|
||||||
})
|
})
|
||||||
: defaultModel,
|
: defaultModel,
|
||||||
modelIds,
|
modelIds,
|
||||||
|
modelCatalog,
|
||||||
runtimeCapabilities,
|
runtimeCapabilities,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -3056,7 +3142,7 @@ export class TeamProvisioningService {
|
||||||
private buildProviderModelLaunchIdentity(params: {
|
private buildProviderModelLaunchIdentity(params: {
|
||||||
request: Pick<
|
request: Pick<
|
||||||
TeamCreateRequest,
|
TeamCreateRequest,
|
||||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'limitContext'
|
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode' | 'limitContext'
|
||||||
>;
|
>;
|
||||||
facts: RuntimeProviderLaunchFacts;
|
facts: RuntimeProviderLaunchFacts;
|
||||||
}): ProviderModelLaunchIdentity {
|
}): ProviderModelLaunchIdentity {
|
||||||
|
|
@ -3068,6 +3154,39 @@ export class TeamProvisioningService {
|
||||||
limitContext: params.request.limitContext,
|
limitContext: params.request.limitContext,
|
||||||
facts: params.facts,
|
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;
|
const resolvedEffort = params.request.effort ?? null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -3090,10 +3209,51 @@ export class TeamProvisioningService {
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
model?: string;
|
model?: string;
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
|
limitContext?: boolean;
|
||||||
facts: RuntimeProviderLaunchFacts;
|
facts: RuntimeProviderLaunchFacts;
|
||||||
}): void {
|
}): void {
|
||||||
const explicitModel = getExplicitLaunchModelSelection(params.model);
|
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.providerId !== 'codex') {
|
||||||
if (params.effort && !isLegacySafeEffort(params.effort)) {
|
if (params.effort && !isLegacySafeEffort(params.effort)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -3133,7 +3293,7 @@ export class TeamProvisioningService {
|
||||||
env: NodeJS.ProcessEnv;
|
env: NodeJS.ProcessEnv;
|
||||||
request: Pick<
|
request: Pick<
|
||||||
TeamCreateRequest,
|
TeamCreateRequest,
|
||||||
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'limitContext'
|
'providerId' | 'providerBackendId' | 'model' | 'effort' | 'fastMode' | 'limitContext'
|
||||||
>;
|
>;
|
||||||
effectiveMembers: TeamCreateRequest['members'];
|
effectiveMembers: TeamCreateRequest['members'];
|
||||||
}): Promise<ProviderModelLaunchIdentity> {
|
}): Promise<ProviderModelLaunchIdentity> {
|
||||||
|
|
@ -3161,6 +3321,8 @@ export class TeamProvisioningService {
|
||||||
providerId: leadProviderId,
|
providerId: leadProviderId,
|
||||||
model: params.request.model,
|
model: params.request.model,
|
||||||
effort: params.request.effort,
|
effort: params.request.effort,
|
||||||
|
fastMode: params.request.fastMode,
|
||||||
|
limitContext: params.request.limitContext,
|
||||||
facts: leadFacts,
|
facts: leadFacts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3172,6 +3334,7 @@ export class TeamProvisioningService {
|
||||||
providerId: memberProviderId,
|
providerId: memberProviderId,
|
||||||
model: member.model,
|
model: member.model,
|
||||||
effort: member.effort,
|
effort: member.effort,
|
||||||
|
limitContext: params.request.limitContext,
|
||||||
facts: memberFacts,
|
facts: memberFacts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -4867,6 +5030,7 @@ export class TeamProvisioningService {
|
||||||
run?.request.providerId ?? persistedTeamMeta?.providerId,
|
run?.request.providerId ?? persistedTeamMeta?.providerId,
|
||||||
run?.request.providerBackendId ?? persistedTeamMeta?.providerBackendId
|
run?.request.providerBackendId ?? persistedTeamMeta?.providerBackendId
|
||||||
),
|
),
|
||||||
|
fastMode: run?.request.fastMode ?? persistedTeamMeta?.fastMode,
|
||||||
members: snapshotMembers,
|
members: snapshotMembers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -6728,6 +6892,8 @@ export class TeamProvisioningService {
|
||||||
request.model,
|
request.model,
|
||||||
launchIdentity
|
launchIdentity
|
||||||
);
|
);
|
||||||
|
const resolvedProviderId = resolveTeamProviderId(request.providerId);
|
||||||
|
const anthropicSettingsArgs = buildAnthropicSettingsArgs(resolvedProviderId, launchIdentity);
|
||||||
const spawnArgs = [
|
const spawnArgs = [
|
||||||
'--input-format',
|
'--input-format',
|
||||||
'stream-json',
|
'stream-json',
|
||||||
|
|
@ -6751,7 +6917,8 @@ export class TeamProvisioningService {
|
||||||
? ['--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions']
|
? ['--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions']
|
||||||
: ['--permission-prompt-tool', 'stdio', '--permission-mode', 'default']),
|
: ['--permission-prompt-tool', 'stdio', '--permission-mode', 'default']),
|
||||||
...(launchModelArg ? ['--model', launchModelArg] : []),
|
...(launchModelArg ? ['--model', launchModelArg] : []),
|
||||||
...(request.effort ? ['--effort', request.effort] : []),
|
...(launchIdentity.resolvedEffort ? ['--effort', launchIdentity.resolvedEffort] : []),
|
||||||
|
...anthropicSettingsArgs,
|
||||||
...(request.worktree ? ['--worktree', request.worktree] : []),
|
...(request.worktree ? ['--worktree', request.worktree] : []),
|
||||||
...parseCliArgs(request.extraCliArgs),
|
...parseCliArgs(request.extraCliArgs),
|
||||||
...providerArgs,
|
...providerArgs,
|
||||||
|
|
@ -6784,6 +6951,7 @@ export class TeamProvisioningService {
|
||||||
providerBackendId: request.providerBackendId,
|
providerBackendId: request.providerBackendId,
|
||||||
model: request.model,
|
model: request.model,
|
||||||
effort: request.effort,
|
effort: request.effort,
|
||||||
|
fastMode: request.fastMode,
|
||||||
skipPermissions: request.skipPermissions,
|
skipPermissions: request.skipPermissions,
|
||||||
worktree: request.worktree,
|
worktree: request.worktree,
|
||||||
extraCliArgs: request.extraCliArgs,
|
extraCliArgs: request.extraCliArgs,
|
||||||
|
|
@ -7185,6 +7353,7 @@ export class TeamProvisioningService {
|
||||||
providerBackendId: request.providerBackendId,
|
providerBackendId: request.providerBackendId,
|
||||||
model: request.model,
|
model: request.model,
|
||||||
effort: request.effort,
|
effort: request.effort,
|
||||||
|
fastMode: request.fastMode,
|
||||||
skipPermissions: request.skipPermissions,
|
skipPermissions: request.skipPermissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -7389,12 +7558,15 @@ export class TeamProvisioningService {
|
||||||
request.model,
|
request.model,
|
||||||
launchIdentity
|
launchIdentity
|
||||||
);
|
);
|
||||||
|
const resolvedProviderId = resolveTeamProviderId(request.providerId);
|
||||||
|
const anthropicSettingsArgs = buildAnthropicSettingsArgs(resolvedProviderId, launchIdentity);
|
||||||
if (launchModelArg) {
|
if (launchModelArg) {
|
||||||
launchArgs.push('--model', launchModelArg);
|
launchArgs.push('--model', launchModelArg);
|
||||||
}
|
}
|
||||||
if (request.effort) {
|
if (launchIdentity.resolvedEffort) {
|
||||||
launchArgs.push('--effort', request.effort);
|
launchArgs.push('--effort', launchIdentity.resolvedEffort);
|
||||||
}
|
}
|
||||||
|
launchArgs.push(...anthropicSettingsArgs);
|
||||||
if (request.worktree) {
|
if (request.worktree) {
|
||||||
launchArgs.push('--worktree', request.worktree);
|
launchArgs.push('--worktree', request.worktree);
|
||||||
}
|
}
|
||||||
|
|
@ -7423,6 +7595,7 @@ export class TeamProvisioningService {
|
||||||
providerBackendId: request.providerBackendId,
|
providerBackendId: request.providerBackendId,
|
||||||
model: request.model,
|
model: request.model,
|
||||||
effort: request.effort,
|
effort: request.effort,
|
||||||
|
fastMode: request.fastMode,
|
||||||
skipPermissions: request.skipPermissions,
|
skipPermissions: request.skipPermissions,
|
||||||
worktree: request.worktree,
|
worktree: request.worktree,
|
||||||
extraCliArgs: request.extraCliArgs,
|
extraCliArgs: request.extraCliArgs,
|
||||||
|
|
|
||||||
|
|
@ -722,6 +722,19 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
const codexActionBusy =
|
const codexActionBusy =
|
||||||
disabled || selectedProviderLoading || connectionSaving || codexAccount.loading;
|
disabled || selectedProviderLoading || connectionSaving || codexAccount.loading;
|
||||||
const runtimeBusy = disabled || selectedProviderLoading || runtimeSaving;
|
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
|
const connectionMethodCardsHint = selectedProvider
|
||||||
? getConnectionMethodCardsHint(selectedProvider)
|
? getConnectionMethodCardsHint(selectedProvider)
|
||||||
: null;
|
: 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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
|
|
@ -1188,6 +1224,50 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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' ? (
|
{selectedProvider.providerId === 'codex' ? (
|
||||||
<div
|
<div
|
||||||
className="space-y-3 rounded-md border p-3"
|
className="space-y-3 rounded-md border p-3"
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,7 @@ export function useSettingsHandlers({
|
||||||
providerConnections: {
|
providerConnections: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
authMode: 'auto',
|
authMode: 'auto',
|
||||||
|
fastModeDefault: false,
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
preferredAuthMode: 'auto',
|
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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reconcileAnthropicRuntimeSelections,
|
||||||
|
resolveAnthropicFastMode,
|
||||||
|
resolveAnthropicRuntimeSelection,
|
||||||
|
} from '@features/anthropic-runtime-profile/renderer';
|
||||||
import {
|
import {
|
||||||
mergeCodexCliStatusWithSnapshot,
|
mergeCodexCliStatusWithSnapshot,
|
||||||
useCodexAccountSnapshot,
|
useCodexAccountSnapshot,
|
||||||
|
|
@ -100,6 +105,7 @@ import type {
|
||||||
EffortLevel,
|
EffortLevel,
|
||||||
Project,
|
Project,
|
||||||
TeamCreateRequest,
|
TeamCreateRequest,
|
||||||
|
TeamFastMode,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
TeamProvisioningMemberInput,
|
TeamProvisioningMemberInput,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
@ -121,6 +127,11 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
|
||||||
return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
|
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 {
|
function isEphemeralRenderedProjectPath(projectPath: string | null | undefined): boolean {
|
||||||
const normalized = normalizePath(projectPath ?? '').toLowerCase();
|
const normalized = normalizePath(projectPath ?? '').toLowerCase();
|
||||||
return (
|
return (
|
||||||
|
|
@ -313,6 +324,9 @@ export const CreateTeamDialog = ({
|
||||||
}: CreateTeamDialogProps): React.JSX.Element => {
|
}: CreateTeamDialogProps): React.JSX.Element => {
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
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 cliStatus = useStore((s) => s.cliStatus);
|
||||||
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
|
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
|
||||||
const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus);
|
const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus);
|
||||||
|
|
@ -396,6 +410,8 @@ export const CreateTeamDialog = ({
|
||||||
const stored = localStorage.getItem('team:lastSelectedEffort');
|
const stored = localStorage.getItem('team:lastSelectedEffort');
|
||||||
return stored === null ? 'medium' : stored;
|
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)
|
// Advanced CLI section state (use teamName-derived key for localStorage)
|
||||||
const advancedKey = sanitizeTeamName(teamName.trim()) || '_new_';
|
const advancedKey = sanitizeTeamName(teamName.trim()) || '_new_';
|
||||||
|
|
@ -456,6 +472,11 @@ export const CreateTeamDialog = ({
|
||||||
localStorage.setItem('team:lastSelectedEffort', value);
|
localStorage.setItem('team:lastSelectedEffort', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSelectedFastMode = (value: TeamFastMode): void => {
|
||||||
|
setSelectedFastModeRaw(value);
|
||||||
|
localStorage.setItem('team:lastSelectedFastMode', value);
|
||||||
|
};
|
||||||
|
|
||||||
const setWorktreeEnabled = (value: boolean): void => {
|
const setWorktreeEnabled = (value: boolean): void => {
|
||||||
setWorktreeEnabledRaw(value);
|
setWorktreeEnabledRaw(value);
|
||||||
localStorage.setItem(`team:lastWorktreeEnabled:${advancedKey}`, String(value));
|
localStorage.setItem(`team:lastWorktreeEnabled:${advancedKey}`, String(value));
|
||||||
|
|
@ -1003,6 +1024,84 @@ export const CreateTeamDialog = ({
|
||||||
),
|
),
|
||||||
[limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId]
|
[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 sanitizedTeamName = sanitizeTeamName(teamName.trim());
|
||||||
const teamNameInlineError = validateTeamNameInline(teamName);
|
const teamNameInlineError = validateTeamNameInline(teamName);
|
||||||
|
|
@ -1026,6 +1125,7 @@ export const CreateTeamDialog = ({
|
||||||
) ?? undefined,
|
) ?? undefined,
|
||||||
model: effectiveModel,
|
model: effectiveModel,
|
||||||
effort: (selectedEffort as EffortLevel) || undefined,
|
effort: (selectedEffort as EffortLevel) || undefined,
|
||||||
|
fastMode: selectedProviderId === 'anthropic' ? selectedFastMode : undefined,
|
||||||
limitContext,
|
limitContext,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined,
|
worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined,
|
||||||
|
|
@ -1043,6 +1143,7 @@ export const CreateTeamDialog = ({
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
effectiveModel,
|
effectiveModel,
|
||||||
selectedEffort,
|
selectedEffort,
|
||||||
|
selectedFastMode,
|
||||||
limitContext,
|
limitContext,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
worktreeEnabled,
|
worktreeEnabled,
|
||||||
|
|
@ -1132,18 +1233,49 @@ export const CreateTeamDialog = ({
|
||||||
args.push('--mcp-config', '<auto>', '--disallowedTools', APP_TEAM_RUNTIME_DISALLOWED_TOOLS);
|
args.push('--mcp-config', '<auto>', '--disallowedTools', APP_TEAM_RUNTIME_DISALLOWED_TOOLS);
|
||||||
if (skipPermissions) args.push('--dangerously-skip-permissions');
|
if (skipPermissions) args.push('--dangerously-skip-permissions');
|
||||||
if (effectiveModel) args.push('--model', effectiveModel);
|
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;
|
return args;
|
||||||
}, [skipPermissions, effectiveModel, selectedEffort]);
|
}, [
|
||||||
|
anthropicFastModeResolution?.resolvedFastMode,
|
||||||
|
anthropicRuntimeSelection?.defaultEffort,
|
||||||
|
effectiveModel,
|
||||||
|
selectedEffort,
|
||||||
|
selectedProviderId,
|
||||||
|
skipPermissions,
|
||||||
|
]);
|
||||||
|
|
||||||
const launchOptionalSummary = useMemo(() => {
|
const launchOptionalSummary = useMemo(() => {
|
||||||
const summary: string[] = [];
|
const summary: string[] = [];
|
||||||
if (prompt.trim()) summary.push('Lead prompt');
|
if (prompt.trim()) summary.push('Lead prompt');
|
||||||
if (skipPermissions) summary.push('Auto-approve tools');
|
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 (worktreeEnabled && worktreeName.trim()) summary.push(`Worktree: ${worktreeName.trim()}`);
|
||||||
if (customArgs.trim()) summary.push('Custom CLI args');
|
if (customArgs.trim()) summary.push('Custom CLI args');
|
||||||
return summary;
|
return summary;
|
||||||
}, [prompt, skipPermissions, worktreeEnabled, worktreeName, customArgs]);
|
}, [
|
||||||
|
anthropicProviderFastModeDefault,
|
||||||
|
customArgs,
|
||||||
|
prompt,
|
||||||
|
selectedFastMode,
|
||||||
|
selectedProviderId,
|
||||||
|
skipPermissions,
|
||||||
|
worktreeEnabled,
|
||||||
|
worktreeName,
|
||||||
|
]);
|
||||||
|
|
||||||
const teamDetailsSummary = useMemo(() => {
|
const teamDetailsSummary = useMemo(() => {
|
||||||
const summary: string[] = [];
|
const summary: string[] = [];
|
||||||
|
|
@ -1212,6 +1344,8 @@ export const CreateTeamDialog = ({
|
||||||
color: request.color,
|
color: request.color,
|
||||||
members: request.members,
|
members: request.members,
|
||||||
cwd: effectiveCwd || undefined,
|
cwd: effectiveCwd || undefined,
|
||||||
|
providerBackendId: request.providerBackendId,
|
||||||
|
fastMode: request.fastMode,
|
||||||
});
|
});
|
||||||
onOpenTeam(request.teamName, effectiveCwd || undefined);
|
onOpenTeam(request.teamName, effectiveCwd || undefined);
|
||||||
resetFormState();
|
resetFormState();
|
||||||
|
|
@ -1389,11 +1523,15 @@ export const CreateTeamDialog = ({
|
||||||
onProviderChange={setSelectedProviderId}
|
onProviderChange={setSelectedProviderId}
|
||||||
onModelChange={setSelectedModel}
|
onModelChange={setSelectedModel}
|
||||||
onEffortChange={setSelectedEffort}
|
onEffortChange={setSelectedEffort}
|
||||||
|
fastMode={selectedFastMode}
|
||||||
|
providerFastModeDefault={anthropicProviderFastModeDefault}
|
||||||
|
onFastModeChange={setSelectedFastMode}
|
||||||
onLimitContextChange={setLimitContext}
|
onLimitContextChange={setLimitContext}
|
||||||
syncModelsWithTeammates={syncModelsWithLead}
|
syncModelsWithTeammates={syncModelsWithLead}
|
||||||
onSyncModelsWithTeammatesChange={handleSyncModelsWithLeadChange}
|
onSyncModelsWithTeammatesChange={handleSyncModelsWithLeadChange}
|
||||||
disableGeminiOption={isGeminiUiFrozen()}
|
disableGeminiOption={isGeminiUiFrozen()}
|
||||||
leadModelIssueText={leadModelIssueText}
|
leadModelIssueText={leadModelIssueText}
|
||||||
|
leadFastModeNotice={anthropicRuntimeNotice}
|
||||||
memberModelIssueById={memberModelIssueById}
|
memberModelIssueById={memberModelIssueById}
|
||||||
headerTop={
|
headerTop={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export interface EffortLevelSelectorProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
providerId?: TeamProviderId;
|
providerId?: TeamProviderId;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
limitContext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
|
export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
|
||||||
|
|
@ -22,9 +23,12 @@ export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
|
||||||
id,
|
id,
|
||||||
providerId,
|
providerId,
|
||||||
model,
|
model,
|
||||||
|
limitContext,
|
||||||
}) => {
|
}) => {
|
||||||
const { providerStatus } = useEffectiveCliProviderStatus(providerId);
|
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 (
|
return (
|
||||||
<div className="mb-3">
|
<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
|
Controls how much reasoning the selected provider invests before responding. Default uses
|
||||||
the provider's standard behavior for the selected model.
|
the provider's standard behavior for the selected model.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reconcileAnthropicRuntimeSelections,
|
||||||
|
resolveAnthropicFastMode,
|
||||||
|
resolveAnthropicRuntimeSelection,
|
||||||
|
} from '@features/anthropic-runtime-profile/renderer';
|
||||||
import {
|
import {
|
||||||
mergeCodexCliStatusWithSnapshot,
|
mergeCodexCliStatusWithSnapshot,
|
||||||
useCodexAccountSnapshot,
|
useCodexAccountSnapshot,
|
||||||
|
|
@ -115,6 +120,7 @@ import type {
|
||||||
Schedule,
|
Schedule,
|
||||||
ScheduleLaunchConfig,
|
ScheduleLaunchConfig,
|
||||||
TeamCreateRequest,
|
TeamCreateRequest,
|
||||||
|
TeamFastMode,
|
||||||
TeamLaunchRequest,
|
TeamLaunchRequest,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
UpdateSchedulePatch,
|
UpdateSchedulePatch,
|
||||||
|
|
@ -216,6 +222,11 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
|
||||||
return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
|
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 {
|
function getProviderLabel(providerId: TeamProviderId): string {
|
||||||
return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic';
|
return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic';
|
||||||
}
|
}
|
||||||
|
|
@ -254,6 +265,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
const { open, onClose } = props;
|
const { open, onClose } = props;
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
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 cliStatus = useStore((s) => s.cliStatus);
|
||||||
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
|
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
|
||||||
const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus);
|
const bootstrapCliStatus = useStore((s) => s.bootstrapCliStatus);
|
||||||
|
|
@ -341,6 +355,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
const stored = localStorage.getItem('team:lastSelectedEffort');
|
const stored = localStorage.getItem('team:lastSelectedEffort');
|
||||||
return stored === null ? 'medium' : stored;
|
return stored === null ? 'medium' : stored;
|
||||||
});
|
});
|
||||||
|
const [selectedFastMode, setSelectedFastModeRaw] = useState<TeamFastMode>(getStoredTeamFastMode);
|
||||||
|
const [anthropicRuntimeNotice, setAnthropicRuntimeNotice] = useState<string | null>(null);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Launch-only state
|
// Launch-only state
|
||||||
|
|
@ -535,6 +551,11 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
localStorage.setItem('team:lastSelectedEffort', value);
|
localStorage.setItem('team:lastSelectedEffort', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSelectedFastMode = (value: TeamFastMode): void => {
|
||||||
|
setSelectedFastModeRaw(value);
|
||||||
|
localStorage.setItem('team:lastSelectedFastMode', value);
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// localStorage migration: schedule → team namespace (one-time)
|
// localStorage migration: schedule → team namespace (one-time)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -625,6 +646,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
);
|
);
|
||||||
setSkipPermissionsRaw(schedule.launchConfig.skipPermissions !== false);
|
setSkipPermissionsRaw(schedule.launchConfig.skipPermissions !== false);
|
||||||
setSelectedEffortRaw(schedule.launchConfig.effort ?? '');
|
setSelectedEffortRaw(schedule.launchConfig.effort ?? '');
|
||||||
|
setSelectedFastModeRaw(getStoredTeamFastMode());
|
||||||
} else {
|
} else {
|
||||||
// Create mode — reset to defaults
|
// Create mode — reset to defaults
|
||||||
setSchedLabel('');
|
setSchedLabel('');
|
||||||
|
|
@ -641,6 +663,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
setSelectedProviderIdRaw(storedProviderId);
|
setSelectedProviderIdRaw(storedProviderId);
|
||||||
setSelectedModelRaw(getStoredTeamModel(storedProviderId));
|
setSelectedModelRaw(getStoredTeamModel(storedProviderId));
|
||||||
setSelectedEffortRaw('medium');
|
setSelectedEffortRaw('medium');
|
||||||
|
setSelectedFastModeRaw(getStoredTeamFastMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
|
|
@ -690,6 +713,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
multimodelEnabled,
|
multimodelEnabled,
|
||||||
storedProviderId,
|
storedProviderId,
|
||||||
storedEffort: storedEffort === null ? 'medium' : storedEffort,
|
storedEffort: storedEffort === null ? 'medium' : storedEffort,
|
||||||
|
storedFastMode: getStoredTeamFastMode(),
|
||||||
storedLimitContext: localStorage.getItem('team:lastLimitContext') === 'true',
|
storedLimitContext: localStorage.getItem('team:lastLimitContext') === 'true',
|
||||||
getStoredModel: getStoredTeamModel,
|
getStoredModel: getStoredTeamModel,
|
||||||
});
|
});
|
||||||
|
|
@ -709,6 +733,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
setSelectedProviderIdRaw(launchPrefill.providerId);
|
setSelectedProviderIdRaw(launchPrefill.providerId);
|
||||||
setSelectedModelRaw(launchPrefill.model);
|
setSelectedModelRaw(launchPrefill.model);
|
||||||
setSelectedEffortRaw(launchPrefill.effort);
|
setSelectedEffortRaw(launchPrefill.effort);
|
||||||
|
setSelectedFastModeRaw(launchPrefill.fastMode);
|
||||||
setLimitContextRaw(launchPrefill.limitContext);
|
setLimitContextRaw(launchPrefill.limitContext);
|
||||||
setSkipPermissionsRaw(
|
setSkipPermissionsRaw(
|
||||||
savedRequest?.skipPermissions ??
|
savedRequest?.skipPermissions ??
|
||||||
|
|
@ -753,6 +778,85 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
) ?? '',
|
) ?? '',
|
||||||
[limitContext, runtimeProviderStatusById, selectedModel, selectedProviderId]
|
[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 selectedModelChecksByProvider = useMemo(() => {
|
||||||
const modelsByProvider = new Map<TeamProviderId, string[]>();
|
const modelsByProvider = new Map<TeamProviderId, string[]>();
|
||||||
const defaultSelectionByProvider = new Map<TeamProviderId, boolean>();
|
const defaultSelectionByProvider = new Map<TeamProviderId, boolean>();
|
||||||
|
|
@ -1237,17 +1341,30 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
runtimeProviderStatusById.get(selectedProviderId)
|
runtimeProviderStatusById.get(selectedProviderId)
|
||||||
);
|
);
|
||||||
if (model) args.push('--model', model);
|
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>');
|
if (!clearContext) args.push('--resume', '<previous>');
|
||||||
return args;
|
return args;
|
||||||
}, [
|
}, [
|
||||||
|
anthropicFastModeResolution?.resolvedFastMode,
|
||||||
|
anthropicRuntimeSelection?.defaultEffort,
|
||||||
isLaunchMode,
|
isLaunchMode,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
limitContext,
|
limitContext,
|
||||||
selectedEffort,
|
selectedEffort,
|
||||||
clearContext,
|
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
|
clearContext,
|
||||||
|
runtimeProviderStatusById,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const launchOptionalSummary = useMemo(() => {
|
const launchOptionalSummary = useMemo(() => {
|
||||||
|
|
@ -1258,6 +1375,11 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
summary.push(`Provider: ${getProviderLabel(selectedProviderId)}`);
|
summary.push(`Provider: ${getProviderLabel(selectedProviderId)}`);
|
||||||
if (selectedModel) summary.push(`Model: ${selectedModel}`);
|
if (selectedModel) summary.push(`Model: ${selectedModel}`);
|
||||||
if (selectedEffort) summary.push(`Effort: ${selectedEffort}`);
|
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 (selectedProviderId === 'anthropic' && limitContext) summary.push('Limited to 200K context');
|
||||||
if (skipPermissions) summary.push('Auto-approve tools');
|
if (skipPermissions) summary.push('Auto-approve tools');
|
||||||
if (clearContext) summary.push('Fresh session');
|
if (clearContext) summary.push('Fresh session');
|
||||||
|
|
@ -1270,6 +1392,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
selectedModel,
|
selectedModel,
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
selectedEffort,
|
selectedEffort,
|
||||||
|
selectedFastMode,
|
||||||
|
anthropicProviderFastModeDefault,
|
||||||
limitContext,
|
limitContext,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
clearContext,
|
clearContext,
|
||||||
|
|
@ -1478,6 +1602,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
runtimeProviderStatusById.get(selectedProviderId)
|
runtimeProviderStatusById.get(selectedProviderId)
|
||||||
),
|
),
|
||||||
effort: (selectedEffort as EffortLevel) || undefined,
|
effort: (selectedEffort as EffortLevel) || undefined,
|
||||||
|
fastMode: selectedProviderId === 'anthropic' ? selectedFastMode : undefined,
|
||||||
limitContext,
|
limitContext,
|
||||||
clearContext: clearContext || undefined,
|
clearContext: clearContext || undefined,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
|
|
@ -1869,14 +1994,18 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
providerId={selectedProviderId}
|
providerId={selectedProviderId}
|
||||||
model={selectedModel}
|
model={selectedModel}
|
||||||
effort={(selectedEffort as EffortLevel) || undefined}
|
effort={(selectedEffort as EffortLevel) || undefined}
|
||||||
|
fastMode={selectedFastMode}
|
||||||
|
providerFastModeDefault={anthropicProviderFastModeDefault}
|
||||||
limitContext={limitContext}
|
limitContext={limitContext}
|
||||||
onProviderChange={setSelectedProviderId}
|
onProviderChange={setSelectedProviderId}
|
||||||
onModelChange={setSelectedModel}
|
onModelChange={setSelectedModel}
|
||||||
onEffortChange={setSelectedEffort}
|
onEffortChange={setSelectedEffort}
|
||||||
|
onFastModeChange={setSelectedFastMode}
|
||||||
onLimitContextChange={setLimitContext}
|
onLimitContextChange={setLimitContext}
|
||||||
syncModelsWithTeammates={syncModelsWithLead}
|
syncModelsWithTeammates={syncModelsWithLead}
|
||||||
onSyncModelsWithTeammatesChange={setSyncModelsWithLead}
|
onSyncModelsWithTeammatesChange={setSyncModelsWithLead}
|
||||||
leadWarningText={leadRuntimeWarningText}
|
leadWarningText={leadRuntimeWarningText}
|
||||||
|
leadFastModeNotice={anthropicRuntimeNotice}
|
||||||
memberWarningById={memberRuntimeWarningById}
|
memberWarningById={memberRuntimeWarningById}
|
||||||
leadModelIssueText={leadModelIssueText}
|
leadModelIssueText={leadModelIssueText}
|
||||||
memberModelIssueById={memberModelIssueById}
|
memberModelIssueById={memberModelIssueById}
|
||||||
|
|
@ -2029,6 +2158,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
id="dialog-effort"
|
id="dialog-effort"
|
||||||
providerId={selectedProviderId}
|
providerId={selectedProviderId}
|
||||||
model={selectedModel}
|
model={selectedModel}
|
||||||
|
limitContext={false}
|
||||||
/>
|
/>
|
||||||
<SkipPermissionsCheckbox
|
<SkipPermissionsCheckbox
|
||||||
id="dialog-skip-permissions"
|
id="dialog-skip-permissions"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface PreviousLaunchParamsLike {
|
||||||
providerBackendId?: string;
|
providerBackendId?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
effort?: string;
|
effort?: string;
|
||||||
|
fastMode?: 'inherit' | 'on' | 'off';
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,6 +23,7 @@ interface LaunchDialogPrefillInput {
|
||||||
multimodelEnabled: boolean;
|
multimodelEnabled: boolean;
|
||||||
storedProviderId: TeamProviderId;
|
storedProviderId: TeamProviderId;
|
||||||
storedEffort: string;
|
storedEffort: string;
|
||||||
|
storedFastMode: 'inherit' | 'on' | 'off';
|
||||||
storedLimitContext: boolean;
|
storedLimitContext: boolean;
|
||||||
getStoredModel: (providerId: TeamProviderId) => string;
|
getStoredModel: (providerId: TeamProviderId) => string;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +33,7 @@ interface LaunchDialogPrefillResult {
|
||||||
providerBackendId?: string;
|
providerBackendId?: string;
|
||||||
model: string;
|
model: string;
|
||||||
effort: string;
|
effort: string;
|
||||||
|
fastMode: 'inherit' | 'on' | 'off';
|
||||||
limitContext: boolean;
|
limitContext: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +65,7 @@ export function resolveLaunchDialogPrefill({
|
||||||
multimodelEnabled,
|
multimodelEnabled,
|
||||||
storedProviderId,
|
storedProviderId,
|
||||||
storedEffort,
|
storedEffort,
|
||||||
|
storedFastMode,
|
||||||
storedLimitContext,
|
storedLimitContext,
|
||||||
getStoredModel,
|
getStoredModel,
|
||||||
}: LaunchDialogPrefillInput): LaunchDialogPrefillResult {
|
}: LaunchDialogPrefillInput): LaunchDialogPrefillResult {
|
||||||
|
|
@ -99,6 +103,8 @@ export function resolveLaunchDialogPrefill({
|
||||||
|
|
||||||
const effort =
|
const effort =
|
||||||
currentLead?.effort ?? savedRequest?.effort ?? previousLaunchParams?.effort ?? storedEffort;
|
currentLead?.effort ?? savedRequest?.effort ?? previousLaunchParams?.effort ?? storedEffort;
|
||||||
|
const fastMode =
|
||||||
|
savedRequest?.fastMode ?? previousLaunchParams?.fastMode ?? storedFastMode ?? 'inherit';
|
||||||
const limitContext =
|
const limitContext =
|
||||||
previousLaunchParams?.limitContext ?? savedRequest?.limitContext ?? storedLimitContext;
|
previousLaunchParams?.limitContext ?? savedRequest?.limitContext ?? storedLimitContext;
|
||||||
|
|
||||||
|
|
@ -113,6 +119,7 @@ export function resolveLaunchDialogPrefill({
|
||||||
? normalizeExplicitTeamModelForUi(providerId, matchingModel)
|
? normalizeExplicitTeamModelForUi(providerId, matchingModel)
|
||||||
: getStoredModel(providerId),
|
: getStoredModel(providerId),
|
||||||
effort,
|
effort,
|
||||||
|
fastMode,
|
||||||
limitContext,
|
limitContext,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { AnthropicFastModeSelector } from '@renderer/components/team/dialogs/AnthropicFastModeSelector';
|
||||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||||
import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector';
|
import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector';
|
||||||
import { LimitContextCheckbox } from '@renderer/components/team/dialogs/LimitContextCheckbox';
|
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 { Button } from '../../ui/button';
|
||||||
|
|
||||||
import type { EffortLevel, TeamProviderId } from '@shared/types';
|
import type { EffortLevel, TeamFastMode, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
interface LeadModelRowProps {
|
interface LeadModelRowProps {
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
model: string;
|
model: string;
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
|
providerFastModeDefault?: boolean;
|
||||||
limitContext: boolean;
|
limitContext: boolean;
|
||||||
onProviderChange: (providerId: TeamProviderId) => void;
|
onProviderChange: (providerId: TeamProviderId) => void;
|
||||||
onModelChange: (model: string) => void;
|
onModelChange: (model: string) => void;
|
||||||
onEffortChange: (effort: string) => void;
|
onEffortChange: (effort: string) => void;
|
||||||
|
onFastModeChange?: (fastMode: TeamFastMode) => void;
|
||||||
onLimitContextChange: (value: boolean) => void;
|
onLimitContextChange: (value: boolean) => void;
|
||||||
syncModelsWithTeammates: boolean;
|
syncModelsWithTeammates: boolean;
|
||||||
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
||||||
warningText?: string | null;
|
warningText?: string | null;
|
||||||
|
fastModeNotice?: string | null;
|
||||||
disableGeminiOption?: boolean;
|
disableGeminiOption?: boolean;
|
||||||
modelIssueText?: string | null;
|
modelIssueText?: string | null;
|
||||||
}
|
}
|
||||||
|
|
@ -42,14 +47,18 @@ export const LeadModelRow = ({
|
||||||
providerId,
|
providerId,
|
||||||
model,
|
model,
|
||||||
effort,
|
effort,
|
||||||
|
fastMode = 'inherit',
|
||||||
|
providerFastModeDefault = false,
|
||||||
limitContext,
|
limitContext,
|
||||||
onProviderChange,
|
onProviderChange,
|
||||||
onModelChange,
|
onModelChange,
|
||||||
onEffortChange,
|
onEffortChange,
|
||||||
|
onFastModeChange,
|
||||||
onLimitContextChange,
|
onLimitContextChange,
|
||||||
syncModelsWithTeammates,
|
syncModelsWithTeammates,
|
||||||
onSyncModelsWithTeammatesChange,
|
onSyncModelsWithTeammatesChange,
|
||||||
warningText,
|
warningText,
|
||||||
|
fastModeNotice,
|
||||||
disableGeminiOption = false,
|
disableGeminiOption = false,
|
||||||
modelIssueText,
|
modelIssueText,
|
||||||
}: LeadModelRowProps): React.JSX.Element => {
|
}: LeadModelRowProps): React.JSX.Element => {
|
||||||
|
|
@ -157,7 +166,18 @@ export const LeadModelRow = ({
|
||||||
id="lead-effort"
|
id="lead-effort"
|
||||||
providerId={providerId}
|
providerId={providerId}
|
||||||
model={model}
|
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' ? (
|
{providerId === 'anthropic' ? (
|
||||||
<LimitContextCheckbox
|
<LimitContextCheckbox
|
||||||
id="lead-limit-context"
|
id="lead-limit-context"
|
||||||
|
|
@ -166,6 +186,12 @@ export const LeadModelRow = ({
|
||||||
disabled={isAnthropicHaikuTeamModel(model)}
|
disabled={isAnthropicHaikuTeamModel(model)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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">
|
<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" />
|
<Info className="mt-0.5 size-3.5 shrink-0 text-sky-400" />
|
||||||
<p className="text-[11px] leading-relaxed text-sky-300">
|
<p className="text-[11px] leading-relaxed text-sky-300">
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ interface MemberDraftRowProps {
|
||||||
inheritedProviderId?: TeamProviderId;
|
inheritedProviderId?: TeamProviderId;
|
||||||
inheritedModel?: string;
|
inheritedModel?: string;
|
||||||
inheritedEffort?: EffortLevel;
|
inheritedEffort?: EffortLevel;
|
||||||
|
limitContext?: boolean;
|
||||||
draftKeyPrefix?: string;
|
draftKeyPrefix?: string;
|
||||||
projectPath?: string | null;
|
projectPath?: string | null;
|
||||||
mentionSuggestions?: MentionSuggestion[];
|
mentionSuggestions?: MentionSuggestion[];
|
||||||
|
|
@ -91,6 +92,7 @@ export const MemberDraftRow = ({
|
||||||
inheritedProviderId = 'anthropic',
|
inheritedProviderId = 'anthropic',
|
||||||
inheritedModel = '',
|
inheritedModel = '',
|
||||||
inheritedEffort,
|
inheritedEffort,
|
||||||
|
limitContext = false,
|
||||||
draftKeyPrefix,
|
draftKeyPrefix,
|
||||||
projectPath,
|
projectPath,
|
||||||
mentionSuggestions = [],
|
mentionSuggestions = [],
|
||||||
|
|
@ -448,6 +450,7 @@ export const MemberDraftRow = ({
|
||||||
id={`member-${member.id}-effort`}
|
id={`member-${member.id}-effort`}
|
||||||
providerId={effectiveProviderId}
|
providerId={effectiveProviderId}
|
||||||
model={effectiveModel}
|
model={effectiveModel}
|
||||||
|
limitContext={limitContext}
|
||||||
/>
|
/>
|
||||||
{lockProviderModel && (
|
{lockProviderModel && (
|
||||||
<p className="text-[11px] text-amber-300">
|
<p className="text-[11px] text-amber-300">
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,9 @@ function areLaunchParamsEquivalent(
|
||||||
left.providerId === right.providerId &&
|
left.providerId === right.providerId &&
|
||||||
left.providerBackendId === right.providerBackendId &&
|
left.providerBackendId === right.providerBackendId &&
|
||||||
left.model === right.model &&
|
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;
|
inheritedProviderId?: TeamProviderId;
|
||||||
inheritedModel?: string;
|
inheritedModel?: string;
|
||||||
inheritedEffort?: EffortLevel;
|
inheritedEffort?: EffortLevel;
|
||||||
|
limitContext?: boolean;
|
||||||
inheritModelSettingsByDefault?: boolean;
|
inheritModelSettingsByDefault?: boolean;
|
||||||
forceInheritedModelSettings?: boolean;
|
forceInheritedModelSettings?: boolean;
|
||||||
modelLockReason?: string;
|
modelLockReason?: string;
|
||||||
|
|
@ -134,6 +135,7 @@ export const MembersEditorSection = ({
|
||||||
inheritedProviderId,
|
inheritedProviderId,
|
||||||
inheritedModel,
|
inheritedModel,
|
||||||
inheritedEffort,
|
inheritedEffort,
|
||||||
|
limitContext = false,
|
||||||
inheritModelSettingsByDefault = false,
|
inheritModelSettingsByDefault = false,
|
||||||
forceInheritedModelSettings = false,
|
forceInheritedModelSettings = false,
|
||||||
modelLockReason,
|
modelLockReason,
|
||||||
|
|
@ -331,6 +333,7 @@ export const MembersEditorSection = ({
|
||||||
inheritedProviderId={inheritedProviderId}
|
inheritedProviderId={inheritedProviderId}
|
||||||
inheritedModel={inheritedModel}
|
inheritedModel={inheritedModel}
|
||||||
inheritedEffort={inheritedEffort}
|
inheritedEffort={inheritedEffort}
|
||||||
|
limitContext={limitContext}
|
||||||
forceInheritedModelSettings={forceInheritedModelSettings}
|
forceInheritedModelSettings={forceInheritedModelSettings}
|
||||||
draftKeyPrefix={draftKeyPrefix}
|
draftKeyPrefix={draftKeyPrefix}
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
|
|
@ -374,6 +377,7 @@ export const MembersEditorSection = ({
|
||||||
inheritedProviderId={inheritedProviderId}
|
inheritedProviderId={inheritedProviderId}
|
||||||
inheritedModel={inheritedModel}
|
inheritedModel={inheritedModel}
|
||||||
inheritedEffort={inheritedEffort}
|
inheritedEffort={inheritedEffort}
|
||||||
|
limitContext={limitContext}
|
||||||
forceInheritedModelSettings={forceInheritedModelSettings}
|
forceInheritedModelSettings={forceInheritedModelSettings}
|
||||||
draftKeyPrefix={draftKeyPrefix}
|
draftKeyPrefix={draftKeyPrefix}
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { MembersEditorSection } from './MembersEditorSection';
|
||||||
|
|
||||||
import type { MemberDraft } from './membersEditorTypes';
|
import type { MemberDraft } from './membersEditorTypes';
|
||||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
import type { EffortLevel, TeamProviderId } from '@shared/types';
|
import type { EffortLevel, TeamFastMode, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
interface TeamRosterEditorSectionProps {
|
interface TeamRosterEditorSectionProps {
|
||||||
members: MemberDraft[];
|
members: MemberDraft[];
|
||||||
|
|
@ -31,10 +31,13 @@ interface TeamRosterEditorSectionProps {
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
model: string;
|
model: string;
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
|
providerFastModeDefault?: boolean;
|
||||||
limitContext: boolean;
|
limitContext: boolean;
|
||||||
onProviderChange: (providerId: TeamProviderId) => void;
|
onProviderChange: (providerId: TeamProviderId) => void;
|
||||||
onModelChange: (model: string) => void;
|
onModelChange: (model: string) => void;
|
||||||
onEffortChange: (effort: string) => void;
|
onEffortChange: (effort: string) => void;
|
||||||
|
onFastModeChange?: (fastMode: TeamFastMode) => void;
|
||||||
onLimitContextChange: (value: boolean) => void;
|
onLimitContextChange: (value: boolean) => void;
|
||||||
syncModelsWithTeammates: boolean;
|
syncModelsWithTeammates: boolean;
|
||||||
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
||||||
|
|
@ -42,6 +45,7 @@ interface TeamRosterEditorSectionProps {
|
||||||
headerBottom?: React.ReactNode;
|
headerBottom?: React.ReactNode;
|
||||||
softDeleteMembers?: boolean;
|
softDeleteMembers?: boolean;
|
||||||
leadWarningText?: string | null;
|
leadWarningText?: string | null;
|
||||||
|
leadFastModeNotice?: string | null;
|
||||||
memberWarningById?: Record<string, string | null | undefined>;
|
memberWarningById?: Record<string, string | null | undefined>;
|
||||||
disableGeminiOption?: boolean;
|
disableGeminiOption?: boolean;
|
||||||
leadModelIssueText?: string | null;
|
leadModelIssueText?: string | null;
|
||||||
|
|
@ -72,10 +76,13 @@ export const TeamRosterEditorSection = ({
|
||||||
providerId,
|
providerId,
|
||||||
model,
|
model,
|
||||||
effort,
|
effort,
|
||||||
|
fastMode,
|
||||||
|
providerFastModeDefault,
|
||||||
limitContext,
|
limitContext,
|
||||||
onProviderChange,
|
onProviderChange,
|
||||||
onModelChange,
|
onModelChange,
|
||||||
onEffortChange,
|
onEffortChange,
|
||||||
|
onFastModeChange,
|
||||||
onLimitContextChange,
|
onLimitContextChange,
|
||||||
syncModelsWithTeammates,
|
syncModelsWithTeammates,
|
||||||
onSyncModelsWithTeammatesChange,
|
onSyncModelsWithTeammatesChange,
|
||||||
|
|
@ -83,6 +90,7 @@ export const TeamRosterEditorSection = ({
|
||||||
headerBottom,
|
headerBottom,
|
||||||
softDeleteMembers = false,
|
softDeleteMembers = false,
|
||||||
leadWarningText,
|
leadWarningText,
|
||||||
|
leadFastModeNotice,
|
||||||
memberWarningById,
|
memberWarningById,
|
||||||
disableGeminiOption = false,
|
disableGeminiOption = false,
|
||||||
leadModelIssueText,
|
leadModelIssueText,
|
||||||
|
|
@ -106,6 +114,7 @@ export const TeamRosterEditorSection = ({
|
||||||
inheritedProviderId={inheritedProviderId}
|
inheritedProviderId={inheritedProviderId}
|
||||||
inheritedModel={inheritedModel}
|
inheritedModel={inheritedModel}
|
||||||
inheritedEffort={inheritedEffort}
|
inheritedEffort={inheritedEffort}
|
||||||
|
limitContext={limitContext}
|
||||||
inheritModelSettingsByDefault={inheritModelSettingsByDefault}
|
inheritModelSettingsByDefault={inheritModelSettingsByDefault}
|
||||||
lockProviderModel={lockProviderModel}
|
lockProviderModel={lockProviderModel}
|
||||||
forceInheritedModelSettings={forceInheritedModelSettings}
|
forceInheritedModelSettings={forceInheritedModelSettings}
|
||||||
|
|
@ -120,14 +129,18 @@ export const TeamRosterEditorSection = ({
|
||||||
providerId={providerId}
|
providerId={providerId}
|
||||||
model={model}
|
model={model}
|
||||||
effort={effort}
|
effort={effort}
|
||||||
|
fastMode={fastMode}
|
||||||
|
providerFastModeDefault={providerFastModeDefault}
|
||||||
limitContext={limitContext}
|
limitContext={limitContext}
|
||||||
onProviderChange={onProviderChange}
|
onProviderChange={onProviderChange}
|
||||||
onModelChange={onModelChange}
|
onModelChange={onModelChange}
|
||||||
onEffortChange={onEffortChange}
|
onEffortChange={onEffortChange}
|
||||||
|
onFastModeChange={onFastModeChange}
|
||||||
onLimitContextChange={onLimitContextChange}
|
onLimitContextChange={onLimitContextChange}
|
||||||
syncModelsWithTeammates={syncModelsWithTeammates}
|
syncModelsWithTeammates={syncModelsWithTeammates}
|
||||||
onSyncModelsWithTeammatesChange={onSyncModelsWithTeammatesChange}
|
onSyncModelsWithTeammatesChange={onSyncModelsWithTeammatesChange}
|
||||||
warningText={leadWarningText}
|
warningText={leadWarningText}
|
||||||
|
fastModeNotice={leadFastModeNotice}
|
||||||
disableGeminiOption={disableGeminiOption}
|
disableGeminiOption={disableGeminiOption}
|
||||||
modelIssueText={leadModelIssueText}
|
modelIssueText={leadModelIssueText}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1526,6 +1526,7 @@ export interface TeamLaunchParams {
|
||||||
providerBackendId?: string;
|
providerBackendId?: string;
|
||||||
model?: string; // 'opus' | 'sonnet' | 'haiku'
|
model?: string; // 'opus' | 'sonnet' | 'haiku'
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
|
fastMode?: 'inherit' | 'on' | 'off';
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4426,6 +4427,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
providerBackendId: request.providerBackendId,
|
providerBackendId: request.providerBackendId,
|
||||||
model: baseModel || 'default',
|
model: baseModel || 'default',
|
||||||
effort: request.effort,
|
effort: request.effort,
|
||||||
|
fastMode: request.fastMode,
|
||||||
limitContext: request.limitContext ?? false,
|
limitContext: request.limitContext ?? false,
|
||||||
};
|
};
|
||||||
saveLaunchParams(request.teamName, params);
|
saveLaunchParams(request.teamName, params);
|
||||||
|
|
@ -4598,6 +4600,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
providerBackendId: request.providerBackendId,
|
providerBackendId: request.providerBackendId,
|
||||||
model: baseModel || 'default',
|
model: baseModel || 'default',
|
||||||
effort: request.effort,
|
effort: request.effort,
|
||||||
|
fastMode: request.fastMode,
|
||||||
limitContext: request.limitContext ?? false,
|
limitContext: request.limitContext ?? false,
|
||||||
};
|
};
|
||||||
saveLaunchParams(request.teamName, params);
|
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', {
|
const providerStatus = createProviderStatus('anthropic', {
|
||||||
id: 'opus',
|
id: 'opus[1m]',
|
||||||
launchModel: 'opus',
|
launchModel: 'opus[1m]',
|
||||||
displayName: 'Opus 4.7',
|
displayName: 'Opus 4.7 (1M)',
|
||||||
hidden: false,
|
hidden: true,
|
||||||
supportedReasoningEfforts: ['low', 'medium', 'high'],
|
supportedReasoningEfforts: [],
|
||||||
defaultReasoningEffort: null,
|
defaultReasoningEffort: null,
|
||||||
inputModalities: ['text', 'image'],
|
inputModalities: ['text', 'image'],
|
||||||
|
supportsFastMode: false,
|
||||||
supportsPersonality: false,
|
supportsPersonality: false,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
upgrade: false,
|
upgrade: false,
|
||||||
|
|
@ -110,11 +111,83 @@ describe('team effort options', () => {
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getTeamEffortOptions({ providerId: 'anthropic', model: 'opus', providerStatus })
|
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([
|
).toEqual([
|
||||||
{ value: '', label: 'Default' },
|
{ value: '', label: 'Default (Medium)' },
|
||||||
{ value: 'low', label: 'Low' },
|
{ value: 'low', label: 'Low' },
|
||||||
{ value: 'medium', label: 'Medium' },
|
{ value: 'medium', label: 'Medium' },
|
||||||
{ value: 'high', label: 'High' },
|
{ value: 'high', label: 'High' },
|
||||||
|
{ value: 'max', label: 'Max' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
getAvailableTeamProviderModelOptions,
|
getAvailableTeamProviderModelOptions,
|
||||||
getAvailableTeamProviderModels,
|
getAvailableTeamProviderModels,
|
||||||
getTeamModelSelectionError,
|
getTeamModelSelectionError,
|
||||||
|
isTeamModelAvailableForUi,
|
||||||
|
normalizeTeamModelForUi,
|
||||||
} from '../teamModelAvailability';
|
} from '../teamModelAvailability';
|
||||||
|
|
||||||
import type { CliProviderStatus } from '@shared/types';
|
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', () => {
|
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', () => {
|
it('uses app-server catalog models even when the static Codex list has not learned a new model yet', () => {
|
||||||
const providerStatus = createCodexProviderStatus(
|
const providerStatus = createCodexProviderStatus(
|
||||||
|
|
@ -158,4 +212,163 @@ describe('team model availability Codex catalog integration', () => {
|
||||||
|
|
||||||
expect(getAvailableTeamProviderModels('codex', providerStatus)).toEqual(['gpt-5.4']);
|
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';
|
import type { CliProviderStatus, EffortLevel, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
const BASE_EFFORT_OPTIONS = [{ value: '', label: 'Default' }] as const;
|
const BASE_EFFORT_OPTIONS = [{ value: '', label: 'Default' }] as const;
|
||||||
|
|
@ -9,6 +11,7 @@ export const TEAM_EFFORT_LABELS: Record<EffortLevel, string> = {
|
||||||
low: 'Low',
|
low: 'Low',
|
||||||
medium: 'Medium',
|
medium: 'Medium',
|
||||||
high: 'High',
|
high: 'High',
|
||||||
|
max: 'Max',
|
||||||
xhigh: 'XHigh',
|
xhigh: 'XHigh',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -59,6 +62,7 @@ function normalizeEfforts(
|
||||||
export function getTeamEffortOptions(params: {
|
export function getTeamEffortOptions(params: {
|
||||||
providerId?: TeamProviderId;
|
providerId?: TeamProviderId;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
limitContext?: boolean;
|
||||||
providerStatus?: CliProviderStatus | null;
|
providerStatus?: CliProviderStatus | null;
|
||||||
}): readonly TeamEffortOption[] {
|
}): readonly TeamEffortOption[] {
|
||||||
const providerId = params.providerId;
|
const providerId = params.providerId;
|
||||||
|
|
@ -66,6 +70,27 @@ export function getTeamEffortOptions(params: {
|
||||||
return BASE_EFFORT_OPTIONS;
|
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 runtimeCapability = params.providerStatus?.runtimeCapabilities?.reasoningEffort;
|
||||||
const catalogModel = getCatalogModel(providerId, params.providerStatus, params.model);
|
const catalogModel = getCatalogModel(providerId, params.providerStatus, params.model);
|
||||||
const catalogEfforts = catalogModel?.supportedReasoningEfforts ?? [];
|
const catalogEfforts = catalogModel?.supportedReasoningEfforts ?? [];
|
||||||
|
|
@ -82,16 +107,6 @@ export function getTeamEffortOptions(params: {
|
||||||
? `Default (${TEAM_EFFORT_LABELS[catalogModel.defaultReasoningEffort]})`
|
? `Default (${TEAM_EFFORT_LABELS[catalogModel.defaultReasoningEffort]})`
|
||||||
: 'Default';
|
: 'Default';
|
||||||
|
|
||||||
if (providerId === 'anthropic') {
|
|
||||||
return [
|
|
||||||
{ value: '', label: defaultLabel },
|
|
||||||
...efforts.map((effort) => ({
|
|
||||||
value: effort,
|
|
||||||
label: TEAM_EFFORT_LABELS[effort],
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
const fallbackEfforts =
|
const fallbackEfforts =
|
||||||
efforts.length > 0 ? efforts : (['low', 'medium', 'high'] as EffortLevel[]);
|
efforts.length > 0 ? efforts : (['low', 'medium', 'high'] as EffortLevel[]);
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,14 @@ export interface CliProviderModelAvailability {
|
||||||
checkedAt?: string | null;
|
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 =
|
export type CliProviderModelCatalogSource =
|
||||||
| 'anthropic-models-api'
|
| 'anthropic-models-api'
|
||||||
|
|
@ -132,6 +139,7 @@ export interface CliProviderModelCatalogItem {
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
supportedReasoningEfforts: CliProviderReasoningEffort[];
|
supportedReasoningEfforts: CliProviderReasoningEffort[];
|
||||||
defaultReasoningEffort: CliProviderReasoningEffort | null;
|
defaultReasoningEffort: CliProviderReasoningEffort | null;
|
||||||
|
supportsFastMode?: boolean;
|
||||||
inputModalities: string[];
|
inputModalities: string[];
|
||||||
supportsPersonality: boolean;
|
supportsPersonality: boolean;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
|
|
@ -169,6 +177,12 @@ export interface CliProviderRuntimeCapabilities {
|
||||||
values: CliProviderReasoningEffort[];
|
values: CliProviderReasoningEffort[];
|
||||||
configPassthrough?: boolean;
|
configPassthrough?: boolean;
|
||||||
};
|
};
|
||||||
|
fastMode?: {
|
||||||
|
supported: boolean;
|
||||||
|
available: boolean;
|
||||||
|
reason?: string | null;
|
||||||
|
source: 'runtime';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CliProviderStatus {
|
export interface CliProviderStatus {
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,7 @@ export interface AppConfig {
|
||||||
providerConnections: {
|
providerConnections: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
authMode: 'auto' | 'oauth' | 'api_key';
|
authMode: 'auto' | 'oauth' | 'api_key';
|
||||||
|
fastModeDefault: boolean;
|
||||||
};
|
};
|
||||||
codex: {
|
codex: {
|
||||||
preferredAuthMode: 'auto' | 'chatgpt' | 'api_key';
|
preferredAuthMode: 'auto' | 'chatgpt' | 'api_key';
|
||||||
|
|
|
||||||
|
|
@ -782,9 +782,10 @@ export interface TeamViewSnapshot {
|
||||||
isAlive?: boolean;
|
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 TeamProviderId = 'anthropic' | 'codex' | 'gemini';
|
||||||
export type TeamProviderBackendId = 'auto' | 'adapter' | 'api' | 'cli-sdk' | 'codex-native';
|
export type TeamProviderBackendId = 'auto' | 'adapter' | 'api' | 'cli-sdk' | 'codex-native';
|
||||||
|
export type TeamFastMode = 'inherit' | 'on' | 'off';
|
||||||
|
|
||||||
export interface ProviderModelLaunchIdentity {
|
export interface ProviderModelLaunchIdentity {
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
|
|
@ -802,6 +803,9 @@ export interface ProviderModelLaunchIdentity {
|
||||||
catalogFetchedAt: string | null;
|
catalogFetchedAt: string | null;
|
||||||
selectedEffort: EffortLevel | null;
|
selectedEffort: EffortLevel | null;
|
||||||
resolvedEffort: EffortLevel | null;
|
resolvedEffort: EffortLevel | null;
|
||||||
|
selectedFastMode?: TeamFastMode | null;
|
||||||
|
resolvedFastMode?: boolean | null;
|
||||||
|
fastResolutionReason?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamLaunchRequest {
|
export interface TeamLaunchRequest {
|
||||||
|
|
@ -812,6 +816,7 @@ export interface TeamLaunchRequest {
|
||||||
providerBackendId?: TeamProviderBackendId;
|
providerBackendId?: TeamProviderBackendId;
|
||||||
model?: string;
|
model?: string;
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
/** When true, context window is limited to 200K tokens instead of the default. */
|
/** When true, context window is limited to 200K tokens instead of the default. */
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
/** When true, skip --resume and start a fresh session (clears context memory). */
|
/** When true, skip --resume and start a fresh session (clears context memory). */
|
||||||
|
|
@ -949,6 +954,7 @@ export interface TeamAgentRuntimeSnapshot {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
runId: string | null;
|
runId: string | null;
|
||||||
providerBackendId?: TeamProviderBackendId;
|
providerBackendId?: TeamProviderBackendId;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
members: Record<string, TeamAgentRuntimeEntry>;
|
members: Record<string, TeamAgentRuntimeEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1061,6 +1067,7 @@ export interface TeamCreateRequest {
|
||||||
providerBackendId?: TeamProviderBackendId;
|
providerBackendId?: TeamProviderBackendId;
|
||||||
model?: string;
|
model?: string;
|
||||||
effort?: EffortLevel;
|
effort?: EffortLevel;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
/** When true, context window is limited to 200K tokens instead of the default. */
|
/** When true, context window is limited to 200K tokens instead of the default. */
|
||||||
limitContext?: boolean;
|
limitContext?: boolean;
|
||||||
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
|
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
|
||||||
|
|
@ -1079,6 +1086,7 @@ export interface TeamCreateConfigRequest {
|
||||||
members: TeamProvisioningMemberInput[];
|
members: TeamProvisioningMemberInput[];
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
providerBackendId?: TeamProviderBackendId;
|
providerBackendId?: TeamProviderBackendId;
|
||||||
|
fastMode?: TeamFastMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamCreateResponse {
|
export interface TeamCreateResponse {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export const PROTECTED_CLI_FLAGS = new Set([
|
||||||
'--effort',
|
'--effort',
|
||||||
'--teammate-mode',
|
'--teammate-mode',
|
||||||
'--resume',
|
'--resume',
|
||||||
|
'--settings',
|
||||||
'--permission-mode',
|
'--permission-mode',
|
||||||
'--permission-prompt-tool',
|
'--permission-prompt-tool',
|
||||||
'--dangerously-skip-permissions',
|
'--dangerously-skip-permissions',
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export const TEAM_EFFORT_LEVELS = [
|
||||||
'medium',
|
'medium',
|
||||||
'high',
|
'high',
|
||||||
'xhigh',
|
'xhigh',
|
||||||
|
'max',
|
||||||
] as const satisfies readonly EffortLevel[];
|
] as const satisfies readonly EffortLevel[];
|
||||||
|
|
||||||
export const LEGACY_TEAM_EFFORT_LEVELS = [
|
export const LEGACY_TEAM_EFFORT_LEVELS = [
|
||||||
|
|
@ -23,8 +24,16 @@ export const CODEX_TEAM_EFFORT_LEVELS = [
|
||||||
'xhigh',
|
'xhigh',
|
||||||
] as const satisfies readonly EffortLevel[];
|
] 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 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 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 {
|
export function isTeamEffortLevel(value: unknown): value is EffortLevel {
|
||||||
return typeof value === 'string' && TEAM_EFFORT_LEVELS.includes(value as 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);
|
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);
|
return LEGACY_TEAM_EFFORT_LEVEL_SET.has(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,5 +66,8 @@ export function formatEffortLevelListForProvider(providerId?: TeamProviderId | n
|
||||||
if (providerId === 'codex') {
|
if (providerId === 'codex') {
|
||||||
return CODEX_TEAM_EFFORT_LEVELS.join(', ');
|
return CODEX_TEAM_EFFORT_LEVELS.join(', ');
|
||||||
}
|
}
|
||||||
|
if (providerId === 'anthropic') {
|
||||||
|
return ANTHROPIC_TEAM_EFFORT_LEVELS.join(', ');
|
||||||
|
}
|
||||||
return LEGACY_TEAM_EFFORT_LEVELS.join(', ');
|
return LEGACY_TEAM_EFFORT_LEVELS.join(', ');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,24 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const RATE_LIMIT_SUBSTRING = "You've hit your limit";
|
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.
|
* Returns true if the message text contains the rate limit indicator.
|
||||||
*/
|
*/
|
||||||
export function isRateLimitMessage(text: string): boolean {
|
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").
|
// words like "reset" (e.g. "reset the 5pm meeting").
|
||||||
if (!isRateLimitMessage(text)) return null;
|
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);
|
const relative = parseRelativeResetDuration(text);
|
||||||
if (relative !== null) {
|
if (relative !== null) {
|
||||||
return new Date(now.getTime() + relative);
|
return new Date(now.getTime() + relative);
|
||||||
|
|
@ -120,6 +140,79 @@ function parseRelativeResetDuration(text: string): number | null {
|
||||||
return 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"
|
// 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 TEAM = 'test-team';
|
||||||
const RATE_LIMIT_MSG = "You've hit your limit. Resets in 5 minutes.";
|
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', () => {
|
describe('AutoResumeService', () => {
|
||||||
const mockConfig = { autoResumeOnRateLimit: false };
|
const mockConfig = { autoResumeOnRateLimit: false };
|
||||||
|
|
@ -60,6 +62,21 @@ describe('AutoResumeService', () => {
|
||||||
expect(provisioningService.sendMessageToTeam).not.toHaveBeenCalled();
|
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 () => {
|
it('reschedules when a later rate-limit message changes the reset time', async () => {
|
||||||
mockConfig.autoResumeOnRateLimit = true;
|
mockConfig.autoResumeOnRateLimit = true;
|
||||||
provisioningService.isTeamAlive.mockReturnValue(true);
|
provisioningService.isTeamAlive.mockReturnValue(true);
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ describe('TeamMemberRuntimeAdvisoryService', () => {
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['rate_limited', 'Provider returned 429 rate limit for this request.'],
|
['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.'],
|
['auth_error', 'Authentication failed due to invalid API key.'],
|
||||||
['network_error', 'Fetch failed because the network connection timed out.'],
|
['network_error', 'Fetch failed because the network connection timed out.'],
|
||||||
['provider_overloaded', 'Service unavailable: provider temporarily unavailable (503).'],
|
['provider_overloaded', 'Service unavailable: provider temporarily unavailable (503).'],
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -50,6 +51,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerBackendId: 'codex-native',
|
providerBackendId: 'codex-native',
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -81,6 +83,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -93,6 +96,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerBackendId: 'codex-native',
|
providerBackendId: 'codex-native',
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -110,6 +114,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -122,6 +127,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerBackendId: 'codex-native',
|
providerBackendId: 'codex-native',
|
||||||
model: 'gpt-5.3-codex',
|
model: 'gpt-5.3-codex',
|
||||||
effort: 'high',
|
effort: 'high',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -142,6 +148,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -154,6 +161,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerBackendId: 'codex-native',
|
providerBackendId: 'codex-native',
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -174,6 +182,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'codex',
|
storedProviderId: 'codex',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
codex: 'gpt-5.4',
|
codex: 'gpt-5.4',
|
||||||
|
|
@ -185,6 +194,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerBackendId: 'codex-native',
|
providerBackendId: 'codex-native',
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -207,6 +217,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -216,8 +227,10 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
|
providerBackendId: undefined,
|
||||||
model: 'haiku',
|
model: 'haiku',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -235,6 +248,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -243,8 +257,10 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
|
providerBackendId: undefined,
|
||||||
model: 'opus',
|
model: 'opus',
|
||||||
effort: 'high',
|
effort: 'high',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: true,
|
limitContext: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -261,6 +277,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -273,6 +290,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerBackendId: 'codex-native',
|
providerBackendId: 'codex-native',
|
||||||
model: 'custom-model[1m]',
|
model: 'custom-model[1m]',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -289,6 +307,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
multimodelEnabled: true,
|
multimodelEnabled: true,
|
||||||
storedProviderId: 'anthropic',
|
storedProviderId: 'anthropic',
|
||||||
storedEffort: 'medium',
|
storedEffort: 'medium',
|
||||||
|
storedFastMode: 'inherit',
|
||||||
storedLimitContext: false,
|
storedLimitContext: false,
|
||||||
getStoredModel: createStoredModelGetter({
|
getStoredModel: createStoredModelGetter({
|
||||||
anthropic: 'haiku',
|
anthropic: 'haiku',
|
||||||
|
|
@ -301,6 +320,7 @@ describe('resolveLaunchDialogPrefill', () => {
|
||||||
providerBackendId: 'codex-native',
|
providerBackendId: 'codex-native',
|
||||||
model: 'custom-model[1m]',
|
model: 'custom-model[1m]',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
|
fastMode: 'inherit',
|
||||||
limitContext: false,
|
limitContext: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,7 @@ function makeAppConfig(multimodelEnabled: boolean): AppConfig {
|
||||||
providerConnections: {
|
providerConnections: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
authMode: 'auto',
|
authMode: 'auto',
|
||||||
|
fastModeDefault: false,
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
preferredAuthMode: 'auto',
|
preferredAuthMode: 'auto',
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ import {
|
||||||
// Helper: every production rate-limit message starts with this substring.
|
// Helper: every production rate-limit message starts with this substring.
|
||||||
// Prefix test inputs so they clear the parser's rate-limit-context gate.
|
// Prefix test inputs so they clear the parser's rate-limit-context gate.
|
||||||
const RL = "You've hit your limit. ";
|
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', () => {
|
describe('isRateLimitMessage', () => {
|
||||||
it('detects the canonical substring', () => {
|
it('detects the canonical substring', () => {
|
||||||
|
|
@ -22,6 +26,10 @@ describe('isRateLimitMessage', () => {
|
||||||
expect(isRateLimitMessage('hit the limit')).toBe(false); // missing "You've"
|
expect(isRateLimitMessage('hit the limit')).toBe(false); // missing "You've"
|
||||||
expect(isRateLimitMessage('')).toBe(false);
|
expect(isRateLimitMessage('')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('detects structured model_cooldown API errors as rate limits', () => {
|
||||||
|
expect(isRateLimitMessage(MODEL_COOLDOWN_API_ERROR)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseRateLimitResetTime', () => {
|
describe('parseRateLimitResetTime', () => {
|
||||||
|
|
@ -41,6 +49,18 @@ describe('parseRateLimitResetTime', () => {
|
||||||
expect(parseRateLimitResetTime('Resets in 2 hours.', now)).toBeNull();
|
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
|
// Relative durations
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue