chatboxai/chatbox · error · ApiError

JSON.stringify(json)

Error message

JSON.stringify(json)

What it means

Thrown by OpenAICompatibleSDK.listModels when the GET {apiHost}/models response JSON lacks a `data` array. The entire parsed JSON is JSON.stringify'd into the ApiError message so the caller sees the unexpected payload. This is a contract violation: the OpenAI-compatible /models endpoint must return { data: [{ id }, ...] }.

Source

Thrown at src/shared/models/openai-compatible.ts:159

) {
  const headers = {
    Authorization: `Bearer ${params.apiKey}`,
    ...(params.extraHeaders || {}),
  }
  const response = params.customFetch
    ? await params.customFetch(`${params.apiHost}/models`, {
        method: 'GET',
        headers,
      })
    : await dependencies.request.apiRequest({
        url: `${params.apiHost}/models`,
        method: 'GET',
        headers,
        useProxy: params.useProxy,
      })
  const json: ListModelsResponse = await response.json()
  if (!json.data) {
    throw new ApiError(JSON.stringify(json))
  }
  return json.data.map((item) => {
    const modelInfo: ProviderModelInfo = {
      modelId: item.id,
      type: 'chat',
    }

    // Add nickname from OpenRouter name field
    if (item.name) {
      modelInfo.nickname = item.name
    }

    // Add context window if available
    if (item.context_length) {
      modelInfo.contextWindow = item.context_length
    }

    // Add capabilities based on architecture

View on GitHub (pinned to 81571269ad)

Solutions

  1. Verify apiHost points to an OpenAI-compatible /models endpoint and the API key has permission to list models.
  2. Open apiHost/models in a browser/curl and confirm the response shape is { data: [{ id: '...' }, ...] }; if not, switch provider type or fix the host.
  3. Remove trailing slashes and duplicate '/v1' segments in apiHost.
  4. Improve the error to surface response.status alongside the body so auth vs. shape issues are distinguishable.

Example fix

// before
const json: ListModelsResponse = await response.json()
if (!json.data) {
  throw new ApiError(JSON.stringify(json))
}
// after — include status and a hint
const json: ListModelsResponse = await response.json()
if (!json.data) {
  throw new ApiError(`listModels: no 'data' field (status ${response.status}): ${JSON.stringify(json)}`)
}
Defensive patterns

Strategy: validation

Validate before calling

// Smoke-test the /models endpoint shape before relying on it.
async function modelsEndpointIsOpenAICompatible(apiHost: string, headers: Record<string,string>): Promise<boolean> {
  try {
    const res = await fetch(`${apiHost.replace(/\/$/, '')}/models`, { headers })
    const json = await res.json().catch(() => null)
    return Boolean(json && Array.isArray(json.data))
  } catch { return false }
}

Type guard

function isListModelsResponse(v: unknown): v is { data: { id: string }[] } {
  return typeof v === 'object' && v !== null && Array.isArray((v as any).data)
}

Try / catch

try {
  return await sdk.listModels()
} catch (e) {
  if (e instanceof ApiError) {
    // e.message is JSON.stringify(json); parse it back to inspect
    const body = JSON.parse(e.message)
    if (body.error) showUser(`Provider error: ${body.error.message ?? body.error}`)
    else showUser('Unexpected /models response — check apiHost and API key.')
    return []
  }
  throw e
}

Prevention

When it happens

Trigger: response.json() resolves to an object without a `data` field — e.g. the endpoint returned an error envelope ({ error: {...} }), an HTML page parsed as empty, an auth challenge, or a non-OpenAI schema (Anthropic-style, Google-style).

Common situations: Wrong apiHost (pointed at a non-OpenAI-compatible API); missing/invalid API key causing a JSON error body; endpoint requires a different path (e.g. /v1/models vs /models); self-hosted server returns its own error shape; apiHost includes a trailing slash doubling the path.

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/02a1049adc0f6222. Report an issue: GitHub.