CherryHQ/cherry-studio · error · PpioApiError
PPIO API error: ${response.status} - ${errorText}
Error message
PPIO API error: ${response.status} - ${errorText} What it means
PpioApiError is thrown by PpioTransport.request() for any non-ok HTTP response from api.ppio.com (submit, poll, or getTaskResult). It is a custom Error subclass carrying `error.statusCode` so the poll loop can decide terminal (4xx≠429) vs transient (5xx/429). The message embeds the status plus the first 500 chars of the body.
Source
Thrown at src/main/ai/provider/custom/ppio/ppioTransport.ts:159
const fetchOptions: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`
},
signal: controller.signal
}
if (method === 'POST') {
fetchOptions.body = JSON.stringify(body)
}
try {
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = (await response.text().catch(() => '')).slice(0, 500)
throw new PpioApiError(`PPIO API error: ${response.status} - ${errorText}`, response.status)
}
const data = await response.json()
return data as T
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
if (externallyAborted) {
throw createAbortError('PPIO API request aborted')
}
throw new Error(`PPIO API request timeout after ${timeout / 1000}s`)
}
throw error
} finally {
clearTimeout(timeoutId)
externalSignal?.removeEventListener('abort', onExternalAbort)
}
}View on GitHub (pinned to 726446b54c)
Solutions
- Inspect error.statusCode and the embedded errorText: 401/403 → fix apiKey; 402/429 → top up credits / back off; 400 with policy → adjust prompt; 400 model-not-found → correct model id.
- For 5xx or 429, retry (the poll loop already does up to maxTransientRetries; for submit, add an outer retry with backoff).
- Verify the model id is still listed in PPIO's catalog and matches a PpioModelDescriptor.
- Confirm baseURL is the PPIO API host (default https://api.ppio.com) and the endpoint path matches the model's descriptor.endpoint.
Example fix
// before
try { await transport.submit(input) } catch (e) { /* opaque */ }
// after — classify by status
try {
await transport.submit(input)
} catch (e) {
if (e instanceof PpioApiError && isTerminalHttpStatus(e.statusCode)) throw e
await backoffRetry(() => transport.submit(input))
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!settings.apiKey) throw new Error('PPIO apiKey is required')
if (!/^https?:\/\//.test(baseURL)) throw new Error(`Invalid PPIO baseURL: ${baseURL}`) Type guard
export function isPpioApiError(e: unknown): e is { statusCode: number; message: string } {
return e instanceof Error && e.name === 'PpioApiError' && typeof (e as any).statusCode === 'number'
} Try / catch
try {
await transport.submit(input)
} catch (e) {
if (isPpioApiError(e) && isTerminalHttpStatus(e.statusCode)) throw e // 4xx≠429
await backoffRetry(() => transport.submit(input)) // 5xx / 429
} Prevention
- Validate apiKey and baseURL before constructing the transport.
- Key off error.statusCode to separate terminal from transient.
- Inspect the embedded errorText — it usually pinpoints the bad field.
When it happens
Trigger: Any PPIO endpoint returns 4xx/5xx. Concrete causes: missing/invalid apiKey (401/403), quota/credit exhausted (402/429), unknown model id (400/404), NSFW content rejected (400 with policy reason), malformed request (422), or 5xx during vendor outage.
Common situations: First run with unset apiKey, credits run out mid-batch, model id drift after PPIO renames models, content policy trip on a prompt, or transient 5xx during peak load.
Related errors
- ModelScope API error: ${response.status} - ${errorText}
- Brave API error: ${response.status} ${response.statusText}\n
- Brave API error: ${webResponse.status} ${webResponse.statusT
- PPIO API request timeout after ${timeout / 1000}s
- responseBody || response.statusText
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/6dfc59145ed120a5.
Report an issue: GitHub.