opencodex Documentation

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

8,749 stars TypeScript TypeScript 1 file · ~8,871 tokens #ai-gateway#ai-tools#anthropic#chatgpt#claude#claude-code#codex#codex-cli
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy opencodex on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name opencodex -p 10100:10100 --restart always opencodex:local
Configured Services & Ports
⚙️ hub
image: opencodex:local
🔌 Ports: ${OPENCODEX_BIND_ADDRESS:-127.0.0.1}:${OPENCODEX_PORT:-10100}:10100
docker-compose.yml
yaml (docker-compose.yml)
name: opencodex

services:
  hub:
    image: opencodex:local
    build:
      context: .
      dockerfile: Dockerfile
      target: runtime
      args:
        # Required when context is a remote Git URL; harmless for local clones.
        BUILDKIT_CONTEXT_KEEP_GIT_DIR: "1"
    init: true
    read_only: true
    environment:
      # A custom CODEX_HOME also requires a matching writable volume target below.
      CODEX_HOME: /home/bun/.codex
    ports:
      - "${OPENCODEX_BIND_ADDRESS:-127.0.0.1}:${OPENCODEX_PORT:-10100}:10100"
    volumes:
      - ocx-state:/home/bun/.opencodex
      - codex-state:/home/bun/.codex
    tmpfs:
      - /tmp:size=64m,mode=1777
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    restart: unless-stopped
    stop_grace_period: 30s

volumes:
  ocx-state:
  codex-state:

File: README.md

make codex open!

bash
npm install @bitkyc08/opencodex
ocx start        # proxy + dashboard on localhost:10100
**Claude Code, running any model.**The picker is stock Claude Code. The brain behind it isn't. **Codex, running any model.**Pick a provider and go — same workflow, different brain.
**Claude Desktop, running any model.**Opus answers, then hands the task to a GPT-5.6 Sol subagent. **Grok Build, running any model.**Sol drives the session and calls a Kimi K3 subagent.

opencodex is a lightweight local proxy that translates Codex's Responses API into whatever your
provider speaks — streaming, tool calls, reasoning tokens, images, in both directions. Use Claude,
Gemini, Grok, GLM, DeepSeek, Kimi, Qwen, Ollama, or any other LLM with Codex, Claude Code, Claude
Desktop, and Grok Build. It can also manage a ChatGPT account pool for Codex auth: add accounts,
refresh their quotas in the dashboard, and let new sessions auto-route to the lowest-usage healthy
account while existing threads stay pinned to the account that started them.

Quick start

For humans

bash
npm install @bitkyc08/opencodex   # Node 18+; the Bun runtime is bundled automatically
ocx start                            # or `ocx service` to run it in the background

Install from source (latest dev, Bun canary)

macOS / Linux:

bash
curl -fsSL https://bun.sh/install | bash && ~/.bun/bin/bun upgrade --canary
git clone https://github.com/lidge-jun/opencodex.git
cd opencodex && ~/.bun/bin/bun install
~/.bun/bin/bun run src/cli/index.ts start

Windows (PowerShell):

powershell
irm bun.sh/install.ps1 | iex; bun upgrade --canary
git clone https://github.com/lidge-jun/opencodex.git
cd opencodex; bun install
bun run src/cli/index.ts start

Source install runs the latest dev branch with Bun canary. Memory ownership
patches, runtime GC improvements, and unreleased fixes are available here before
they reach the npm package.

Open http://localhost:10100 and configure everything in the web dashboard — add providers
(40+ built-ins, or any OpenAI-compatible endpoint), pick models, manage accounts. ocx gui
re-opens the dashboard at any time.
It can also manage a ChatGPT account pool for Codex auth. Add multiple ChatGPT / Codex accounts,
refresh their 5h / weekly / 30d quota in the dashboard. Under quota routing, new sessions can use
the lowest-usage healthy account; round-robin and fill-first use their own policies. Existing Codex
threads normally retain affinity to the account that started them, so long SSH, tmux, or
mobile-connected sessions do not jump accounts mid-conversation — but quota re-evaluation, failover,
account exclusion, affinity expiry, or 401/403 and 429 recovery can rebind them. Give the accounts a
selection order when one of them — usually your Codex Desktop login — should only be reached for
once the others are drained.

For agents

bash
npm install @bitkyc08/opencodex
ocx start     # or `ocx service`
ocx init      # interactive setup: writes ~/.opencodex/config.json and wires Codex

ocx init never starts the proxy; start it first (or after — either order works, but headless
commands like ocx provider add and ocx combo set talk to the live proxy and exit nonzero
when it is unreachable). ocx status / ocx doctor / ocx health report the running state.

Agents installing or running opencodex: read
AGENTS_INSTALL.md. An interactive ocx start may ask once whether to
star this repository — that is the user's decision, never an agent's. The CLI suppresses the
prompt for agent-driven runs and the API refuses them with 403 agent_consent_required.

Supported platforms

OS Status Service manager
macOS (arm64 / x64) Fully supported launchd
Linux (x64 / arm64) Fully supported systemd (user unit)
Windows (x64) Fully supported Task Scheduler (hidden) / opt-in native service (--native, WinSW)

