agent-ecosystem/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts
2026-04-17 22:45:19 +03:00

376 lines
11 KiB
TypeScript

import { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog';
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import type { TeamProviderId, TeamProvisioningPrepareResult } from '@shared/types';
export type ProviderPrepareCheckStatus = 'ready' | 'notes' | 'failed';
type PrepareProvisioningFn = (
cwd?: string,
providerId?: TeamProviderId,
providerIds?: TeamProviderId[],
selectedModels?: string[],
limitContext?: boolean
) => Promise<TeamProvisioningPrepareResult>;
interface ProviderPrepareDiagnosticsProgress {
details: string[];
completedCount: number;
totalCount: number;
}
export interface ProviderPrepareDiagnosticsModelResult {
status: 'ready' | 'notes' | 'failed';
line: string;
warningLine?: string | null;
}
export interface ProviderPrepareDiagnosticsCachedSnapshot {
status: ProviderPrepareCheckStatus | 'checking';
details: string[];
completedCount: number;
totalCount: number;
}
export interface ProviderPrepareDiagnosticsResult {
status: ProviderPrepareCheckStatus;
details: string[];
warnings: string[];
modelResultsById: Record<string, ProviderPrepareDiagnosticsModelResult>;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getModelLabel(providerId: TeamProviderId, modelId: string): string {
if (isDefaultProviderModelSelection(modelId)) {
return 'Default';
}
return getProviderScopedTeamModelLabel(providerId, modelId) ?? modelId;
}
export function buildProviderPrepareModelCheckingLine(
providerId: TeamProviderId,
modelId: string
): string {
return `${getModelLabel(providerId, modelId)} - checking...`;
}
function buildModelSuccessLine(providerId: TeamProviderId, modelId: string): string {
return `${getModelLabel(providerId, modelId)} - verified`;
}
export function getProviderPrepareCachedSnapshot({
providerId,
selectedModelIds,
cachedModelResultsById,
}: {
providerId: TeamProviderId;
selectedModelIds: string[];
cachedModelResultsById?: Record<string, ProviderPrepareDiagnosticsModelResult>;
}): ProviderPrepareDiagnosticsCachedSnapshot {
const reusableModelResultsById = cachedModelResultsById ?? {};
const orderedModelIds = Array.from(
new Set(selectedModelIds.map((modelId) => modelId.trim()).filter(Boolean))
);
let completedCount = 0;
let hasFailure = false;
let hasNotes = false;
let hasChecking = false;
const details = orderedModelIds.map((modelId) => {
const cachedResult = reusableModelResultsById[modelId];
if (!cachedResult) {
hasChecking = true;
return buildProviderPrepareModelCheckingLine(providerId, modelId);
}
completedCount += 1;
if (cachedResult.status === 'failed') {
hasFailure = true;
} else if (cachedResult.status === 'notes') {
hasNotes = true;
}
return cachedResult.line;
});
return {
status: hasChecking ? 'checking' : hasFailure ? 'failed' : hasNotes ? 'notes' : 'ready',
details,
completedCount,
totalCount: orderedModelIds.length,
};
}
function stripSelectedModelPrefix(modelId: string, message: string): string {
const trimmed = message.trim();
if (!trimmed) {
return trimmed;
}
const patterns = [
new RegExp(`^Selected model ${escapeRegExp(modelId)} is unavailable\\.\\s*`, 'i'),
new RegExp(`^Selected model ${escapeRegExp(modelId)} could not be verified\\.\\s*`, 'i'),
new RegExp(`^Selected model ${escapeRegExp(modelId)} verified for launch\\.\\s*`, 'i'),
];
for (const pattern of patterns) {
if (pattern.test(trimmed)) {
return trimmed.replace(pattern, '').trim();
}
}
return trimmed;
}
function decodeQuotedJsonString(value: string): string {
try {
return JSON.parse(`"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`) as string;
} catch {
return value;
}
}
function normalizeModelReason(rawReason: string | null | undefined): string | null {
const trimmed = rawReason?.trim() ?? '';
if (!trimmed) {
return null;
}
if (
/The '[^']+' model is not supported when using Codex with a ChatGPT account\./i.test(trimmed)
) {
return 'Not available with Codex ChatGPT subscription';
}
if (/The requested model is not available for your account\./i.test(trimmed)) {
return 'Not available for this account';
}
if (
trimmed.toLowerCase().includes('timeout running:') ||
trimmed.toLowerCase().includes('timed out') ||
trimmed.toLowerCase().includes('etimedout')
) {
return 'Model verification timed out';
}
const detailMatch = /"detail":"((?:\\"|[^"])*)"/i.exec(trimmed);
if (detailMatch?.[1]) {
return normalizeModelReason(detailMatch[1].replace(/\\"/g, '"').trim());
}
const messageMatch = /"message":"((?:\\"|[^"])*)"/i.exec(trimmed);
if (messageMatch?.[1]) {
const decodedMessage = messageMatch[1].replace(/\\"/g, '"');
const nestedDetailMatch = /"detail":"([^"]+)"/i.exec(decodedMessage);
if (nestedDetailMatch?.[1]) {
return normalizeModelReason(nestedDetailMatch[1].trim());
}
return normalizeModelReason(decodeQuotedJsonString(decodedMessage).trim());
}
return trimmed;
}
function getResultReason(modelId: string, result: TeamProvisioningPrepareResult): string | null {
const candidates = [...(result.details ?? []), ...(result.warnings ?? []), result.message]
.map((entry) => entry?.trim() ?? '')
.filter(Boolean);
for (const candidate of candidates) {
const stripped = stripSelectedModelPrefix(modelId, candidate);
if (stripped) {
return normalizeModelReason(stripped);
}
}
return null;
}
function buildModelFailureLine(
providerId: TeamProviderId,
modelId: string,
kind: 'unavailable' | 'check failed',
reason: string | null
): string {
const label = getModelLabel(providerId, modelId);
return reason ? `${label} - ${kind} - ${reason}` : `${label} - ${kind}`;
}
function createRuntimeDetailLines(result: TeamProvisioningPrepareResult): string[] {
return [...(result.details ?? []), ...(result.warnings ?? [])];
}
export async function runProviderPrepareDiagnostics({
cwd,
providerId,
selectedModelIds,
prepareProvisioning,
limitContext,
onModelProgress,
cachedModelResultsById,
}: {
cwd: string;
providerId: TeamProviderId;
selectedModelIds: string[];
prepareProvisioning: PrepareProvisioningFn;
limitContext?: boolean;
onModelProgress?: (progress: ProviderPrepareDiagnosticsProgress) => void;
cachedModelResultsById?: Record<string, ProviderPrepareDiagnosticsModelResult>;
}): Promise<ProviderPrepareDiagnosticsResult> {
const runtimeResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
undefined,
limitContext
);
const runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
const runtimeWarnings = [...(runtimeResult.warnings ?? [])];
if (!runtimeResult.ready) {
return {
status: 'failed',
details: [...runtimeDetailLines, ...(runtimeResult.message ? [runtimeResult.message] : [])],
warnings: runtimeWarnings,
modelResultsById: {},
};
}
if (selectedModelIds.length === 0) {
return {
status: runtimeWarnings.length > 0 ? 'notes' : 'ready',
details: runtimeDetailLines,
warnings: runtimeWarnings,
modelResultsById: {},
};
}
const orderedModelIds = Array.from(
new Set(selectedModelIds.map((modelId) => modelId.trim()).filter(Boolean))
);
const reusableModelResultsById = cachedModelResultsById ?? {};
const modelResultsById = new Map<string, ProviderPrepareDiagnosticsModelResult>();
const modelLines = new Map<string, string>();
let completedCount = 0;
let hasFailure = false;
let hasNotes = runtimeWarnings.length > 0;
const modelWarnings: string[] = [];
for (const modelId of orderedModelIds) {
const cachedResult = reusableModelResultsById[modelId];
if (cachedResult) {
modelResultsById.set(modelId, cachedResult);
modelLines.set(modelId, cachedResult.line);
completedCount += 1;
if (cachedResult.status === 'failed') {
hasFailure = true;
} else if (cachedResult.status === 'notes') {
hasNotes = true;
}
if (cachedResult.warningLine) {
modelWarnings.push(cachedResult.warningLine);
}
continue;
}
modelLines.set(modelId, buildProviderPrepareModelCheckingLine(providerId, modelId));
}
const emitProgress = (): void => {
onModelProgress?.({
details: [
...runtimeDetailLines,
...orderedModelIds.map((modelId) => modelLines.get(modelId) ?? ''),
],
completedCount,
totalCount: orderedModelIds.length,
});
};
emitProgress();
await Promise.all(
orderedModelIds
.filter((modelId) => !modelResultsById.has(modelId))
.map(async (modelId) => {
try {
const modelResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
[modelId],
limitContext
);
if (!modelResult.ready) {
hasFailure = true;
const line = buildModelFailureLine(
providerId,
modelId,
'unavailable',
getResultReason(modelId, modelResult) ?? normalizeModelReason(modelResult.message)
);
modelLines.set(modelId, line);
modelResultsById.set(modelId, {
status: 'failed',
line,
warningLine: null,
});
} else if ((modelResult.warnings?.length ?? 0) > 0) {
hasNotes = true;
const reason = getResultReason(modelId, modelResult);
const line = buildModelFailureLine(providerId, modelId, 'check failed', reason);
modelLines.set(modelId, line);
modelWarnings.push(line);
modelResultsById.set(modelId, {
status: 'notes',
line,
warningLine: line,
});
} else {
const line = buildModelSuccessLine(providerId, modelId);
modelLines.set(modelId, line);
modelResultsById.set(modelId, {
status: 'ready',
line,
warningLine: null,
});
}
} catch (error) {
hasNotes = true;
const reason = normalizeModelReason(
error instanceof Error ? error.message.trim() : String(error).trim()
);
const line = buildModelFailureLine(providerId, modelId, 'check failed', reason || null);
modelLines.set(modelId, line);
modelWarnings.push(line);
modelResultsById.set(modelId, {
status: 'notes',
line,
warningLine: line,
});
} finally {
completedCount += 1;
emitProgress();
}
})
);
const dedupedWarnings = Array.from(new Set([...runtimeWarnings, ...modelWarnings]));
const selectedModelResultsById = Object.fromEntries(
orderedModelIds
.map((modelId) => [modelId, modelResultsById.get(modelId)] as const)
.filter((entry): entry is [string, ProviderPrepareDiagnosticsModelResult] =>
Boolean(entry[1])
)
);
return {
status: hasFailure ? 'failed' : hasNotes ? 'notes' : 'ready',
details: [
...runtimeDetailLines,
...orderedModelIds.map((modelId) => modelLines.get(modelId) ?? ''),
],
warnings: dedupedWarnings,
modelResultsById: selectedModelResultsById,
};
}