{"record":{"id":"78068c2ae915aa52","repo":"moeru-ai/airi","slug":"llm-auto-disabling-content-part-arrays-for-ke","errorCode":null,"errorMessage":"[llm] Auto-disabling content-part arrays for \"${key}\" and retrying once","messagePattern":"\\[llm\\] Auto-disabling content-part arrays for \"(.+?)\" and retrying once","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/stores/ai/chat-llm/llm.ts","lineNumber":51,"sourceCode":"      builtinToolsResolver,\n    })\n\n    try {\n      await runStream()\n    }\n    catch (err) {\n      if (isToolRelatedError(err)) {\n        console.warn(`[llm] Auto-disabling tools for \"${key}\" due to tool-related error`)\n        toolsCompatibility.value.set(key, false)\n      }\n      // NOTICE:\n      // Auto-degrade content-part arrays to plain strings on the next attempt\n      // when the provider returned the Rust/serde-style \"expected a string\"\n      // 400. We retry once inline so the user's failing turn recovers without\n      // requiring them to resend; subsequent calls reuse the cached degrade.\n      // See: https://github.com/moeru-ai/airi/issues/1500\n      if (isContentArrayRelatedError(err) && contentArrayCompatibility.value.get(key) !== false) {\n        console.warn(`[llm] Auto-disabling content-part arrays for \"${key}\" and retrying once`)\n        contentArrayCompatibility.value.set(key, false)\n        await runStream()\n        return\n      }\n      throw err\n    }\n  }\n\n  async function models(apiUrl: string, apiKey: string) {\n    if (apiUrl === '')\n      return []\n\n    try {\n      return await listModels({\n        baseURL: (apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`) as `${string}/`,\n        apiKey,\n      })\n    }","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/stores/ai/chat-llm/llm.ts#L33-L69","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await runStream()\n}\ncatch (err) {\n  if (isContentArrayRelatedError(err) && contentArrayCompatibility.value.get(key) !== false) {\n    contentArrayCompatibility.value.set(key, false)\n    await runStream() // one inline retry with plain-string content\n    return\n  }\n  throw err\n}","preventionTips":["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)."],"tags":["llm","content-parts","serde","provider-compatibility","auto-degrade"],"backgroundTag":"content-array-incompatible","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}