moeru-ai/airi · error · Error

Responses file requires exactly one source

Error message

Responses file requires exactly one source

What it means

While reading a `function_call_output` back into the portable model, an `input_file` part must provide exactly one of `file_data` (inline base64 data) or `file_url` (a remote URL); providing both, or neither, is ambiguous and rejected. The Responses API treats these as mutually exclusive file sources.

Solutions

  1. Set exactly one of file_data or file_url on each input_file part; null out the other field.
  2. Prefer file_url when the file is remotely reachable, and file_data (base64) when it must be inlined.
  3. Add a guard in the tool's output builder that asserts exactly one source is present before returning the part.
  4. Inspect the raw function_call_output JSON to see which builder set both/neither and fix at the source.

Example fix

// before: both sources set
{ type: 'input_file', file_data: 'aGVsbG8=', file_url: 'https://example.com/hello.txt', filename: 'hello.txt' }

// after: exactly one source
{ type: 'input_file', file_url: 'https://example.com/hello.txt', filename: 'hello.txt' }
Defensive patterns

Strategy: validation

Validate before calling

function assertFilePartSingleSource(part) {
  if (part.type !== 'input_file') return true
  const sources = [part.file_data != null, part.file_url != null].filter(Boolean).length
  if (sources !== 1) throw new Error('input_file must set exactly one of file_data or file_url')
  return true
}

Type guard

const isWellFormedFilePart = (part) => part.type !== 'input_file' || ((part.file_data != null) !== (part.file_url != null))

Try / catch

try {
  await streamResponses({ config, conversation, scope, onEvent })
} catch (e) {
  if (e.message === 'Responses file requires exactly one source') {
    // rebuild the tool's file parts keeping only one source, then retry
  } else throw e
}

Prevention

When it happens

Trigger: A tool emits `{ type: 'input_file' }` with both file_data and file_url set, or with both null/undefined. Happens when a tool builder fills both fields 'to be safe', or when a part is constructed from defaults leaving both unset.

Common situations: Custom tool output builders populating input_file from a file descriptor where both data and URL happen to exist; refactors that renamed fields and left the other one set; constructing parts from a template without clearing unused source fields.

Related errors


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

Appendix: source

Thrown at packages/core-agent/src/runtime/responses.ts:107

  })
}

function readToolResultContent(content: Extract<ItemParam, { type: 'function_call_output' }>['output']): InputSegment[] {
  if (typeof content === 'string')
    return [{ type: 'text', text: content }]
  return content.map((part) => {
    switch (part.type) {
      case 'input_text': return { type: 'text', text: part.text }
      case 'input_image':
        if (!part.image_url)
          throw new Error('Responses image output requires a URL')
        return { type: 'image', url: part.image_url, detail: part.detail ?? undefined }
      case 'input_file':
        if (part.file_data != null && part.file_url == null)
          return { type: 'file', data: part.file_data, name: part.filename ?? undefined }
        if (part.file_url != null && part.file_data == null)
          return { type: 'file', url: part.file_url, name: part.filename ?? undefined }
        throw new Error('Responses file requires exactly one source')
      case 'input_video': throw new Error('Video tool output is not supported by the conversation model')
    }
    throw new Error('Unsupported Responses tool output')
  })
}

type AssistantContent = Exclude<Extract<ItemParam, { role: 'assistant' }>['content'], string>[number]

function readCitations(part: Extract<AssistantContent, { type: 'output_text' }>): Citation[] | undefined {
  return part.annotations?.map(entry => ({
    url: entry.url,
    title: entry.title,
    startIndex: entry.start_index,
    endIndex: entry.end_index,
  }))
}

function readOutput(items: ItemParam[]): ProjectionEntry[] {

View on GitHub (pinned to 438a067dde)