fix(validate): normalize provider backend ids

This commit is contained in:
777genius 2026-04-20 22:06:35 +03:00
parent 66409ca56b
commit 98a9c25cfe
7 changed files with 100 additions and 48 deletions

View file

@ -1,6 +1,7 @@
import { validateTeamName } from '@main/ipc/guards'; import { validateTeamName } from '@main/ipc/guards';
import { getErrorMessage } from '@shared/utils/errorHandling'; import { getErrorMessage } from '@shared/utils/errorHandling';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isAbsolute } from 'path'; import { isAbsolute } from 'path';
import type { HttpServices } from './index'; import type { HttpServices } from './index';
@ -100,7 +101,13 @@ function parseLaunchRequest(teamName: string, body: unknown): TeamLaunchRequest
throw new HttpBadRequestError('providerId must be anthropic, codex, or gemini'); throw new HttpBadRequestError('providerId must be anthropic, codex, or gemini');
})(); })();
const prompt = assertOptionalString(payload.prompt, 'prompt'); const prompt = assertOptionalString(payload.prompt, 'prompt');
const providerBackendId = assertOptionalString(payload.providerBackendId, 'providerBackendId'); const rawProviderBackendId = assertOptionalString(payload.providerBackendId, 'providerBackendId');
const providerBackendId = migrateProviderBackendId(providerId, rawProviderBackendId);
if (rawProviderBackendId && !providerBackendId) {
throw new HttpBadRequestError(
'providerBackendId must be one of auto, adapter, api, cli-sdk, or codex-native'
);
}
const model = assertOptionalString(payload.model, 'model'); const model = assertOptionalString(payload.model, 'model');
const effort = assertOptionalEffort(payload.effort); const effort = assertOptionalEffort(payload.effort);
const clearContext = assertOptionalBoolean(payload.clearContext, 'clearContext'); const clearContext = assertOptionalBoolean(payload.clearContext, 'clearContext');

View file

