포스트

LangChain `ChatOpenRouter` 완전 가이드

LangChain `ChatOpenRouter` 완전 가이드

작성 기준: 2025년 4월 공식 LangChain 문서 및 최신 릴리즈 정보 반영
원문 출처: https://docs.langchain.com/oss/python/integrations/chat/openrouter


목차

  1. 개요 및 배경
  2. OpenRouter란 무엇인가
  3. ChatOpenRouter의 탄생 배경
  4. 전체 아키텍처 다이어그램
  5. 통합 패키지 상세 정보
  6. 설치 및 인증 설정
  7. 모델 인스턴스화 및 기본 호출
  8. 스트리밍
  9. 도구 호출 (Tool Calling)
  10. 구조화된 출력 (Structured Output)
  11. 추론 출력 (Reasoning Output)
  12. 멀티모달 입력
  13. 토큰 사용량 메타데이터
  14. 응답 메타데이터
  15. 프로바이더 라우팅
  16. 앱 어트리뷰션 (Attribution)
  17. LangSmith 트레이싱 연동
  18. 실전 패턴 및 베스트 프랙티스
  19. ChatOpenAI 방식과의 비교
  20. JavaScript/TypeScript 지원

1. 개요 및 배경

ChatOpenRouter는 LangChain이 OpenRouter 플랫폼과 공식적으로 통합하기 위해 만든 퍼스트파티(first-party) 패키지다. 기존에 개발자들이 ChatOpenAIbase_url을 OpenRouter 엔드포인트로 교체하는 편법을 많이 사용해왔는데, 이 방식은 여러 문제—구조화된 출력 깨짐, 추론 콘텐츠 누락, 프로바이더별 기능 미지원, 트레이싱 오류 등—를 지속적으로 유발했다. LangChain 팀은 이 문제를 근본적으로 해결하기 위해 공식 OpenRouter Python SDK를 기반으로 전용 통합 패키지를 구축했다.

2025년 4월 현재 langchain-openrouter 패키지는 베타(beta) 상태이며, Python 환경에서 모든 핵심 기능을 완전히 지원한다. JavaScript/TypeScript용 @langchain/openrouter도 별도로 제공된다.


2. OpenRouter란 무엇인가

