본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 27. 00:53

50줄의 C# 코드로 Claude, Gemini 또는 Llama에서 OpenAI Codex CLI 실행하기

요약

OpenAI Codex CLI가 Responses API만 지원하는 한계를 극복하기 위해, C# 스크립트를 사용하여 Claude, Gemini, Llama 등 다양한 모델을 연결하는 방법을 소개합니다. Microsoft.Extensions.AI를 활용해 Chat Completion과 Responses API를 동시에 지원하는 브릿지 서버를 구축할 수 있습니다.

핵심 포인트

  • Codex CLI의 Responses API 제약을 C# 스크립트로 해결
  • Microsoft.Extensions.AI를 통한 벤더 중립적 인터페이스 구현
  • OpenRouter를 연동하여 Claude, Gemini 등 다양한 모델 활용 가능
  • Cadenza.Agent SDK를 사용해 단일 .cs 파일로 에이전트 서버 구축

OpenAI의 Codex CLI는 셸 도구, apply_patch, 계획 추적(plan tracking) 등 훌륭한 에디터-에이전트 UX를 제공합니다. 문제는 2026년 2월 현재, 이 도구가 오직 OpenAI의 Responses API만 지원한다는 점입니다. Chat Completion 지원은 중단되었습니다 (codex-rs/model-provider-info/src/lib.rs: WireApi 열거형(enum)에 Responses라는 하나의 변체(variant)만 존재함). 만약 Ollama, LM Studio, 혹은 여러분이 즐겨 사용하는 Llama 실행기처럼 Chat-Completion 전용 엔드포인트(endpoint)를 연결하고 싶었다면, 운이 좋지 않은 상황입니다.

하지만 Codex CLI는 Responses 규격을 따르는 어떠한 서버와도 통신할 수 있습니다. 이를 위해 정확히 model_provider 설정 블록을 갖추고 있습니다. 따라서 여러분이 원하는 모델을 기반으로 Responses 형태의 HTTP 엔드포인트를 구축할 수 있다면, Codex는 범용 프런트엔드(front-end)가 되고 여러분은 두뇌(brain)를 직접 선택할 수 있게 됩니다.

제가 사용하고 있는 트릭은 다음과 같습니다. Microsoft.Extensions.AI의 벤더 중립적인 IChatClient 추상화(abstraction)를 기반으로, OpenAI Chat Completion 서버와 Responses API 서버 역할을 동시에 수행하는 50줄짜리 C# 스크립트입니다. 그런 다음 이를 OpenRouter로 연결합니다. OpenRouter를 통하면 하나의 API 키로 Claude, Gemini, Llama, GPT 등 수백 개의 모델을 사용할 수 있습니다. 그리고 Codex가 OpenAI 대신 제가 만든 로컬 스크립트와 통신하도록 설정합니다.

결과물: Anthropic의 Claude 3.5 Sonnet(또는 그날그날 기분에 따라 원하는 모델) 위에서 실행되는 OpenAI Codex CLI

구성 요소

저는 제가 배포한 MSBuild SDK인 Cadenza.Agent를 사용합니다. 이 SDK는 단일 .cs 파일을 실행 가능한 에이전트 서버로 변환해 줍니다. 이는 .NET 10의 파일 기반 프로그램을 위한 소규모 단일 파일 스크립팅 SDK 제품군의 일부입니다. dotnet run script.cs와 유사한 개념이지만, 더 풍부한 Tier-1 API(Tool, UseOllama, UseOpenAi, Run 등)를 제공합니다. Agent 변체는 다음을 노출합니다:

  • POST /v1/chat/completions — Aider / Continue / Cursor / Copilot BYOK / sgpt 용
  • POST /v1/responses — Codex CLI 용

두 엔드포인트 모두 여러분이 설정한 동일한 IChatClient를 기반으로 작동합니다. 백엔드(backend)를 교체하더라도 통신 형식(wire-format)은 그대로 유지됩니다.

