moeru-ai/airi · warning

[llm] Auto-disabling content-part arrays for "${key}" and re

Error message

[llm] Auto-disabling content-part arrays for "${key}" and retrying once

What it means

The chat LLM wrapper caught the provider error matched by isContentArrayRelatedError() — the Rust/serde-style 400 where an OpenAI-compatible server answers 'expected a string' because it cannot parse content sent as an array of typed parts ({type:'text',...}). Referenced as moeru-ai/airi#1500. The wrapper flips contentArrayCompatibility[key] = false and immediately retries once with plain-string content so the user's turn still completes.

Source

Thrown at packages/stage-ui/src/stores/ai/chat-llm/llm.ts:51

      builtinToolsResolver,
    })

    try {
      await runStream()
    }
    catch (err) {
      if (isToolRelatedError(err)) {
        console.warn(`[llm] Auto-disabling tools for "${key}" due to tool-related error`)
        toolsCompatibility.value.set(key, false)
      }
      // NOTICE:
      // Auto-degrade content-part arrays to plain strings on the next attempt
      // when the provider returned the Rust/serde-style "expected a string"
      // 400. We retry once inline so the user's failing turn recovers without
      // requiring them to resend; subsequent calls reuse the cached degrade.
      // See: https://github.com/moeru-ai/airi/issues/1500
      if (isContentArrayRelatedError(err) && contentArrayCompatibility.value.get(key) !== false) {
        console.warn(`[llm] Auto-disabling content-part arrays for "${key}" and retrying once`)
        contentArrayCompatibility.value.set(key, false)
        await runStream()
        return
      }
      throw err
    }
  }

  async function models(apiUrl: string, apiKey: string) {
    if (apiUrl === '')
      return []

    try {
      return await listModels({
        baseURL: (apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`) as `${string}/`,
        apiKey,
      })
    }

View on GitHub (pinned to 677329427f)

Solutions

  1. No user action required for the current turn — the wrapper already retried with plain strings.
  2. Upgrade the OpenAI-compatible server to a version that accepts content-part arrays if you need multimodal/text-part content.
  3. If it recurs every session, note the compatibility map is per provider key and resets on reload; fixing the server removes the extra round trip.
  4. For proxies, relax JSON typing of request.content to accept string-or-array (serde untagged).
Defensive patterns

Strategy: fallback

Try / catch

try {
  await runStream()
}
catch (err) {
  if (isContentArrayRelatedError(err) && contentArrayCompatibility.value.get(key) !== false) {
    contentArrayCompatibility.value.set(key, false)
    await runStream() // one inline retry with plain-string content
    return
  }
  throw err
}

Prevention

When it happens

Trigger: Streaming a chat turn where message content is a content-part array to an OpenAI-compatible backend whose serde deserializer only accepts a string; the first attempt 400s, the logged downgrade happens, and the inline retry with string content succeeds.

Common situations: Self-hosted servers (llama.cpp server, older vLLM, various Rust-based proxies) with strict content typing; multimodal-style payloads hitting a text-only schema; version drift after upgrading a local inference server.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/78068c2ae915aa52. Report an issue: GitHub.