OpenRouter는 멀티 프로바이더 LLM 통합 API다. 단일 엔드포인트(https://openrouter.ai/api/v1)를 통해 OpenAI, Anthropic, Google, Meta, DeepSeek, Mistral 등 수십 개의 AI 모델을 호출할 수 있는 플랫폼이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
                         ┌──────────────┐
                         │  개발자 앱    │
                         └──────┬───────┘
                                │ 단일 API 키 / 단일 엔드포인트
                         ┌──────▼───────┐
                         │  OpenRouter  │
                         │  (라우터)    │
                         └──────┬───────┘
          ┌─────────────┬───────┼─────────────┬─────────────┐
    ┌─────▼────┐  ┌─────▼────┐ ┌▼──────────┐ ┌▼──────────┐ ┌▼──────────┐
    │ Anthropic│  │  OpenAI  │ │  Google   │ │   Meta    │ │ DeepSeek  │
    │ Claude   │  │  GPT-4o  │ │ Gemini    │ │  Llama    │ │    R1     │
    └──────────┘  └──────────┘ └───────────┘ └───────────┘ └───────────┘

OpenRouter의 주요 가치는 다음과 같다.

  • 단일 API 키로 수백 개의 모델에 접근 가능
  • 프로바이더별 비용, 속도, 레이턴시 기반 자동 라우팅
  • 프롬프트 캐싱, 추론 토큰, 멀티모달 입력 등 고급 기능 통일 인터페이스 제공
  • 데이터 수집 거부 정책 설정 가능 (프라이버시 요구사항 충족)

3. ChatOpenRouter의 탄생 배경

기존 방식의 문제점

ChatOpenAIbase_url을 교체하는 방식은 이론적으로는 작동하지만 실제로 여러 한계가 있었다.

문제 영역구체적 증상
구조화된 출력with_structured_output 호출 시 스키마 불일치
추론 콘텐츠reasoning 블록이 응답에 포함되지 않음
프로바이더 기능cache_control, data_collection 등 파라미터 미전달
LangSmith 트레이싱모든 모델이 openai로 표시되어 디버깅 불가
스트리밍일부 프로바이더의 빈 청크로 인한 토큰 누락

공식 통합 패키지의 해결

langchain-openrouter는 공식 OpenRouter Python SDK(openrouter 패키지)를 기반으로 구축되어 위 문제를 근본적으로 해결한다. LangSmith 트레이싱은 실제 프로바이더명(anthropic, openai 등)을 정확히 기록하며, reasoning 블록도 content_blocks를 통해 올바르게 노출된다.


4. 전체 아키텍처 다이어그램

graph TD
    A[개발자 코드] -->|import| B[langchain_openrouter]
    B --> C[ChatOpenRouter 클래스]
    C --> D[공식 OpenRouter Python SDK]
    D -->|HTTPS REST| E[OpenRouter API\nhttps://openrouter.ai/api/v1]

    E -->|라우팅| F{프로바이더 선택}
    F -->|order / only| G[Anthropic\nClaude Sonnet/Opus]
    F -->|order / only| H[OpenAI\nGPT-4o / o3]
    F -->|order / only| I[Google\nGemini 2.5 Pro]
    F -->|order / only| J[Meta\nLlama 4 Maverick]
    F -->|order / only| K[DeepSeek\nR1]

    C --> L[LangChain 표준 인터페이스]
    L --> M[invoke / stream / astream]
    L --> N[bind_tools]
    L --> O[with_structured_output]
    L --> P[LangSmith 트레이싱]

    style C fill:#4A90D9,color:#fff
    style E fill:#F5A623,color:#fff
    style L fill:#7ED321,color:#fff
sequenceDiagram
    participant Dev as 개발자
    participant LC as LangChain\nChatOpenRouter
    participant OR as OpenRouter API
    participant Prov as AI 프로바이더\n(e.g., Anthropic)

    Dev->>LC: model.invoke(messages)
    LC->>LC: 파라미터 직렬화\n(tools, reasoning, cache_control 등)
    LC->>OR: POST /v1/chat/completions\n+ OPENROUTER_API_KEY
    OR->>OR: 프로바이더 라우팅 결정\n(비용/속도/순서 기반)
    OR->>Prov: 실제 모델 API 호출
    Prov-->>OR: 응답 (reasoning 블록 포함)
    OR-->>LC: 정규화된 응답
    LC-->>Dev: AIMessage\n(content_blocks, usage_metadata, response_metadata)

5. 통합 패키지 상세 정보

Python 패키지

항목
클래스명ChatOpenRouter
패키지명langchain-openrouter
PyPI 주소https://pypi.org/project/langchain-openrouter/
직렬화 지원beta
JS/TS 지원❌ (Python 전용, JS는 별도 패키지)
기반 SDK공식 openrouter Python SDK

지원 기능 매트릭스

기능지원 여부
Tool Calling (도구 호출)
Structured Output (구조화된 출력)
Image Input (이미지 입력)
Audio Input (오디오 입력)
Video Input (비디오 입력)
Token-level Streaming (토큰 스트리밍)
Native Async (네이티브 비동기)
Token Usage (토큰 사용량 추적)
Logprobs (로그 확률)
Reasoning Tokens (추론 토큰)
Prompt Caching (프롬프트 캐싱)

6. 설치 및 인증 설정

패키지 설치

pip 또는 uv 중 선호하는 방식으로 설치한다.

1
2
3
4
5
# pip 사용
pip install -U langchain-openrouter

# uv 사용 (권장 — 더 빠른 의존성 해결)
uv add langchain-openrouter

API 키 발급

  1. https://openrouter.ai/ 에서 계정 생성
  2. https://openrouter.ai/settings/keys 에서 API 키 생성
  3. 환경 변수로 설정
1
2
3
4
5
6
import getpass
import os

# OPENROUTER_API_KEY 환경 변수 설정
if not os.getenv("OPENROUTER_API_KEY"):
    os.environ["OPENROUTER_API_KEY"] = getpass.getpass("OpenRouter API 키 입력: ")

또는 .env 파일에 설정:

1
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxx

LangSmith 트레이싱 연동 (선택사항)

LangSmith를 사용하면 모든 LLM 호출을 시각적으로 추적하고 디버깅할 수 있다. ChatOpenRouter는 LangSmith와 통합 시 실제 프로바이더명을 올바르게 기록한다는 점이 이전 ChatOpenAI 방식과의 핵심 차이점이다.

1
2
os.environ["LANGSMITH_API_KEY"] = getpass.getpass("LangSmith API 키 입력: ")
os.environ["LANGSMITH_TRACING"] = "true"

7. 모델 인스턴스화 및 기본 호출

인스턴스 생성

1
2
3
4
5
6
7
8
9
10
from langchain_openrouter import ChatOpenRouter

model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",  # OpenRouter 모델 식별자
    temperature=0,                          # 0: 결정적 출력, 1: 창의적 출력
    max_tokens=1024,                        # 최대 생성 토큰 수
    max_retries=2,                          # 실패 시 재시도 횟수 (각 150초 backoff 추가)
    # api_key="...",                        # 코드 내 직접 주입도 가능
    # timeout_ms=30000,                     # 요청 타임아웃 (밀리초)
)

model 파라미터에는 OpenRouter 모델 식별자를 {provider}/{model-name} 형식으로 지정한다. 사용 가능한 전체 모델 목록은 https://openrouter.ai/models 에서 확인할 수 있다.

주요 모델 예시:

  • anthropic/claude-sonnet-4.5 — Claude Sonnet 4.5
  • anthropic/claude-opus-4.6 — Claude Opus 4.6
  • openai/gpt-4o — GPT-4o
  • openai/gpt-4.1 — GPT-4.1
  • openai/o3 — OpenAI o3 (추론 모델)
  • google/gemini-2.5-pro-preview — Gemini 2.5 Pro
  • google/gemini-2.5-flash — Gemini 2.5 Flash
  • meta-llama/llama-4-maverick — Llama 4 Maverick
  • deepseek/deepseek-r1 — DeepSeek R1 (추론 모델)

기본 호출 (invoke)

1
2
3
4
5
6
7
8
9
10
11
messages = [
    (
        "system",
        "당신은 영어를 프랑스어로 번역하는 도우미입니다. 사용자 문장을 번역하세요.",
    ),
    ("human", "I love programming."),
]

ai_msg = model.invoke(messages)
print(ai_msg.content)
# "J'adore la programmation."

invoke는 동기적으로 전체 응답을 받아온다. 메시지는 튜플 (role, content) 또는 LangChain 메시지 객체 형태로 전달할 수 있다.


8. 스트리밍

긴 텍스트 생성 시 토큰이 생성되는 즉시 화면에 출력하려면 stream 또는 astream을 사용한다.

동기 스트리밍

1
2
for chunk in model.stream("바다에 대한 짧은 시를 써줘."):
    print(chunk.text, end="", flush=True)

비동기 스트리밍

1
2
async for chunk in model.astream("바다에 대한 짧은 시를 써줘."):
    print(chunk.text, end="", flush=True)

비동기 스트리밍은 FastAPI나 비동기 서버 환경에서 응답성을 크게 향상시킨다.

스트리밍 중 토큰 사용량 수집

스트리밍 완료 후 마지막 청크에 집계된 토큰 사용량이 포함된다.

1
2
3
4
5
6
7
stream = model.stream("농담 하나 해줘.")
full = next(stream)
for chunk in stream:
    full += chunk  # 청크 누적

print(full.usage_metadata)
# {'input_tokens': 12, 'output_tokens': 25, 'total_tokens': 37}

9. 도구 호출 (Tool Calling)

ChatOpenRouter는 OpenAI 호환 도구 호출 형식을 사용한다. 도구의 이름, 설명, 파라미터 스키마를 정의하면, 모델이 적절한 도구를 선택하고 그 인자를 JSON으로 반환한다.

도구 바인딩

Pydantic 클래스, dict 스키마, LangChain 도구, 함수 등 다양한 형태로 도구를 바인딩할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pydantic import BaseModel, Field


class GetWeather(BaseModel):
    """특정 위치의 현재 날씨를 가져옵니다."""
    location: str = Field(description="도시와 주, 예: San Francisco, CA")


class SearchWeb(BaseModel):
    """웹에서 정보를 검색합니다."""
    query: str = Field(description="검색 쿼리 문자열")


# 여러 도구를 한 번에 바인딩
model_with_tools = model.bind_tools([GetWeather, SearchWeb])

도구 호출 실행

1
2
3
4
5
6
7
8
ai_msg = model_with_tools.invoke("샌프란시스코 날씨가 어때?")

# tool_calls 속성에 표준화된 도구 호출 정보가 담김
print(ai_msg.tool_calls)
# [{'name': 'GetWeather',
#   'args': {'location': 'San Francisco, CA'},
#   'id': 'call_abc123',
#   'type': 'tool_call'}]

tool_calls 속성은 모델 프로바이더에 무관한 표준화된 형식으로 도구 호출 정보를 제공한다. 이를 통해 Anthropic이든 OpenAI든 동일한 코드로 도구 결과를 처리할 수 있다.

Strict 모드

strict=True를 전달하면 모델 출력이 JSON 스키마와 정확히 일치하도록 강제한다. 프로덕션 환경에서 스키마 준수가 중요한 경우 권장된다.

1
model_with_tools = model.bind_tools([GetWeather], strict=True)

도구 호출 흐름

sequenceDiagram
    participant User as 사용자
    participant Model as ChatOpenRouter
    participant Tool as 실제 도구 함수

    User->>Model: "샌프란시스코 날씨?"
    Model-->>User: AIMessage\n(tool_calls=[{name: GetWeather, args: {location: ...}}])
    User->>Tool: GetWeather(location="San Francisco, CA")
    Tool-->>User: "맑음, 18°C"
    User->>Model: ToolMessage(content="맑음, 18°C")
    Model-->>User: "샌프란시스코는 현재 맑고 18°C입니다."

10. 구조화된 출력 (Structured Output)

with_structured_output 메서드를 사용하면 모델이 Pydantic 객체나 dict 형태의 구조화된 응답을 반환하도록 강제할 수 있다. 두 가지 방식이 제공된다.

방식 1: function_calling (기본값)

도구 호출 메커니즘을 통해 구조화된 출력을 얻는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain_openrouter import ChatOpenRouter
from pydantic import BaseModel, Field

model = ChatOpenRouter(model="openai/gpt-4.1")

class Movie(BaseModel):
    """영화 정보."""
    title: str = Field(description="영화 제목")
    year: int = Field(description="개봉 연도")
    director: str = Field(description="감독 이름")
    rating: float = Field(description="10점 만점 평점")

structured_model = model.with_structured_output(Movie)
response = structured_model.invoke("영화 인셉션에 대한 정보를 알려줘")
print(response)
# Movie(title='Inception', year=2010, director='Christopher Nolan', rating=8.8)

방식 2: json_schema

JSON Schema 기반 구조화 출력 방식이다. 일부 모델에서 더 안정적으로 작동한다.

1
structured_model = model.with_structured_output(Movie, method="json_schema")

Strict 모드

1
structured_model = model.with_structured_output(Movie, method="json_schema", strict=True)

strict=Truefunction_callingjson_schema 방식에서 사용 가능하며, json_mode에서는 지원되지 않는다.

에이전트 응답 포맷 지정

에이전트의 최종 응답을 구조화된 형태로 받고 싶을 때 ProviderStrategy를 활용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from langchain.agents import create_agent
from langchain.agents.structured_output import ProviderStrategy
from pydantic import BaseModel

class WeatherResponse(BaseModel):
    temperature: float
    condition: str
    city: str

def weather_tool(location: str) -> str:
    """특정 위치의 날씨를 가져옵니다."""
    return f"{location}: 맑음, 22°C"

agent = create_agent(
    model="openrouter:openai/gpt-4.1",
    tools=[weather_tool],
    response_format=ProviderStrategy(WeatherResponse),
)

result = agent.invoke({
    "messages": [{"role": "user", "content": "서울 날씨는?"}]
})
print(result["structured_response"])
# WeatherResponse(temperature=22.0, condition='맑음', city='서울')

11. 추론 출력 (Reasoning Output)

Claude Sonnet 4.5, DeepSeek R1, OpenAI o3 등 추론(reasoning) 모델은 답변 생성 전에 내부적으로 사고 과정을 거친다. ChatOpenRouter는 이 추론 과정을 reasoning 파라미터로 제어하고, 결과를 content_blocks를 통해 노출한다.

추론 활성화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    max_tokens=16384,  # 추론 토큰 포함 충분한 컨텍스트 확보
    reasoning={
        "effort": "high",       # 추론 토큰 예산 제어
        "summary": "auto",      # 추론 요약 반환 여부
    },
)