제가 사용 중인 LLM은 OpenRouter입니다. 이 서비스는 OpenAI의 Chat Completion 통신 형식(wire format)을 사용하면서 Base URL만 다르게 제공하므로, Microsoft.Extensions.AI.OpenAI의 드롭인(drop-in) ChatClient를 사용하기에 완벽합니다. 환경 변수 하나만 설정하면 어떤 모델이든 사용할 수 있습니다.

Codex의 설정(configuration)을 위해 저는 CODEX_HOME 환경 변수 트릭을 사용합니다. ~/.codex/config.toml 파일을 직접 수정하는 대신, Codex가 샘플 로컬 디렉토리를 가리키도록 설정하면 해당 디렉토리에서 새로운 config.toml을 로드합니다. 이는 사용자의 글로벌 설정(global config)을 전혀 건드리지 않는 독립적인(self-contained) 샘플을 배포할 수 있음을 의미합니다.

스크립트

전체 백엔드가 파일 하나에 담겨 있습니다:

#!/usr/bin/env dotnet run
#:sdk Cadenza.Agent@1.0.14

...

그게 전부입니다. 프로젝트 파일도, .csproj도, Program.cs도 없습니다. 상단의 #:sdk 지시어(directive)는 .NET 10 파일 기반 프로그램 시스템에 Cadenza.Agent를 SDK로 사용하도록 알려줍니다. 이 SDK는 HTTP 서버, 응답(Responses) 통신 형식, 모든 패키지 참조를 가져오며, Tool, UseOllama, UseChatClient, Run을 직접 호출할 수 있는 순수 이름(bare names)으로 노출합니다.

실행하기

스크립트를 agent-codex-openrouter.cs로 저장한 뒤 다음을 수행하세요:

# 터미널 1 — 에이전트 서버 시작
$env:OPENROUTER_API_KEY = "sk-or-v1-..."
$env:OPENROUTER_MODEL   = "anthropic/claude-3.5-sonnet"  # 또는 다른 OpenRouter 슬러그(slug)
...

첫 실행 시 의존성(dependencies)인 Microsoft.Extensions.AI, OpenAI SDK, ASP.NET Core를 가져옵니다. 그 이후에는 1초도 안 되어 부팅됩니다. 스크립트는 두 번째 터미널에 필요한 내용을 정확히 출력합니다:

Codex config generated at: D:\work\.cadenza-codex-openrouter

In another terminal, run:
...

이 내용을 다른 터미널에 붙여넣고 codex를 실행하면, Codex UX를 통해 Claude 3.5 Sonnet(또는 선택한 OpenRouter 모델)과 채팅할 수 있습니다. shellapply_patch와 같은 도구(Tools)는 Codex가 매 요청마다 직접 전송합니다. 에이전트는 이를 모델로 전달하고 모델의 function_call 출력을 스트리밍하여 다시 돌려주므로, Codex가 이를 로컬에서 실행할 수 있게 됩니다.

내부에서 일어나는 일

Codex가 POST /v1/responses를 보낼 때, 에이전트는 다음과 같은 작업을 수행합니다:

  1. Responses 입력 파싱 (Parse the Responses input). Codex는 message / function_call / function_call_output 배열을 전송하며, 우리는 이를 Microsoft.Extensions.AIIList<ChatMessage> 형태로 평탄화(flatten)합니다.
  2. previous_response_id 준수. Codex는 전체 이력을 다시 보내는 대신 이 ID를 사용하여 턴(turn)을 체이닝합니다. 에이전트는 컨텍스트를 재구성할 수 있도록 과거 턴에 대한 제한된 인메모리 딕셔너리(in-memory dictionary)를 유지합니다.
  3. Codex의 도구(tools) 전달. Codex의 shell, apply_patch, update_plan은 가공되지 않은 스키마(raw schemas) 형태로 전달됩니다. 우리는 이를 JSON 스키마는 있지만 실제 핸들러는 없는 PassthroughFunction 인스턴스로 모델에 선언합니다. 이 엔드포인트의 경우 함수 호출 미들웨어(function-invocation middleware)를 우회하므로, 모델이 생성하는 모든 함수 호출은 Codex로 직접 스트리밍됩니다.
  4. IChatClient.GetStreamingResponseAsync 호출. 이는 사용자가 구성한 백엔드(OpenRouter, Ollama, OpenAI, Anthropic, Azure OpenAI 등)로 요청을 전달합니다.
  5. Responses SSE로 재방출. ChatResponseUpdate 스트림은 Codex가 기대하는 약 15가지의 SSE 이벤트 유형으로 변환됩니다: response.created, response.in_progress, response.output_item.added, response.output_text.delta, response.function_call_arguments.delta, response.completed 등이 있습니다.

