콘텐츠로 이동

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로 한 턴을 실행합니다.

export CONTINUUM_ROUTER_API_KEY="your-router-key"
codex exec "say ok"

응답은 백엔드가 서비스하는 모델에서 돌아옵니다. 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를 직접 서비스하지는 않으며, 두 방향은 따로 설정합니다.

함께 보기