ai_msg = model.invoke("529의 제곱근은?")

# 추론 블록 접근
for block in ai_msg.content_blocks:
    if block["type"] == "reasoning":
        print("[추론 과정]:", block["reasoning"])
    elif block["type"] == "text":
        print("[최종 답변]:", block["text"])

effort 파라미터 값

설명
"xhigh"최대 추론 토큰 할당 (가장 정확, 가장 느림)
"high"높은 추론 예산
"medium"균형 잡힌 설정
"low"최소한의 추론
"minimal"거의 추론하지 않음
"none"추론 비활성화

summary 파라미터 값

설명
"auto"모델이 적절한 수준 자동 결정
"concise"간결한 요약
"detailed"상세한 추론 과정

주의: effort와 토큰 예산의 매핑은 모델마다 다르다. Google Gemini 모델은 effort를 내부 thinkingLevel로 매핑하며, 정확한 토큰 수가 아닌 수준값으로 처리된다.

추론 토큰 사용량 확인

1
2
3
4
5
6
7
print(ai_msg.usage_metadata)
# {
#   'input_tokens': 39,
#   'output_tokens': 98,
#   'total_tokens': 137,
#   'output_token_details': {'reasoning': 62}
# }

output_token_details.reasoning 필드를 통해 추론에 소모된 토큰 수를 확인할 수 있다. 이는 비용 예측과 성능 최적화에 중요한 정보다.