IChatClient 추상화는 이 시스템을 조합 가능(composable)하게 만드는 핵심 기술입니다. Cadenza.Agent는 OpenRouter가

결과적으로: 사용자의 글로벌 ~/.codex/config.toml은 전혀 수정되지 않은 상태로 유지됩니다. Ollama 백엔드, OpenRouter 백엔드, gpt-5 추론 노력(reasoning effort) 조정 등 서로 다른 샘플들은 각각 격리된 디렉토리를 갖게 됩니다. 10개의 샘플을 가지고 있어도 서로 간섭하지 않습니다. 팀원과 설정을 공유하고 싶으신가요? .cs 파일을 전달하기만 하면 됩니다. 팀원의 codex 명령은 스크립트가 생성한 로컬 디렉토리를 가리키게 됩니다.

"Defaulting to fallback metadata" 경고 메시지 억제하기

Codex가 인식하지 못하는 모델 ID를 지정하면, 컨텍스트 창(context window) 및 출력 제한에 대해 기본 메타데이터(default metadata)를 사용하며 매 턴마다 다음과 같은 경고를 출력합니다:

⚠ Model metadata for `cadenza-codex-openrouter` not found.
   Defaulting to fallback metadata; this can degrade performance and cause issues.

이는 사용자의 슬러그(slug)를 선언하는 JSON 파일을 가리키는 model_catalog_json 설정 키를 통해 억제할 수 있습니다. 스키마는 codex-rs/protocol/src/openai_models.rs::ModelInfo이며, 17개의 필수 필드가 필요합니다. 샘플에는 완전한 카탈로그 항목이 포함되어 있습니다. 만약 컨텍스트 창이 더 작은 모델(예: 128K인 openai/gpt-4o-mini)로 교체한다면, context_windowmax_context_window를 그에 맞춰 낮추십시오. Codex는 프롬프트를 이 수치에 맞춰 자르기(truncate) 때문에, 값을 너무 크게 선언하면 백엔드 모델에서 조용히 토큰 오버플로(token overflow)가 발생할 수 있습니다.

또한 주의할 점: model_catalog_json은 Codex에 내장된 카탈로그와 병합(merge)되는 것이 아니라 이를 **대체(replace)**합니다. 만약 gpt-5-codex가 커스텀 슬러그와 함께 계속 작동하기를 원한다면, 해당 모델도 JSON에 포함시켜야 합니다.

내가 겪었던 실수(그리고 해결 방법)

처음 실행했을 때, Codex가 시작을 거부했습니다:

Error loading configuration: failed to parse model_catalog_json path
`...\cadenza-catalog.json` as JSON: expected value at line 1 column 1

원인은 BOM(Byte Order Mark)이었습니다. .NET의 Encoding.UTF8은 BOM을 생성하는 변형이므로, File.WriteAllText(path, content, Encoding.UTF8)를 사용하면 데이터 앞에 EF BB BF가 추가됩니다. Codex가 사용하는 Rust의 serde_json은 엄격한 사양 준수를 위해 이를 거부합니다. RFC 8259 규격에 따르면 JSON 구현체는 BOM을 추가해서는 안 됩니다(MUST NOT).

Cadenza의 Fs.WriteText는 해당 BOM을 생성하는 기본 설정을 상속받고 있었습니다. 이를 new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)로 전환하여 수정하였으며, SDK를 1.0.14 버전으로 배포했습니다. 동일한 수정 사항이 Console.OutputEncoding에도 적용됩니다. 이 수정이 없었다면 dotnet-script | jq 명령 시 파이프(pipe) 데이터가 손상되었을 것입니다.

