본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 14. 18:53

Codex CLI 롤아웃 트레이스(rollout traces) 역공학

요약

본 글은 Codex CLI를 DeepSeek으로 구동하기 위해 번역 프록시(translation proxy)를 구축하는 과정에서 발생한 기술적 경험을 다룹니다. 특히, OpenAI의 Responses API 형식을 Chat Completions 형식으로 변환할 때 발생하는 세부적인 호환성 문제와, 실제 롤아웃 트레이스 파일의 구조가 개발자가 예상했던 표준화된 이벤트 타입과 크게 다르다는 점을 발견했습니다. 이러한 불일치 분석은 코딩 에이전트(coding agents)를 위한 런타임 툴링(runtime tooling)을 구축하는 데 있어 매우 중요한 시사점을 제공합니다.

핵심 포인트

  • Codex CLI의 OpenAI Responses API 호출을 DeepSeek Chat Completions 형식으로 변환하기 위해 프록시가 필요했다.
  • 프록시 구현 시, Codex가 기대하는 `function_call` 상태(`in_progress`)를 정확히 유지하는 것이 핵심이었다.
  • 개발자가 예상한 표준화된 이벤트 타입(예: `exec_command_begin`, `agent_reasoning`)과 실제 롤아웃 트레이스 파일의 구조는 완전히 달랐다.
  • 실제 트레이스 데이터는 `session_meta`, `event_msg`, `response_item` 등 다양한 고유한 타입을 가지며, 이는 에이전트 툴링 설계 시 고려해야 할 중요한 사실이다.

Codex CLI를 위한 DeepSeek 프록시를 구축하며 주말을 보냈습니다. 실제 트레이싱(tracing) 데이터를 생성하기 위해서였습니다. 프록시 구축 자체는 간단했습니다. 하지만 실제 트레이스 파일을 확인했을 때 발견한 것은 그렇지 않았습니다. 문서화된 형식과 실제 형식이 일치하지 않았습니다. 이것은 그 불일치에 관한 이야기입니다. 즉, 제가 무엇을 예상했는지, 무엇을 발견했는지, 그리고 코딩 에이전트(coding agents)를 위한 런타임 툴링(runtime tooling)을 구축하고 있다면 이것이 왜 중요한지에 대한 이야기입니다.

설정: 왜 프록시인가?
Codex CLI는 OpenAI의 Responses API(그들의 SDK를 통해)를 사용합니다. DeepSeek은 Chat Completions만 지원합니다. DeepSeek을 백엔드로 사용하려면 번역 프록시(translation proxy)가 필요했습니다. 즉, Responses API 호출을 가로채서 Chat Completions으로 번역해야 했습니다. 프록시(tools/codex_deepseek_proxy.py)는 간단했습니다:

  • /responses (또는 /v1/responses)에서 POST 요청을 수락합니다.
  • 입력 필드(Responses API 형식)를 Chat Completions 메시지로 번역합니다.
  • 도구 정의(tool definitions)를 {"name": "bash", "parameters": {...}}에서 {"type": "function", "function": {"name": "bash", "parameters": {...}}}로 번역합니다.
  • DeepSeek으로 전송하고, Chat Completions 응답을 수신한 뒤, 이를 다시 Responses API 이벤트 형식으로 번역합니다.
  • 응답을 JSON 본문으로 반환합니다 (SSE 스트림이 아님 — DeepSeek의 스트리밍 출력은 호환되지 않습니다).

번역은 기계적이었지만, 한 가지 결정적인 세부 사항이 있었습니다. Codex는 function_call 항목의 상태(status)가 "completed"가 아닌 "in_progress"이기를 기대한다는 점입니다. 이 상태는 Codex에게 도구가 호출되었지만 아직 완료되지 않았음을 알려줍니다. 즉, function_call_output이 도착하기를 기다리고 있는 상태입니다. 이를 "completed"로 설정하면 Codex는 도구가 이미 실행되었으며 출력값이 없다고 생각합니다.

프록시가 정상적으로 작동하게 된 후, Codex CLI는 DeepSeek을 대상으로 문제없이 실행되었습니다. 이제 저는 실제 트레이스 데이터를 갖게 되었습니다.

제가 예상했던 것
Codex CLI는 세션 데이터를 ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl 경로에 롤아웃 JSONL 파일로 저장합니다. 각 줄은 type 필드와 payload를 가진 JSON 객체입니다.