12. 멀티모달 입력

OpenRouter는 이미지, 오디오, 비디오, PDF 등 다양한 형태의 입력을 지원한다. 단, 모든 모달리티가 모든 모델에서 지원되는 것은 아니므로 https://openrouter.ai/models 에서 모델별 지원 여부를 확인해야 한다.

지원 입력 방식

방식이미지오디오비디오PDF
HTTP/HTTPS URL
Base64 인라인

오디오는 URL 방식이 지원되지 않으므로 반드시 Base64로 인코딩하여 전달해야 한다.

이미지 입력

URL 방식:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain_openrouter import ChatOpenRouter
from langchain.messages import HumanMessage

model = ChatOpenRouter(model="openai/gpt-4o")

message = HumanMessage(
    content=[
        {"type": "text", "text": "이 이미지를 설명해줘."},
        {
            "type": "image",
            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
        },
    ]
)
response = model.invoke([message])
print(response.content)

Base64 방식:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import base64
import httpx

image_url = "https://picsum.photos/id/237/200/300"
image_data = base64.b64encode(
    httpx.get(image_url, follow_redirects=True).content
).decode("utf-8")

message = HumanMessage(
    content=[
        {"type": "text", "text": "이 이미지를 설명해줘."},
        {
            "type": "image",
            "base64": image_data,
            "mime_type": "image/jpeg",
        },
    ]
)

오디오 입력

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
from pathlib import Path
from langchain_openrouter import ChatOpenRouter
from langchain.messages import HumanMessage

model = ChatOpenRouter(model="google/gemini-2.5-flash")

audio_data = base64.b64encode(
    Path("/path/to/audio.wav").read_bytes()
).decode("utf-8")

message = HumanMessage(
    content=[
        {"type": "text", "text": "이 오디오를 텍스트로 변환해줘."},
        {
            "type": "audio",
            "base64": audio_data,
            "mime_type": "audio/wav",
        },
    ]
)
response = model.invoke([message])

비디오 입력