@ -91,7 +91,7 @@ import {
PROTECTED_CLI_FLAGS, PROTECTED_CLI_FLAGS,
} from '@shared/utils/cliArgsParser'; } from '@shared/utils/cliArgsParser';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { isTeamProviderBackendId, migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isRateLimitMessage } from '@shared/utils/rateLimitDetector'; import { isRateLimitMessage } from '@shared/utils/rateLimitDetector';
import { import {
buildStandaloneSlashCommandMeta, buildStandaloneSlashCommandMeta,
@ -186,6 +186,8 @@ import type {
TeamLaunchResponse, TeamLaunchResponse,
TeamMemberActivityMeta, TeamMemberActivityMeta,
TeamMessageNotificationData, TeamMessageNotificationData,
TeamProviderBackendId,
TeamProviderId,
TeamProvisioningPrepareResult, TeamProvisioningPrepareResult,
TeamProvisioningProgress, TeamProvisioningProgress,
TeamSummary, TeamSummary,
@ -1131,8 +1133,9 @@ function parseOptionalMemberProviderId(
} }
function parseOptionalProviderBackendId( function parseOptionalProviderBackendId(
value: unknown value: unknown,
): { valid: true; value: string | undefined } | { valid: false; error: string } { providerId?: TeamProviderId
): { valid: true; value: TeamProviderBackendId | undefined } | { valid: false; error: string } {
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
return { valid: true, value: undefined }; return { valid: true, value: undefined };
} }
@ -1146,7 +1149,19 @@ function parseOptionalProviderBackendId(
if (trimmed.length > 64) { if (trimmed.length > 64) {
return { valid: false, error: 'providerBackendId too long (max 64)' }; return { valid: false, error: 'providerBackendId too long (max 64)' };
} }
return { valid: true, value: trimmed }; if (providerId) {
const migratedBackendId = migrateProviderBackendId(providerId, trimmed);
if (migratedBackendId) {
return { valid: true, value: migratedBackendId };
}
} else if (isTeamProviderBackendId(trimmed)) {
return { valid: true, value: trimmed };
}
return {
valid: false,
error: 'providerBackendId must be one of auto, adapter, api, cli-sdk, or codex-native',
};
} }
function parseOptionalMemberEffort( function parseOptionalMemberEffort(
@ -1242,7 +1257,16 @@ async function validateProvisioningRequest(
if (payload.prompt !== undefined && typeof payload.prompt !== 'string') { if (payload.prompt !== undefined && typeof payload.prompt !== 'string') {
return { valid: false, error: 'prompt must be a string' }; return { valid: false, error: 'prompt must be a string' };
} }
const providerBackendValidation = parseOptionalProviderBackendId(payload.providerBackendId); const providerId =
payload.providerId === 'codex'
? 'codex'
: payload.providerId === 'gemini'
? 'gemini'
: 'anthropic';
const providerBackendValidation = parseOptionalProviderBackendId(
payload.providerBackendId,
providerId
);
if (!providerBackendValidation.valid) { if (!providerBackendValidation.valid) {
return { valid: false, error: providerBackendValidation.error }; return { valid: false, error: providerBackendValidation.error };
} }
@ -1297,12 +1321,7 @@ async function validateProvisioningRequest(
members, members,
cwd, cwd,
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined, prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
providerId: providerId,
payload.providerId === 'codex'
? 'codex'
: payload.providerId === 'gemini'
? 'gemini'
: 'anthropic',
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: isValidEffort(payload.effort) ? payload.effort : undefined, effort: isValidEffort(payload.effort) ? payload.effort : undefined,
@ -1413,7 +1432,16 @@ async function handleLaunchTeam(
if (payload.model !== undefined && typeof payload.model !== 'string') { if (payload.model !== undefined && typeof payload.model !== 'string') {
return { success: false, error: 'model must be a string' }; return { success: false, error: 'model must be a string' };
} }
const providerBackendValidation = parseOptionalProviderBackendId(payload.providerBackendId); const providerId =
payload.providerId === 'codex'
? 'codex'
: payload.providerId === 'gemini'
? 'gemini'
: 'anthropic';
const providerBackendValidation = parseOptionalProviderBackendId(
payload.providerBackendId,
providerId
);
if (!providerBackendValidation.valid) { if (!providerBackendValidation.valid) {
return { success: false, error: providerBackendValidation.error }; return { success: false, error: providerBackendValidation.error };
} }
@ -1439,15 +1467,13 @@ async function handleLaunchTeam(
const members = membersMeta?.members ?? []; const members = membersMeta?.members ?? [];
const resolvedProviderId = const resolvedProviderId =
payload.providerId === 'codex' providerId === 'codex' || providerId === 'gemini'
? 'codex' ? providerId
: payload.providerId === 'gemini' : meta?.providerId === 'codex'
? 'gemini' ? 'codex'
: meta?.providerId === 'codex' : meta?.providerId === 'gemini'
? 'codex' ? 'gemini'
: meta?.providerId === 'gemini' : 'anthropic';
? 'gemini'
: 'anthropic';
const createRequest: TeamCreateRequest = { const createRequest: TeamCreateRequest = {
teamName: tn, teamName: tn,
@ -1501,12 +1527,7 @@ 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,
payload.providerId === 'codex'
? 'codex'
: payload.providerId === 'gemini'
? 'gemini'
: 'anthropic',
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: isValidEffort(payload.effort) ? payload.effort : undefined, effort: isValidEffort(payload.effort) ? payload.effort : undefined,

View file

@ -38,10 +38,9 @@ import { getMemberColorByName } from '@shared/constants/memberColors';
import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team'; import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team';
import { resolveLanguageName } from '@shared/utils/agentLanguage'; import { resolveLanguageName } from '@shared/utils/agentLanguage';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults'; import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
import { getErrorMessage } from '@shared/utils/errorHandling';
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
import { parseCliArgs } from '@shared/utils/cliArgsParser'; import { parseCliArgs } from '@shared/utils/cliArgsParser';
import { deriveContextMetrics, inferContextWindowTokens } from '@shared/utils/contextMetrics'; import { deriveContextMetrics, inferContextWindowTokens } from '@shared/utils/contextMetrics';
import { getErrorMessage } from '@shared/utils/errorHandling';
import { import {
isInboxNoiseMessage, isInboxNoiseMessage,
isMeaningfulBootstrapCheckInMessage, isMeaningfulBootstrapCheckInMessage,
@ -57,6 +56,7 @@ import {
parseAllTeammateMessages, parseAllTeammateMessages,
type ParsedTeammateContent, type ParsedTeammateContent,
} from '@shared/utils/teammateMessageParser'; } from '@shared/utils/teammateMessageParser';
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName'; import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import { import {
@ -4540,7 +4540,10 @@ export class TeamProvisioningService {
teamName, teamName,
updatedAt, updatedAt,
runId: run?.runId ?? null, runId: run?.runId ?? null,
providerBackendId: run?.request.providerBackendId ?? persistedTeamMeta?.providerBackendId, providerBackendId: migrateProviderBackendId(
run?.request.providerId ?? persistedTeamMeta?.providerId,
run?.request.providerBackendId ?? persistedTeamMeta?.providerBackendId
),
members: snapshotMembers, members: snapshotMembers,
}; };

View file

@ -50,14 +50,15 @@ import {
normalizeCreateLaunchProviderForUi, normalizeCreateLaunchProviderForUi,
} from '@renderer/utils/geminiUiFreeze'; } from '@renderer/utils/geminiUiFreeze';
import { normalizePath } from '@renderer/utils/pathNormalize'; import { normalizePath } from '@renderer/utils/pathNormalize';
import { nameColorSet } from '@renderer/utils/projectColor';
import { resolveUiOwnedProviderBackendId } from '@renderer/utils/providerBackendIdentity'; import { resolveUiOwnedProviderBackendId } from '@renderer/utils/providerBackendIdentity';
import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus'; import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
import { nameColorSet } from '@renderer/utils/projectColor';
import { import {
getTeamModelSelectionError, getTeamModelSelectionError,
normalizeExplicitTeamModelForUi, normalizeExplicitTeamModelForUi,
} from '@renderer/utils/teamModelAvailability'; } from '@renderer/utils/teamModelAvailability';
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog'; import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection'; import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import { import {
@ -1454,8 +1455,10 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
selectedProviderId, selectedProviderId,
runtimeProviderStatusById.get(selectedProviderId) runtimeProviderStatusById.get(selectedProviderId)
) ?? ) ??
previousLaunchParams?.providerBackendId ?? migrateProviderBackendId(
savedLaunchProviderBackendId ?? selectedProviderId,
previousLaunchParams?.providerBackendId ?? savedLaunchProviderBackendId
) ??
undefined, undefined,
model: computeEffectiveTeamModel(selectedModel, limitContext, selectedProviderId), model: computeEffectiveTeamModel(selectedModel, limitContext, selectedProviderId),
effort: (selectedEffort as EffortLevel) || undefined, effort: (selectedEffort as EffortLevel) || undefined,

View file

@ -1,10 +1,11 @@
import { import {
formatProviderBackendLabel, formatProviderBackendLabel,
getDefaultProviderBackendId, getDefaultProviderBackendId,
isTeamProviderBackendId,
migrateProviderBackendId, migrateProviderBackendId,
} from '@shared/utils/providerBackend'; } from '@shared/utils/providerBackend';
import type { CliProviderStatus, TeamProviderId } from '@shared/types'; import type { CliProviderStatus, TeamProviderBackendId, TeamProviderId } from '@shared/types';
function normalizeOptionalBackendId(value: string | null | undefined): string | undefined { function normalizeOptionalBackendId(value: string | null | undefined): string | undefined {
const trimmed = value?.trim(); const trimmed = value?.trim();
@ -15,14 +16,17 @@ export { formatProviderBackendLabel, getDefaultProviderBackendId };
export function resolveEffectiveProviderBackendId( export function resolveEffectiveProviderBackendId(
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
): string | undefined { ): TeamProviderBackendId | undefined {
return normalizeOptionalBackendId(provider?.resolvedBackendId ?? provider?.selectedBackendId); const backendId = normalizeOptionalBackendId(
provider?.resolvedBackendId ?? provider?.selectedBackendId
);
return isTeamProviderBackendId(backendId) ? backendId : undefined;
} }
export function resolveUiOwnedProviderBackendId( export function resolveUiOwnedProviderBackendId(
providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined, providerId: TeamProviderId | CliProviderStatus['providerId'] | undefined,
provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined provider: Pick<CliProviderStatus, 'selectedBackendId' | 'resolvedBackendId'> | null | undefined
): string | undefined { ): TeamProviderBackendId | undefined {
return migrateProviderBackendId( return migrateProviderBackendId(
providerId, providerId,
provider?.selectedBackendId ?? provider?.resolvedBackendId provider?.selectedBackendId ?? provider?.resolvedBackendId

View file

@ -784,7 +784,7 @@ export interface TeamViewSnapshot {
export type EffortLevel = 'low' | 'medium' | 'high'; export type EffortLevel = 'low' | 'medium' | 'high';
export type TeamProviderId = 'anthropic' | 'codex' | 'gemini'; export type TeamProviderId = 'anthropic' | 'codex' | 'gemini';
export type TeamProviderBackendId = string; export type TeamProviderBackendId = 'auto' | 'adapter' | 'api' | 'cli-sdk' | 'codex-native';
export interface TeamLaunchRequest { export interface TeamLaunchRequest {
teamName: string; teamName: string;

View file

@ -1,6 +1,12 @@
import type { TeamProviderId } from '@shared/types'; import type { TeamProviderBackendId, TeamProviderId } from '@shared/types';
type RuntimeProviderId = TeamProviderId; const TEAM_PROVIDER_BACKEND_IDS = new Set<TeamProviderBackendId>([
'auto',
'adapter',
'api',
'cli-sdk',
'codex-native',
]);
function normalizeOptionalBackendId(value: unknown): string | undefined { function normalizeOptionalBackendId(value: unknown): string | undefined {
if (typeof value !== 'string') { if (typeof value !== 'string') {
@ -11,8 +17,8 @@ function normalizeOptionalBackendId(value: unknown): string | undefined {
} }
export function getDefaultProviderBackendId( export function getDefaultProviderBackendId(
providerId: TeamProviderId | RuntimeProviderId | undefined providerId: TeamProviderId | undefined
): string | undefined { ): TeamProviderBackendId | undefined {
return providerId === 'codex' ? 'codex-native' : undefined; return providerId === 'codex' ? 'codex-native' : undefined;
} }
@ -27,20 +33,28 @@ export function isLegacyCodexProviderBackendId(
); );
} }
export function migrateProviderBackendId( export function isTeamProviderBackendId(
providerId: TeamProviderId | RuntimeProviderId | undefined,
providerBackendId: string | null | undefined providerBackendId: string | null | undefined
): string | undefined { ): providerBackendId is TeamProviderBackendId {
return (
!!providerBackendId && TEAM_PROVIDER_BACKEND_IDS.has(providerBackendId as TeamProviderBackendId)
);
}
export function migrateProviderBackendId(
providerId: TeamProviderId | undefined,
providerBackendId: string | null | undefined
): TeamProviderBackendId | undefined {
const normalizedBackendId = normalizeOptionalBackendId(providerBackendId); const normalizedBackendId = normalizeOptionalBackendId(providerBackendId);
if (providerId !== 'codex') { if (providerId !== 'codex') {
return normalizedBackendId; return isTeamProviderBackendId(normalizedBackendId) ? normalizedBackendId : undefined;
} }
if (!normalizedBackendId || isLegacyCodexProviderBackendId(normalizedBackendId)) { if (!normalizedBackendId || isLegacyCodexProviderBackendId(normalizedBackendId)) {
return 'codex-native'; return 'codex-native';
} }
return normalizedBackendId; return isTeamProviderBackendId(normalizedBackendId) ? normalizedBackendId : undefined;
} }
export function formatProviderBackendLabel( export function formatProviderBackendLabel(