Codex CLI의 소스 코드(특히 protocol.rs)를 읽은 결과, 다음과 같은 이벤트 타입들을 예상했습니다:

  • exec_command_begin / exec_command_end — 명령 실행 경계 (command execution boundaries)
  • mcp_tool_call_begin / mcp_tool_call_end — MCP 도구 호출 경계 (MCP tool call boundaries)
  • agent_reasoning — 모델의 내부 추론 (the model's internal reasoning)

저는 이들을 기반으로 첫 번째 파서(parser)를 모델링했습니다. 하지만 결과는 세션당 정확히 1개의 이벤트만 생성되었습니다. 무언가 크게 잘못되었습니다.

실제 형상은 어떤 모습인가

원시 롤아웃(raw rollout) 파일을 덤프했습니다. 다음은 실제 형식입니다 (Codex v0.130.0을 기준으로 검증됨):

{ "timestamp" : "..." , "type" : "session_meta" , "payload" :{ "model_provider" : "deepseek" , "cli_version" : "0.130.0" }}
{ "timestamp" : "..." , "type" : "event_msg" , "payload" :{ "type" : "task_started" , ... }}
{ "timestamp" : "..." , "type" : "response_item" , "payload" :{ "type" : "message" , "role" : "developer" , "content" :[ ... ]}}
{ "timestamp" : "..." , "type" : "turn_context" , "payload" :{ "model" : "deepseek-chat" , ... }}
{ "timestamp" : "..." , "type" : "event_msg" , "payload" :{ "type" : "token_count" , ... }}
{ "timestamp" : "..." , "type" : "response_item" , "payload" :{ "type" : "function_call" , "name" : "exec_command" , "arguments" : "{}" , "call_id" : "call_xxx" }}
{ "timestamp" : "..." , "type" : "response_item" , "payload" :{ "type" : "function_call_output" , "call_id" : "call_xxx" , "output" : "..." }}
{ "timestamp" : "..." , "type" : "response_item" , "payload" :{ "type" : "message" , "role" : "assistant" , "content" :[{ "type" : "output_text" , "text" : "..." }]}}
{ "timestamp" : "..." , "type" : "event_msg" , "payload" :{ "type" : "agent_message" , "message" : "..." }}
{ "timestamp" : "..." , "type" : "event_msg" , "payload" :{ "type" : "task_complete" , ... }}

핵심 패턴은 다음과 같습니다:

  • response_item/function_call — 모델이 도구 호출(tool invocation)을 요청했습니다. name, arguments (JSON 문자열), 그리고 call_id를 포함합니다.
  • response_item/function_call_output — 해당 호출의 결과입니다.

call_id (해당하는 function_call과 쌍을 이룸) 및 output (문자열)을 포함합니다. event_msg/agent_message — 모델의 추론 (reasoning) 텍스트입니다. 이곳에 사고/추론 (thinking/reasoning) 블록이 위치합니다. response_item/message (role=assistant) — 사용자에 대한 모델의 텍스트 응답입니다. event_msg/token_count — 곳곳에 흩어져 있는 토큰 사용량 (token usage) 추적입니다. exec_command_begin, exec_command_end, mcp_tool_call_begin 또는 mcp_tool_call_end 이벤트는 존재하지 않습니다. 적어도 v0.130.0 롤아웃 (rollout) 형식에는 없습니다.

나를 놀라게 한 세 가지 사항:

  1. call_id 페어링 (pairing), 중첩 (nesting) 아님
    도구 호출 (Tool invocations)과 그 결과는 중첩된 이벤트가 아니라, call_id에 의해 연결된 평면 (flat) 구조입니다. function_call 라인이 나타난 후, 나중에 (잠재적으로 여러 줄 뒤에) 동일한 call_id를 가진 일치하는 function_call_output이 나타납니다. 그 사이에는 token_count 이벤트, 추론 (reasoning) 메시지, 또는 다른 함수 호출이 있을 수 있습니다. 이는 파서 (parser)가 시작/종료 중첩 구조를 가정하는 대신, 대기 중인 호출을 버퍼에 저장하고 call_id로 매칭해야 함을 의미합니다.

  2. 토큰 카운트 (Token counts)가 어디에나 있음
    event_msg/token_count 이벤트가 거의 모든 유의미한 이벤트 사이에 나타납니다. 이들은 예측 가능한 리듬을 따르는 것 같지 않습니다. 때로는 함수 호출 전에, 때로는 후에, 때로는 추론 블록 사이에 나타납니다. 인과적 추적 (causal tracing) 관점에서는 노이즈이지만, 이벤트 체인을 깨뜨리지 않고 이를 처리해야 합니다.

  3. 명시적인 인과 관계 (causality) 없음
    롤아웃 형식에는 parent_event_id 또는 그에 상응하는 인과 관계 필드가 없습니다. 인과 관계는 순서로부터 추론되어야 합니다. 모델은 function_call_output을 받은 다음 다음에 무엇을 할지 결정하므로, 출력 이후의 다음 function_call 또는 agent_message는 인과적으로 그 출력에 의존합니다. 이는 Copilot 및 Continue.dev의 로그 기반 테일러 (log-based tailers)가 사용하는 것과 동일한 시간적 휴리스틱 (temporal heuristic)입니다.

번역 체인 (The translation chain)
Codex CLI가 DeepSeek 프록시(proxy)를 통해 실행될 때 실제로 일어나는 과정은 다음과 같습니다:

Codex CLI (Responses API) → POST /responses { input: [...], tools: [...] } → 프록시가 input을 messages, tools로 번역 → 함수 정의 (function definitions) → DeepSeek Chat Completions API → 프록시가 response를 번역 → Responses API 이벤트 (events) → Codex가 call_id가 포함된 function_call을 수신 → Codex가 도구 (tool)를 실행 → Codex가 function_call_output을 다시 전송 → 프록시가 다음 요청으로 번역 → task_complete가 될 때까지 루프 반복

프록시에서의 각 루프 반복은 단일 Chat Completions 호출입니다. 응답에는 다음 중 하나가 포함됩니다:

  • 도구 호출 (Tool calls, function_call 항목) → Responses API 출력 항목으로 번역
  • 텍스트 응답 (text response, message content) → 어시스턴트 메시지 (assistant message)로 번역
  • 둘 다 (모델은 동일한 응답 내에서 텍스트와 도구 호출을 모두 반환할 수 있음)

런타임 도구 (runtime tooling)에 대한 시사점
코딩 에이전트 (coding agents)를 위한 관측성 (observability) 또는 트레이싱 (tracing)을 구축하고 있다면, Codex CLI 포맷은 몇 가지 교훈을 줍니다:

  1. 소스 코드 주석을 믿지 말고, 와이어 데이터 (wire data)를 믿으세요.
    protocol.rs는 한 가지 포맷을 제안했지만, 실제 롤아웃 (rollout) 파일은 다른 포맷을 사용했습니다. 소스 코드는 내부 데이터 구조를 보여줄 뿐, 직렬화 포맷 (serialization format)을 보여주는 것이 아닙니다.

  2. call_id 페어링 (pairing)은 반복되는 패턴입니다.
    Codex CLI와 OpenAI의 Responses API 모두 function_call을 그 출력값과 연결하기 위해 call_id를 사용합니다. 이는 중첩 (nesting) 구조가 아니라 평면적인 키-값 (key-value) 관계입니다. 파서 (parser) 설계도 이에 맞춰야 합니다: call_id별로 버퍼링 (buffer)하고, 도착 시 매칭 (match)하십시오.

  3. 로그 기반 인과관계 (Log-based causality)는 기본 모델이 아니라 대체 수단입니다.
    Codex CLI 롤아웃 데이터에는 인과적 연결 (causal links)이 없습니다. 이는 추론되어야만 합니다. 이는 약 80%의 사례에서는 괜찮지만, 어떤 function_call_output이 어떤 후속 function_call을 트리거했는지 항상 알 수 없음을 의미합니다.

  4. 이벤트 스트림 (event stream)은 이질적 (heterogeneous)입니다.
    토큰 수 (token counts), 메타데이터 (metadata), 제어 이벤트 (control events)가 함수 호출 (function calls)과 섞여 있습니다. 견고한 파서는 고정된 이벤트 순서를 가정하지 않고 신호 (signal)와 노이즈 (noise)를 구분할 수 있어야 합니다.

제가 최종적으로 구축한 오픈 소스 구현체인 파서 (causetrace/hooks/codex_parser.py)는 다음을 처리합니다:

  • response_item/function_call → call_id에 의해 추적되는 대기 중인 호출 (pending call) 생성
  • response_item/function_call_output → call_id로 매칭하여, 해당 이벤트를 tool_output으로 업데이트
  • event_msg/agent_message → 인과적 부모 (causal parent) 연결을 포함하는 추론 (reasoning) 이벤트 생성
  • response_item/message (assistant) → 응답 텍스트 (response text) 이벤트 생성

이 파서는 465줄의 롤아웃 (rollout) 파일을 116개의 인과적으로 연결된 (causally-linked) 이벤트로 변환합니다. 이는 파서 정확도를 사실상 0% (protocol.rs 기반의 시도)에서 실제 포맷에서 발견 가능한 이벤트의 100%로 개선한 결과입니다.

전체 소스 코드는 다음에서 확인할 수 있습니다: https://github.com/milkoor/causetrace/blob/main/causetrace/hooks/codex_parser.py
그리고 실제 트레이스 (traces)를 가능하게 했던 DeepSeek 프록시 (proxy)는 다음과 같습니다: https://github.com/milkoor/causetrace/blob/main/tools/codex_deepseek_proxy.py

이 글은 코딩 에이전트 런타임 관측성 (runtime observability)에 관한 시리즈의 두 번째 글입니다. 첫 번째 게시물: Coding agents produce causal DAGs, not logs .

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0