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

  1. 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.
  2. For 5xx or 429, retry (the poll loop already does up to maxTransientRetries; for submit, add an outer retry with backoff).
  3. Verify the model id is still listed in PPIO's catalog and matches a PpioModelDescriptor.
  4. 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

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


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/6dfc59145ed120a5. Report an issue: GitHub.