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
- No user action required for the current turn — the wrapper already retried with plain strings.
- Upgrade the OpenAI-compatible server to a version that accepts content-part arrays if you need multimodal/text-part content.
- 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.
- 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
- Send content as a plain string by default when the provider is known-strict.
- Persist contentArrayCompatibility per provider to skip the wasted first 400 on later sessions.
- Update OpenAI-compatible servers to accept string-or-array content (serde untagged).
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
- [llm] Auto-disabling tools for "${key}" due to tool-related
- [llm-streaming-control] signal handler failed
- [llm-streaming-control] handler failed
- [parseActEmotion] Failed to parse ACT payload JSON: "${paylo
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/78068c2ae915aa52.
Report an issue: GitHub.