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
- 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
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
- 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
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
- Chat Completions does not support file URLs
- Unsupported Chat content part
- A motion sample occurs after the recording duration.
- Aliyun NLS credentials are incomplete.
- Archive entry not found
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)