File: README.md
make codex open!
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
npm install @bitkyc08/opencodex # Node 18+; the Bun runtime is bundled automatically
ocx start # or `ocx service` to run it in the backgroundInstall from source (latest dev, Bun canary)
macOS / Linux:
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 startWindows (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 startSource 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
npm install @bitkyc08/opencodex
ocx start # or `ocx service`
ocx init # interactive setup: writes ~/.opencodex/config.json and wires Codexocx 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 interactiveocx startmay 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 with403 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 stoprestores 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 unboundedMaporSet
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:
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
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 opencodexUnpinned 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 withRetry-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
ocx uninstall # stop, remove service/shim, restore native Codex, clean up state
npm uninstall -g @bitkyc08/opencodexRemote 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 asx-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 inCONTRIBUTING.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.
git clone https://github.com/lidge-jun/opencodex.git
cd opencodex
bun install
bun run typecheck
bun run testSee 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
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 로컬 클론: gitignoredform 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은 옵션이다.
- discover: Tier 1 hosted
web_search로 해당 영역의 upstream 최신 커밋/파일을 찾는다. - prove: 후보 URL(또는 raw 파일)을 열어 실제 wire/모델 목록을 확인한다. 스니펫만으로 확정하지 않는다.
- diff: opencodex 해당 지점(아래 표)과 동작을 대조하고, 차이를 해당 phase devlog(
_plan/·_fin/)에 file:line 근거로 기록한다.
GitHub raw로 바로 여는 예:
# 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 한다. 자동으로 무시된다.
# 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착수/대조 전 최신화:
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 --onelineopencodex 대조 표면 (따라잡을 지점)
| 표면 | 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 atocx startand the Bun--allow-scriptsreinstall path. - The dashboard must poll both the job endpoint and
/healthzwhile 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 ranocx 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.proxystate, with the value hidden - the running opencodex proxy process environment when a recorded PID is
available and Linux/proc//environcan 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
xhighto upstreammaxreasoning effort mappingpreserveReasoningContentModels
This is not enabled for every OpenAI-compatible provider or for the legacydeepseek-reasoner preset.
Consequences
- Existing saved
deepseekconfigs receive the new metadata at route time. - DeepSeek V4 tool-call/subagent turns keep
reasoning_contenton assistant
history messages, including messages that also carrytool_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 underdocs/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-uiafter 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-softring을 함께 사용한다. - 네이티브 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, rovingtabIndex를 연결하고 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를 만들기 전
gui/src/ui.tsx와gui/src/styles.css에 같은 역할의 컴포넌트가 있는지 검색한다.- 기존 컴포넌트를 변형으로 확장할지, 새 컴포넌트가 필요한지 판단한다.
- 새 값이 아니라 기존 semantic token으로 표현 가능한지 확인한다.
- 새 토큰이 필요하면 최소 두 곳에서 재사용될 역할인지 설명하고 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을 사용한다.
권장 구현 예시
설정 이름
설정 설명
<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로 직접 비교한다.
리뷰 명령
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를 사용한다.
cd gui
OPENCODEX_PROXY_TARGET=http://127.0.0.1:10101 bun run dev --host 127.0.0.1File: 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을 제거한다.
모션은 상태 이해를 돕는 범위에서만 사용하며 장식 목적의 반복 애니메이션은 추가하지 않는다.