서버
127.0.0.1)에 열고, 뒤에서 Anthropic·Gemini·Bedrock·OpenRouter 같은 제공자로 요청을 나눠 보내며 누가 어떤 모델을 얼마까지 쓸 수 있는지를 SQLite에 기록하는 Python + Rust 프로젝트입니다. 쌓인 트래픽으로 라우터를 학습하는 기능까지 한 저장소에 들어 있습니다.experiential은 호스팅·BYOK(자기 키)·로컬 모델을 하나의 OpenAI 호환 API 뒤에 모으고, 사용자·에이전트별 모델 권한과 월 예산을 걸며, 지나간 트래픽으로 라우터를 최적화하는 오픈소스 게이트웨이입니다. README는 이 세 가지를 프로젝트의 목적으로 직접 적습니다.
설치는 pip install experiential, 실행 명령은 exp입니다(pyproject.toml의 [project.scripts]가 exp = "exp.cli:main"을 가리킵니다). 이 글은 커밋 c847e2a(버전 0.7.178) 소스를 읽기만 하고 설치·실행하지 않은 결과이며, 명령의 실제 동작은 소스와 문서 설명 기준입니다.
직원마다 개인 카드로 결제하면 누가 얼마를 썼는지 모릅니다. 법인카드 창구를 하나 두면 부서별 한도를 걸고, 결제할 수 없는 업종을 막고, 영수증을 한곳에 모을 수 있습니다.
experiential에서 창구는 게이트웨이, 직원은 identity(사용자·에이전트), 한도는 월 예산, 업종 제한은 grant와 가드레일, 영수증은 사용량 원장입니다. 모아 둔 영수증으로 “어떤 일엔 더 싼 카드로 충분했다”를 분석해 라우팅 규칙을 만드는 단계가 세 번째 목적입니다.
AGENTS.md는 루트 CLI 명령 집합이 정확히 build, capture, config, eval, login, optimize, run 일곱 개라고 못 박고, 테스트(exp/cli/app_test.py)가 이를 강제한다고 적습니다. 인자 없이 exp만 치면 홈 화면이 열립니다.
| 명령 | 하는 일 (docs/usage.md 기준) | 결과물 |
|---|---|---|
exp / exp run | 로컬 게이트웨이 시작, 처음이면 설정 마법사 | OpenAI 호환 엔드포인트, 헬스 라우트 |
exp config gateway … | 제공자·사용자·키·권한·별칭·풀·월 한도 관리 | 비공개 SQLite 설정과 영수증(JSON) |
exp capture | macOS에서 Codex·Claude Code 트래픽 수집(실험적) | 클라우드 트레이스 |
exp build | 트레이스를 가져와 시나리오 추출, 시뮬레이션 준비 | 프로젝트 디렉터리 |
exp eval | 프로젝트 시나리오로 모델 비교 | JSON 증거, 파레토 리포트 |
exp optimize router|model | 라우터 학습·검증 / Tinker SFT 사전 점검 | 동결 정책, 별칭 |
exp login | Experiential Cloud 로그인 | 조직 키 저장 |
핵심은 게이트웨이(exp run) 하나만 써도 가치가 있고, 학습형 라우팅(build→optimize)은 그 위에 얹는 선택 기능이라는 점입니다.
Claude Code·Codex 같은 코딩 에이전트와 자체 앱이 서로 다른 키를 쓰고 있다면, 호출 경로를 하나로 모으는 구조를 코드로 확인할 수 있습니다. Rust 비동기 서버와 Python 제어부가 나뉜 설계도 볼 만합니다.
이번 분석 대상은 TrendShift 트렌딩 15위로 선정된 항목입니다. 스타 수 같은 외부 수치는 이 글에서 직접 확인하지 않았으므로 적지 않습니다. 소스에서 확인되는 것은 개발 활동입니다. 고정한 커밋 시점에 이력이 842개 커밋이고, 마지막 커밋 날짜는 2026-10-09, 커밋 제목에 버전 0.7.178이 붙어 있으며 릴리스 목록에는 v0.7.178·v0.7.177·v0.7.175가 보입니다.
| 관점 | experiential에서 확인되는 것 | 근거 |
|---|---|---|
| 데이터 평면 | Rust(axum·tokio·reqwest) 네이티브 서버가 모든 라우트를 처리. 확장이 없으면 Python 대체 없이 실패 | gateway-architecture.md, native/Cargo.toml |
| 입력 표면 | Chat Completions·Responses(WebSocket 포함)·Anthropic Messages·임베딩·이미지·오디오를 한 서버가 제공 | gateway-architecture.md의 Supported surface |
| 비용 통제 | 팀·사용자·풀·배포 단위 월(UTC) 하드 한도, 요청 시 최대 비용을 먼저 예약 | runtime/gateway/budgets.py |
| 안전장치 | 정책 기반 가드레일(입력·출력 검사, 차단·수정·관찰 모드)과 장애 시 다음 배포로 넘어가는 failover | gateway-guardrails.md, gateway-failover-rules.md |
| 학습형 라우팅 | 수집한 트레이스로 시뮬레이션·판정 후 kNN 라우터 정책을 동결해 서빙 | common/routing/policy.py, optimize/router/ |
또 하나 눈에 띄는 점은 저장소의 엄격함입니다. AGENTS.md는 새 소스 파일을 1,000줄 미만으로 유지하고, Ruff 포맷·린트와 ty 타입 검사를 전체 저장소에서 통과해야 하며, 패키지 간 import 방향(common→runtime→simulation→optimize→cli)을 AST 검사로 강제한다고 적습니다. 테스트 파일(*_test.py)이 601개입니다.
README가 말하는 “생산 트래픽으로 품질·속도·비용에 맞춘 라우터를 만든다”는 효과는 이 글이 성능을 측정해 확인한 것이 아닙니다. 소스에서 확인한 것은 그 절차를 지키도록 강제하는 계약(고정 해시, 보류된 검증 세트, 예산 상한)입니다.
고정 커밋에서 git ls-files 기준 추적 파일은 1,536개입니다. 확장자별로는 Python 1,269개, Rust 183개, 마크다운 52개가 대부분입니다. 물리 줄 수는 Python 약 43.9만 줄(테스트 포함), Rust 약 7.4만 줄입니다. 줄 수에는 테스트·주석·빈 줄이 섞여 있으니 규모 감각으로만 보세요.
| 층 | 기술 | 역할 |
|---|---|---|
| CLI | Typer·Click·Rich | exp 명령, 대화형 선택기, 진행 표시 |
| 계약·검증 | Pydantic ≥ 2.12 | 정책·예산·결정 기록 같은 불변 데이터 모델 (ContractModel) |
| 제공자 클라이언트 | openai ≥ 3, boto3, google-auth, httpx | OpenAI·Anthropic·Azure·Bedrock·Gemini/Vertex·Fireworks 등 어댑터 |
| 데이터 평면 | Rust: axum 0.8, tokio, reqwest(rustls, http2), rusqlite, PyO3 0.29 | 소켓·업스트림 호출·SSE 인코딩을 GIL 밖에서 처리 |
| 저장 | SQLite(WAL 모드, 버전형 마이그레이션) | ROOT/gateway/gateway.db에 키 지문·권한·사용량·시도 기록 |
| 수치·토큰 | numpy, tiktoken(o200k), google-re2 | kNN 계산, 요청 입력 토큰 추정, 정규식 안전 실행 |
| 패키징 | hatchling, maturin, uv | Python 휠 + 네이티브 확장(exp-gateway-native 0.3.x) |
이 저장소에는 웹 프런트엔드가 없습니다. 대시보드는 호스티드 Platform(platform.experientiallabs.ai)이 맡고, 로컬에서는 GET /usage·/usage.json이 내용 없는 사용량 집계를 보여 줍니다. CI는 .github/workflows/의 gate.yml(전체 품질·구조 검사), gateway-latency.yml(지연 배지), python-package.yml 세 개입니다.
sft 추가 기능: tinker·tinker-cookbook (미세조정)dev 추가 기능: pytest·pytest-xdist·ruff·ty·maturin·websockets 등exp-mitmproxy)에 의존docs/reference/gateway-architecture.md가 설명하는 구조를 단순화하면 아래와 같습니다. 요청이 밖으로 나가기 전에 인증→권한→예산 예약이 끝나야 하고, 응답이 끝난 뒤에 정산(settle)이 일어납니다.
start_attempt로 예약하고 결과를 정산합니다. 재시도와 failover도 이 단위로 기록됩니다.failover_only_on에 실패 종류(throttled, timeout, refusal:cyber_policy 등)를 적으면 그 실패 뒤에만 쓰이는 대기 자리가 됩니다.exp/runtime/gateway/budgets.py는 월 한도의 범위를 네 가지로 제한하고, 종류마다 있어야 하는 식별자 조합을 검사합니다.
class BudgetScopeKind(StrEnum):
TEAM = "team"
IDENTITY = "identity"
POOL = "pool"
DEPLOYMENT = "deployment"
# _require_scope_shape 안의 기대 조합 (identity_id, alias_id, pool_id, deployment_id)
expected = {
BudgetScopeKind.TEAM: (False, False, False, False),
BudgetScopeKind.IDENTITY: (True, False, False, False),
BudgetScopeKind.POOL: (False, True, True, False),
BudgetScopeKind.DEPLOYMENT: (False, True, True, True),
}[self.kind]
if present != expected:
raise ValueError(f"{self.kind.value} budget scope has invalid identifiers")
“팀 한도에 사용자 ID가 섞여 들어오는” 설정 실수가 저장 단계에서 걸러지는 방식입니다.
가드레일은 선택 기능이며 기본은 꺼짐입니다. 조직·사용자별 GuardrailPolicy를 켜면 요청마다 GuardrailSession이 정책을 한 번 고정하고, 입력 검사→(재작성)→출력 검사를 같은 파이프라인으로 돌립니다. 소스 문서에서 확인되는 특징은 아래와 같습니다.
mode="observe"는 차단·재작성 없이 입력만 관찰하며, 큐가 차면 “허용”이 아니라 skipped로 기록합니다.exp/common/routing/policy.py의 KnnRouterPolicy는 후보 모델 목록, 기준(baseline) 모델, 임베딩 모델, 학습에 쓴 번들의 해시, KnnGuard 임계값을 한 객체에 묶어 동결합니다. 요청의 “첫 사용자 의도·초기 컨텍스트·도구·허용 태그”만 특징으로 쓰고(request-visible-v2), 결정 기록(RoutingDecision)에는 이웃 수·짝지은 관측 수·fallback_reason이 남고, 임베딩 호출이 실패하면 동결된 보수적 기준 모델을 씁니다. 검증에 쓰는 관측 수는 최소 8개 이상이어야 합니다(minimum_paired_observations ≥ 8).
과거 사례집에서 “비슷한 질문을 어떤 모델이 어떻게 풀었는지”를 찾아 보고, 싼 모델로도 충분했다는 근거가 충분할 때만 싼 모델을 고르는 방식입니다. 임베딩이 실패하는 경우 등에는 기본 모델로 돌아갑니다.
| 경로 | 무엇을 보나 |
|---|---|
exp/cli/app.py | 루트 명령 조립만 담당. 구현은 하위 패키지로 분리 |
exp/runtime/gateway/budgets.py | UTC 월 예산과 원자적 예약 |
exp/runtime/gateway/guardrails/ | 정책·분류기·관찰·정규식·네이티브 검사 (약 40개 파일) |
exp/runtime/gateway/native/src/ | route_chat.rs, route_responses.rs, route_messages.rs, waterfall/, capture/, settlement.rs |
exp/common/routing/ | 정책·특징 추출·사례 은행 |
docs/reference/ | 설계 근거. 소스보다 먼저 읽기 좋음 |
규칙 하나가 구조를 지탱합니다. AGENTS.md에 따르면 common은 runtime·simulation·optimize·cli를 import할 수 없고, runtime은 simulation 이상을, simulation은 optimize·cli를 import할 수 없습니다. 이 방향이 AST 검사로 닫혀 있어 순환이 생기지 않습니다.
OpenAI Chat, Responses, Anthropic Messages가 한 서버에서 동시에 제공됩니다. 방언(dialect)이 openai_responses, anthropic_messages, openai_compatible, gemini_generate_content, bedrock_converse_stream으로 나뉘어 있어, 서로 다른 스트리밍 형식(SSE vs AWS 이벤트스트림)을 어떻게 한 경로로 다루는지 볼 수 있습니다.
아이디어: docs/reference/gateway-architecture.md의 방언 목록을 따라 native/src/dialects/의 파일 이름을 대응시켜 보세요.
호출 전 최대 비용 예약, 호출 후 정산, 끊긴 연결의 추정 비용 처리(disconnect_estimate.py)가 SQLite 트랜잭션 위에 만들어져 있습니다. 예약과 확정이 분리돼야 하는 이유를 코드로 확인할 수 있습니다.
PyO3 확장이 소켓과 스트림을 맡고, Python은 JSON 문자열 콜백으로 판단만 내립니다. “무거운 경로는 Rust, 정책은 Python”의 경계를 어디에 긋는지 보는 좋은 사례입니다.
학습 시 쓴 데이터와 보류 검증 데이터를 분리하고, 해시로 정책에 묶습니다. “보류 증거는 fit 증거와 정책 고정 뒤에만 열린다”는 단계 경계(exp.compose_router)를 읽어 보세요.
1,000줄 제한, 명령 목록 고정, import 방향 같은 규칙이 문서가 아니라 테스트·CI(gate.yml)에 있습니다.
| 기능 | 조건 | 근거 |
|---|---|---|
| 패키지·게이트웨이 | Python ≥ 3.12, 외부 모델 호출용 API 키 또는 로컬 서버 | pyproject.toml의 requires-python |
exp capture | macOS, Python ≥ 3.13, 네트워크 확장 승인, 인증서 신뢰 | docs/usage.md |
| 개발 빌드 | uv, Rust 툴체인(네이티브 확장 소스 빌드 시), 캡처 의존성의 첫 빌드에 Rust가 필요할 수 있음 | README.md, docs/usage.md |
| 학습형 라우터 | 임베딩·판정용 유료 모델 호출 비용. 명령 실행 전 비용 예비 계산(preflight)과 기본 $50 경고 기준 | docs/usage.md, README |
GPU는 요구 사항에 나오지 않습니다. 이 프로젝트는 모델을 직접 돌리지 않고 호출을 중계하며, 미세조정은 외부 서비스(Tinker)에 맡깁니다.
로컬 트래픽 캡처. docs/usage.md에 따르면 회계 DB는 내용을 담지 않지만, 로컬 게이트웨이는 기본으로 완료된 요청·응답 내용을 별도 캡처합니다. 끄려면 --ghost를 씁니다. 프롬프트에 민감 정보가 있다면 먼저 이 동작을 확인하세요.
제품 텔레메트리. README는 익명 집계 PostHog 텔레메트리가 기본 활성이며 프롬프트·모델 이름·자격증명은 보내지 않는다고 적습니다. exp config telemetry disable로 끌 수 있습니다.
exp capture. 앱 간 트래픽을 읽기 위해 네트워크 확장과 인증서 승인을 요구하는 실험 기능입니다. 문서가 단계와 이유를 밝히고 있지만, 시스템 수준 권한이라는 점은 알고 쓰세요.
pip install experiential
exp # 처음이면 설정 마법사: 제공자, 별칭, 한도, 키 발급
export EXP_GATEWAY_KEY=... # 마법사가 한 번 보여 주는 키
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Authorization: Bearer $EXP_GATEWAY_KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"opus-5","messages":[{"role":"user","content":"Help me"}]}'
별칭 opus-5는 README 예시이며, 실제로는 마법사에서 정한 이름을 씁니다. 확인할 것: 응답과 GET /usage 집계가 일치하는지.
같은 게이트웨이에서 아래 명령으로 호출자가 보는 것을 확인합니다. 저장된 상태가 없고 HTTP 요청만 보냅니다(docs/usage.md).
exp config gateway models --json # 내 키에 허용된 별칭 목록
exp config gateway key check --json # 키 유효성 (키를 저장하지 않음)
exp config gateway call opus-5 "한 줄로 자기소개"
exp config gateway ...의 월 한도 명령으로 아주 작은 한도를 걸고, 호출이 어떤 오류로 막히는지 응답 본문과 /usage.json을 비교합니다. 이어서 budgets.py의 BudgetScope 검증 코드를 읽고 네 범위 중 어디에 걸렸는지 맞춰 보세요.
README가 안내하는 공개 데이터셋을 받아 exp build를 돌립니다. 비용이 드는 단계는 비용 확인 화면에서 멈추므로, 먼저 --dry-run(문서에 따르면 유료 호출 0회)으로 흐름만 확인하세요. 단, dry run은 대화형 설정을 건너뛰므로 models.toml에 제공자 연결과 모델 별칭, world_model·judge·embedder 세 역할이 미리 설정돼 있어야 합니다. 없으면 model configuration is incomplete before build 오류로 끝나니, 먼저 exp config providers로 구성하세요(app.py _load_or_setup_catalog). 앞의 exp config gateway 실습과는 다른 설정입니다.
curl -L -o traces.otel.jsonl \
https://huggingface.co/datasets/experiential-labs/wmo-terminal-tasks-traces/resolve/540883e451dc13d34fb50fdd36b143cb0f1fb0db/traces.otel.jsonl
exp build support-agent --traces traces.otel.jsonl --source otlp --dry-run
--dry-run만 쓰면 입력이 없어 BadParameter로 끝납니다. 소스 exp/cli/build/app.py가 dry run과 자동화 실행에는 -t/--traces PATH 또는 --import-id ID를 요구하고(208-213줄), docs/usage.md도 자동화에서는 --traces와 --source를 함께 주라고 안내합니다. otlp는 CANONICAL_TRACE_SOURCES에 있는 소스 이름이며 생략해도 기본값입니다(257줄). 이 명령을 직접 실행해 확인하지는 않았습니다.
그다음 optimize/router/fit/workflow.py와 common/routing/policy.py를 열어 “정책 객체에 무엇이 묶이는가”를 정리해 보세요.
| 주차 | 주제 | 할 일 |
|---|---|---|
| 1주 | OpenAI 호환 API 이해 | Chat Completions·Responses 요청/응답, 스트리밍(SSE) 직접 구현해 보기. 이 저장소의 docs/reference/openai-compatible-recipes.md 읽기 |
| 2주 | 게이트웨이 운영 | 가상 키·grant·월 한도를 바꿔 가며 거부 사례 수집. gateway-failover-rules.md로 failover 시나리오 그려 보기 |
| 3주 | Rust 비동기 서버 | native/src/route_chat.rs→relay.rs→settlement.rs 순으로 호출 흐름 추적. axum·tokio 입문 문서 병행 |
| 4주 | 트래픽 기반 최적화 | kNN 라우터, 판정(judge) 편향, 보류 검증 세트의 의미를 정리. 직접 만든 작은 라우팅 실험과 비교 |
| 용어 | 뜻 |
|---|---|
| BYOK | Bring Your Own Key. 자기 제공자 키를 연결해 사용. 호스티드 게이트웨이에서는 pass-through |
| 가상 키(virtual key) | 게이트웨이가 사용자에게 발급하는 키. 원문은 한 번만 표시, 저장은 지문 |
| 별칭(alias) | 클라이언트가 쓰는 공개 모델 이름. 뒤의 실제 모델 풀은 운영자가 정함 |
| grant | identity가 어떤 별칭을 쓸 수 있는지의 명시적 허가 |
| 풀(pool)·배포(deployment) | 같은 별칭 뒤의 실제 모델 호출 후보들과 그 하나하나 |
| admission / settle | 요청 허가 단계 / 결과 정산 단계 |
| failover | 실패 종류에 따라 다음 배포로 넘어가는 동작 |
| 가드레일 | 입력·출력을 검사해 차단·수정·관찰하는 정책 층 |
| ghost 모드 | --ghost. 트래픽 내용 캡처를 끄고 인증·회계는 유지 |
| kNN 정책 | 비슷한 과거 사례의 결과를 보고 모델을 고르는 정책 |
| OTLP / 트레이스 | OpenTelemetry 형식의 에이전트 실행 기록 |
| SFT | 지도 미세조정. 이 프로젝트는 외부 Tinker 서비스로 실행 |
exp build --source gatewayCargo.toml도 Apache-2.0외부 문서는 바뀔 수 있습니다. 이 글의 파일 경로와 동작 설명은 커밋 c847e2a에 맞춘 것입니다. 소스에서 확인하지 못한 항목(스타 수, 실제 라우팅 성능, 호스티드 서비스의 운영 방식)은 적지 않았습니다.
| 항목 | 내용 | 기준 |
|---|---|---|
| 저장소 | experientiallabs/experiential | 공식 GitHub |
| 선정 위치 | TrendShift 트렌딩 #15 | 2026-10-25 분석일 |
| 패키지 버전 | 0.7.178 (네이티브 0.3.143) | pyproject.toml, Cargo.toml |
| 고정 커밋 | c847e2a · 2026-10-09 · 커밋 842개 | git 이력 |
| 추적 파일 | 1,536개 (Python 1,269 · Rust 183 · 마크다운 52) | git ls-files |
| 테스트 파일 | *_test.py 601개 | git ls-files |
| 라이선스 | Apache-2.0 | LICENSE |