비디오 입력은 내부적으로 OpenRouter의 video_url 형식으로 자동 변환된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# URL 방식
message = HumanMessage(
    content=[
        {"type": "text", "text": "이 영상을 설명해줘."},
        {
            "type": "video",
            "url": "https://example.com/video.mp4",
        },
    ]
)

# Base64 방식
video_data = base64.b64encode(Path("/path/to/video.mp4").read_bytes()).decode("utf-8")
message = HumanMessage(
    content=[
        {"type": "text", "text": "이 영상을 설명해줘."},
        {
            "type": "video",
            "base64": video_data,
            "mime_type": "video/mp4",
        },
    ]
)

PDF 입력

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from langchain_openrouter import ChatOpenRouter
from langchain.messages import HumanMessage

model = ChatOpenRouter(model="google/gemini-2.5-pro-preview")

# URL 방식
message = HumanMessage(
    content=[
        {"type": "text", "text": "이 문서를 요약해줘."},
        {
            "type": "file",
            "url": "https://example.com/document.pdf",
            "mime_type": "application/pdf",
        },
    ]
)

# Base64 방식
pdf_data = base64.b64encode(Path("/path/to/doc.pdf").read_bytes()).decode("utf-8")
message = HumanMessage(
    content=[
        {"type": "text", "text": "이 문서를 요약해줘."},
        {
            "type": "file",
            "base64": pdf_data,
            "mime_type": "application/pdf",
        },
    ]
)

13. 토큰 사용량 메타데이터

API 호출 후 usage_metadata 속성을 통해 토큰 사용량을 확인할 수 있다. 이는 비용 추적 및 최적화에 필수적인 정보다.

기본 토큰 사용량

1
2
3
ai_msg = model.invoke("농담 하나 해줘.")
print(ai_msg.usage_metadata)
# {'input_tokens': 12, 'output_tokens': 25, 'total_tokens': 37}

추론 토큰 (Reasoning Tokens)

추론 모델을 사용하거나 reasoning 파라미터를 활성화하면 output_token_details.reasoning에 내부 추론에 소모된 토큰 수가 표시된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    reasoning={"effort": "high"},
)

ai_msg = model.invoke("529의 제곱근은?")
print(ai_msg.usage_metadata)
# {
#   'input_tokens': 39,
#   'output_tokens': 98,
#   'total_tokens': 137,
#   'output_token_details': {'reasoning': 62}
# }

프롬프트 캐싱 (Cached Input Tokens)

Anthropic Claude 모델은 cache_control 파라미터를 통한 프롬프트 캐싱을 지원한다. 캐싱을 활용하면 동일한 시스템 프롬프트를 반복 사용할 때 비용을 크게 절감할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from langchain_openrouter import ChatOpenRouter

model = ChatOpenRouter(model="anthropic/claude-sonnet-4.5")

# 긴 시스템 프롬프트 (캐싱 대상)
long_system = "당신은 도움이 되는 어시스턴트입니다. " * 200

messages = [
    ("system", [{
        "type": "text",
        "text": long_system,
        "cache_control": {"type": "ephemeral"}  # 캐싱 지시
    }]),
    ("human", "안녕?"),
]

# 첫 번째 호출: 캐시 생성
ai_msg = model.invoke(messages)
print(ai_msg.usage_metadata)
# {
#   'input_tokens': 1210,
#   'output_tokens': 12,
#   'total_tokens': 1222,
#   'input_token_details': {'cache_creation': 1201}
# }

# 두 번째 호출: 캐시 읽기 (비용 절감!)
ai_msg = model.invoke(messages)
print(ai_msg.usage_metadata)
# {
#   'input_tokens': 1210,
#   'output_tokens': 12,
#   'total_tokens': 1222,
#   'input_token_details': {'cache_read': 1201}
# }

: cache_control이 없으면 캐싱이 발생하지 않으며 input_token_details 필드 자체가 응답에 나타나지 않는다.

토큰 메타데이터 구조 요약

graph TD
    A[usage_metadata] --> B[input_tokens]
    A --> C[output_tokens]
    A --> D[total_tokens]
    A --> E[input_token_details]
    A --> F[output_token_details]
    E --> G[cache_read\n캐시에서 읽은 토큰]
    E --> H[cache_creation\n캐시에 쓴 토큰]
    F --> I[reasoning\n추론에 소모된 토큰]

14. 응답 메타데이터

response_metadata 속성에는 프로바이더 및 모델에 대한 메타정보가 담겨 있다.

1
2
3
4
5
6
7
8
9
10
11
ai_msg = model.invoke("농담 하나 해줘.")
print(ai_msg.response_metadata)
# {
#   'model_name': 'anthropic/claude-sonnet-4.5',
#   'id': 'gen-1771043112-yLUz3txgvHSjkyCQK8KQ',
#   'created': 1771043112,
#   'object': 'chat.completion',
#   'finish_reason': 'stop',
#   'logprobs': None,
#   'model_provider': 'openrouter'
# }
필드설명
model_name사용된 모델 식별자
id요청 고유 ID (OpenRouter generation ID)
created생성 타임스탬프 (Unix epoch)
object응답 객체 타입
finish_reason생성 종료 이유 (stop, tool_calls, length 등)
logprobs로그 확률 (활성화 시)
model_provider현재 항상 "openrouter"
native_finish_reason(선택적) 실제 프로바이더의 원본 종료 이유