Requires Node 18+. The Bun runtime is bundled on npm install — no separate
Bun install needed, no WSL needed on Windows. If npm blocked the bundled runtime's install scripts,
see the installation docs.

Highlights

  • Use any LLM with Codex, Claude Code, Claude Desktop, and Grok Build — 40+ providers out of
    the box, each keeping its own native UI.

  • Pool ChatGPT accounts — thread affinity, quota-aware auto-switching, cooldown and
    fail-closed auth handling.

    Provider-policy note: Account pooling is for routing and operational resilience only; it does
    not guarantee protection from provider rate limits, enforcement, suspension, or other account
    actions. OpenCodex does not endorse using additional accounts to circumvent provider limits or
    sharing account credentials between people. You are responsible for complying with each
    provider's current terms. See the
    Codex Auth account-pool guidance
    and OpenAI's current Terms of Use.

  • Combos — one virtual model id with failover or weighted round-robin across providers. See
    the combo guide.

  • Sub-agents on any model — feature routed models in Codex's sub-agent picker, with v1/v2
    surface control and fallback chains. See the
    sub-agent guide.

  • Log in once, skip the API key — OAuth for xAI, Anthropic, and Kimi; or forward
    codex login, paste a key, or use ${ENV_VAR} references.

  • Web search & vision sidecars — non-OpenAI models get real web search and image understanding
    through a sidecar over your ChatGPT login.

  • See what's happening — the dashboard shows providers, OAuth status, model selection, and a
    live request log with cache token counts.

  • Clean exit, zero residue — ocx stop restores Codex to its original configuration.

  • Bounded memory ownership — every long-lived cache, ring buffer, and protocol-translation
    store has a finite cap, byte budget, or active reconciliation. No unbounded Map or Set
    survives a config reload.

Memory ownership details

OpenCodex tracks 36 categories of process-retained state. Each has a documented bound:

  • 12 retained stores (request log, debug rings, image cache, model cache, vision
    descriptions, cursor blobs, responses continuation, etc.) are byte-accounted and
    evicted by the app-owned memory budget (default 256 MiB).
  • 4 observed buffers (translator accumulators, image/OAuth/Grok tails) are
    monitored for in-flight byte pressure without eviction.
  • 24 state-store registrations handle expiry sweeps (60 s interval) and
    config-generation reconciliation so stale provider/account keys are removed.
  • Path and fingerprint memos (workspace metadata, hardened identities, installation
    salts, mode-hint capabilities) use insertion-order LRU caps (8–128 entries).
  • Model-cache generation tombstones are deleted after reconciliation; a global
    generation increment prevents stale in-flight discoveries from repopulating removed
    providers.
  • Lab event-id deduplication runs under a ledger lock from disk, with no
    process-level RAM index.

Run GET /api/system/memory (with the admin token) to inspect live retained bytes,
eviction counters, and watchdog samples.

Model routing

Target any configured provider and model with the provider/model syntax:

bash
codex -m "anthropic/claude-opus-5" "Explain this stack trace"
codex -m "google/gemini-3-pro" "Write unit tests for auth.ts"
codex -m "ollama/llama3" "Refactor this function"

Omit the provider/ prefix to use the default provider or auto-match by model name pattern.
Provider model ids containing / are exposed with inner slashes aliased to -; the raw
full-slash form keeps working too. Details: model routing docs.

Providers & adapters

OpenAI (ChatGPT login or API key), Anthropic, Google Gemini, xAI, Kimi, Azure OpenAI, Ollama
(local + Cloud), Cursor (experimental), and every OpenAI-compatible endpoint — plus DeepSeek,
Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging Face, NVIDIA NIM, MiniMax,
Qwen Cloud, SiliconFlow, and more. Full list: ocx init or the
provider docs.

CLI

bash
ocx init                       # interactive setup (writes config, wires Codex, offers the shim)
ocx start [--port 10100]       # start the proxy in the foreground
ocx stop                       # stop + restore native Codex
ocx service [install|start|stop|status|uninstall|remove]  # background service
ocx codex-shim install         # start the proxy on demand whenever `codex` launches
ocx health [--json]            # check immediate proxy liveness
ocx ready [--json] [--wait [--timeout <seconds>]]  # check post-sync readiness
ocx status                     # is the proxy running?
ocx gui                        # open the web dashboard
ocx provider <...>             # manage providers (list/add/edit/test/remove)
ocx account <...>              # manage ChatGPT accounts & API-key pools
ocx combo <...>                # manage failover / round-robin combos
ocx v2 <...>                   # multi-agent v1/v2 surface controls
ocx update [--tag preview]     # update opencodex

Unpinned starts may pick another free port if the preferred one is busy; an explicit --port
never hops. Full reference: CLI docs.

Health and readiness

GET /healthz reports immediate proxy liveness. The unauthenticated GET /readyz endpoint reports
post-sync readiness with the sanitized JSON identity {service, version, uptime, pid, port, status}.
It returns 200 when status is ready; pending and terminal failed return 503 with
Retry-After: 1.

