본문으로 건너뛰기

LangChain (Python)

ChatOpenAI 는 base_url 에 /v1 까지, ChatAnthropic 은 /v1 없이.

AI 에게 넘기기ChatGPT 에서 열기 (새 창)Claude 에서 열기 (새 창)원문 열기

LangChain 의 ChatOpenAI(langchain-openai)와 ChatAnthropic(langchain-anthropic)에 주소와 키만 넣으면 돼요. 회사를 섞거나 claude-fable-5-1 · claude-opus-5-5 · claude-sonnet-5-5 로 구조화 출력을 받으시려면 ChatOpenAI 를 쓰세요 — 그 밖의 Claude(claude-sonnet-5 · claude-opus-5 · claude-haiku-5-5 등)는 ChatAnthropic 의 with_structured_output 도 돼요. thinking · Claude 서버 도구(웹 검색 등)처럼 Anthropic 칸을 그대로 쓰시려면 ChatAnthropic 을 쓰세요 — ChatOpenAI 가 부르는 채팅 문은 서버 도구를 400 으로 돌려보내요. cache_control 은 ChatOpenAI 로 부른 Claude 에도 저희가 옮겨요(글 조각 · system · 도구 정의 · 도구 결과 · 몸통 맨 위의 칸).

pip install -U "langchain-openai>=1.0" "langchain-anthropic>=1.0"
import os
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic

key = os.environ["ONEPORT_API_KEY"]  # 원포트 키(sk-so-...)

# OpenAI 형식 — base_url 에 /v1 까지. Claude · Gemini id 를 넣어도 돼요.
# gpt-6-sol 같은 생각하는 GPT 는 생각한 토큰도 max_tokens 에 들어가요(벤더 동작) — 너무 작으면 답 없이 finish_reason length 로 끝나요.
# max_retries=0 — SDK 가 연결 오류 · 시간 초과를 스스로 다시 보내 두 번 사용하지 않게요. 시간 초과는 300초(.invoke 는 120초 안에 답이 다 안 오면 422 · 긴 답은 .stream()).
gpt = ChatOpenAI(model="gpt-6-sol", base_url="https://oneport.kr/v1", api_key=key, max_tokens=2000,
                 max_retries=0, timeout=300)
print(gpt.invoke("한 문장으로 자기소개해 주세요").text)  # .text 는 langchain-core 1.0 부터 속성이에요(0.3 은 .text())

# Anthropic 형식 — base_url 에 /v1 을 붙이지 않아요(Anthropic SDK 가 /v1/messages 를 붙여요). 시간 초과 칸 이름이 달라요.
claude = ChatAnthropic(model="claude-sonnet-5", base_url="https://oneport.kr", api_key=key, max_tokens=2000,
                       max_retries=0, default_request_timeout=300)