native_finish_reason은 프로바이더마다 종료 이유 명칭이 다를 때 원본 값을 보존하기 위해 포함된다.


15. 프로바이더 라우팅

OpenRouter의 가장 강력한 기능 중 하나는 프로바이더 라우팅이다. 동일한 모델이 여러 프로바이더(예: Anthropic 직접, Google Cloud를 통한 Claude 등)에서 서빙될 때, 어떤 프로바이더를 우선 사용할지 세밀하게 제어할 수 있다.

라우팅 의사결정 흐름

flowchart TD
    A[ChatOpenRouter.invoke] --> B{openrouter_provider\n파라미터 있음?}
    B -->|없음| C[기본 로드밸런싱\n낮은 비용 선호]
    B -->|있음| D{order 지정?}
    D -->|있음| E[순서대로 시도]
    E --> F{allow_fallbacks?}
    F -->|True| G[목록 외 프로바이더도\n폴백 허용]
    F -->|False| H[목록 내 프로바이더만 사용]
    D -->|없음| I{only 지정?}
    I -->|있음| J[지정 프로바이더만 사용\n폴백 없음]
    I -->|없음| K{ignore 지정?}
    K -->|있음| L[해당 프로바이더 제외\n나머지에서 선택]
    K -->|없음| M{sort 지정?}
    M -->|throughput| N[최고 속도 프로바이더]
    M -->|latency| O[최저 레이턴시 프로바이더]
    M -->|없음| C

프로바이더 순서 지정

1
2
3
4
5
6
7
8
# Anthropic을 우선, 불가능 시 Google로 폴백
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    openrouter_provider={
        "order": ["Anthropic", "Google"],
        "allow_fallbacks": True,  # 기본값: 목록 외 폴백도 허용
    },
)

프로바이더 필터링

1
2
3
4
5
6
7
8
9
10
11
# 특정 프로바이더만 사용 (폴백 없음)
model = ChatOpenRouter(
    model="openai/gpt-4o",
    openrouter_provider={"only": ["OpenAI", "Azure"]},
)

# 특정 프로바이더 제외
model = ChatOpenRouter(
    model="meta-llama/llama-4-maverick",
    openrouter_provider={"ignore": ["DeepInfra"]},
)

비용/속도/레이턴시 기준 정렬

1
2
3
4
5
6
7
8
9
10
11
# 가장 빠른 처리량 (tokens/sec 최대) 우선
model = ChatOpenRouter(
    model="openai/gpt-4o",
    openrouter_provider={"sort": "throughput"},
)

# 가장 낮은 응답 레이턴시 우선
model = ChatOpenRouter(
    model="openai/gpt-4o",
    openrouter_provider={"sort": "latency"},
)

데이터 수집 거부

GDPR, 기업 데이터 보호 정책 등으로 인해 AI 프로바이더가 요청 데이터를 저장하거나 학습에 사용하지 못하도록 설정할 수 있다.

1
2
3
4
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    openrouter_provider={"data_collection": "deny"},
)

이 설정은 데이터를 수집하는 프로바이더를 자동으로 라우팅에서 제외한다.

양자화 수준 필터링

오픈소스 모델(Llama, Mistral 등)은 여러 정밀도(quantization)로 서빙된다. 특정 정밀도 수준을 요구할 때 사용한다.

1
2
3
4
model = ChatOpenRouter(
    model="meta-llama/llama-4-maverick",
    openrouter_provider={"quantizations": ["fp16", "bf16"]},  # 고정밀도만 허용
)
양자화 값설명
fp1616비트 부동소수점 (고품질)
bf16뇌 부동소수점 16비트 (고품질)
int88비트 정수 (중간 품질, 빠름)
int44비트 정수 (낮은 품질, 가장 빠름)

Route 파라미터

1
2
3
4
5
6
7
8
9
10
11
12
# fallback: 자동 장애 조치 활성화 (기본 동작)
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    route="fallback",
)

# sort: openrouter_provider에 설정된 정렬 전략 기반 라우팅
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    openrouter_provider={"sort": "latency"},
    route="sort",
)

복합 옵션 조합

여러 라우팅 옵션을 함께 적용할 수 있다.

1
2
3
4
5
6
7
8
9
model = ChatOpenRouter(
    model="openai/gpt-4o",
    openrouter_provider={
        "order": ["OpenAI", "Azure"],
        "allow_fallbacks": False,        # 엄격 — 목록 내 프로바이더만
        "require_parameters": True,      # 모든 파라미터 지원하는 프로바이더만
        "data_collection": "deny",       # 데이터 수집 거부
    },
)

16. 앱 어트리뷰션 (Attribution)

OpenRouter 대시보드에서 앱별 API 사용량을 추적하기 위해 앱 정보를 HTTP 헤더로 전달할 수 있다. 이는 멀티 앱을 운영하는 개발자나 팀에게 유용하다.

1
2
3
4
5
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    app_url="https://myapp.com",    # HTTP-Referer 헤더 → 환경변수: OPENROUTER_APP_URL
    app_title="My App",             # X-Title 헤더 → 환경변수: OPENROUTER_APP_TITLE
)