ocx ready [--json] [--wait [--timeout <seconds>]] performs one probe by default. --wait polls
for up to 45 seconds by default, but exits immediately when it observes terminal failed;
--timeout <seconds> sets a 1–300 second limit, requires --wait, and accepts only positive integers. CLI --json output is
{ready, status, pid, port}, where status is ready, pending, failed, or unreachable.

Exit Result
0 Ready
1 Not ready: pending, failed, timeout, or unreachable
64 Invalid arguments

An older proxy without /readyz fails closed as unreachable with exit 1, while ocx health
remains compatible.

Autostart: service vs shim

Use the service (ocx service) for an always-on proxy that restarts on crash. Use the
shim (ocx codex-shim install) for lightweight, on-demand startup without a background
daemon. Remove them with ocx service uninstall / ocx codex-shim uninstall.

Uninstall

bash
ocx uninstall                  # stop, remove service/shim, restore native Codex, clean up state
npm uninstall -g @bitkyc08/opencodex

Remote access

By default opencodex binds to 127.0.0.1 and needs no extra authentication. Binding beyond
loopback ("hostname": "0.0.0.0") requires a bearer token — the proxy refuses to start
without OPENCODEX_API_AUTH_TOKEN, and every client request must carry it as
x-opencodex-api-key. Details: configuration reference.

Documentation

The public docs — install, providers, routing, combos, sub-agents, sidecars, integrations, and
the CLI/config/management-API references — are built from docs-site/ and
published to opencodex.me.

Maintainer source-of-truth notes live under structure/, contributor setup in
CONTRIBUTING.md, and security reporting in SECURITY.md.
Report undisclosed vulnerabilities privately through
GitHub private vulnerability reporting,
not a public issue.

Development

Source development requires the bun CLI on your PATH. This is separate from the published npm
package's bundled Bun runtime, which is used only by installed ocx commands.

bash
git clone https://github.com/lidge-jun/opencodex.git
cd opencodex
bun install
bun run typecheck
bun run test

See Contributing.

Disclaimer

opencodex is an independent, community-maintained project and is not affiliated with or endorsed by OpenAI, Anthropic, or any other provider.

Some providers — notably Anthropic (Claude) — may suspend or restrict accounts that route API traffic through third-party proxies. Use at your own risk (UAYOR). Before connecting a provider, review its Terms of Service to confirm that proxy-based access is permitted. The opencodex maintainers are not responsible for any account actions taken by upstream providers.

License

MIT


File: devlog/_chase/README.md

_chase: upstream 따라잡기 노트

opencodex 프록시 레이어가 따라잡는 두 upstream의 추적 노트. 코드를 통째로
vendoring하지 않는다. 실제로 따라잡을 표면은 좁다: API 변환(translator/wire)과
모델명/카탈로그 컨텍스트 두 갈래뿐이라, 무거운 클론 대신 노트 + on-demand 대조로 간다.

두 upstream