for chunk in claude.stream("한 문장으로 자기소개해 주세요"):
    print(chunk.text, end="", flush=True)
  • ChatOpenAI 는 reasoning={…} 딕셔너리나 내장 도구(웹 검색 · 파일 검색 등)를 쓰거나 use_responses_api=True 면 Responses API(/v1/responses)로 보내요(SDK 동작) — 그 문은 OpenAI 모델만 받아서 Claude · Gemini 는 400 이에요. 회사를 섞을 땐 이 셋을 빼세요. reasoning_effort="low" 는 /v1/chat/completions 로 그대로 가서 섞어도 돼요 — Claude 로는 저희가 effort 로 옮기고, effort 를 안 받는 모델이면 빼고 응답 머리글 x-gateway-ignored-params 에 적어요. 다만 도구를 붙인 GPT 호출엔 주지 마세요(아래 「구조화 출력」 절의 GPT 도구 줄).
  • base_url 을 안 넘기면 ChatOpenAI 는 OPENAI_API_BASE · OPENAI_BASE_URL, ChatAnthropic 은 ANTHROPIC_API_URL · ANTHROPIC_BASE_URL 환경변수를 읽어요(SDK 동작). ChatAnthropic 쪽 값도 /v1 없이예요.
  • max_tokens 를 적어 두세요 — ChatAnthropic 은 안 적으면 모델 정보로 정하고(SDK 동작), 큰 값이면 저희가 그만큼 미리 잡아 잔액이 적을 때 402 가 날 수 있어요(끝나면 쓴 만큼만 사용해요).
  • ChatOpenAI 든 ChatAnthropic 이든 claude-sonnet-5 · claude-opus-5-5 를 스트림 없이(.invoke) 짧은 user 글 하나로 부르면(SystemMessage 하나가 같이 있어도 같아요) 지갑이 미리 잡는 몫이 커요 — ChatOpenAI 로 보낸 것도 저희가 Claude 몸통으로 옮긴 뒤에 재서 같은 특례가 걸려요. 짧다는 건 Claude 몸통으로 옮긴 JSON 이 16,384바이트 이하라는 뜻이에요. 자료를 붙인 RAG 질문도 그 안이면 걸려요(한글 약 5,000자). claude-sonnet-5 에 max_tokens 2,000 이면 약 80 크레딧이에요(claude-opus-5-5 는 약 160 크레딧 · 10월 기준 · 정본은 GET https://oneport.kr/v1/models). 이 특례는 도구나 with_structured_output 을 붙인 호출과 .stream(), anthropic-version 을 2023-06-01 아닌 판으로 싣거나 anthropic-beta · openai-beta 를 실은 호출엔 걸리지 않고, 걸리면 max_tokens 를 줄여도 거의 안 줄어요. 잔액이 적어 402 가 나면 .stream() 으로 받으세요 — 스트림은 보통 셈으로 잡아요. 조건과 셈은 「남은 한도」 장(https://oneport.kr/docs/reference/key.md)의 지갑 특례에 있어요.
  • 모델 id 는 GET https://oneport.kr/v1/models 의 id 를 글자 그대로 넣어요. ChatOpenAI 에 넣을 글 모델은 kind 가 "chat" 인 줄이에요 — pricing.rates 는 임베딩 · 그림 · 소리 · 영상 줄에도 있어서 rates 로는 못 가려요. 오류 갈래는 「에러」 장(https://oneport.kr/docs/errors.md)과 같아요 — ChatOpenAI 는 openai SDK, ChatAnthropic 은 anthropic SDK 의 예외를 그대로 올려요(SDK 동작).

재시도 · 시간 초과 · 중복 방지#

요청 하나에 Idempotency-Key 하나 — 객체에 실려서 호출마다 새로 만들어요
import os, uuid
from langchain_openai import ChatOpenAI

key = os.environ["ONEPORT_API_KEY"]

def keyed(model: str, idem: str) -> ChatOpenAI:
    return ChatOpenAI(model=model, base_url="https://oneport.kr/v1", api_key=key, max_tokens=2000,
                      max_retries=0, timeout=300, default_headers={"Idempotency-Key": idem})

idem = f"lc-{uuid.uuid4().hex}"  # 같은 요청을 다시 보낼 땐 이 값 그대로 · 고친 요청은 새 값
answer = keyed("gpt-6-sol", idem).invoke("한 문장으로 자기소개해 주세요")
  • 같은 값으로 만든 객체로 다시 부르면 같은 키 · 같은 본문이에요 — 「에러」 장 표의 잠시 뒤 다시 보낼 갈래만 Retry-After 뒤 그렇게 보내요. x-should-retry: false 여도 504 error.reason "timeout" · 조회 주소와 같이 온 503(request_state_unavailable · request_store_unavailable)은 이 갈래예요 — 둘 다 Retry-After 가 없으니 간격은 직접 정해요. max_tokens 를 줄이는 등 고친 요청은 새 값으로 만든 객체로 보내요(같은 값에 본문이 바뀌면 409 idempotency_conflict).
  • ChatAnthropic 도 default_headers={"Idempotency-Key": idem} 로 실어요(SDK 동작) — /v1/messages 도 같은 규칙이에요. 키를 실은 객체는 max_retries 를 켜 둬도 두 번 실행되지 않아요 — 앞 요청이 접수됐으면 409 request_already_exists 로 멈춰요(x-should-retry: false 라 SDK 도 다시 안 보내요). 위 예제의 0 은 첫 예제와 맞춘 값이에요.
  • 409 request_already_exists · 422 upstream_execution_unconfirmed 면 다시 보내지 말고 응답 머리글 x-gateway-status-url(예외의 e.response.headers — 같은 값이 Location 머리글 · 몸통 error.status_url 에도 와요)을 같은 API 키로 GET 해요 — /v1/requests/<id> 꼴 상대 주소라 앞에 https://oneport.kr 를 붙여요. 실행 · 결과 · 정산 칸은 「요청 상태」 장(https://oneport.kr/docs/reference/requests.md)이에요. 답 본문은 다시 못 받아요 — 꼭 필요하면 새 값으로 새로 부르는 거라 새로 사용해요.
  • ChatOpenAI 의 .stream() 이 도중에 끊기면, 저희 쪽 끊김은 상태 코드 없는 openai.APIError 로, 손님 쪽 연결이 끊기거나 읽기 시간을 넘기면 httpx 예외(httpx.RemoteProtocolError · httpx.ReadTimeout 등)로 그대로 올라와요(SDK 동작). 조각을 하나라도 받았으면 어느 쪽이든 잘린 답이니 「에러」 장 「잘린 답 이어 받기」로 이어 받으세요.
  • .with_retry() · .with_fallbacks() 는 기본으로 모든 예외에 다시 보내요(SDK 동작) — retry_if_exception_type · exceptions_to_handle 로 좁혀 쓰세요.

구조화 출력(with_structured_output)#

ChatOpenAI 기본 방식 — Claude · GPT 둘 다 같은 코드
import os
from pydantic import BaseModel
from langchain_openai import ChatOpenAI

class RuleAnswer(BaseModel):
    title: str
    summary: str

key = os.environ["ONEPORT_API_KEY"]
llm = ChatOpenAI(model="claude-sonnet-5-5", base_url="https://oneport.kr/v1", api_key=key, max_tokens=2000)
answer = llm.with_structured_output(RuleAnswer).invoke("수습기간 규정을 제목과 한 줄 요약으로 정리해 주세요")
print(answer.title, answer.summary)
  • ChatOpenAI 의 기본 방식(json_schema · SDK 동작)은 Claude id 로 보내도 저희가 Claude 구조화 출력(output_config.format)으로 옮겨서 그 스키마대로 답해요. GPT 는 그대로 가요.
  • 도구 방식(ChatOpenAI 의 method="function_calling" · ChatAnthropic 의 with_structured_output)은 도구를 강제로 부르게 해요(SDK 동작). claude-fable-5-1 · claude-opus-5-5 · claude-sonnet-5-5 는 강제 도구 호출을 받지 않아요 — ChatOpenAI 로 보내면 저희가 auto 로 낮춰 보내서(응답 머리글 x-gateway-ignored-params 에 tool_choice) 모델이 도구를 안 부르면 읽을 게 없고, ChatAnthropic 으로 보내면 몸통을 그대로 넘겨 400 이에요. 이 세 모델은 ChatOpenAI 기본 방식으로 쓰세요. claude-sonnet-5 와 claude-sonnet-5-5 는 다른 모델이에요(10월 기준 값은 같아요) — claude-sonnet-5 는 강제 도구 호출을 받아요.
  • GPT 에 도구를 실으면(bind_tools 든 with_structured_output 의 도구 방식이든) — gpt-6-sol · gpt-6-luna · gpt-6.1-terra · gpt-5.6-luna · gpt-5.6-sol · gpt-5.6-terra 는 reasoning_effort 를 안 주셨을 때 저희가 none 을 채워 보내고 응답 머리글 x-gateway-mapped-params 에 reasoning_effort->none 을 적어요(none 밖의 값을 주시면 그대로 가서 벤더가 400 · 벤더 동작). gpt-6-astra · gpt-6.1-sol 은 이 문에서 도구를 못 받아 400 이라 구조화 출력은 기본 방식으로 쓰세요. Claude 는 ChatOpenAI · ChatAnthropic 둘 다 bind_tools 의 함수 도구를 받아요.
  • 구조화 출력에서 토큰 수도 같이 보시려면 with_structured_output(RuleAnswer, include_raw=True) 로 받으세요 — 결과의 "raw" 가 원래 메시지라 usage_metadata · response_metadata 가 있고, 답은 "parsed" 에 와요(SDK 동작).

쓴 토큰 · 크레딧 확인#

스트림으로 받아도 끝에 토큰 수가 와요 — ChatOpenAI 경로는 모델과 상관없이 저희가 usage 조각을 싣고, ChatAnthropic 스트림은 끝 message_delta 이벤트의 usage 에 와요(LangChain 에선 usage_metadata 로 읽어요 · SDK 동작). 호출 한 번에 쓴 크레딧은 원포트 응답 usage 의 cost_credits 로 와요(스트림이면 끝 usage · 정산이 2초 안에 안 끝나면 그 칸 없이 와요). cost_credits 는 usage_metadata 엔 안 옮겨져요 — .invoke 결과면 원응답 usage 가 response_metadata 에 남아요(ChatOpenAI 는 "token_usage", ChatAnthropic 은 "usage" · SDK 동작 · 판마다 달라요). 스트림으로 받았거나 그 칸이 없으면 토큰 × GET https://oneport.kr/v1/models 의 pricing.rates 단가 ÷ 1,000,000 으로 셈하세요(100만 토큰당 크레딧 · 1 크레딧 = 2원 · usage_metadata.input_tokens 는 두 클래스 모두 캐시 읽기 · 쓰기를 포함한 합계예요(SDK 동작) · 캐시 토큰 셈은 「글 — /v1/chat/completions」 장 — Claude 1시간 캐시 쓰기는 cache_write 가 아니라 입력 단가의 2배예요). ChatOpenAI 로 부른 Claude 의 캐시 쓰기 수는 usage_metadata 에 안 와요(SDK 동작) — .invoke 면 response_metadata["token_usage"]["prompt_tokens_details"] 의 cache_write_tokens(1시간 몫은 cache_write_1h_tokens)에서 읽어요. 잔액은 GET https://oneport.kr/v1/credits 의 balance_credits, 이 키에 남은 몫은 GET https://oneport.kr/v1/key 의 month · day 예요. 둘 다 같은 키를 머리글에 실어 부르고 요금이 안 붙어요. 잔액은 답이 끝난 뒤에 줄고 1 크레딧 아래를 버려서, 잔액 앞뒤 차이로 호출 값을 재지 마세요.

임베딩(OpenAIEmbeddings)#

임베딩은 OpenAIEmbeddings(model="text-embedding-3-small", base_url="https://oneport.kr/v1", api_key=key, max_retries=0) 로 붙여요 — 모델은 GET https://oneport.kr/v1/models 에서 kind 가 "embedding" 인 줄이에요. OpenAIEmbeddings 는 기본으로 글을 토큰 번호 배열로 바꿔 보내요(SDK 동작) — text-embedding-… 은 그 배열을 그대로 받고, gemini-embedding-… 이나 전송 정책을 켠 스페이스 키라면 check_embedding_ctx_length=False 를 주어 글 그대로 보내세요(그 스페이스 키는 토큰 번호(숫자) 배열 input 을 403 space_content_policy 로 거절해요 — error.reason 이 token_input 이면 이 경우예요 · 글 배열은 받아요). OpenAIEmbeddings 는 응답의 usage 를 돌려주지 않아요(SDK 동작) — 쓴 몫을 세려면 openai SDK 의 client.embeddings.create(…) 로 불러 usage.prompt_tokens 를 읽어요(임베딩 응답엔 usage.cost_credits 가 없어요). 위 「재시도 · 시간 초과 · 중복 방지」 절의 글 문 규칙(Idempotency-Key · 시간 제한 300초 · 504 timeout 다시 보내기)은 임베딩 문엔 안 맞아요 — 402 hold_exceeds_balance 나 504 면 chunk_size(한 요청에 넣는 개수 · SDK 동작)를 줄이고, timeout 칸에 넣을 시간 · 쓴 몫 셈 · 묶음이 잡는 몫은 「임베딩」 장(https://oneport.kr/docs/reference/embeddings.md)을 따라요.