엄격한 파서(parser)를 사용하는 환경에서 파일을 작성하는 본인의 .NET 코드를 점검해 볼 가치가 있습니다. 만약 File.WriteAllText(path, text, Encoding.UTF8)를 사용하고 있다면, BOM을 생성하고 있는 것입니다. 해결 방법은 단 한 줄입니다:

File.WriteAllText(path, text, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));

이 패턴이 중요하다고 생각하는 이유

Codex CLI의 툴 루프(tool loop)는 진정으로 유용합니다. Responses API에 종속되는 현상은, 방치할 경우 오픈 툴 생태계를 파괴하는 벤더 결합(vendor coupling)의 일종처럼 느껴집니다. model_providers 설정과 wire_api = "responses"라는 탈출구(escape hatch)는 OpenAI가 "사용자가 이를 다른 곳에서도 원할 수 있음을 인정한다"라고 명시적으로 말하는 것과 같으며, 그 제안을 받아들이는 것이 올바른 행보입니다.

직접 제어할 수 있는 Responses 서버를 갖게 되면 생태계가 열립니다. 오프라인 작업을 위해 월 0달러인 로컬 Ollama 모델에서 Codex를 사용하고 싶나요? UseChatClientUseOllama로 바꾸기만 하면 됩니다. 동일한 스크립트, 동일한 Codex 설정에 '두뇌'만 바뀌는 것입니다. Codex 세션마다 프로젝트에 고정된 시스템 프롬프트(system prompt)를 주입하고 싶나요? Run() 호출 전에 추가하면 됩니다. 감사를 위해 모든 Codex 턴(turn)을 로그로 남기고 싶나요? IChatClient를 본인만의 미들웨어(middleware)로 래핑(wrap)하면 됩니다. 프롬프트 크기에 따라 OpenRouter와 로컬 모델 사이를 라운드 로빈(round-robin) 방식으로 전환하고 싶나요? C#으로 로직을 작성하여 동일한 엔드포인트(endpoint)를 통해 서비스하면 됩니다.

이 프로젝트를 지속 가능하게 만드는 것은 바로 단일 파일(single-file) 형식입니다. 유지 관리해야 할 프로젝트도, 관리해야 할 SDK도, 배포해야 할 별도의 바이너리(binary)도 없습니다. 그저 저장소(repo)에 복사하기만 하면 되는 .cs 파일 하나뿐입니다. 만약 dotnet run script.cs를 사용할 수 있다면 (.NET 10 이상에서는 가능합니다), 스크립트는 바로 실행됩니다.

시도해보기

.NET 10을 설치한 후, 다음을 실행하세요:

dotnet new install Cadenza.Templates
dotnet new cadenza-agent -n my-codex-backend -o ./my-codex-backend
cd my-codex-backend
...

또는 Cadenza repository에서 즉시 실행 가능한 샘플을 가져올 수 있습니다 — agent-codex-openrouter.cs가 위의 버전입니다. 해당 저장소에는 agent-codex-backend.cs (Ollama 변형)와 agent-openrouter.cs (Aider / Continue / Cursor를 위한 Chat Completion 변형)도 포함되어 있습니다.

이 내용이 유용하다면, 어떤 백엔드 (backend)를 연결했는지 알려주세요. 누군가 오프라인 코딩을 위한 로컬 폴백 (local fallback) 기능과 함께 미세 조정된 (fine-tuned) 로컬 모델에서 Codex를 실행하는 데 성공할지 궁금합니다 — 그것이 제 다음 실험 목록에 있는 과제입니다.

Cadenza는 MIT 라이선스를 따릅니다. 출처: https://github.com/rkttu/cadenza. Cadenza.Agent 패키지는 이 글을 쓰는 시점을 기준으로 1.0.14 버전으로 배포되고 있습니다.

커버 이미지 출처: UnsplashLukas

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0