slug repo 역할 로컬
gjc (jawcode) lidge-jun/jawcode (packages/ai/src/providers/*.ts) opencodex 어댑터의 직접 포팅 출처 (1차 SOT) /Users/jun/Developer/new/700_projects/jawcode 에 이미 존재
cca router-for-me/CLIProxyAPI (Go) wire/auth/quirks 외부 교차검증 SOT (2차) 없음. 필요 시 _cca/로 shallow clone

주의: jawcode는 gajae-code(에이전트)가 아니다. 프록시 프로바이더 레이어는
jawcode에 있고, gajae-code는 별개 코딩 에이전트다.

Layout

text
devlog/_chase/
  00_parity-index.md   # 3 upstream parity 인덱스 + 분석 HEAD 기록 (tracked)
  01_overview.md       # chase 정의 · 갭 4종(G1-G4) · 우선순위 (tracked)
  02_gap_inventory.md  # 축별 앞섬/뒤처짐 + 항목별 G-tag·상태 (tracked)
  03_follow_index.md   # 실행 우선순위 Tier 1/2/3 (tracked)
  10_jawcode.md        # gjc parity: provider/wire/model 1:1 대조 (tracked)
  20_cli-proxy-api.md  # cca parity: executor/auth/translator 교차검증 (tracked)
  30_litellm.md        # litellm parity: 카탈로그/커버리지 폭 참조 (tracked)
  _model/              # 모델/provider 현재 구조·변경 절차·upstream delta (tracked docs)
  _gjc/                # (옵션) jawcode 로컬 클론/심볼릭: gitignored
  _cca/                # (옵션) CLIProxyAPI 로컬 클론: gitignored
  _litellm/            # (옵션) litellm 로컬 클론: gitignored

form vs action (jawcode struct_har ↔ chase와 동형)

jawcode는 struct_har/(형태 스냅샷, 자동생성)와 struct_har/chase/(행동, 수동)를 나눈다.
opencodex는 이미 structure/(patched SoT)와 src/(코드 정본)가 form 역할을 하므로,
chase는 행동 레이어만 둔다.

form (스냅샷) action (행동)
10_/20_/30_ dossier + repo structure/·src/ 01_overview · 02_gap_inventory · 03_follow_index
upstream 규모·HEAD·wire 정본 갭·다음 경로·완료 기준·우선순위

devlog/ 전체가 root .gitignore에서 무시된다. 노트(*.md)는 의도적으로
force-add(git add -f)해서 추적하고, 클론 디렉터리(_gjc/, _cca/)는 그대로
무시된 채 둔다.

따라잡는 방식 (vendoring 안 함)

두 repo 모두 공개라 cxc-search 사다리로 원본을 열어 대조한다. clone은 옵션이다.

  1. discover: Tier 1 hosted web_search로 해당 영역의 upstream 최신 커밋/파일을 찾는다.
  2. prove: 후보 URL(또는 raw 파일)을 열어 실제 wire/모델 목록을 확인한다. 스니펫만으로 확정하지 않는다.
  3. diff: opencodex 해당 지점(아래 표)과 동작을 대조하고, 차이를 해당 phase devlog(_plan/·_fin/)에 file:line 근거로 기록한다.

GitHub raw로 바로 여는 예:

bash
# CLIProxyAPI translator 한 파일 열기 (clone 없이)
curl -fsSL https://raw.githubusercontent.com/router-for-me/CLIProxyAPI/main/internal/translator/antigravity/gemini/antigravity_gemini_request.go

(옵션) 로컬 클론

대량 grep이나 file:line 인용이 잦아지면 그때만 shallow clone 한다. 자동으로 무시된다.

bash
# cca: 로컬에 없으므로 필요 시
git clone --depth 1 https://github.com/router-for-me/CLIProxyAPI devlog/_chase/_cca

# gjc: 이미 워크스페이스에 있어 보통 심볼릭이면 충분
ln -s /Users/jun/Developer/new/700_projects/jawcode devlog/_chase/_gjc

착수/대조 전 최신화:

bash
git -C devlog/_chase/_cca fetch origin && git -C devlog/_chase/_cca log -1 --oneline
git -C /Users/jun/Developer/new/700_projects/jawcode fetch origin && \
  git -C /Users/jun/Developer/new/700_projects/jawcode log -1 --oneline

opencodex 대조 표면 (따라잡을 지점)

표면 opencodex gjc(jawcode) cca(CLIProxyAPI)
API 변환 (Google/Antigravity) src/adapters/google.ts, google-antigravity-wire.ts, google-antigravity-replay.ts packages/ai/src/providers/*.ts internal/translator/antigravity/**, internal/runtime/executor/antigravity_executor.go
API 변환 (Kiro) src/adapters/kiro*.ts packages/ai/src/providers/amazon-bedrock.ts 등 (없음)
auth/refresh src/oauth/*, src/lib/gcp-adc.ts jawcode oauth utils internal/auth/**
모델/카탈로그 src/providers/antigravity-models.ts, kiro-models.ts, src/codex/catalog.ts jawcode models.json + static lists cmd/fetch_antigravity_models/main.go

모델/provider 변경 전에는 _model/README.md를 먼저 읽는다. upstream 비교의 상세
file:line은 10_jawcode.md(gjc), 20_cli-proxy-api.md(cca) 참고.


File: docs/adr/0001-gui-update-worker.md

ADR 0001: GUI self-update runs through a worker job

Status

Accepted

Context

The dashboard needs buttons for ocx sync and opencodex self-update. ocx sync is safe to run in
the proxy process because it refreshes Codex config/catalog state. ocx update is different: npm
installs may replace the package files currently serving the GUI, and the existing CLI update path
can print to inherited stdio and exit the process.

Decision

GUI self-update is not executed directly in the request handler. The dashboard calls management
API endpoints that create an update job in OPENCODEX_HOME/update-job.json. The proxy starts a
detached hidden CLI worker, and the worker performs the install command and optional restart.

For npm installs, the worker runs the Node launcher path (node bin/ocx.mjs update --tag <tag>) so
the existing npm self-update guard is reused. For Bun global installs, it runs the existing Bun
global update command. Source checkouts remain manual-only and show git pull && bun install && bun run build:gui.

After an update requests a restart, the worker now waits for an identity-checked /healthz to
return and remain healthy for a short stability window before marking the job successful. This
keeps update-job.json honest on Windows cases where npm leaves the bundled Bun runtime in a bad
state and the restarted proxy dies a few seconds later.

For npm installs specifically, node ocx.mjs update already stops the proxy and reinstalls /
starts the managed service (or falls back to a direct start). When a background service was
installed, the worker therefore confirms that self-update restart first — but only skips the
redundant second service install when /healthz shows update-correlated evidence (a new PID
vs the pre-update capture, and/or the job's target version). A bare healthy identity is not
enough: a surviving pre-update process would otherwise look like success. Direct (non-service)
npm installs skip the probe-first path entirely, because the launcher only prints ocx start
and never brings the proxy back on its own — waiting would always burn the full health timeout
before the worker's explicit restart. A second install would call stopWindows() on the healthy
listener and often fail elevation from the non-interactive worker, leaving the captured port
(default 10100) dead until a manual restart. Bun global installs still always take the explicit
restart path because bun add -g does not restart the proxy.

Consequences

  • The GUI request handler stays responsive and does not overwrite its own running module graph.
  • Update status survives a proxy restart because it is stored in the opencodex config directory.
  • Restart handling can branch between service-managed installs and direct detached proxy starts.
  • A completed install can still finish with status: "failed" when the replacement proxy never
    becomes healthy or flaps during the stability window; the job log then points the user at
    ocx start and the Bun --allow-scripts reinstall path.
  • The dashboard must poll both the job endpoint and /healthz while reconnecting.

File: docs/adr/0002-doctor-proxy-env-diagnostics.md

ADR 0002: Doctor separates shell proxy env from running proxy process env

Status

Accepted

Context

ocx doctor used to show only the proxy variables visible to the ocx doctor
process. That is accurate for the current shell, but misleading when a user
started ocx start or a service from a different environment and then ran
ocx doctor in a new terminal.

Environment variables are inherited by child processes, not shared globally
between existing shells. A new terminal can therefore show HTTP_PROXY as unset
while the already-running opencodex proxy process still has it set.

Decision

ocx doctor reports three separate proxy surfaces:

  • the current doctor process environment
  • the effective config.proxy state, with the value hidden
  • the running opencodex proxy process environment when a recorded PID is
    available and Linux /proc//environ can be read

The process environment diagnostic reports only presence/absence of known proxy
keys. It never prints or stores proxy values because proxy URLs can contain
credentials.

Consequences

  • Users can distinguish "this new terminal has no proxy env" from "the running
    proxy process has no proxy env".
  • Linux and WSL get an accurate runtime-process check without service-manager
    parsing.
  • Non-Linux platforms show the running-process env check as unavailable instead
    of pretending service environment inspection is portable.

File: docs/adr/0003-deepseek-v4-thinking-history.md

ADR 0003: DeepSeek V4 thinking history is model-scoped metadata

Status

Accepted

Context

DeepSeek V4 thinking mode returns assistant reasoning_content and expects that
field to be passed back in later multi-turn and tool-call requests. If the
gateway drops that history, DeepSeek can reject the next request with a 400 that
says the reasoning_content from thinking mode must be passed back.

opencodex already has a provider flag for OpenAI-compatible chat models that
require reasoning history replay: preserveReasoningContentModels.

Decision

DeepSeek V4 thinking models are marked in the provider registry with:

  • model-scoped Codex reasoning levels
  • xhigh to upstream max reasoning effort mapping
  • preserveReasoningContentModels

This is not enabled for every OpenAI-compatible provider or for the legacy
deepseek-reasoner preset.

Consequences

  • Existing saved deepseek configs receive the new metadata at route time.
  • DeepSeek V4 tool-call/subagent turns keep reasoning_content on assistant
    history messages, including messages that also carry tool_calls.
  • The older DeepSeek reasoner behavior remains unchanged.

File: docs/adr/0004-gui-toggle-contrast-and-nav-spacing.md

ADR 0004: GUI toggle contrast and sidebar item spacing

Status

Accepted

Context

In dark mode, enabled switches used the monochrome accent for the track. The bright track could
visually merge with the switch knob, making the enabled state and knob position difficult to read.
Sidebar navigation items also had no vertical separation, so adjacent hover and active backgrounds
appeared as one continuous block.

Decision

Keep the existing shared switch components and CSS tokens. Use the existing dark-theme success
green for enabled switch tracks while retaining a dark knob, and make .switch consume the same
toggle tokens as the label-based .toggle control.

Lay out the sidebar nav as a vertical flex container with a 4px gap. This preserves each item's
full click target while separating adjacent hover and active surfaces.

Consequences

  • Enabled switches have distinct track and knob silhouettes in dark mode.
  • Both switch implementations share one enabled-state color rule.
  • Sidebar hover and active states remain visually separate without adding divider noise.
  • No component API or dependency changes are required.

File: docs/adr/0005-gui-design-token-system.md

ADR 0005: GUI uses role-based CSS design tokens

Status

Accepted

Context

The GUI accumulated thirteen font sizes, several near-duplicate weights, page-local radii, and
inline typography values. Those values made equivalent labels, controls, helper text, and machine
data render differently across pages and made dark/light maintenance harder.

The GUI must also remain usable when served locally or offline, including Korean locales. A remote
font or runtime styling dependency would add network and packaging failure modes to a management
surface that should remain available while the proxy is being repaired.

Decision

Use native CSS custom properties in gui/src/styles.css as the runtime source of truth. Define:

  • semantic light/dark color tokens;
  • a Korean-safe UI font stack and a separate code/data font stack;
  • eight role-based type sizes, four weights, and four line heights;
  • a 4px-based spacing scale;
  • shared radius, control height, icon size, and motion tokens;
  • small typography utility classes for TSX contexts that previously used inline numeric values.

Keep component styling in the existing CSS and React primitives instead of adding CSS-in-JS,
Tailwind, a component framework, or a remote font. Document the contract under
docs/design-system/ and require new visual values to use tokens.

For local integrated visual QA, use Vite's opt-in OPENCODEX_PROXY_TARGET proxy so the development
GUI can call the running management API through the same origin without changing production output.

Alternatives considered

  • CSS-in-JS: strong component co-location, but adds runtime and migration cost for no functional gain.
  • Utility framework: broad ecosystem, but would duplicate the existing CSS and enlarge the change surface.
  • Remote or bundled web font: stronger cross-platform identity, but remote loading harms offline reliability
    and bundling adds package size/licensing work. System Korean fallbacks are sufficient for this console.
  • Page-local cleanup only: smaller initial diff, but leaves the inconsistency mechanism intact.

Consequences

  • Equivalent UI roles render consistently across all pages and locales.
  • Dark/light changes can be made at the token layer.
  • New contributors have a documented component and token contract.
  • Existing layout-specific inline values remain allowed when they are algorithmic rather than visual roles.
  • A future branded font can be introduced by changing --font-ui after packaging and licensing are decided.

File: docs/design-system/components.md

Components

App shell and navigation

  • 데스크톱은 232px sidebar와 main content의 2열 구조다.
  • 760px 이하에서는 sidebar가 off-canvas drawer로 전환된다.
  • .nav-item은 아이콘 17px, control text, 4px 세로 간격을 사용한다.
  • hover와 active는 같은 surface family를 쓰되 active는 semibold로 구분한다.
  • 메뉴마다 margin을 직접 추가하지 않고 .sidebar nav의 gap을 사용한다.

Buttons

변형 클래스 용도
Primary .btn.btn-primary 저장, 로그인, 확정
Ghost .btn.btn-ghost 보조 액션, 취소
Danger .btn.btn-danger 삭제, 중단
Small .btn.btn-sm 필터, 행 내부 액션
Icon .btn-icon 닫기, 도움말, 제거

버튼은 기본적으로 control text와 medium weight를 사용한다. 액션 중요도는 크기가 아니라
색상 변형으로 표현한다. 아이콘 전용 버튼에는 반드시 접근 가능한 label/title을 제공한다.

Inputs and selects

  • .input, .select-trigger, .select-option, .field-label을 재사용한다.
  • 라벨은 label text, 입력값은 control text를 사용한다.
  • placeholder는 --faint, 값은 --text, 오류는 --red를 사용한다.
  • focus는 border와 --accent-soft ring을 함께 사용한다.
  • 네이티브 select를 새로 만들기보다 ui.tsx의 Select를 우선한다.

Cards and panels

  • .card: 경계와 배경만 제공한다. 내부 padding은 문맥이 소유한다.
  • .panel: 기본 18px padding이 포함된 독립 구획이다.
  • .panel-accent: 선택되거나 강조된 구획이다.
  • 카드 중첩은 최대 한 단계로 제한한다. 정보를 나누기 위해 무조건 카드부터 추가하지 않는다.

Tables and rows

  • .tbl/.tbl-wrap은 데이터 표의 기본 조합이다.
  • 본문은 control text, 헤더는 label text + medium weight다.
  • 숫자 열은 .num 또는 .mono로 tabular 숫자를 사용한다.
  • 작은 화면에서는 열을 억지로 접지 않고 .tbl-wrap에서 가로 스크롤한다.

Badges and status

  • .badge-green: 성공, 연결됨, 활성
  • .badge-amber: 경고, 다음 적용, 주의 필요
  • .badge-muted: 중립 메타데이터
  • .notice-ok/.notice-err/.notice-warn: 한 줄 이상의 상태 메시지

배지는 핵심 본문을 대신하지 않는다. 매우 작은 .text-micro는 짧은 상태 문자열에만 허용한다.

Toggle and switch

두 마크업 형태가 있지만 동일한 토큰을 사용한다.

  • 버튼형: ui.tsx의 Switch, .switch, .knob
  • label/input형: .toggle, .slider

켜짐은 --toggle-on-bg, 꺼짐은 --toggle-off-bg, 핸들은 --toggle-dot-color를 사용한다.
다크 모드 활성 상태는 녹색 트랙과 짙은 핸들로 분리한다. 화면별 토글 색상 override는 금지한다.

Icons

  • 기본 아이콘은 icons.tsx의 동일한 outline family를 사용한다.
  • 기본 크기는 --icon-md(16px), 조밀한 메타 UI는 --icon-sm(14px), 상단 액션은 --icon-lg(20px)다.
  • 아이콘만으로 의미가 불명확하면 텍스트를 함께 배치한다.
  • active 아이콘은 --text, inactive 아이콘은 --faint를 사용한다.

Responsive behavior

  • 760px 이하: drawer navigation, 세로형 form row, 44px 터치 대상
  • 640px 이하: dashboard stat 2열
  • 데이터 표와 heatmap은 정보 손실보다 가로 스크롤을 우선한다.
  • 모바일에서 제목/본문 크기를 임의 축소하지 않는다. 같은 역할은 같은 type token을 유지한다.

Provider workspace

  • 넓은 화면의 workspace는 일반 페이지의 읽기 폭보다 넓게 쓸 수 있지만, rail은 240–280px 범위로 고정하고 detail은 minmax(0, 1fr)로 확장한다. viewport가 아니라 workspace container 폭을 기준으로 overview를 한 열로 바꾸고, rail + detail 최소 폭을 확보할 수 없으면 세로로 쌓는다.
  • rail 행 문법은 원본 브랜드 아이콘 | 두 줄 copy | 상태 trail이다. 첫 줄에는 표시 이름과 Free/Local 예외 badge만, 둘째 줄에는 모델 수와 충돌 시에만 config id를 둔다. 이름과 메타는 한 줄 ellipsis를 사용하며 전체 값은 title과 접근 가능한 이름으로 보존한다.
  • provider SVG는 catalog의 원본 색을 가진 `` 경로를 유지한다. workspace 색으로 mask/recolor하지 않는다. asset이 없는 fallback 아이콘만 현재 text token을 따른다.
  • Ready/설정 필요/비활성 텍스트는 그룹 heading과 option의 접근 가능한 이름이 소유한다. 상태 점은 비어 있는 aria-hidden 보조 표시이며 내부에 텍스트를 넣지 않는다.
  • listbox 자체와 native option button을 동시에 Tab stop으로 만들지 않는다. option button이 focus를 소유하고 ArrowUp/ArrowDown/Home/End 이동은 listbox의 bubbled key event로 처리한다.
  • detail tab은 tablist/tab/tabpanel, aria-controls/aria-labelledby, roving tabIndex를 연결하고 ArrowLeft/ArrowRight/Home/End로 선택과 focus를 함께 이동한다. 좁은 폭에서는 tab을 숨기거나 세로 글자로 깨지 말고 가로 스크롤한다.
  • 인증 가능한 provider는 Accounts 또는 API keys를 독립 detail tab으로 노출한다. active account는 focus 가능한 상태로 유지하고 text + aria-current로 선택을 알린다. loading/error/empty/reauth/switching은 색만으로 표현하지 않는다.
  • 계정 행, title, aria-label, 확인창, toast에는 masked email 또는 번역된 순번만 사용한다. 저장소의 opaque account id와 token 원문은 사용자 표면에 노출하지 않는다.

분할 필터 (Segmented filters)

  • 서로 배타적인 선택지는 네이티브 button을 인접한 pill 형태로 묶고, 컨테이너에는 role="group"과 번역된 이름을 제공한다.
  • 각 버튼은 선택 상태를 aria-pressed로 노출하며 모두 일반 Tab 순서에 남긴다. 화살표 키와 roving focus를 구현하지 않는 한 radio/radiogroup 역할을 사용하지 않는다.
  • 버튼의 시각적 라벨과 별개로 번역된 aria-label을 명시한다. 좁은 화면에서 브랜드 텍스트를 아이콘으로 접어도 접근 가능한 이름은 유지해야 한다.
  • 모바일 버튼은 최소 44px 터치 높이를 보장한다. 공간이 부족하면 선택지를 숨기지 말고 완전한 그룹 단위로 줄바꿈하거나 세로로 쌓는다.
  • 여러 segmented filter가 함께 있으면 데이터 차원 순서대로 배치한다. Usage에서는 소스 그룹을 기간 그룹보다 먼저 둔다.

File: docs/design-system/contributing.md

Design System Contribution Guide

새 UI를 만들기 전

  1. gui/src/ui.tsx와 gui/src/styles.css에 같은 역할의 컴포넌트가 있는지 검색한다.
  2. 기존 컴포넌트를 변형으로 확장할지, 새 컴포넌트가 필요한지 판단한다.
  3. 새 값이 아니라 기존 semantic token으로 표현 가능한지 확인한다.
  4. 새 토큰이 필요하면 최소 두 곳에서 재사용될 역할인지 설명하고 ADR 또는 Decision Log를 남긴다.

금지 규칙

  • TSX에 숫자형 fontSize, fontWeight, lineHeight, letterSpacing 추가
  • hex/rgb 색상을 페이지 컴포넌트에 직접 추가
  • borderRadius: 999처럼 토큰을 우회하는 값 추가
  • 한 화면만을 위한 새로운 폰트 family 추가
  • 기존 Switch, Select, .btn, .card, .panel, .badge와 중복되는 컴포넌트 생성
  • hover만 있고 keyboard focus가 없는 인터랙션 추가

허용되는 예외

차트 셀 크기, 가상화 row height, viewport 계산처럼 데이터/알고리즘에 종속된 값은 인라인으로
남길 수 있다. 다만 시각적 역할을 나타내는 값은 반드시 token을 사용한다.

권장 구현 예시

tsx
설정 이름
  설정 설명
  <Switch on={enabled} onClick={toggle} label="설정 이름" />

검증 체크리스트

  • 라이트/다크에서 텍스트와 상태 색상이 읽힌다.
  • 페이지 제목, 본문, 라벨, 코드가 역할별 type token을 사용한다.
  • 데스크톱과 760px 이하 viewport에서 overflow/clipping이 없다.
  • hover, active, focus-visible, disabled 상태가 존재한다.
  • 토글과 입력이 실제 상태를 변경한다.
  • bun run lint가 오류 없이 끝난다.
  • bun run build가 성공한다.
  • Playwright 또는 Browser로 console error와 framework overlay가 없음을 확인한다.
  • 시각 변경 시 전/후 또는 기준/구현 스크린샷을 view_image로 직접 비교한다.

리뷰 명령

bash
rg -n 'font-size:\s*[0-9]|font-weight:\s*[0-9]' gui/src/styles.css
rg -n 'fontSize:\s*[0-9]|fontWeight:\s*[0-9]|lineHeight:\s*[0-9]' gui/src --glob '*.tsx'
rg -n 'border-radius:\s*[0-9]|borderRadius:\s*[0-9]' gui/src --glob '*.{css,tsx}'
cd gui && bun run lint && bun run build

첫 세 명령은 결과가 없어야 한다.

실제 로컬 API와 함께 화면을 확인할 때는 Vite의 opt-in proxy를 사용한다.

bash
cd gui
OPENCODEX_PROXY_TARGET=http://127.0.0.1:10101 bun run dev --host 127.0.0.1

File: docs/design-system/foundations.md

Foundations

Color

색상 토큰은 역할을 나타낸다. 컴포넌트에서 hex 값을 직접 사용하지 않는다.

역할 토큰 라이트 다크
앱 배경 --bg #ffffff #212121
사이드바 --rail #f9f9f9 #171717
기본 표면 --surface #ffffff #262626
올라온 표면 --raised #f4f4f4 #303030
기본 텍스트 --text #0d0d0d #ececec
보조 텍스트 --muted #6e6e6e #a6a6a6
약한 텍스트 --faint #707070 #9a9a9a
성공/활성 --green #0a7d5c #4ecb9d
경고 --amber #9a4a08 #fbbf24
위험 --red #b91c1c #f87171

--accent는 기본 액션과 포커스에 사용한다. 활성 토글은 다크 모드에서 트랙과 핸들이
겹쳐 보이지 않도록 --toggle-on-bg와 --toggle-dot-color를 사용한다.

Typography

Font families

  • --font-ui: 일반 UI, 제목, 본문, 버튼, 입력. Pretendard/Noto Sans KR/Apple SD Gothic Neo/Malgun Gothic을 포함해 한글 fallback을 보장한다.
  • --font-code: 모델 ID, URL, 버전, 토큰 수, 로그, 코드. 숫자는 tabular 형태로 정렬한다.

외부 CDN 폰트를 사용하지 않는다. 프록시 관리 화면은 오프라인에서도 열려야 하고, 폰트
다운로드 실패가 레이아웃 이동이나 한글 누락으로 이어지면 안 되기 때문이다.

Type scale

역할 클래스 토큰 크기 대표 용도
Micro .text-micro --text-micro 10px 매우 작은 상태 배지
Caption .text-caption --text-caption 11px 메타데이터, 보조 수치
Label .text-label --text-label 12px 필드 라벨, 표 헤더, 코드
Control .text-control --text-control 13px 버튼, 메뉴, 필터, 알림
Body .text-body --text-body 14px 본문, 카드 제목
Subtitle .text-subtitle --text-subtitle 16px 모달 제목, 브랜드명
Title .text-title --text-title 20px 페이지 제목, 주요 수치
Display .text-display --text-display 24px 제한적인 대형 수치/빈 상태

굵기는 regular(400), medium(500), semibold(600), bold(700) 네 단계만 사용한다.
행간은 tight(1.2), ui(1.35), body(1.5), relaxed(1.6) 네 단계만 사용한다.

Spacing

기본 단위는 4px이다.

토큰 값 토큰 값
--space-0-5 2px --space-1 4px
--space-1-5 6px --space-2 8px
--space-3 12px --space-4 16px
--space-5 20px --space-6 24px
--space-8 32px --space-10 40px
--space-12 48px --space-16 64px

컴팩트 컨트롤 내부는 4–8px, 카드 내부는 12–20px, 페이지 구획은 24–64px 범위를 사용한다.

Radius and controls

토큰 값 용도
--radius-2xs 4px 작은 차트 셀, 포커스 보정
--radius-xs 6px 칩, 작은 아이콘 표면
--radius-sm 8px 입력, 메뉴 항목, 작은 카드
--radius 12px 기본 카드/패널
--radius-lg 16px 모달
--radius-round 50% 점, 원형 아이콘
--radius-pill 999px 버튼, 토글, segmented control

컨트롤 높이는 28/34/40/44px 단계다. 44px는 모바일 터치 영역에 사용한다.

Motion

  • --motion-fast: 120ms: hover, border, color 변화
  • --motion-normal: 180ms: drawer, toggle, 위치 변화
  • prefers-reduced-motion: reduce에서는 transition과 animation을 제거한다.

모션은 상태 이해를 돕는 범위에서만 사용하며 장식 목적의 반복 애니메이션은 추가하지 않는다.