import type { PiModel } from './types.js'; interface ProviderErrorFacts { codes: number[]; messages: string[]; reasons: string[]; statuses: string[]; } const MAX_ERROR_DEPTH = 6; const MAX_PROVIDER_MESSAGE_CHARS = 600; function modelRef(model: PiModel): string { return `${model.provider}/${model.id}`; } function providerLabel(provider: string): string { const labels: Record = { anthropic: 'Anthropic', google: 'Google', 'google-vertex': 'Google Vertex AI', openai: 'OpenAI', 'openai-codex': 'OpenAI Codex', openrouter: 'OpenRouter', }; return labels[provider] ?? provider; } function parseJsonString(value: string): unknown | undefined { const trimmed = value.trim(); if ( (!trimmed.startsWith('{') || !trimmed.endsWith('}')) && (!trimmed.startsWith('[') || !trimmed.endsWith(']')) ) { return undefined; } try { return JSON.parse(trimmed); } catch { return undefined; } } function collectErrorFacts( value: unknown, facts: ProviderErrorFacts, depth = 0, seen = new Set(), ): void { if (depth > MAX_ERROR_DEPTH || value === null || value === undefined) return; if (typeof value === 'string') { const parsed = parseJsonString(value); if (parsed !== undefined) { collectErrorFacts(parsed, facts, depth + 1, seen); return; } const message = value.trim(); if (message) facts.messages.push(message); return; } if (typeof value !== 'object') return; if (seen.has(value)) return; seen.add(value); if (Array.isArray(value)) { for (const item of value) collectErrorFacts(item, facts, depth + 1, seen); return; } const record = value as Record; if (typeof record.code === 'number') facts.codes.push(record.code); if (typeof record.reason === 'string') facts.reasons.push(record.reason.trim()); if (typeof record.status === 'string') facts.statuses.push(record.status.trim()); const priorityKeys = ['error', 'message', 'details']; for (const key of priorityKeys) { if (key in record) collectErrorFacts(record[key], facts, depth + 1, seen); } for (const [key, child] of Object.entries(record)) { if (!priorityKeys.includes(key) && !['code', 'reason', 'status'].includes(key)) { collectErrorFacts(child, facts, depth + 1, seen); } } } function errorFacts(raw: string): ProviderErrorFacts { const facts: ProviderErrorFacts = { codes: [], messages: [], reasons: [], statuses: [], }; collectErrorFacts(raw, facts); return facts; } function firstHttpCode(facts: ProviderErrorFacts, raw: string): number | undefined { const collected = facts.codes.find((code) => code >= 400 && code <= 599); if (collected !== undefined) return collected; const match = /\b([45]\d\d)\b/.exec(raw); return match ? Number.parseInt(match[1], 10) : undefined; } function preferredReason(facts: ProviderErrorFacts, raw: string): string | undefined { const apiKeyReason = /\b(API_KEY_[A-Z_]+)\b/.exec(raw); if (apiKeyReason) return apiKeyReason[1]; const collected = facts.reasons.find(Boolean); if (collected) return collected; const match = /\b(API_KEY_[A-Z_]+|RESOURCE_EXHAUSTED|PERMISSION_DENIED|UNAUTHENTICATED|NOT_FOUND)\b/.exec( raw, ); return match?.[1]; } function providerMessage(facts: ProviderErrorFacts, raw: string): string { const message = facts.messages.find( (candidate) => !/^(forbidden|unauthorized|not found|permission denied)$/i.test(candidate) && !parseJsonString(candidate), ) ?? facts.messages[0] ?? raw; const compact = message.replace(/\s+/g, ' ').trim(); if (compact.length <= MAX_PROVIDER_MESSAGE_CHARS) return compact; return `${compact.slice(0, MAX_PROVIDER_MESSAGE_CHARS)}…`; } function errorTag(code: number | undefined, reason: string | undefined): string { const values = [code?.toString(), reason].filter(Boolean); return values.length > 0 ? ` (${values.join(' ')})` : ''; } function alternateModelGuidance(): string { return 'Choose another ready model with /pdf-parse models.'; } export function formatAuthResolutionFailure(model: PiModel, raw: string): string { const facts = errorFacts(raw); const message = providerMessage(facts, raw); return [ `Pi could not resolve credentials for ${modelRef(model)}.`, message, `Run /login ${model.provider} to add or replace the provider credential.`, alternateModelGuidance(), ].join(' '); } export function formatProviderFailure(model: PiModel, raw: string): string { const facts = errorFacts(raw); const code = firstHttpCode(facts, raw); const reason = preferredReason(facts, raw); const message = providerMessage(facts, raw); const haystack = [raw, ...facts.reasons, ...facts.statuses, ...facts.messages] .join(' ') .toUpperCase(); const label = providerLabel(model.provider); const reference = modelRef(model); const tag = errorTag(code, reason); if (haystack.includes('NO API KEY') || haystack.includes('MISSING API KEY')) { return [ `Pi did not supply a usable credential for ${reference}.`, `Run /login ${model.provider} to add or replace it.`, alternateModelGuidance(), `Provider message: ${message}`, ].join(' '); } if (reason === 'API_KEY_SERVICE_BLOCKED' || haystack.includes('API_KEY_SERVICE_BLOCKED')) { return [ `${label} rejected the Pi credential for ${reference}${tag}.`, 'The key is configured in Pi, but Google blocked it from the Gemini API.', 'Create or update a Gemini API key in Google AI Studio so it is authorized for the Gemini API,', `then run /login ${model.provider} to replace the credential.`, alternateModelGuidance(), `Provider message: ${message}`, ].join(' '); } if ( code === 401 || haystack.includes('UNAUTHENTICATED') || haystack.includes('INVALID API KEY') || haystack.includes('API KEY NOT VALID') || haystack.includes('INVALID_API_KEY') ) { return [ `${label} rejected the Pi credential for ${reference}${tag}.`, `Run /login ${model.provider} to replace or re-authenticate it.`, alternateModelGuidance(), `Provider message: ${message}`, ].join(' '); } if ( code === 429 || haystack.includes('RESOURCE_EXHAUSTED') || haystack.includes('RATE LIMIT') || haystack.includes('QUOTA') ) { return [ `${label} quota or rate limit blocked ${reference}${tag}.`, 'Check provider quota or billing, wait if the limit is temporary, or choose another model.', alternateModelGuidance(), `Provider message: ${message}`, ].join(' '); } if ( code === 404 || haystack.includes('NOT_FOUND') || haystack.includes('MODEL_NOT_FOUND') || /\bMODEL\b.*\bNOT FOUND\b/i.test(haystack) ) { return [ `${label} reports that the model is unavailable: ${reference}${tag}.`, 'The model may not be enabled for this credential, region, or endpoint.', alternateModelGuidance(), `Provider message: ${message}`, ].join(' '); } if (code === 403 || haystack.includes('PERMISSION_DENIED')) { return [ `${label} denied access to ${reference}${tag}.`, `Run /login ${model.provider} if the credential is wrong, or check the provider project permissions.`, alternateModelGuidance(), `Provider message: ${message}`, ].join(' '); } return `${label} request failed for ${reference}${tag}. Provider message: ${message}`; }