환경 변수로도 설정 가능하다.

1
2
export OPENROUTER_APP_URL="https://myapp.com"
export OPENROUTER_APP_TITLE="My App"

추가로 마켓플레이스 카테고리도 지정할 수 있다.

1
2
3
4
model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    app_categories=["cli-agent", "programming-app"],  # X-OpenRouter-Categories 헤더
)

17. LangSmith 트레이싱 연동

ChatOpenRouter는 LangSmith와 완전히 통합된다. 이전에 ChatOpenAI + base_url 방식을 사용했을 때 모든 모델이 openai로 표시되던 문제가 해결되어, 이제 실제 프로바이더명이 정확히 기록된다.

graph LR
    A[앱 코드] -->|invoke| B[ChatOpenRouter]
    B -->|트레이스 전송| C[LangSmith]
    B -->|API 호출| D[OpenRouter]
    C --> E[대시보드\n- 프로바이더별 분류\n- 모델별 비용 분석\n- 레이턴시 추적\n- 토큰 사용량 시각화]
1
2
3
4
5
6
7
8
9
10
11
import os

os.environ["LANGSMITH_API_KEY"] = "ls__xxxx"
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "my-openrouter-project"  # 선택사항

from langchain_openrouter import ChatOpenRouter

model = ChatOpenRouter(model="anthropic/claude-sonnet-4.5")
ai_msg = model.invoke("안녕!")
# → LangSmith에 프로바이더: anthropic, 모델: claude-sonnet-4.5 로 정확히 기록됨

18. 실전 패턴 및 베스트 프랙티스

패턴 1: 다중 모델 비교 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_openrouter import ChatOpenRouter

models = [
    "anthropic/claude-sonnet-4.5",
    "openai/gpt-4o",
    "google/gemini-2.5-flash",
    "deepseek/deepseek-r1",
]

prompt = "머신러닝에서 과적합(overfitting)을 방지하는 방법 3가지를 간결하게 설명해줘."

results = {}
for model_id in models:
    model = ChatOpenRouter(model=model_id, temperature=0)
    response = model.invoke(prompt)
    results[model_id] = response.content
    print(f"\n[{model_id}]:\n{response.content[:200]}...")

패턴 2: 비용 최적화 라우팅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 개발/테스트: 빠른 처리량 우선
dev_model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    openrouter_provider={"sort": "throughput"},
)

# 프로덕션: 데이터 수집 거부 + Anthropic 직접 우선
prod_model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    openrouter_provider={
        "order": ["Anthropic"],
        "allow_fallbacks": False,
        "data_collection": "deny",
    },
)

패턴 3: 장문 처리 + 프롬프트 캐싱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from langchain_openrouter import ChatOpenRouter

model = ChatOpenRouter(model="anthropic/claude-sonnet-4.5")

# 긴 시스템 컨텍스트를 캐싱으로 비용 절감
system_context = """
당신은 금융 분석 전문가입니다. 아래 회사 재무 데이터를 기반으로 분석합니다.
[...매우 긴 재무 보고서 데이터...]
""" * 50  # 수천 토큰 분량

# cache_control로 캐싱 지정
messages_template = [
    ("system", [{
        "type": "text",
        "text": system_context,
        "cache_control": {"type": "ephemeral"}
    }]),
]

# 여러 질문을 같은 캐시된 컨텍스트로 처리
questions = [
    "2024년 매출 성장률은?",
    "주요 리스크 요인은?",
    "경쟁사 대비 마진 차이는?",
]

for q in questions:
    msgs = messages_template + [("human", q)]
    response = model.invoke(msgs)
    print(f"Q: {q}\nA: {response.content}\n")
    # 첫 호출 이후 시스템 프롬프트 캐시 재사용 → 비용 절감

패턴 4: 추론 모델을 활용한 복잡한 문제 해결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from langchain_openrouter import ChatOpenRouter

# 고난이도 문제에 추론 모델 활용
reasoning_model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    max_tokens=16384,
    reasoning={"effort": "high", "summary": "detailed"},
)

problem = """
다음 알고리즘 문제를 해결하는 최적의 Python 코드를 작성하세요:
주어진 정수 배열에서 합이 K가 되는 두 수의 인덱스를 찾아라.
배열 크기 n은 최대 10^5이고, 각 원소는 -10^9 ~ 10^9 범위이다.
시간복잡도 O(n), 공간복잡도 O(n)으로 해결하라.
"""

response = reasoning_model.invoke(problem)

# 추론 과정과 최종 답변 분리 출력
for block in response.content_blocks:
    if block["type"] == "reasoning":
        print("=== 추론 과정 ===")
        print(block["reasoning"][:500], "...")
    elif block["type"] == "text":
        print("\n=== 최종 답변 ===")
        print(block["text"])

# 추론 토큰 사용량
print("\n=== 토큰 사용량 ===")
print(response.usage_metadata)

패턴 5: LangChain 에이전트와 통합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from langchain_openrouter import ChatOpenRouter
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain.tools import tool
from langchain.prompts import ChatPromptTemplate

