moeru-ai/airi · error · Error

Chat file requires exactly one source

Error message

Chat file requires exactly one source

What it means

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.

Solutions

  1. Ensure each file part sets exactly one of file.file_data or file.file_id, never both and never neither
  2. If you have both a provider id and inline data, pick one: prefer file_id when the file was already uploaded to the provider
  3. Add a validation pass over content parts before calling chatContentToInputSegments to reject ambiguous file parts

Example fix

// before
{ type: 'file', file: { file_data: base64, file_id: 'file_123', filename: 'report.pdf' } }
// after
{ type: 'file', file: { file_id: 'file_123', filename: 'report.pdf' } }
Defensive patterns

Strategy: validation

Validate before calling

function hasExactlyOneFileSource(part) {
  if (part.type !== 'file') return true
  const sources = [part.file.file_data !== undefined, part.file.file_id !== undefined].filter(Boolean).length
  return sources === 1
}
if (!parts.every(hasExactlyOneFileSource)) throw new Error('file part must set exactly one of file_data or file_id')

Type guard

function isValidFilePart(part) {
  return part.type !== 'file' || ((part.file.file_data !== undefined) !== (part.file.file_id !== undefined))
}

Try / catch

try {
  const segments = chatContentToInputSegments(content)
} catch (err) {
  if (err.message === 'Chat file requires exactly one source') {
    console.error('Ambiguous file part:', JSON.stringify(content))
  }
  throw err
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17). Data as JSON: /api/errors/a337a8f40a30ae0f. Report an issue: GitHub.

Appendix: source

Thrown at packages/core-agent/src/messages/chat-completions.ts:31

 * chatContentToInputSegments('hello')
 * // => [{ type: 'text', text: 'hello' }]
 */
export function chatContentToInputSegments(content: string | CommonContentPart[] | undefined): InputSegment[] {
  if (content == null)
    return []
  if (typeof content === 'string')
    return [{ type: 'text', text: content }]
  return content.map((part) => {
    switch (part.type) {
      case 'text': return { type: 'text', text: part.text }
      case 'image_url': return { type: 'image', url: part.image_url.url, detail: part.image_url.detail }
      case 'input_audio': return { type: 'audio', ...part.input_audio }
      case 'file':
        if (part.file.file_data !== undefined && part.file.file_id === undefined)
          return { type: 'file', data: part.file.file_data, name: part.file.filename }
        if (part.file.file_id !== undefined && part.file.file_data === undefined)
          return { type: 'file', providerFileId: part.file.file_id, name: part.file.filename }
        throw new Error('Chat file requires exactly one source')
    }
    throw new Error('Unsupported Chat content part')
  })
}

/**
 * Reads Chat-shaped storage or SDK output into portable message semantics.
 * This is an ingress boundary; Responses never calls the Chat request renderer.
 *
 * @example
 * chatMessagesToProjectionEntries([{ role: 'user', content: 'Hello' }])[0].segments
 * // => [{ type: 'text', text: 'Hello' }]
 */
export function chatMessagesToProjectionEntries(messages: (ChatMessage | { role: 'error', content: string })[], idPrefix = 'message'): ProjectionEntry[] {
  return messages.map((message, index) => {
    const id = `${idPrefix}-${index}`
    if (message.role === 'error')
      return { id, role: 'user', segments: [{ type: 'text', text: `User encountered error: ${message.content}` }] }

View on GitHub (pinned to 438a067dde)