moeru-ai/airi · error · Error
Chat Completions does not support file URLs
Error message
Chat Completions does not support file URLs
What it means
writeContent renders a portable file segment into a Chat Completions content part. Chat Completions file input only supports inline file_data or a provider file_id; a segment carrying a URL has no Chat Completions representation, so the renderer throws instead of emitting a part the API would reject.
Solutions
- Replace the URL with inline data: fetch the file yourself and set segment.data to its base64/data-URL content
- Upload the file to the provider's file API and use segment.providerFileId instead of segment.url
- Route this conversation through a protocol/renderer that supports file URLs (e.g. Responses-style rendering) if URL references must be preserved
Example fix
// before
segments: [{ type: 'file', url: 'https://example.com/doc.pdf', name: 'doc.pdf' }]
// after
segments: [{ type: 'file', data: base64OrDataUrl, name: 'doc.pdf' }] Defensive patterns
Strategy: validation
Validate before calling
function chatFileSegmentIsRenderable(seg) {
return seg.type !== 'file' || !seg.url
}
if (!message.segments.every(chatFileSegmentIsRenderable)) throw new Error('File URL segments are not renderable to Chat Completions') Type guard
function hasFileUrl(segments) {
return segments.some(s => s.type === 'file' && typeof s.url === 'string')
} Try / catch
try {
return conversationToChatMessages(conversation, supportsContentArray)
} catch (err) {
if (err.message === 'Chat Completions does not support file URLs') {
// hydrate url segments to inline data or provider ids, then retry
return conversationToChatMessages(await inlineFileUrls(conversation), supportsContentArray)
}
throw err
} Prevention
- Prefer data or providerFileId over url when authoring file segments
- Fetch URL-referenced files and inline them before rendering to Chat Completions
- Upload files to the provider and store the returned file id instead of a link
- If conversations may contain URL file segments, detect them up front and choose a renderer that supports them
When it happens
Trigger: Calling conversationToChatMessages (through renderEntry) with a portable message whose segments include { type: 'file', url: 'https://...' } — i.e. a file referenced by URL rather than inline data or a provider file id.
Common situations: Porting a conversation authored for the Responses API (which supports file URLs) to Chat Completions; building file segments from web links or object-storage URLs; switching providers where the previous API accepted URL-based file references.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Chat file requires exactly one source
- Tool messages require a correlated tool result
- Electron IPC is not available in this renderer context
- Unsupported Chat content part
- Vite did not expose a reachable dev server URL.
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/190040807f130e7c.
Report an issue: GitHub.
Appendix: source
Thrown at packages/core-agent/src/messages/chat-completions.ts:82
}
if (message.role === 'system' || message.role === 'developer')
return { id, role: message.role, segments: typeof message.content === 'string' ? [{ type: 'text', text: message.content }] : message.content.map(part => ({ type: 'text', text: part.text })) }
return { id, role: message.role, segments: chatContentToInputSegments(message.content) }
})
}
/** Reads persisted Chat records; the storage identity scopes turn, round, and invocation ids. */
export function chatMessagesToTurns(messages: Parameters<typeof chatMessagesToProjectionEntries>[0], idPrefix?: string): Turn[] {
return readTurns(chatMessagesToProjectionEntries(messages, idPrefix))
}
function writeContent(segment: MessageSegment): CommonContentPart {
switch (segment.type) {
case 'image': return { type: 'image_url', image_url: { url: segment.url, detail: segment.detail } }
case 'audio': return { type: 'input_audio', input_audio: { data: segment.data, format: segment.format } }
case 'file':
if (segment.url)
throw new Error('Chat Completions does not support file URLs')
return { type: 'file', file: { file_data: segment.data, filename: segment.name, file_id: segment.providerFileId } }
default: return { type: 'text', text: renderSegmentText(segment) }
}
}
function renderEntry(message: ProjectionEntry, supportsContentArray: boolean): ChatMessage[] {
const result: ChatMessage[] = []
const role = message.role === 'context' || message.role === 'event' || message.role === 'summary' ? 'user' : message.role
let parts: CommonContentPart[] = []
let assistantParts: Array<{ type: 'text', text: string } | { type: 'refusal', refusal: string }> = []
let calls: NonNullable<Extract<ChatMessage, { role: 'assistant' }>['tool_calls']> = []
function flush() {
if (role === 'assistant') {
if (assistantParts.length || calls.length) {
const content = !supportsContentArray || assistantParts.every(part => part.type === 'text')
? assistantParts.map(part => part.type === 'text' ? part.text : part.refusal).join('')
: assistantParts
result.push({ role, content, ...(calls.length ? { tool_calls: calls } : {}) })View on GitHub (pinned to 438a067dde)