{"record":{"id":"a337a8f40a30ae0f","repo":"moeru-ai/airi","slug":"chat-file-requires-exactly-one-source","errorCode":null,"errorMessage":"Chat file requires exactly one source","messagePattern":"Chat file requires exactly one source","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core-agent/src/messages/chat-completions.ts","lineNumber":31,"sourceCode":" * chatContentToInputSegments('hello')\n * // => [{ type: 'text', text: 'hello' }]\n */\nexport function chatContentToInputSegments(content: string | CommonContentPart[] | undefined): InputSegment[] {\n  if (content == null)\n    return []\n  if (typeof content === 'string')\n    return [{ type: 'text', text: content }]\n  return content.map((part) => {\n    switch (part.type) {\n      case 'text': return { type: 'text', text: part.text }\n      case 'image_url': return { type: 'image', url: part.image_url.url, detail: part.image_url.detail }\n      case 'input_audio': return { type: 'audio', ...part.input_audio }\n      case 'file':\n        if (part.file.file_data !== undefined && part.file.file_id === undefined)\n          return { type: 'file', data: part.file.file_data, name: part.file.filename }\n        if (part.file.file_id !== undefined && part.file.file_data === undefined)\n          return { type: 'file', providerFileId: part.file.file_id, name: part.file.filename }\n        throw new Error('Chat file requires exactly one source')\n    }\n    throw new Error('Unsupported Chat content part')\n  })\n}\n\n/**\n * Reads Chat-shaped storage or SDK output into portable message semantics.\n * This is an ingress boundary; Responses never calls the Chat request renderer.\n *\n * @example\n * chatMessagesToProjectionEntries([{ role: 'user', content: 'Hello' }])[0].segments\n * // => [{ type: 'text', text: 'Hello' }]\n */\nexport function chatMessagesToProjectionEntries(messages: (ChatMessage | { role: 'error', content: string })[], idPrefix = 'message'): ProjectionEntry[] {\n  return messages.map((message, index) => {\n    const id = `${idPrefix}-${index}`\n    if (message.role === 'error')\n      return { id, role: 'user', segments: [{ type: 'text', text: `User encountered error: ${message.content}` }] }","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/core-agent/src/messages/chat-completions.ts#L13-L49","documentation":"chatContentToInputSegments converts Chat Completions content parts into portable input segments. A 'file' part must carry exactly one of file_data (inline bytes/data URL) or file_id (provider-uploaded file reference); carrying neither, or both, is ambiguous, so the library throws instead of guessing a precedence.","triggerScenarios":"Passing a Chat content part { type: 'file', file: {} } (no source), { type: 'file', file: { file_data, file_id } } (both sources set), or file_data === undefined && file_id === undefined through chatContentToInputSegments (called by chatMessagesToProjectionEntries and replaceToolCallResult).","commonSituations":"Hand-constructing file content parts without filling either field; deserializing stored Chat messages where one field was dropped; merging part objects where a provider file id was added on top of inline data; switching providers and copying both fields defensively.","solutions":["Ensure each file part sets exactly one of file.file_data or file.file_id, never both and never neither","If you have both a provider id and inline data, pick one: prefer file_id when the file was already uploaded to the provider","Add a validation pass over content parts before calling chatContentToInputSegments to reject ambiguous file parts"],"exampleFix":"// before\n{ type: 'file', file: { file_data: base64, file_id: 'file_123', filename: 'report.pdf' } }\n// after\n{ type: 'file', file: { file_id: 'file_123', filename: 'report.pdf' } }","handlingStrategy":"validation","validationCode":"function hasExactlyOneFileSource(part) {\n  if (part.type !== 'file') return true\n  const sources = [part.file.file_data !== undefined, part.file.file_id !== undefined].filter(Boolean).length\n  return sources === 1\n}\nif (!parts.every(hasExactlyOneFileSource)) throw new Error('file part must set exactly one of file_data or file_id')","typeGuard":"function isValidFilePart(part) {\n  return part.type !== 'file' || ((part.file.file_data !== undefined) !== (part.file.file_id !== undefined))\n}","tryCatchPattern":"try {\n  const segments = chatContentToInputSegments(content)\n} catch (err) {\n  if (err.message === 'Chat file requires exactly one source') {\n    console.error('Ambiguous file part:', JSON.stringify(content))\n  }\n  throw err\n}","preventionTips":["Set only one of file_data or file_id when building file parts","Validate content parts before ingress with a small predicate over part.type","When migrating from one provider to another, strip the other provider's file id field","Never merge raw provider part objects; build parts explicitly"],"tags":["chat-completions","file-content","validation","ingress"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}