동일한 Python 코드를 사용하여 OpenAI, Claude, Gemini API에 이미지 및 PDF를 전송하는 방법
요약
OpenAI, Anthropic, Google의 API를 사용하여 이미지와 PDF를 전송할 때 발생하는 서로 다른 요청 구조를 통합하는 방법을 설명합니다. 'llm-api-adapter' 라이브러리를 활용해 단일 Python 인터페이스로 멀티 모델 멀티모달 입력을 처리하는 튜토리얼을 제공합니다.
핵심 포인트
- llm-api-adapter를 사용하여 모델별 상이한 API 형식을 통합 관리 가능
- 이미지 및 PDF 데이터를 URL 또는 바이트(bytes) 형태로 전송하는 방법 안내
- OpenAI, Claude, Gemini SDK 없이 requests 라이브러리만으로 동작
- MIME 타입을 명시하여 확장자가 없는 URL 이미지 처리 가능
OpenAI, Anthropic의 Claude, 그리고 Google의 Gemini는 모두 이미지와 PDF를 처리할 수 있습니다. 번거로운 부분은 애플리케이션이 이들 중 하나 이상을 지원해야 할 때 시작됩니다. 각 API는 서로 다른 요청 구조(request structure), 서로 다른 필드 이름, 그리고 때로는 서로 다른 기능(capabilities)을 요구하기 때문입니다.
이 튜토리얼은 동일한 Python 인터페이스를 통해 세 가지 제공업체 모두에 이미지와 PDF를 전송하는 방법을 보여줍니다. 예제에서는 각 제공업체를 직접 호출하고 공유된 UserMessage를 필요한 와이어 포맷(wire format)으로 변환하는 경량 SDK인 llm-api-adapter를 사용합니다.
Repository: github.com/Inozem/llm_api_adapter
라이브러리 설치
pip install "llm-api-adapter>=0.5.1"
PDF 입력을 위해서는 0.5.1 이상의 버전이 필요합니다. 이미지 입력은 0.5.0에서 도입되었습니다.
이 패키지는 OpenAI, Anthropic 또는 Google SDK를 요구하지 않습니다. 요청을 직접 전송하며 유일한 런타임 의존성(runtime dependency)으로 requests를 사용합니다.
제공업체(Provider) 설정
호출하려는 제공업체와 모델에 대해 하나의 어댑터(adapter)를 생성합니다:
import os
from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
...
아래의 메시지 예제 파일들은 해당 설정과 독립적입니다. Claude 또는 Gemini를 사용하려면 다른 어댑터 인스턴스를 생성하지만, UserMessage, ImagePart, 그리고 DocumentPart는 변경되지 않고 그대로 유지됩니다.
URL로부터 이미지 전송하기
가장 간단한 옵션은 공개 URL이 있는 이미지입니다:
from llm_api_adapter.models.messages.chat_message import UserMessage
from llm_api_adapter.models.messages.file_parts import ImagePart
...
ImagePart는 URL 확장자로부터 MIME 타입을 감지합니다. 이 예제에서 .jpg는 image/jpeg가 됩니다.
공유 인터페이스 하에서, 어댑터는 이 파일을 선택된 제공업체에 필요한 형식으로 변환합니다. 애플리케이션은 별도의 OpenAI, Claude, Gemini 메시지 빌더(message builders)를 가질 필요가 없습니다.
확장자가 없는 이미지 URL
일부 이미지 URL은 파일명으로 끝나지 않습니다:
이런 경우에는 MIME 타입 (MIME type)을 명시적으로 전달하세요:
image = ImagePart(
url="https://api.example.com/files/42",
media_type="image/jpeg",
...
이렇게 하면 응답 헤더 (response headers)를 통해 추측하거나 메시지 직렬화 (serialization) 과정에서 파일을 다운로드할 필요가 없습니다.
바이트 (bytes)로부터 이미지 전송하기
이미지가 로컬에 있거나, 애플리케이션에서 생성되었거나, 인증 뒤에 저장되어 있거나, 또는 이미 메모리에 있는 경우에는 바이트 (bytes)를 사용하세요:
from llm_api_adapter.models.messages.chat_message import UserMessage
from llm_api_adapter.models.messages.file_parts import ImagePart
...
가공되지 않은 바이트 (raw bytes)를 전달할 때는 media_type이 필수입니다. 어댑터 (adapter)는 제공자 (provider) 요청을 생성할 때만 데이터를 base64 인코딩 (base64-encodes)합니다.
한 메시지에 여러 이미지 전송하기
files 필드는 리스트 (list)를 허용하므로, 동일한 인터페이스 (interface)를 비교 분석이나 전후 비교 (before-and-after analysis)에 사용할 수 있습니다:
message = UserMessage(
"Compare these two images and list the visible differences.",
files=[
...
텍스트는 사용자 메시지의 첫 번째 부분으로 유지되며, 그 뒤에 제공된 순서대로 파일들이 뒤따릅니다.
바이트 (bytes)로부터 PDF 전송하기
PDF 입력에는 DocumentPart를 사용합니다. 나머지 요청 방식은 이미지 예시와 동일합니다:
from llm_api_adapter.models.messages.chat_message import UserMessage
from llm_api_adapter.models.messages.file_parts import DocumentPart
...
PDF 바이트 (bytes)는 가장 이식성 높은 문서 경로를 제공합니다. 이는 지원되는 OpenAI, Anthropic, Google 요청 형식 전반에서 작동합니다.
DocumentPart는 현재 모든 가능한 문서 형식이 아닌 PDF 입력을 나타냅니다. 이는 의도된 설계입니다. 이식성 있어 보이지만 제공자마다 다르게 실패하는 일반적인 FilePart보다는, 좁고 검증된 계약 (contract)이 더 유용하기 때문입니다.
URL로부터 PDF 전송하기
URL로부터 문서 파트 (document part)를 생성할 수도 있습니다:
message = UserMessage(
"Extract the main conclusions from this report.",
files=[
...
DocumentPart는 .pdf URL 확장자를 통해 application/pdf를 감지합니다. 확장자가 없는 URL의 경우, MIME 타입을 명시적으로 전달하세요:
document = DocumentPart(
url="https://api.example.com/files/42",
media_type="application/pdf",
...
PDF URL 지원에는 한 가지 중요한 호환성 경계가 있습니다:
- Anthropic은 PDF URL을 허용합니다.
- Google은 PDF URL을 허용합니다.
- OpenAI Responses는 PDF URL을 허용합니다.
- OpenAI Chat Completions는 PDF 바이트 (bytes)는 허용하지만, PDF URL은 허용하지 않습니다.
선택된 OpenAI 경로가 PDF URL을 표현할 수 없는 경우, 어댑터는 파일을 조용히 다운로드하거나, 엔드포인트를 변경하거나, 잘못된 페이로드 (payload)를 보내는 대신 명확한 ValueError를 발생시킵니다.
해당 경로의 경우, 사용자의 애플리케이션에서 자체적인 타임아웃 (timeout), 인증 (authentication), 크기 및 보안 규칙에 따라 파일을 다운로드한 다음, 이를 바이트 (bytes)로 전달하세요.
OpenAI, Claude, Gemini 간의 전환
메시지는 특정 제공자 (provider)에 종속되지 않습니다:
message = UserMessage(
"Describe this image.",
files=[ImagePart(url="https://example.com/photo.jpg")],
...
어댑터 설정만 변경하면 됩니다:
import os
from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
...
제공자를 전환할 때 이미지나 문서 메시지를 다시 구축할 필요가 없습니다. 이것이 추상화 (abstraction)의 주요 경계입니다: 애플리케이션 중심의 이식 가능한 입력을 정규화 (normalize)한 다음, 각 API에 맞게 별도로 직렬화 (serialize)합니다.
호환성 매트릭스 (Compatibility matrix)
| 입력 (Input) | OpenAI | Claude | Gemini |
|---|---|---|---|
| 이미지 URL (Image URL) | 예 | 예 | 예 |
| ... |
정확한 모델 또한 요청된 모달리티 (modality)를 지원해야 합니다. 유효한 제공자 페이로드 (provider payload)라 할지라도 텍스트 전용 모델이 이미지나 문서를 수락하게 만들 수는 없습니다.
일반적인 오류
MIME 타입 없이 바이트 전달
다음은 유효하지 않습니다:
ImagePart(data=image_bytes)
어댑터는 임의의 바이트로부터 MIME 타입을 안전하게 추론할 수 없습니다. 명시적으로 전달하세요:
ImagePart(
data=image_bytes,
media_type="image/png",
...
동일한 규칙이 PDF 바이트에도 적용됩니다:
DocumentPart(
data=pdf_bytes,
media_type="application/pdf",
...
URL과 bytes를 모두 제공하는 경우
파일 파트(file part)는 반드시 하나의 소스만 가져야 합니다:
ImagePart(
url="https://example.com/photo.jpg",
data=image_bytes,
...
이는 모호하며 에러를 발생시킵니다. url 또는 data 중 하나만 사용하세요.
잘못된 파일 클래스 사용
ImagePart는 image/* MIME 타입을 검증합니다. DocumentPart는 현재 application/pdf를 검증합니다.
다음은 조기에 실패하는 사례입니다:
ImagePart(
data=pdf_bytes,
media_type="application/pdf",
...
요청이 이미 전송된 후 제공자(provider)별 HTTP 에러를 기다리는 것보다 조기 검증(Early validation)을 하는 것이 더 바람직합니다.
OpenAI Chat Completions를 통해 PDF URL 전송하기
해당 API 경로에는 PDF bytes를 사용하거나, Responses API를 통해 라우팅되는 OpenAI 모델을 선택하세요. 어댑터(adapter)는 이러한 차이점을 숨기지 않는데, 이를 숨기는 것은 애플리케이션의 동작을 변경하기 때문입니다.
어댑터가 정규화(normalize)하는 것
공통 Python 인터페이스는 반복적인 페이로드(payload) 변환 작업을 숨겨줍니다:
- OpenAI는 Chat Completions와 Responses에 대해 서로 다른 이미지 및 파일 블록을 사용합니다.
- Anthropic은 URL 또는 base64 소스를 가진 이미지 또는 문서 콘텐츠 블록을 사용합니다.
- Google은 URL에는
fileData를, bytes에는inlineData를 사용합니다.
어댑터는 ImagePart와 DocumentPart를 이러한 구조로 변환합니다. 어댑터는 모든 제공자가 모든 엔드포인트를 통해 모든 소스 타입을 지원하는 것처럼 가장하지 않습니다.
이러한 구분이 중요합니다. 유용한 멀티 제공자(multi-provider) SDK는 제공자별 애플리케이션 코드를 줄여주어야 하지만, 실제 기능적 경계는 명시적인 에러로 노출해야 합니다.
이 접근 방식이 유용한 경우
이 인터페이스는 다음과 같은 상황에 적합합니다:
- 애플리케이션이 OpenAI, Claude 또는 Gemini를 직접 호출할 때
- 메시지 객체를 다시 구축하지 않고 제공자를 변경하고 싶을 때
- URL과 메모리 내(in-memory) 파일 입력이 모두 필요할 때
- 에이전트 프레임워크(agent framework)나 호스팅된 프록시(hosted proxy) 대신 가벼운 SDK를 원할 때
- 지원되지 않는 조합의 경우 HTTP 요청이 발생하기 전에 실패하기를 원할 때
애플리케이션이 제공자(provider)를 절대 변경하지 않거나, 특정 제공자 전용의 파일 저장 워크플로우(file storage workflow)에 크게 의존하는 경우에는 제공자 네이티브 SDK(provider-native SDK)가 더 나은 선택일 수 있습니다.
전체 예제
다음 예제는 제공자를 설정 가능하게 유지하면서 로컬 PDF를 전송하는 방법을 보여줍니다:
import os
from llm_api_adapter.models.messages.chat_message import UserMessage
...
LLM_PROVIDER와 그에 상응하는 모델 및 API 키 변수를 변경하세요. 문서 메시지(document message)는 변경되지 않고 그대로 유지됩니다.
이 목표는 세 개의 서로 다른 API를 완전히 동일하게 만드는 것이 아닙니다. 기능이 진정으로 이식 가능한(portable) 부분에서는 애플리케이션 코드를 안정적으로 유지하고, 나머지 차이점은 명시적으로 만드는 것이 목표입니다.
소스 코드는 다음 리포지토리에서 확인할 수 있습니다:
github.com/Inozem/llm_api_adapter
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기