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

  1. Replace the URL with inline data: fetch the file yourself and set segment.data to its base64/data-URL content
  2. Upload the file to the provider's file API and use segment.providerFileId instead of segment.url
  3. 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

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


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)