Codex CLI와 통합하기¶
Codex CLI는 OpenAI의 터미널 코딩 에이전트입니다. 프로바이더 설정에 따라 OpenAI Responses API나 OpenAI Chat Completions API를 말하는 서버를 호출할 수 있으므로, Codex CLI 클라이언트를 Continuum Router에 연결해 실행할 수 있습니다. 이 문서는 CLI가 클라이언트이고 라우터가 서버인 방향을 다룹니다.
반대 방향은 별개의 주제입니다. 라우터는 OpenAI Codex 백엔드, 그중에서도 ChatGPT Plus, Pro, Enterprise 구독을 자체 클라이언트를 위한 업스트림으로 사용할 수도 있습니다. 그 설정은 ChatGPT 구독 / Codex 헤드리스 로그인에 설명되어 있으며, 이 문서에는 필요하지 않습니다.
통신 방식 선택¶
Codex CLI는 프로바이더별로 wire_api를 지정해 통신 방식을 고릅니다. 두 방식 모두 라우터와 동작하지만 조건이 다릅니다.
wire_api | CLI가 호출하는 엔드포인트 | 라우터에 필요한 것 |
|---|---|---|
responses | POST /v1/responses | Responses API를 직접 제공하며 패스스루로 도달하는 백엔드, 또는 CLI의 권고 필드를 responses_drop에 나열한 변환 백엔드. |
chat | POST /v1/chat/completions | 라우터가 서비스하는 아무 백엔드. CLI가 대화 기록을 직접 다시 보냅니다. |
Responses 방식은 CLI의 모든 기능을 사용할 수 있지만, 요청 본문에 네이티브 Responses 백엔드만 읽을 수 있는 필드와 도구 형태가 들어 있습니다. Chat Completions 방식은 CLI가 일반 채팅 요청을 보내므로 어디서나 동작하며, 대신 현재 동작하지 않는 것에 정리한 Responses 전용 동작을 사용할 수 없습니다.
Responses 방식¶
다음 프로바이더 항목을 ~/.codex/config.toml에 추가하고, CLI가 /responses를 덧붙이도록 라우터의 /v1 접두사까지 지정합니다.
model = "gpt-5"
model_provider = "continuum"
[model_providers.continuum]
name = "Continuum Router"
base_url = "http://localhost:8000/v1"
env_key = "CONTINUUM_ROUTER_API_KEY"
wire_api = "responses"
env_key는 라우터 API 키를 담은 환경 변수 이름입니다. CLI는 이 값을 Authorization: Bearer <값>으로 보내므로, 변수에는 라우터가 받아들이는 키를 넣어야 합니다. 라우터의 API 키 인증이 허용 모드(permissive)라면 비어 있지 않은 아무 값이나 사용할 수 있습니다. model은 백엔드 중 하나가 서비스하는 모델 ID여야 하며, 그렇지 않으면 라우터가 404 model_not_found로 응답합니다.
Chat Completions 방식¶
~/.codex/config.toml의 같은 프로바이더 항목에서 wire_api = "chat"으로 바꿉니다.
model = "my-model"
model_provider = "continuum"
[model_providers.continuum]
name = "Continuum Router"
base_url = "http://localhost:8000/v1"
env_key = "CONTINUUM_ROUTER_API_KEY"
wire_api = "chat"
이 방식에서 CLI는 매 턴 전체 대화를 채팅 메시지로 보냅니다. 서버 측 응답 상태에 의존하지 않으며, 다른 방식에서 보내던 Responses 전용 필드나 호스티드 도구는 Chat Completions에 대응 형식이 없으므로 전송하지 않습니다.
백엔드 전제 조건¶
Responses 방식은 라우터가 변경 없이 전달할 수 있는 /v1/responses 엔드포인트를 백엔드가 제공해야 합니다. backends[].responses_api가 이 동작을 고르며, 기본값 auto는 백엔드 유형으로 결정합니다.
openai와azure: 기본적으로 패스스루입니다. 추가 설정이 필요 없습니다.- OpenAI 와이어 형식을 쓰는 나머지 유형(
generic,continuumrouter,vllm,sglang,tensorrt-llm,ollama,llamacpp,mlxcel,lmstudio): 기본적으로 Chat Completions로 변환합니다. 백엔드가 Responses API를 직접 구현하면responses_api: native를 추가합니다. anthropic,bedrock,gemini: 전달할 Responses 엔드포인트가 없으므로 이 유형에서responses_api: native는 설정 로드 오류입니다. Responses 방식은 이 백엔드에서 변환으로만 처리됩니다. 변환 백엔드는 CLI가 보내는 권고 필드를 버리는 데 동의한 경우에만 CLI의 기본 요청 본문을 처리할 수 있습니다(변환 백엔드). 그렇지 않으면 Chat Completions 방식을 사용합니다.
대화 상태를 직접 보관하는 에이전트 런타임처럼 Responses API를 제공하는 셀프 호스팅 백엔드는 한 줄만 추가하면 됩니다.
backends:
- name: agent-runtime
type: generic
url: https://agents.example.internal
responses_api: native
api_key: "${AGENT_RUNTIME_KEY}"
models:
- gpt-5
수명 주기 호출과 핫 리로드를 포함한 전체 계약은 네이티브 Responses API에 있습니다.
변환 백엔드와 responses_drop¶
변환 백엔드는 CLI가 매 턴 무조건 보내는 권고 필드를 받아들이면 Responses 방식을 처리할 수 있습니다. backends[].responses_drop에 그 필드를 나열합니다.
backends:
- name: vllm-b300
type: vllm
url: http://10.0.0.21:8000
responses_api: auto
responses_drop: [include, prompt_cache_key, reasoning.summary]
models:
- gpt-5
CLI의 기본 요청 본문은 세 필드를 모두 담습니다(include: ["reasoning.encrypted_content"], prompt_cache_key, reasoning: {"summary": "auto"}). 따라서 CLI를 서비스하려면 세 필드를 모두 나열해야 합니다. 목록은 백엔드별 설정이며 기본값은 비어 있습니다. 선택한 백엔드가 이름을 올리기 전까지 해당 필드는 계속 거부됩니다.
나열할 수 있는 필드는 권고 필드뿐입니다: include(모든 값이 허용 목록에 있을 때만, 현재는 reasoning.encrypted_content뿐), prompt_cache_key, safety_identifier, reasoning.summary. 의미 필드(text, conversation, max_tool_calls, reasoning.mode, reasoning.context, 그리고 모든 도구 및 입력 항목 거부)는 항상 거부됩니다. 알 수 없는 이름이나 의미 필드 이름은 설정 로드 오류입니다.
필드를 버리면 클라이언트는 두 가지 개선을 잃습니다.
- 암호화된 추론 재생 없음: 다음 턴에는 암호화된 추론 콘텐츠가 실리지 않으므로, 모델은 눈에 보이는 대화 기록만 봅니다. 변환 백엔드의 클라이언트는 이미 이렇게 동작합니다.
- 업스트림 프롬프트 캐시 힌트 없음: 백엔드는
prompt_cache_key를 받지 않으므로, 그 키로 동작하던 공급자 측 접두사 캐싱은 사용되지 않습니다. 라우터 자체의 프롬프트, 접두사, 시맨틱 캐시는 요청 내용을 기준으로 키를 만들므로 영향을 받지 않습니다.
응답은 버린 필드를 x-continuum-dropped-fields로 알립니다. 예를 들어 include, prompt_cache_key처럼 정렬하고 중복을 제거해 표시합니다. 응답 캐시 적중에서도 같은 헤더가 나오며, responses_drop_fields_total 카운터는 처리된 요청마다 버린 필드 하나당 한 번 증가합니다. responses_api: native에서는 이 목록이 완전히 무시됩니다.
확인¶
라우터를 시작한 뒤 CLI로 한 턴을 실행합니다.
응답은 백엔드가 서비스하는 모델에서 돌아옵니다. Responses 방식이라면 라우터 로그에 /v1/chat/completions 변환 호출 대신 해당 백엔드로 전달한 POST /v1/responses가 기록됩니다.
오류와 해결 방법¶
라우터는 전달할 수 없는 필드를 조용히 버리지 않고 거부하므로, 오류 메시지에 문제가 된 필드가 그대로 나옵니다. 네이티브 전용 거부 메시지는 다음과 같습니다.
Request field 'prompt_cache_key' requires a backend with native Responses API support; the selected convert_to_chat_completions strategy would lose it. Set 'responses_api: native' on a backend that serves the Responses API itself, or send the request over the Chat Completions wire from a client that can omit the field
| 관찰되는 오류 | 원인 | 해결 방법 |
|---|---|---|
prompt_cache_key, include, reasoning.mode/context/summary, text, safety_identifier, conversation, max_tool_calls를 지목하는 400 unsupported_request_parameter | 선택한 백엔드가 변환 방식으로 서비스되어 네이티브 전용 필드를 전달할 수 없습니다. CLI는 매 턴 prompt_cache_key, include: ["reasoning.encrypted_content"], reasoning: {"summary": "auto"}를 보냅니다. | Responses API를 직접 제공하는 백엔드에 responses_api: native를 설정하거나, 변환 백엔드의 responses_drop에 권고 필드를 추가하거나, CLI에서 wire_api = "chat"을 사용합니다. 권고 필드만 나열할 수 있으며, 거부된 필드가 의미 필드여도 메시지에는 responses_drop이 나옵니다. |
tools[custom], tools[local_shell], 또는 tools[custom] member 'run_command' of namespace 'ns' 같은 네임스페이스 멤버를 지목하는 400 unsupported_request_parameter | 변환 방식에 해당 도구를 옮길 Chat Completions, Anthropic, Gemini 형식이 없습니다. 다른 호출 규약을 모델에 설명하면 모델 동작이 바뀌기 때문입니다. | 네이티브 Responses 백엔드를 사용하거나, 해당 도구를 보내지 않는 CLI와 통신 방식을 사용합니다. |
tools[<type>]을 지목하며 "not permitted by this router's responses_tools policy"가 붙은 400 unsupported_request_parameter | responses_tools.unmodeled_types의 기본값이 deny이므로, 라우터가 모델링하지 않는 도구 유형은 운영자가 허용할 때까지 모든 백엔드에서 거부됩니다. | 백엔드가 해당 유형을 지원하는지 확인한 뒤 responses_tools.allow에 추가하거나, 그 도구를 보내지 않는 클라이언트 버전을 사용합니다. 변환 백엔드에서는 허용해도 네이티브 전용 도구 검사에서 다시 거부됩니다. |
tools[web_search] 같은 호스티드 도구를 지목하는 정책 기반 400 unsupported_request_parameter | 운영자가 모델링된 호스티드 도구 유형을 거부했습니다. | responses_tools.deny에서 해당 유형을 빼거나 modeled_hosted_types: allow로 설정합니다. |
422 Unprocessable Entity | 모델링된 필드의 형식이 잘못되었습니다. 예를 들어 name이 없는 function 도구나 잘못된 tool_choice입니다. | 요청을 수정합니다. 알 수 없는 도구 type은 422가 아니며, 라우터는 네이티브 백엔드에서 그대로 보존하고 위에 설명한 대로 정책이나 변환 단계에서 거부합니다. |
404 model_not_found | config.toml의 model을 활성화된 백엔드가 서비스하지 않습니다. | model을 라우터의 GET /v1/models가 나열하는 ID로 설정합니다. |
responses_tools 섹션은 Responses 도구 정책에 설명되어 있으며, 여기서 거부된 요청은 어떤 백엔드에도 도달하지 않습니다.
현재 동작하지 않는 것¶
- 변환은 백엔드가 동의한 경우에만 Responses 전용 필드를 처리합니다.
text,safety_identifier,conversation,max_tool_calls,reasoning.mode,reasoning.context는 네이티브 백엔드에만 도달합니다. 권고 필드(include,prompt_cache_key,safety_identifier,reasoning.summary)는 선택한 백엔드가responses_drop에 나열한 경우에만 버려지며, 그렇지 않으면 거부됩니다. 라우터는 Chat Completions, Anthropic, Gemini 백엔드에서 이 필드를 흉내 내지 않습니다. responses_drop이 없으면 CLI의 기본 요청 본문으로 변환 백엔드를 사용할 수 없습니다. Codex는 Responses 방식에서 기본으로prompt_cache_key,include,reasoning.summary를 보내므로, 백엔드가 세 필드를 모두 나열하기 전까지 변환 백엔드는 첫 턴부터 요청을 거부합니다. 버리기를 허용하지 않아야 하는 백엔드에는 Chat Completions 방식을 사용할 수 있습니다.client_metadata는 모델링되지 않습니다. Codex는 요청마다 이 필드를 덧붙입니다. 네이티브 백엔드는 그대로 받고, 변환 방식은 무시합니다.- 모델링되지 않은 도구 유형은 기본으로 거부됩니다. 나중에 추가된 Codex 도구 유형은, 백엔드가 전달할 수 있더라도 운영자가 허용할 때까지
responses_tools정책에서 거부됩니다. - 호스티드 도구는 백엔드에 달려 있습니다. CLI의
web_search도구는 네이티브 백엔드에 그대로 도달하며, 실행 여부는 백엔드가 결정합니다. 변환 방식에는 이를 호스티드로 실행하는 경로가 없습니다. Chat Completions 변환은web_search라는 이름의 함수 도구로 서술하고, Anthropic 변환은 이 도구를 버립니다. 변환 백엔드에서는responses_drop이 해당 필드를 나열하지 않는 한 CLI의 기본 요청 본문이 먼저 네이티브 전용 필드 거부를 만납니다. - ChatGPT 구독 업스트림은 CLI를 위한 백엔드가 아닙니다. 라우터를 ChatGPT 구독에 로그인해도 그 구독이 CLI를 직접 서비스하지는 않으며, 두 방향은 따로 설정합니다.
함께 보기¶
- 네이티브 Responses API:
responses_api필드와 패스스루 계약입니다. - Responses API: 라우터의
/v1/responses표면입니다. - ChatGPT 구독 / Codex 헤드리스 로그인: Codex가 업스트림인 반대 방향입니다.
- Claude Code와 통합하기: Anthropic Messages 클라이언트에 같은 클라이언트-라우터 패턴을 적용한 문서입니다.