def get_stock_price(ticker: str) -> str:
    """주식 가격을 가져옵니다."""
    # 실제 구현에서는 외부 API 호출
    prices = {"AAPL": 185.50, "GOOGL": 175.20, "MSFT": 420.30}
    return f"{ticker}: ${prices.get(ticker, 'N/A')}"

def calculate_portfolio_value(holdings: str) -> str:
    """포트폴리오 가치를 계산합니다. holdings는 'TICKER:SHARES' 형태."""
    # 간단한 예시
    return f"포트폴리오 총 가치: $50,000"

model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4.5",
    temperature=0,
)

tools = [get_stock_price, calculate_portfolio_value]

prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 포트폴리오 관리 어시스턴트입니다."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({
    "input": "AAPL 10주와 GOOGL 5주 포트폴리오의 현재 가치는?"
})
print(result["output"])

19. ChatOpenAI 방식과의 비교

기존에 많이 사용되던 ChatOpenAI + base_url 방식과 ChatOpenRouter 방식을 비교한다.

graph LR
    subgraph "기존 방식 (비권장)"
        A[ChatOpenAI\nbase_url=openrouter] -->|문제| B1[구조화 출력 깨짐]
        A -->|문제| B2[reasoning 블록 누락]
        A -->|문제| B3[LangSmith에 openai로\n잘못 표시]
        A -->|문제| B4[cache_control\n미전달]
    end

    subgraph "새로운 방식 (권장)"
        C[ChatOpenRouter] -->|해결| D1[완전한 구조화 출력]
        C -->|해결| D2[reasoning 블록 정확 노출]
        C -->|해결| D3[LangSmith 정확한\n프로바이더 추적]
        C -->|해결| D4[모든 OpenRouter 파라미터\n완전 지원]
    end
비교 항목ChatOpenAI + base_urlChatOpenRouter
구조화된 출력⚠️ 불안정✅ 완전 지원
추론 토큰 접근❌ 미지원content_blocks
LangSmith 트레이싱⚠️ 모두 openai로 표시✅ 실제 프로바이더명
프롬프트 캐싱cache_control 미전달✅ 완전 지원
프로바이더 라우팅❌ 불가✅ 세밀한 제어
데이터 수집 정책❌ 설정 불가data_collection: deny
공식 지원❌ 편법✅ 퍼스트파티 통합
유지보수❌ 수동 업데이트✅ OpenRouter SDK 자동 연동

20. JavaScript/TypeScript 지원

Python 외에도 JavaScript/TypeScript 환경을 위한 @langchain/openrouter 패키지가 별도로 제공된다.

1
npm install @langchain/openrouter @langchain/core
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { ChatOpenRouter } from "@langchain/openrouter";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

// 기본 사용
const model = new ChatOpenRouter({
  model: "openai/gpt-4o",
  // apiKey: "...",  // 또는 OPENROUTER_API_KEY 환경변수
});

const response = await model.invoke([
  { role: "user", content: "안녕! 반가워." }
]);
console.log(response.content);

// 스트리밍
const stream = await model.stream([{ role: "user", content: "짧은 시를 써줘" }]);
for await (const chunk of stream) {
  process.stdout.write(chunk.content as string);
}

// 도구 호출
const calculator = tool(
  async ({ a, b }: { a: number; b: number }) => `${a + b}`,
  {
    name: "add",
    description: "두 수를 더합니다",
    schema: z.object({ a: z.number(), b: z.number() }),
  }
);

const modelWithTools = new ChatOpenRouter({ model: "openai/gpt-4o" }).bindTools([calculator]);
const toolResponse = await modelWithTools.invoke("3 + 7은 얼마야?");
console.log(toolResponse.tool_calls);

참고: JS/TS 패키지는 Python 패키지와 별도로 개발되며, SDK 의존성 없이 직접 OpenRouter REST API를 호출하는 방식으로 구현되어 있다. 현재 버전은 0.2.1 (2025년 4월 기준).


부록: ChatOpenRouter 전체 파라미터 참조

파라미터타입기본값설명
modelstr필수모델 식별자 (provider/model-name)
temperaturefloat1.0출력 무작위성 (0~2)
max_tokensintNone최대 생성 토큰
max_retriesint2실패 시 재시도 횟수
timeout_msintNone요청 타임아웃 (밀리초)
api_keystrenv varOpenRouter API 키
reasoningdictNone추론 설정 (effort, summary)
openrouter_providerdictNone프로바이더 라우팅 설정
routestr"fallback"라우팅 전략
app_urlstrLangChain docs앱 URL (어트리뷰션)
app_titlestr"LangChain"앱 이름 (어트리뷰션)
app_categorieslistNone마켓플레이스 카테고리
top_pfloatNone핵 샘플링 파라미터
frequency_penaltyfloatNone반복 패널티
presence_penaltyfloatNone새로운 주제 장려 패널티
seedintNone재현 가능한 출력을 위한 시드
stoplistNone중단 시퀀스
nint1생성할 응답 수
streamingboolFalse스트리밍 모드 여부

참고 자료


이 문서는 2025년 4월 15일 공식 LangChain 문서 및 최신 GitHub 릴리즈 정보를 기반으로 작성되었습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.