Codex를 위한 로컬 우선(Local-First) 할당량 모니터 및 계정 전환기 구축
요약
Codex 사용자의 계정 관리와 할당량 모니터링을 돕는 오픈 소스 유틸리티 'CodexControl'을 소개합니다. 실시간 할당량 상태를 읽고 여러 계정을 간편하게 전환할 수 있는 기능을 제공합니다.
핵심 포인트
- 실시간 할당량 상태를 직접 요청하여 추정치가 아닌 정확한 데이터 제공
- 5시간 및 7일 단위의 서로 다른 사용 윈도우를 독립적으로 모니터링
- macOS 및 Windows를 지원하는 크로스 플랫폼 메뉴 바 유틸리티
- 캐시 우회 및 반복 읽기를 통해 데이터의 신뢰성과 정확성 확보
Codex 계정 관리는 여러 계정이 서로 다른 할당량 윈도우(quota windows), 서로 다른 초기화 시간, 그리고 서로 다른 로컬 세션 상태(local session states)를 갖기 전까지는 단순해 보입니다.
그 시점이 되면, 두 가지 질문이 놀라울 정도로 어려워집니다:
- 지금 실제로 사용 가능한 계정은 무엇인가?
- 현재 로컬 Codex 설치 프로그램이 사용 중인 계정은 무엇인가?
저는 원래 대시보드를 열거나, 로컬 활동으로부터 사용량을 추정하거나, 인증 파일을 수동으로 교체하지 않고도 이 질문들에 답할 수 있는 작은 메뉴 바 유틸리티를 원했습니다.
그것이 바로 CodexControl이 되었습니다: macOS 및 Windows용 Codex를 위한 오픈 소스(open-source), 로컬 우선(local-first) 할당량 모니터 및 계정 전환기입니다.
이 제품은 의도적으로 딱 두 가지만 수행합니다:
- 실시간 Codex 할당량 상태(quota state) 읽기;
- 활성화된 로컬 Codex 계정 전환.
범위를 좁게 유지했기에 인터페이스는 단순했습니다. 구현, 특히 크로스 플랫폼(cross-platform) 전환은 처음 생각했던 것보다 덜 단순했습니다.
할당량 모니터에 실시간 데이터가 필요한 이유
API가 권위 있는 할당량 정보를 노출하지 않을 때는 사용량 추정치가 유용할 수 있습니다.
Codex는 이미 인증된 로컬 세션을 통해 읽을 수 있는 계정 상태를 가지고 있으므로, 저는 토큰 수, 요청 로그(request logs) 또는 작업에 소비된 시간으로부터 남은 용량을 추론하고 싶지 않았습니다.
CodexControl은 저장된 각 계정의 로컬 인증 상태를 사용하여 실시간 할당량 정보를 직접 요청합니다.
이는 추정치가 여러 방식으로 어긋날 수 있기 때문에 중요합니다:
- 다른 컴퓨터에서 사용이 발생할 수 있음;
- 백그라운드 활동이 로컬에서 보이지 않을 수 있음;
- 서로 다른 모델이나 작업이 용량을 다르게 소비할 수 있음;
- 서버가 할당량 상태를 조정하거나 정규화(normalize)할 수 있음;
- 실제 윈도우가 변경된 후에도 캐시된 응답이 계속 보일 수 있음.
이 앱은 간접적으로 재구성하려고 시도하는 대신, 제공자(provider)의 응답을 신뢰할 수 있는 단일 원천(source of truth)으로 취급합니다.
할당량은 단일 백분율이 아닙니다
Codex 계정은 하나 이상의 사용 윈도우(usage window)를 노출할 수 있습니다.
이 앱에서 가장 중요한 두 가지 윈도우는 다음과 같습니다:
- 더 짧은 5시간 간격의 윈도우;
- 더 긴 7일 간격의 윈도우.
이러한 윈도우들은 매우 다른 상태를 가질 수 있습니다.
어떤 계정은 단기 윈도우가 소진되었음에도 불구하고 여전히 주간 용량을 가지고 있을 수 있습니다. 반면, 또 다른 계정은 단기 윈도우는 거의 가득 찼지만 남은 주간 용량이 적을 수도 있습니다.
이 두 가지를 하나의 숫자로 합치면 사용자가 내려야 할 운영상의 결정을 숨기게 됩니다.
따라서 CodexControl은 각 윈도우를 독립적으로 유지하고 각각의 정보를 보여줍니다:
- 남은 용량 또는 사용된 용량;
- 초기화 시간;
- 현재 가용성;
- 정규화된 표시 상태.
또한 이 앱은 단순히 “나중에 초기화됨(resets later)”과 같은 모호한 라벨만 표시하는 대신 정확한 초기화 타임스탬프를 보존합니다.
정확성이 1차 요구사항이 되다
수량 할당량을 모니터링하는 것은 숫자를 신뢰할 수 없다면 빠르게 가치를 잃습니다.
저는 실시간 읽기(live reads) 주위에 여러 보호 장치를 추가했습니다:
- 캐시를 우회하는 네트워크 세션;
- 동등성 검사(equivalence checks)가 포함된 반복 읽기;
- 일관되지 않은 응답 거부;
- 독립적인 값을 병합하지 않는 윈도우별 정규화;
- 실패한 새로고침 후 오래된 스냅샷 삭제;
- 필요할 때 토큰을 새로 고친 후 재시도.
가장 중요한 원칙은 이것이었습니다:
누락된 값이 확신에 차 있지만 오래된 값보다 낫다.
새로고침이 실패하면, 명확한 '오래됨(stale)' 상태 없이 이전 할당량을 계속 표시하는 것은 사용자가 더 이상 사용할 수 없는 계정으로 전환하도록 유도할 수 있습니다.
따라서 이 앱은 오류를 노출하고 다른 새로고침을 요청하는 것을 선호합니다.
계정 목록은 장식을 위한 것이 아니라 행동을 위해 정렬된다
알파벳 순서로 정렬된 원시(raw) 계정 목록만으로는 핵심 질문에 답할 수 없습니다.
사용자는 보통 다음에 어떤 계정을 활성화해야 할지 결정하려고 합니다.
CodexControl은 현재 사용 가능한 계정이 더 쉽게 접근할 수 있도록 실용적인 유용성에 따라 계정을 정렬합니다. 표시 로직은 실시간 윈도우 상태, 인증 건전성(authentication health), 그리고 해당 계정의 활성화 여부를 고려합니다.
또한 이 목록은 로컬 관리 작업을 지원합니다:
- 계정 추가 (add an account);
- 할당량 새로고침 (refresh quota);
- 재인증 (reauthenticate);
- 레이블 변경 (relabel);
- 제거 (remove);
- 계정 디렉토리 열기 (open the account directory);
- 주변 (ambient) Codex ID 전환 (switch the ambient Codex identity).
정기적인 새로고침은 몇 분마다 실행되지만, 사용자가 즉각적인 답변이 필요한 경우 수동 새로고침도 사용할 수 있습니다.
로컬 계정은 별도의 Codex 홈으로 저장됩니다
관리되는 각 계정은 고유한 로컬 Codex 홈(home)과 인증 상태를 가집니다.
주변 (ambient) ~/.codex 디렉토리는 일반적인 Codex CLI 및 데스크톱 워크플로에서 현재 사용 중인 계정을 나타냅니다.
단순화된 모델은 다음과 같습니다:
관리되는 계정 A 홈 ─┐
관리되는 계정 B 홈 ─┼─> CodexControl
관리되는 계정 C 홈 ─┘
...
전환(Switching)한다는 것은 선택된 관리 계정을 주변 (ambient) ID로 만드는 것을 의미합니다.
정확히 필요한 작업은 플랫폼과 현재의 Codex Desktop 구현 방식에 따라 달라집니다.
macOS: 네이티브 메뉴 바 워크플로
macOS 애플리케이션은 SwiftUI와 AppKit으로 구축되었으며 메뉴 바(menu bar)에 상주합니다.
이러한 폼 팩터(form factor)는 작업 내용과 일치합니다:
- 할당량(quota)을 훑어보기;
- 계정 선택하기;
- 전환하기;
- 업무로 복귀하기.
항상 열려 있어야 하는 커다란 대시보드는 필요하지 않습니다.
macOS에서 앱은 로컬 Codex 홈을 읽고, 필요할 때 계정 토큰을 새로고침하며, 실시간 할당량을 가져오고, 전환 시 주변 (ambient) 세션을 교체하며, 새로운 ID가 적용되도록 Codex Desktop을 재시작합니다.
이 프로젝트에는 패키징, 서명(signing), 공증(notarization), 그리고 Homebrew cask 지원이 포함되어 있어, 이 유틸리티가 단순한 스크립트 모음이 아닌 일반적인 데스크톱 애플리케이션처럼 동작할 수 있습니다.
Windows를 통해 전환이 단순한 파일 복사가 아님을 확인
첫 번째 Windows 구현에서는 다음 파일만 교체하여 계정을 전환했습니다:
~/.codex/auth.json
하지만 이는 현재의 Codex Desktop 빌드에 충분하지 않았습니다.
CLI ID는 변경되었지만, 데스크톱 애플리케이션은 계속해서 로그인 화면이나 오래된 세션을 보여줄 수 있었습니다.
그 이유는 ID가 여러 상태 계층(state layers)에 존재했기 때문입니다.
계층 1: CLI 및 글로벌 Codex 상태
로컬 Codex 디렉토리에는 다음이 포함되었습니다:
auth.json
.codex-global-state.json
.codex-global-state.json.bak
글로벌 상태 (global-state) 파일들은 인증 (authentication) 파일이 변경된 후에도 이전의 creator_id를 유지할 수 있었습니다.
따라서 전환 (switch) 흐름에서는 두 글로벌 상태 파일 모두에서 이전 프로바이더 계정 ID를 대상 계정 ID로 다시 작성 (rewrite)해야 했습니다.
계층 2: Codex Desktop MSIX 세션 상태
Windows 데스크톱 앱 또한 MSIX 패키지 캐시 내에 브라우저 및 애플리케이션 세션 상태 (session state)를 유지했습니다:
%LOCALAPPDATA%\Packages\OpenAI.Codex_*\LocalCache\Roaming\Codex
CLI 파일을 업데이트하는 것만으로는 이 데스크톱 세션 계층을 자동으로 교체할 수 없었습니다.
이로 인해 나타나는 증상들은 혼란스러웠습니다:
- 전환이 완료된 것처럼 보이지만, 데스크톱 앱에는 여전히 이전 신원 (identity)이 표시됨;
- 데스크톱 앱이 로그인 화면에서 다시 열림;
- 앱이 종료되었으나 복사 오류 발생 후 재실행에 실패함;
- 대상 계정이 유효한 CLI 인증을 가지고 있음에도 아직 일치하는 데스크톱 세션이 없음.
계정별 데스크톱 세션 상태 백업
이제 Windows 재시작 흐름은 데스크톱 세션 상태를 계정별 데이터로 취급합니다.
전환 과정에서 다음과 같은 작업을 수행할 수 있습니다:
- Codex Desktop 중지;
- 현재 데스크톱 세션을 떠나는 계정의 관리되는 홈 (managed home) 디렉토리로 백업;
auth.json및 글로벌 신원 상태 업데이트;- 사용 가능한 경우 대상 계정의 저장된 데스크톱 세션 복구;
- Codex Desktop 재실행.
백업 및 복구 프로세스는 첫 번째 복사 문제 발생 시 전체 전환을 실패시키는 대신, 로깅 (logging)과 함께 항목별로 작동합니다.
또한 중요한 첫 번째 사용 사례 (first-use case)가 있습니다.
대상 계정에는 아직 저장된 데스크톱 세션 스냅샷 (snapshot)이 없을 수 있습니다. 첫 번째 전환 시, Codex Desktop은 해당 계정의 상태를 스스로 조정 (reconcile)해야 할 수도 있습니다. 일단 계정이 활성화되고 세션이 백업되면, 이후의 전환에서는 더 일관되게 이를 복구할 수 있습니다.
이것은 이 프로젝트에서 얻은 가장 명확한 엔지니어링 교훈이었습니다:
인증 상태 (Authentication state)와 애플리케이션 세션 상태 (application session state)는 서로 연관되어 있지만, 동일한 것은 아니다.
계정 식별자(Account identity)는 이메일만으로 의존할 수 없다
계정들이 식별 필드(identifying fields)를 공유할 때 또 다른 미묘한 문제가 발생했다.
이메일이나 인증 대상(authentication subject)만을 계정 키(account key)로 사용하면, 서로 다른 제공자 계정(provider accounts)들이 하나의 행으로 합쳐질 수 있다.
CodexControl은 제공자 계정 ID(provider account ID)를 우선시하며, 다른 필드들은 오직 폴백(fallback) 용도로만 사용한다.
이는 다음과 같은 상황에서 중요하다:
- 여러 계정이 동일한 이메일 주소를 사용하는 경우
- 조직이 계정 메타데이터(account metadata)를 변경하는 경우
- 인증 레코드(authentication records)가 교체(rotated)되는 경우
- 오래된 관리형 홈(managed homes)과 새로운 관리형 홈이 일시적으로 공존하는 경우
안정적인 식별자(Stable identity)는 사용 가능한 가장 제공자 특화적인 식별자(provider-specific identifier)로부터 도출되어야 한다.
삭제된 계정은 삭제된 상태로 유지되어야 한다
애플리케이션은 디스크에서 관리형 Codex 홈(managed Codex homes)을 찾아낸다.
이로 인해 라이프사이클(lifecycle) 문제가 발생한다. 만약 다음 시작 시에 오래된 중복 홈이 다시 발견된다면, 가시적인 목록에서 계정을 삭제하는 것만으로는 충분하지 않다.
따라서 삭제 흐름(removal flow)은 다음과 같은 작업을 수행해야 한다:
- 삭제된 식별자(removed identity)를 기록
- 이후의 탐색 과정에서 삭제된 식별자를 필터링
- 동일한 제공자 계정에 대한 중복 관리형 홈을 제거
- 오래된 인증 데이터(stale authentication data)로부터 계정을 재생성하는 것을 방지
삭제는 단순한 UI 동작이 아니라 상태 관리(state-management) 기능이다.
교체된 리프레시 토큰(refresh tokens)으로부터의 복구
사용자는 동일한 계정에 대해 여러 개의 로컬 인증 홈(local auth homes)을 가질 수 있으며, 오래된 홈에는 만료된 리프레시 토큰(stale refresh tokens)이 포함되어 있을 수 있다.
리프레시(refresh) 실패가 항상 계정 자체가 유효하지 않음을 의미하지는 않는다. 더 최신의 일치하는 홈에 현재 자격 증명(credentials)이 포함되어 있을 수 있기 때문이다.
복구 경로(recovery path)는 재인증(reauthentication)이 필요하다고 선언하기 전에, 일치하는 로컬 식별자를 검색하고 더 최신의 인증 상태(authentication state)를 사용할 수 있다.
이를 통해 유효한 로컬 자격 증명이 이미 존재하는 경우 사용자가 다시 로그인해야 하는 상황을 방지할 수 있다.
동시에, 애플리케이션은 이미 재인증이 필요하다고 알려진 계정에 대해 자동 재시도 루프(automatic retry loops)를 중단한다. 수동 리프레시 및 재인증은 명시적인 동작으로 남는다.
플랫폼이 동일한 척하지 않는 교차 플랫폼 구현
이 프로젝트는 각 플랫폼에서 서로 다른 네이티브 접근 방식을 사용합니다:
| 플랫폼 | 구현 방식 |
|---|---|
| macOS | SwiftUI 및 AppKit 메뉴 바 애플리케이션 |
| Windows | Python 트레이 우선 (tray-first) 데스크톱 애플리케이션 |
두 플랫폼 모두를 하나의 크로스 플랫폼 (cross-platform) UI 프레임워크로 강제하는 것도 가능했을 것입니다.
하지만 저는 공유 UI 코드를 만드는 것 자체를 목표로 삼지 않기로 했습니다.
공유되는 제품 동작 (product behavior)이 더 중요합니다:
- 계정 탐색 (account discovery);
- 신원 정규화 (identity normalization);
- 실시간 할당량 읽기 (live quota reads);
- 토큰 갱신 (token refresh);
- 정렬 및 표현 규칙 (sorting and presentation rules);
- 전환 의미론 (switching semantics);
- 합성 데모 데이터 (synthetic demo data);
- 리포지토리 위생 (repository hygiene).
플랫폼별 세션 처리 (session handling)는 플랫폼별로 유지됩니다.
이는 특히 Windows에서 중요한데, MSIX 패키지 상태와 재시작 동작은 macOS에는 의미 있는 대응물이 없는 코드를 필요로 하기 때문입니다.
로컬 우선 (Local-first)이 오프라인을 의미하는 것은 아니다
CodexControl은 로컬 우선 (local-first) 방식이지만, 인증된 할당량 정보를 갱신하기 위해 여전히 OpenAI에 접속합니다.
이 차이점은 소유권과 저장소에 관한 것입니다:
- 계정 파일은 사용자의 컴퓨터에 머뭅니다;
- 앱은 기존의 로컬 Codex 인증 상태를 읽습니다;
- CodexControl 클라우드 계정 데이터베이스는 존재하지 않습니다;
- 스크린샷과 공개 예제에는 합성 신원 (synthetic identities)을 사용합니다;
- 라이브 토큰, 스냅샷, 데스크톱 세션은 리포지토리에 포함되도록 설계되지 않았습니다.
애플리케이션은 일반적인 플랫폼 애플리케이션 데이터 디렉토리 아래에 자체적인 로컬 상태를 저장합니다.
자격 증명 (credentials) 및 세션 데이터는 일반적인 설정값보다 더 주의 깊은 취급을 받아야 합니다. 리포지토리 보안 정책은 사용자에게 auth.json, 토큰, 스냅샷, 개인 경로 또는 실제 계정 스크린샷을 공개 보고서에 포함하지 말라고 명시적으로 안내합니다.
합성 데모 데이터는 보안 설계의 일부이다
할당량 대시보드는 자연스럽게 계정 이름, 이메일, 플랜 상세 정보, 초기화 시간 및 사용 값을 표시합니다.
실제 스크린샷은 예상보다 더 많은 정보를 유출할 수 있습니다.
따라서 이 리포지토리는 문서화 및 제품 이미지를 위해 합성 계정 (synthetic accounts)과 스냅샷을 사용합니다.
이는 단순히 마케팅적인 고려 사항이 아닙니다. 안전한 데모 모드는 다음과 같은 작업들을 가능하게 합니다:
- UI 상태 재현 (reproduce UI states);
- 정렬 및 경고 동작 테스트 (test sorting and warning behavior);
- 일관된 스크린샷 캡처 (capture screenshots consistently);
- 프로젝트의 공개 검토 (review the project publicly);
- 모든 이미지의 수동 비식별화 작업 방지 (avoid manually redacting every image).
신뢰할 수 있는 합성 데이터 (synthetic-data) 경로는 기여자들에게 스크린샷을 찍을 때마다 "주의해 달라"고 요청하는 것보다 훨씬 안전한 경우가 많습니다.
릴리스 엔지니어링 (Release engineering)
CodexControl은 두 플랫폼 모두를 위해 태그 기반 (tag-driven) 릴리스 워크플로를 포함하고 있습니다.
macOS 경로는 다음과 같은 작업을 수행할 수 있습니다:
- 구성 시 Developer ID 인증서 가져오기 (import a Developer ID certificate);
- 앱 빌드 (build the app);
- 번들 서명 (sign the bundle);
- 아티팩트 공증 (notarize artifacts);
- ZIP 파일 및 체크섬 (checksum) 게시.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기