agent-ecosystem/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts

1514 lines
47 KiB
TypeScript

import { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog';
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import type {
TeamProviderId,
TeamProvisioningModelCheckRequest,
TeamProvisioningModelVerificationMode,
TeamProvisioningPrepareResult,
} from '@shared/types';
export type ProviderPrepareCheckStatus = 'ready' | 'notes' | 'failed';
type PrepareProvisioningFn = (
cwd?: string,
providerId?: TeamProviderId,
providerIds?: TeamProviderId[],
selectedModels?: string[],
limitContext?: boolean,
modelVerificationMode?: TeamProvisioningModelVerificationMode,
selectedModelChecks?: TeamProvisioningModelCheckRequest[]
) => Promise<TeamProvisioningPrepareResult>;
interface ProviderPrepareDiagnosticsProgress {
status: ProviderPrepareCheckStatus | 'checking';
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>;
}
type TeamProvisioningPrepareIssue = NonNullable<TeamProvisioningPrepareResult['issues']>[number];
export function buildReusableProviderPrepareModelResults(
modelResultsById: Record<string, ProviderPrepareDiagnosticsModelResult>
): Record<string, ProviderPrepareDiagnosticsModelResult> {
return Object.fromEntries(
Object.entries(modelResultsById).filter(([, result]) => result.status !== 'notes')
);
}
export function mergeReusableProviderPrepareModelResults(
existingModelResultsById:
| Record<string, ProviderPrepareDiagnosticsModelResult>
| null
| undefined,
modelResultsById: Record<string, ProviderPrepareDiagnosticsModelResult>
): Record<string, ProviderPrepareDiagnosticsModelResult> {
const mergedModelResultsById = { ...(existingModelResultsById ?? {}) };
for (const [modelId, result] of Object.entries(modelResultsById)) {
if (result.status === 'notes') {
delete mergedModelResultsById[modelId];
continue;
}
mergedModelResultsById[modelId] = result;
}
return mergedModelResultsById;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function uniquePrepareLines(lines: (string | null | undefined)[]): string[] {
const seen = new Set<string>();
const uniqueLines: string[] = [];
for (const line of lines) {
const trimmed = line?.trim() ?? '';
if (!trimmed || seen.has(trimmed)) {
continue;
}
seen.add(trimmed);
uniqueLines.push(trimmed);
}
return uniqueLines;
}
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`;
}
function normalizeSelectedModelChecks(
providerId: TeamProviderId,
selectedModelIds: readonly string[],
selectedModelChecks?: readonly TeamProvisioningModelCheckRequest[]
): TeamProvisioningModelCheckRequest[] {
const rawChecks: TeamProvisioningModelCheckRequest[] =
selectedModelChecks && selectedModelChecks.length > 0
? [...selectedModelChecks]
: selectedModelIds.map((model) => ({ providerId, model }));
const seen = new Set<string>();
const normalized: TeamProvisioningModelCheckRequest[] = [];
for (const check of rawChecks) {
const model = check.model.trim();
if (!model) {
continue;
}
const key = `${check.providerId}\n${model}\n${check.effort ?? ''}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
normalized.push({
providerId: check.providerId,
model,
...(check.effort ? { effort: check.effort } : {}),
});
}
return normalized;
}
function selectModelChecksForIds(
modelChecks: readonly TeamProvisioningModelCheckRequest[],
modelIds: readonly string[]
): TeamProvisioningModelCheckRequest[] {
const modelIdSet = new Set(modelIds);
return modelChecks.filter((check) => modelIdSet.has(check.model));
}
function buildModelAvailableLine(providerId: TeamProviderId, modelId: string): string {
return `${getModelLabel(providerId, modelId)} - available for launch`;
}
function buildModelCompatibilityPendingLine(providerId: TeamProviderId, modelId: string): string {
return `${getModelLabel(providerId, modelId)} - compatible, deep verification pending...`;
}
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)} verification deferred\\.\\s*`, 'i'),
new RegExp(`^Selected model ${escapeRegExp(modelId)} verified for launch\\.\\s*`, 'i'),
new RegExp(`^Selected model ${escapeRegExp(modelId)} is available for launch\\.\\s*`, 'i'),
new RegExp(
`^Selected model ${escapeRegExp(modelId)} is compatible\\. Deep verification pending\\.\\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 on this Codex native runtime';
}
if (/The requested model is not available for your account\./i.test(trimmed)) {
return 'Not available for this account';
}
if (/token refresh failed:\s*401/i.test(trimmed)) {
return 'OpenCode provider authentication failed (token refresh 401)';
}
if (/unauthorized|forbidden|\b401\b|\b403\b/i.test(trimmed)) {
return 'OpenCode provider authentication failed';
}
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 looksLikeOpenCodeRuntimeFailureReason(reason: string | null | undefined): boolean {
const lower = reason?.trim().toLowerCase() ?? '';
if (!lower) {
return false;
}
return (
lower.includes('opencode /experimental/tool') ||
lower.includes('/experimental/tool') ||
lower.includes('mcp_unavailable') ||
lower.includes('unable to connect') ||
lower.includes('runtime store') ||
lower.includes('opencode cli') ||
lower.includes('opencode runtime binary')
);
}
function getBlockingProviderIssue(
providerId: TeamProviderId,
result: TeamProvisioningPrepareResult
): TeamProvisioningPrepareIssue | null {
return (
result.issues?.find(
(entry) =>
entry.scope === 'provider' &&
entry.severity === 'blocking' &&
(!entry.providerId || entry.providerId === providerId) &&
entry.message.trim().length > 0
) ?? null
);
}
function isAdvisoryOpenCodeDeepVerificationIssue(
issue: TeamProvisioningPrepareIssue | null,
reason: string | null | undefined
): boolean {
if (issue?.code?.trim().toLowerCase() !== 'unknown_error') {
return false;
}
const lower = [issue.message, reason]
.map((entry) => entry?.trim() ?? '')
.filter(Boolean)
.join('\n')
.toLowerCase();
if (!lower) {
return false;
}
const hasHardRuntimeMarker =
lower.includes('mcp_unavailable') ||
lower.includes('not_authenticated') ||
lower.includes('authentication') ||
lower.includes('credential') ||
lower.includes('api key') ||
lower.includes('/experimental/tool') ||
lower.includes('runtime store') ||
lower.includes('opencode cli') ||
lower.includes('opencode runtime binary');
if (hasHardRuntimeMarker) {
return false;
}
return (
lower.includes('unable to connect') ||
lower.includes('timed out') ||
lower.includes('timeout') ||
lower.includes('aborted') ||
lower.includes('econnreset') ||
lower.includes('econnrefused') ||
lower.includes('fetch failed') ||
lower.includes('socket hang up') ||
lower.includes('networkerror')
);
}
function buildOpenCodeAdvisoryDeepVerificationWarning(reason: string | null | undefined): string {
const normalizedReason =
normalizeModelReason(reason?.trim() ?? '') || 'Model ping was not confirmed';
return `OpenCode model ping was not confirmed. ${normalizedReason}`;
}
function isProviderLevelOpenCodeBusyDeepVerificationWarning(value: string): boolean {
const lower = value.trim().toLowerCase();
return (
lower.includes('opencode is currently busy') &&
lower.includes('deep model verification') &&
lower.includes('idle')
);
}
function createOpenCodeAdvisoryDeepVerificationModelResult(
providerId: TeamProviderId,
modelId: string
): ProviderPrepareDiagnosticsModelResult {
const line = `${getModelLabel(providerId, modelId)} - ping not confirmed`;
return {
// TODO: Introduce a dedicated `unconfirmed` model result status for deep-ping advisory results.
status: 'notes',
line,
warningLine: line,
};
}
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 getModelScopedEntries(modelId: string, result: TeamProvisioningPrepareResult): string[] {
const escapedModelId = escapeRegExp(modelId);
const scopedPattern = new RegExp(`^Selected model ${escapedModelId}\\b`, 'i');
return [...(result.details ?? []), ...(result.warnings ?? []), result.message]
.map((entry) => entry?.trim() ?? '')
.filter(Boolean)
.filter((entry) => scopedPattern.test(entry));
}
function isModelScopedEntryForAnyModel(modelIds: readonly string[], entry: string): boolean {
const trimmed = entry.trim();
if (!trimmed) {
return false;
}
return modelIds.some((modelId) =>
new RegExp(`^Selected model ${escapeRegExp(modelId)}\\b`, 'i').test(trimmed)
);
}
function looksLikeSingleModelBatchFailure(
modelId: string,
result: TeamProvisioningPrepareResult
): boolean {
const candidates = [...(result.details ?? []), ...(result.warnings ?? []), result.message]
.map((entry) => entry?.trim() ?? '')
.filter(Boolean);
const modelLower = modelId.toLowerCase();
return candidates.some((candidate) => {
const lower = candidate.toLowerCase();
return (
lower.includes(modelLower) ||
lower.includes('requested model') ||
lower.includes('model is not supported') ||
lower.includes('model is not available') ||
lower.includes('selected model')
);
});
}
function getScopedModelReason(modelId: string, entries: string[]): string | null {
for (const entry of entries) {
const stripped = stripSelectedModelPrefix(modelId, entry);
if (!stripped) {
continue;
}
const normalized = normalizeModelReason(stripped);
if (normalized) {
return normalized;
}
}
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 buildModelVerificationDeferredLine(
providerId: TeamProviderId,
modelId: string,
reason: string | null
): string {
const label = getModelLabel(providerId, modelId);
return reason
? `${label} - verification deferred - ${reason}`
: `${label} - verification deferred`;
}
function createRuntimeDetailLines(result: TeamProvisioningPrepareResult): string[] {
return uniquePrepareLines([...(result.details ?? []), ...(result.warnings ?? [])]);
}
function createRuntimeWarningLines(result: TeamProvisioningPrepareResult): string[] {
return uniquePrepareLines(
(result.warnings ?? [])
.map((warning) => normalizeRuntimeFailureDetailLine(warning))
.filter(Boolean)
);
}
function normalizeRuntimeFailureDetailLine(
detail: string | null | undefined,
code?: string | null
): string | null {
const trimmed = detail?.trim();
if (!trimmed) {
return null;
}
if (/opencode cli (?:not detected on path|not found)/i.test(trimmed)) {
return 'OpenCode runtime binary is not installed or not reachable by launch preflight.';
}
const lower = trimmed.toLowerCase();
if (
lower.includes('unable to connect') &&
(lower.includes('/experimental/tool') ||
lower.includes('mcp_unavailable') ||
code?.trim().toLowerCase() === 'mcp_unavailable')
) {
const connectionDetail = trimmed.includes(' - ') ? trimmed.split(' - ').pop()?.trim() : trimmed;
const base = 'OpenCode app MCP is unreachable. Retry launch to refresh the app MCP bridge.';
return connectionDetail && connectionDetail !== trimmed
? `${base} Details: ${connectionDetail}`
: base;
}
return trimmed;
}
function createRuntimeFailureDetailLines(
runtimeDetailLines: readonly string[],
message: string | null | undefined
): string[] {
return uniquePrepareLines(
[...runtimeDetailLines, message]
.map((detail) => normalizeRuntimeFailureDetailLine(detail))
.filter(Boolean)
);
}
function extractTimedOutPreflightProbeModelId(detail: string): string | null {
const trimmed = detail.trim();
if (!trimmed) {
return null;
}
if (
!trimmed.toLowerCase().includes('preflight check for `') ||
!trimmed.toLowerCase().includes('-p` did not complete')
) {
return null;
}
const match = /--model\s+([^\s]+)/i.exec(trimmed);
return match?.[1]?.trim() || null;
}
function isSuppressibleGenericPreflightWarning(detail: string): boolean {
const lower = detail.trim().toLowerCase();
if (!lower) {
return false;
}
return (
lower.includes('one-shot diagnostic') ||
lower.includes('preflight check failed') ||
(lower.includes('preflight check for `') && lower.includes('-p` did not complete')) ||
lower.includes('preflight ping completed but did not return the expected pong')
);
}
function suppressSupersededRuntimeWarnings(params: {
runtimeDetailLines: string[];
runtimeWarnings: string[];
modelResultsById: Map<string, ProviderPrepareDiagnosticsModelResult>;
}): {
runtimeDetailLines: string[];
runtimeWarnings: string[];
} {
const suppressedEntries = new Set<string>();
const allSelectedModelsReady =
params.modelResultsById.size > 0 &&
Array.from(params.modelResultsById.values()).every((result) => result.status === 'ready');
for (const warning of params.runtimeWarnings) {
const probedModelId = extractTimedOutPreflightProbeModelId(warning);
if (probedModelId) {
if (params.modelResultsById.get(probedModelId)?.status !== 'ready') {
continue;
}
suppressedEntries.add(warning);
continue;
}
if (allSelectedModelsReady && isSuppressibleGenericPreflightWarning(warning)) {
suppressedEntries.add(warning);
}
}
return {
runtimeDetailLines: params.runtimeDetailLines.filter(
(detail) => !suppressedEntries.has(detail)
),
runtimeWarnings: params.runtimeWarnings.filter((warning) => !suppressedEntries.has(warning)),
};
}
function getProgressStatus(params: {
completedCount: number;
totalCount: number;
runtimeWarnings: string[];
modelResultsById: Map<string, ProviderPrepareDiagnosticsModelResult>;
}): ProviderPrepareCheckStatus | 'checking' {
if (params.completedCount < params.totalCount) {
return 'checking';
}
if (Array.from(params.modelResultsById.values()).some((result) => result.status === 'failed')) {
return 'failed';
}
if (
params.runtimeWarnings.length > 0 ||
Array.from(params.modelResultsById.values()).some((result) => result.status === 'notes')
) {
return 'notes';
}
return 'ready';
}
function resolveModelResultFromBatch(
providerId: TeamProviderId,
modelId: string,
result: TeamProvisioningPrepareResult,
isOnlyModel: boolean
): ProviderPrepareDiagnosticsModelResult {
const modelScopedEntries = getModelScopedEntries(modelId, result);
const hasModelScopedEntries = modelScopedEntries.length > 0;
const scopedReason = getScopedModelReason(modelId, modelScopedEntries);
const fallbackBatchReason = isOnlyModel
? (getResultReason(modelId, result) ?? normalizeModelReason(result.message))
: null;
const hasVerifiedLine = modelScopedEntries.some((entry) =>
/selected model .* verified for launch\./i.test(entry)
);
if (hasVerifiedLine) {
return {
status: 'ready',
line: buildModelSuccessLine(providerId, modelId),
warningLine: null,
};
}
const hasAvailableLine = modelScopedEntries.some((entry) =>
/selected model .* is available for launch\./i.test(entry)
);
if (hasAvailableLine) {
return {
status: 'ready',
line: buildModelAvailableLine(providerId, modelId),
warningLine: null,
};
}
const hasCompatibilityLine = modelScopedEntries.some((entry) =>
/selected model .* is compatible\. deep verification pending\./i.test(entry)
);
if (hasCompatibilityLine) {
if (providerId !== 'opencode') {
return {
status: 'ready',
line: buildModelAvailableLine(providerId, modelId),
warningLine: null,
};
}
return {
status: 'notes',
line: buildModelCompatibilityPendingLine(providerId, modelId),
warningLine: null,
};
}
const hasVerificationDeferredLine = modelScopedEntries.some((entry) =>
/selected model .* verification deferred\./i.test(entry)
);
if (hasVerificationDeferredLine) {
const line = buildModelVerificationDeferredLine(providerId, modelId, scopedReason);
return {
status: 'notes',
line,
warningLine: line,
};
}
const hasUnavailableLine = modelScopedEntries.some((entry) =>
/selected model .* is unavailable\./i.test(entry)
);
if (hasUnavailableLine || (!result.ready && isOnlyModel)) {
return {
status: 'failed',
line: buildModelFailureLine(
providerId,
modelId,
'unavailable',
scopedReason ?? fallbackBatchReason
),
warningLine: null,
};
}
const hasVerificationWarningLine = modelScopedEntries.some((entry) =>
/selected model .* could not be verified\./i.test(entry)
);
if (
hasVerificationWarningLine ||
((result.warnings?.length ?? 0) > 0 && isOnlyModel && hasModelScopedEntries)
) {
const line = buildModelFailureLine(providerId, modelId, 'check failed', scopedReason);
return {
status: 'notes',
line,
warningLine: line,
};
}
if (result.ready && (result.warnings?.length ?? 0) > 0 && !hasModelScopedEntries) {
if (providerId !== 'opencode') {
return {
status: 'ready',
line: buildModelAvailableLine(providerId, modelId),
warningLine: null,
};
}
return {
status: 'notes',
line: buildModelCompatibilityPendingLine(providerId, modelId),
warningLine: null,
};
}
if (result.ready) {
return {
status: 'ready',
line: buildModelSuccessLine(providerId, modelId),
warningLine: null,
};
}
const line = buildModelFailureLine(
providerId,
modelId,
'check failed',
scopedReason ?? 'Model verification failed'
);
return {
status: 'notes',
line,
warningLine: line,
};
}
function resolveModelResultFromCompatibilityBatch(
providerId: TeamProviderId,
modelId: string,
result: TeamProvisioningPrepareResult,
isOnlyModel: boolean
): { kind: 'compatible' } | { kind: 'terminal'; result: ProviderPrepareDiagnosticsModelResult } {
const modelScopedEntries = getModelScopedEntries(modelId, result);
const scopedReason = getScopedModelReason(modelId, modelScopedEntries);
const fallbackBatchReason = isOnlyModel
? (getResultReason(modelId, result) ?? normalizeModelReason(result.message))
: null;
const hasVerifiedLine = modelScopedEntries.some((entry) =>
/selected model .* verified for launch\./i.test(entry)
);
if (hasVerifiedLine) {
return {
kind: 'terminal',
result: {
status: 'ready',
line: buildModelSuccessLine(providerId, modelId),
warningLine: null,
},
};
}
const hasCompatibilityLine = modelScopedEntries.some((entry) =>
/selected model .* is compatible\. deep verification pending\./i.test(entry)
);
if (hasCompatibilityLine || (result.ready && modelScopedEntries.length === 0)) {
return { kind: 'compatible' };
}
const hasAvailableLine = modelScopedEntries.some((entry) =>
/selected model .* is available for launch\./i.test(entry)
);
if (hasAvailableLine) {
return {
kind: 'terminal',
result: {
status: 'ready',
line: buildModelAvailableLine(providerId, modelId),
warningLine: null,
},
};
}
const hasUnavailableLine = modelScopedEntries.some((entry) =>
/selected model .* is unavailable\./i.test(entry)
);
const hasVerificationWarningLine = modelScopedEntries.some((entry) =>
/selected model .* could not be verified\./i.test(entry)
);
if (hasUnavailableLine || (!result.ready && isOnlyModel && !hasVerificationWarningLine)) {
return {
kind: 'terminal',
result: {
status: 'failed',
line: buildModelFailureLine(
providerId,
modelId,
'unavailable',
scopedReason ?? fallbackBatchReason
),
warningLine: null,
},
};
}
if (hasVerificationWarningLine) {
const line = buildModelFailureLine(
providerId,
modelId,
'check failed',
scopedReason ?? fallbackBatchReason
);
return {
kind: 'terminal',
result: {
status: result.ready ? 'notes' : 'failed',
line,
warningLine: result.ready ? line : null,
},
};
}
return {
kind: 'terminal',
result: {
status: 'notes',
line: buildModelFailureLine(
providerId,
modelId,
'check failed',
scopedReason ?? fallbackBatchReason ?? 'Model verification failed'
),
warningLine: buildModelFailureLine(
providerId,
modelId,
'check failed',
scopedReason ?? fallbackBatchReason ?? 'Model verification failed'
),
},
};
}
function looksLikeSingleModelCredentialFailure(result: TeamProvisioningPrepareResult): boolean {
const combined = [...(result.details ?? []), ...(result.warnings ?? []), result.message]
.map((entry) => entry?.trim() ?? '')
.filter(Boolean)
.join('\n')
.toLowerCase();
const hasCredentialContext =
combined.includes('token') ||
combined.includes('authenticat') ||
combined.includes('credential') ||
combined.includes('api key') ||
combined.includes('apikey') ||
combined.includes('not_authenticated');
const hasAuthStatus =
combined.includes('unauthorized') ||
combined.includes('forbidden') ||
/\b401\b/.test(combined) ||
/\b403\b/.test(combined);
return (
combined.includes('token refresh failed') ||
combined.includes('authentication failed') ||
combined.includes('not_authenticated') ||
(hasCredentialContext && hasAuthStatus)
);
}
function getOpenCodeProviderScopedFailureFromModelScopedEntries(
modelIds: readonly string[],
result: TeamProvisioningPrepareResult
): string | null {
if (result.ready) {
return null;
}
for (const modelId of modelIds) {
const scopedEntries = getModelScopedEntries(modelId, result);
for (const entry of scopedEntries) {
const stripped = stripSelectedModelPrefix(modelId, entry);
if (looksLikeOpenCodeRuntimeFailureReason(stripped)) {
return stripped;
}
}
}
return null;
}
function shouldSurfaceProviderRuntimeFailureInsteadOfModelFailure(params: {
result: TeamProvisioningPrepareResult;
modelIds: readonly string[];
modelScopedEntriesPresent: boolean;
runtimeDetailLines: readonly string[];
runtimeWarnings: readonly string[];
}): boolean {
if (params.result.ready || params.modelScopedEntriesPresent) {
return false;
}
const hasRuntimeScopedDiagnostics =
params.runtimeDetailLines.length > 0 ||
params.runtimeWarnings.length > 0 ||
Boolean(params.result.message?.trim());
if (!hasRuntimeScopedDiagnostics) {
return false;
}
const canTreatAsLegacySingleModelFailure =
params.modelIds.length === 1 &&
(looksLikeSingleModelBatchFailure(params.modelIds[0], params.result) ||
looksLikeSingleModelCredentialFailure(params.result)) &&
params.runtimeWarnings.length === 0;
return !canTreatAsLegacySingleModelFailure;
}
export async function runProviderPrepareDiagnostics({
cwd,
providerId,
selectedModelIds,
prepareProvisioning,
limitContext,
onModelProgress,
cachedModelResultsById,
selectedModelChecks,
}: {
cwd: string;
providerId: TeamProviderId;
selectedModelIds: string[];
selectedModelChecks?: TeamProvisioningModelCheckRequest[];
prepareProvisioning: PrepareProvisioningFn;
limitContext?: boolean;
onModelProgress?: (progress: ProviderPrepareDiagnosticsProgress) => void;
cachedModelResultsById?: Record<string, ProviderPrepareDiagnosticsModelResult>;
}): Promise<ProviderPrepareDiagnosticsResult> {
const normalizedModelChecks = normalizeSelectedModelChecks(
providerId,
selectedModelIds,
selectedModelChecks
);
const hasExplicitModelChecks = (selectedModelChecks?.length ?? 0) > 0;
const orderedModelIds = Array.from(new Set(normalizedModelChecks.map((check) => check.model)));
if (orderedModelIds.length === 0) {
const runtimeResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
undefined,
limitContext
);
const runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
const runtimeWarnings = createRuntimeWarningLines(runtimeResult);
if (!runtimeResult.ready) {
return {
status: 'failed',
details: createRuntimeFailureDetailLines(runtimeDetailLines, runtimeResult.message),
warnings: runtimeWarnings,
modelResultsById: {},
};
}
return {
status: runtimeWarnings.length > 0 ? 'notes' : 'ready',
details: runtimeDetailLines,
warnings: runtimeWarnings,
modelResultsById: {},
};
}
const reusableModelResultsById = cachedModelResultsById ?? {};
const modelResultsById = new Map<string, ProviderPrepareDiagnosticsModelResult>();
const modelLines = new Map<string, string>();
let runtimeDetailLines: string[] = [];
let runtimeWarnings: string[] = [];
let completedCount = 0;
let hasFailure = false;
let hasNotes = false;
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 => {
const filteredRuntime = suppressSupersededRuntimeWarnings({
runtimeDetailLines,
runtimeWarnings,
modelResultsById,
});
onModelProgress?.({
status: getProgressStatus({
completedCount,
totalCount: orderedModelIds.length,
runtimeWarnings: filteredRuntime.runtimeWarnings,
modelResultsById,
}),
details: [
...filteredRuntime.runtimeDetailLines,
...orderedModelIds.map((modelId) => modelLines.get(modelId) ?? ''),
],
completedCount,
totalCount: orderedModelIds.length,
});
};
emitProgress();
const uncachedModelIds = orderedModelIds.filter((modelId) => !modelResultsById.has(modelId));
if (uncachedModelIds.length === 0) {
const runtimeResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
undefined,
limitContext
);
runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
runtimeWarnings = createRuntimeWarningLines(runtimeResult);
if (!runtimeResult.ready) {
return {
status: 'failed',
details: createRuntimeFailureDetailLines(runtimeDetailLines, runtimeResult.message),
warnings: runtimeWarnings,
modelResultsById: {},
};
}
} else {
const recordTerminalModelResult = (
modelId: string,
resolvedResult: ProviderPrepareDiagnosticsModelResult
): void => {
modelLines.set(modelId, resolvedResult.line);
modelResultsById.set(modelId, resolvedResult);
completedCount += 1;
if (resolvedResult.status === 'failed') {
hasFailure = true;
} else if (resolvedResult.status === 'notes') {
hasNotes = true;
}
if (resolvedResult.warningLine) {
modelWarnings.push(resolvedResult.warningLine);
}
};
if (providerId === 'opencode') {
const compatibilityPassedModelIds: string[] = [];
try {
const compatibilityResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
uncachedModelIds,
limitContext,
'compatibility',
...(hasExplicitModelChecks
? [selectModelChecksForIds(normalizedModelChecks, uncachedModelIds)]
: [])
);
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
);
runtimeWarnings = createRuntimeWarningLines(compatibilityResult).filter(
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
);
const hasModelScopedEntries = uncachedModelIds.some(
(modelId) => getModelScopedEntries(modelId, compatibilityResult).length > 0
);
const hasNonModelScopedDiagnostics =
runtimeDetailLines.length > 0 || runtimeWarnings.length > 0;
const hasSingleModelFallbackReason =
uncachedModelIds.length === 1 &&
looksLikeSingleModelBatchFailure(uncachedModelIds[0], compatibilityResult);
const providerScopedFailure = getOpenCodeProviderScopedFailureFromModelScopedEntries(
uncachedModelIds,
compatibilityResult
);
const structuredProviderScopedIssue = getBlockingProviderIssue(
providerId,
compatibilityResult
);
const structuredProviderScopedFailure =
structuredProviderScopedIssue?.message.trim() ?? null;
if (structuredProviderScopedFailure || providerScopedFailure) {
return {
status: 'failed',
details: [
normalizeRuntimeFailureDetailLine(
structuredProviderScopedFailure ?? providerScopedFailure ?? 'OpenCode failed',
structuredProviderScopedIssue?.code
) ?? 'OpenCode failed',
],
warnings: [],
modelResultsById: {},
};
}
if (
shouldSurfaceProviderRuntimeFailureInsteadOfModelFailure({
result: compatibilityResult,
modelIds: uncachedModelIds,
modelScopedEntriesPresent: hasModelScopedEntries,
runtimeDetailLines,
runtimeWarnings,
}) ||
(!compatibilityResult.ready &&
!hasModelScopedEntries &&
(uncachedModelIds.length > 1 ||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason)))
) {
return {
status: 'failed',
details: createRuntimeFailureDetailLines(
runtimeDetailLines,
compatibilityResult.message
),
warnings: runtimeWarnings,
modelResultsById: {},
};
}
if (!hasModelScopedEntries && uncachedModelIds.length === 1) {
runtimeDetailLines = [];
runtimeWarnings = [];
}
for (const modelId of uncachedModelIds) {
const compatibilityResolution = resolveModelResultFromCompatibilityBatch(
providerId,
modelId,
compatibilityResult,
uncachedModelIds.length === 1
);
if (compatibilityResolution.kind === 'compatible') {
modelLines.set(modelId, buildModelCompatibilityPendingLine(providerId, modelId));
compatibilityPassedModelIds.push(modelId);
continue;
}
recordTerminalModelResult(modelId, compatibilityResolution.result);
}
} catch (error) {
hasNotes = true;
const reason = normalizeModelReason(
error instanceof Error ? error.message.trim() : String(error).trim()
);
for (const modelId of uncachedModelIds) {
const line = buildModelFailureLine(providerId, modelId, 'check failed', reason || null);
recordTerminalModelResult(modelId, {
status: 'notes',
line,
warningLine: line,
});
}
}
emitProgress();
if (compatibilityPassedModelIds.length === 0) {
const filteredRuntime = suppressSupersededRuntimeWarnings({
runtimeDetailLines,
runtimeWarnings,
modelResultsById,
});
const dedupedWarnings = Array.from(
new Set([...filteredRuntime.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 || dedupedWarnings.length > 0
? 'notes'
: 'ready',
details: [
...filteredRuntime.runtimeDetailLines,
...orderedModelIds.map((modelId) => modelLines.get(modelId) ?? ''),
],
warnings: dedupedWarnings,
modelResultsById: selectedModelResultsById,
};
}
try {
const batchedModelResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
compatibilityPassedModelIds,
limitContext,
'deep',
...(hasExplicitModelChecks
? [selectModelChecksForIds(normalizedModelChecks, compatibilityPassedModelIds)]
: [])
);
runtimeDetailLines = createRuntimeDetailLines(batchedModelResult).filter(
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
);
runtimeWarnings = createRuntimeWarningLines(batchedModelResult).filter(
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
);
const hasModelScopedEntries = compatibilityPassedModelIds.some(
(modelId) => getModelScopedEntries(modelId, batchedModelResult).length > 0
);
const hasNonModelScopedDiagnostics =
runtimeDetailLines.length > 0 || runtimeWarnings.length > 0;
const hasSingleModelFallbackReason =
compatibilityPassedModelIds.length === 1 &&
looksLikeSingleModelBatchFailure(compatibilityPassedModelIds[0], batchedModelResult);
const providerScopedFailure = getOpenCodeProviderScopedFailureFromModelScopedEntries(
compatibilityPassedModelIds,
batchedModelResult
);
const structuredProviderScopedIssue = getBlockingProviderIssue(
providerId,
batchedModelResult
);
const structuredProviderScopedFailure =
structuredProviderScopedIssue?.message.trim() ?? null;
let handledAdvisoryDeepFailure = false;
let handledBusyDeepDeferral = false;
if (structuredProviderScopedFailure || providerScopedFailure) {
const failureReason =
structuredProviderScopedFailure ?? providerScopedFailure ?? 'OpenCode failed';
if (
isAdvisoryOpenCodeDeepVerificationIssue(structuredProviderScopedIssue, failureReason)
) {
hasNotes = true;
runtimeDetailLines = [];
runtimeWarnings = uniquePrepareLines([
...runtimeWarnings,
buildOpenCodeAdvisoryDeepVerificationWarning(failureReason),
]);
for (const modelId of compatibilityPassedModelIds) {
recordTerminalModelResult(
modelId,
createOpenCodeAdvisoryDeepVerificationModelResult(providerId, modelId)
);
}
handledAdvisoryDeepFailure = true;
} else {
return {
status: 'failed',
details: [
normalizeRuntimeFailureDetailLine(
failureReason,
structuredProviderScopedIssue?.code
) ?? failureReason,
],
warnings: [],
modelResultsById: {},
};
}
}
if (
!handledAdvisoryDeepFailure &&
batchedModelResult.ready &&
!hasModelScopedEntries &&
runtimeWarnings.some(isProviderLevelOpenCodeBusyDeepVerificationWarning)
) {
runtimeDetailLines = [];
runtimeWarnings = [];
for (const modelId of compatibilityPassedModelIds) {
recordTerminalModelResult(modelId, {
status: 'ready',
line: buildModelAvailableLine(providerId, modelId),
warningLine: null,
});
}
handledBusyDeepDeferral = true;
}
if (
!handledAdvisoryDeepFailure &&
!handledBusyDeepDeferral &&
(shouldSurfaceProviderRuntimeFailureInsteadOfModelFailure({
result: batchedModelResult,
modelIds: compatibilityPassedModelIds,
modelScopedEntriesPresent: hasModelScopedEntries,
runtimeDetailLines,
runtimeWarnings,
}) ||
(!batchedModelResult.ready &&
!hasModelScopedEntries &&
(compatibilityPassedModelIds.length > 1 ||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason))))
) {
return {
status: 'failed',
details: createRuntimeFailureDetailLines(
runtimeDetailLines,
batchedModelResult.message
),
warnings: runtimeWarnings,
modelResultsById: {},
};
}
if (
!handledAdvisoryDeepFailure &&
!handledBusyDeepDeferral &&
!hasModelScopedEntries &&
compatibilityPassedModelIds.length === 1
) {
runtimeDetailLines = [];
runtimeWarnings = [];
}
if (!handledAdvisoryDeepFailure && !handledBusyDeepDeferral) {
for (const modelId of compatibilityPassedModelIds) {
recordTerminalModelResult(
modelId,
resolveModelResultFromBatch(
providerId,
modelId,
batchedModelResult,
compatibilityPassedModelIds.length === 1
)
);
}
}
} catch (error) {
hasNotes = true;
const reason = normalizeModelReason(
error instanceof Error ? error.message.trim() : String(error).trim()
);
for (const modelId of compatibilityPassedModelIds) {
const line = buildModelFailureLine(providerId, modelId, 'check failed', reason || null);
recordTerminalModelResult(modelId, {
status: 'notes',
line,
warningLine: line,
});
}
} finally {
emitProgress();
}
} else {
try {
const compatibilityResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
uncachedModelIds,
limitContext,
'compatibility',
...(hasExplicitModelChecks
? [selectModelChecksForIds(normalizedModelChecks, uncachedModelIds)]
: [])
);
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
);
runtimeWarnings = createRuntimeWarningLines(compatibilityResult).filter(
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
);
const hasModelScopedEntries = uncachedModelIds.some(
(modelId) => getModelScopedEntries(modelId, compatibilityResult).length > 0
);
const hasNonModelScopedDiagnostics =
runtimeDetailLines.length > 0 || runtimeWarnings.length > 0;
const hasSingleModelFallbackReason =
uncachedModelIds.length === 1 &&
looksLikeSingleModelBatchFailure(uncachedModelIds[0], compatibilityResult);
if (
!compatibilityResult.ready &&
!hasModelScopedEntries &&
(uncachedModelIds.length > 1 ||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason))
) {
return {
status: 'failed',
details: createRuntimeFailureDetailLines(
runtimeDetailLines,
compatibilityResult.message
),
warnings: runtimeWarnings,
modelResultsById: {},
};
}
if (!hasModelScopedEntries && uncachedModelIds.length === 1) {
runtimeDetailLines = [];
runtimeWarnings = [];
}
for (const modelId of uncachedModelIds) {
recordTerminalModelResult(
modelId,
resolveModelResultFromBatch(
providerId,
modelId,
compatibilityResult,
uncachedModelIds.length === 1
)
);
}
emitProgress();
if (!hasFailure) {
try {
const deepResult = await prepareProvisioning(
cwd,
providerId,
[providerId],
undefined,
limitContext,
'deep'
);
runtimeDetailLines = createRuntimeDetailLines(deepResult).filter(
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
);
runtimeWarnings = createRuntimeWarningLines(deepResult).filter(
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
);
if (
!deepResult.ready &&
runtimeDetailLines.length === 0 &&
runtimeWarnings.length === 0
) {
runtimeWarnings = uniquePrepareLines([deepResult.message]);
}
} catch (deepError) {
hasNotes = true;
runtimeWarnings = uniquePrepareLines([
normalizeModelReason(
deepError instanceof Error ? deepError.message.trim() : String(deepError).trim()
) ?? 'One-shot diagnostic failed',
]);
}
}
} catch (error) {
hasNotes = true;
const reason = normalizeModelReason(
error instanceof Error ? error.message.trim() : String(error).trim()
);
for (const modelId of uncachedModelIds) {
const line = buildModelFailureLine(providerId, modelId, 'check failed', reason || null);
recordTerminalModelResult(modelId, {
status: 'notes',
line,
warningLine: line,
});
}
} finally {
emitProgress();
}
}
}
const filteredRuntime = suppressSupersededRuntimeWarnings({
runtimeDetailLines,
runtimeWarnings,
modelResultsById,
});
const dedupedWarnings = Array.from(
new Set([...filteredRuntime.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 || dedupedWarnings.length > 0 ? 'notes' : 'ready',
details: [
...filteredRuntime.runtimeDetailLines,
...orderedModelIds.map((modelId) => modelLines.get(modelId) ?? ''),
],
warnings: dedupedWarnings,
modelResultsById: selectedModelResultsById,
};
}