CLI 세션 API 설정 (Evo CLI 조직 로그인)
원천 · framework/evomind/web/workflow-project-evo-service기준 · 릴리스 v0.12.0커밋 · 2e32c6cc파일 · docs/operations/cli-session.md기준선 · 2026-11-12
Evo CLI 의 조직 사용자는 Keycloak 에 디바이스 코드로 로그인한 뒤, 그 access token 으로 evo-api 에서 CLI 세션을 받는다.
세션에는 게이트웨이 주소(/v1), 만료가 있는 개인 API 키(sk-…), 허용 모델이 들어 있고, CLI 는 그 키로
/v1/models·/v1/chat/completions 를 호출한다. 웹과 같은 사용자·그룹 권한·쿼터가 적용된다.
evo CLI ──디바이스 코드 로그인──▶ Keycloakevo CLI ──POST /api/cli/session (Authorization: Bearer <Keycloak access token>)──▶ evo-apievo CLI ◀── { user, org, endpoints[{provider, base_url, api_key, models}], default_model, api_key_expires_at }evo CLI ──Bearer sk-…──▶ evo-api /v1/*이 기능은 기본 꺼짐이다. 꺼진 상태에서는 POST /api/cli/session 이 항상 403 {"error":"cli_disabled"} 이고
기존 동작은 달라지지 않는다.
1. Keycloak 준비
섹션 제목: “1. Keycloak 준비”- realm(예:
evo)에 공개 클라이언트evo-cli를 둔다. 디바이스 코드(OAuth 2.0 Device Authorization Grant)를 켜고 표준 흐름은 끈다. 토큰의azp가evo-cli여야 한다. - 사용자·그룹은 웹 로그인과 같은 규칙으로 읽는다(웹 OIDC 설정과 같은
OIDC_GROUPS_CLAIM·OIDC_GROUP_PREFIX). 역할 claim 은 읽지 않는다.- 로그인 자격은 그룹 소속이다: 토큰의
groups에 앱의 기본 그룹user또는admin과 같은 이름이 있어야 한다. 없으면403 forbidden이고 앱에 계정이 만들어지지 않는다. 자세한 규칙은web-oidc.md의 그룹 소속 절을 본다. - 그룹: access token 에
groupsclaim(전체 경로 아님)을 넣는다. 없으면 userinfo 로 보강한다. 그룹이 없는 사용자는 claim 항목 자체가 빠진다(‘그룹 없음’).
- 로그인 자격은 그룹 소속이다: 토큰의
- 웹과 CLI 가 같은 Keycloak 사용자를 쓰면 같은 계정·그룹으로 묶인다.
2. 환경변수 (.env)
섹션 제목: “2. 환경변수 (.env)”| 이름 | 기본값 | 설명 |
|---|---|---|
CLI_SESSION_ENABLED |
false |
true 로 켠다. |
CLI_OIDC_ISSUER_URI |
(비움) | 발급자 주소 https://<keycloak>/realms/<realm>. 비우면 OIDC_ISSUER_URI 를 쓴다. 이 값이 없으면 기능이 꺼진 것처럼 동작한다. |
CLI_OIDC_CLIENT_IDS |
evo-cli |
허용 클라이언트(azp), 쉼표 구분. 웹 클라이언트와 MCP 서버 전용 클라이언트(Evo CLI auth_client_id)는 넣지 않는다(§3.1). |
CLI_OIDC_AUDIENCE |
(비움) | 토큰 aud 에 있어야 할 값(예: evo-api). 비우면 aud 를 보지 않는다. 기본은 꺼 둔다(§3.1). |
CLI_KEY_TTL |
PT12H |
CLI 세션 키 수명(ISO-8601). |
CLI_ORG_NAME |
(비움) | 응답 org.name. 비우면 org 를 생략한다. |
CLI_GATEWAY_BASE_URL |
(비움) | 응답 base_url 을 고정할 때만. 비우면 요청의 외부 주소 + /v1. |
CLI_RATE_LIMIT_PER_MINUTE |
20 |
IP·사용자별 분당 상한. 0 이하면 제한 없음. |
CLI_LITELLM_CONNECTION |
(비움) | evo-lite(LiteLLM) 사람별 키 발급을 켠다. 관리자 화면에 등록된 OpenAI 호환 연결의 이름(그 연결 주소 재사용). §7 |
CLI_LITELLM_ADMIN_KEY |
(비움) | evo-lite 관리 키(키 발급·삭제 권한). 연결의 호출용 키와 별개이며 .env/CI 변수에만 둔다. |
CLI_LITELLM_TEAM_ID |
(비움) | 이 인스턴스의 evo-lite 팀 ID(선택). 비우면 팀 없이 발급한다(개인 사용자 단위 인스턴스는 비우는 것을 권장). 지정하면 evo-lite 에 그 팀이 먼저 있어야 한다 — 없는 팀의 키는 발급은 되지만 /v1/models·채팅이 404 이다. |
CLI_LITELLM_PUBLIC_URL |
(비움) | CLI 에 알려 줄 evo-lite 주소. 비우면 연결 주소를 그대로 쓴다(내부 주소면 지정). |
GATEWAY_USER_PREFIX |
(빈 값) | 업스트림 요청 user 필드의 렐름 표기. 비우면 발급자 주소의 마지막 경로를 쓴다(§6). |
CLI_LITELLM_BUDGET_DURATION |
1mo |
월 사용 금액 한도의 evo-lite 예산 주기(LiteLLM budget_duration 표기). §9 |
CLI_LITELLM_SPEND_CACHE_TTL |
PT30S |
채팅 전 누적 금액 조회 결과를 재사용하는 시간. PT0S 면 매번 조회한다. §9 |
CLI_LITELLM_SPEND_TIMEOUT |
PT3S |
누적 금액 조회 호출 하나의 제한 시간. 넘으면 조회 실패로 보고 허용한다. §9 |
시크릿은 필요 없다(공개 클라이언트). 발급자·클라이언트 ID 는 비밀이 아니지만 내부 주소는 저장소에 커밋하지 않고 .env 에만 둔다.
HTTPS 발급자만 사용한다. 다중 인스턴스에서는 요청 제한이 인스턴스별로 적용된다.
3. 검증 규칙
섹션 제목: “3. 검증 규칙”토큰은 표준 OIDC 로만 검증한다(제품 관리 API 를 쓰지 않는다).
- Discovery(
/.well-known/openid-configuration)의jwks_uri로 서명 검증(키 캐시, 알 수 없는kid면 다시 조회). Discovery 의issuer가 설정값과 다르면 거부한다. iss·exp·nbf(시계 오차 60초),azp가 허용 목록에 있는지,sub존재.- RS256 만 받는다.
alg=none·HMAC 토큰은 거부한다. CLI_OIDC_AUDIENCE가 있으면aud(문자열 또는 목록)에 그 값이 있어야 한다. 없거나 다르면401 invalid_token이다. 세션·텔레메트리(/api/cli/telemetry·/api/cli/otel/v1/traces) 모두 같은 검증을 쓴다.
3.1 다른 서버로 간 토큰 재사용 막기
섹션 제목: “3.1 다른 서버로 간 토큰 재사용 막기”Evo CLI 0.23.1 은 같은 로그인 토큰(evo-cli 클라이언트)을 evo-api 말고도 evo-dispatch, 사용자가 "auth": "org" 로 지정한 MCP 서버에 보낸다. 그 토큰이 evo-api 에서도 통하므로, 토큰을 받은 다른 서버의 운영자가 토큰 수명 안에 그 사용자의 CLI 키를 받을 수 있다.
- 막는 방법: 허용 클라이언트(
azp) 검사와 Evo CLI 의 MCP 서버별 전용 로그인 클라이언트(auth_client_id, Evo CLI 1.0.0-dev 부터 — evo #479)를 함께 쓴다.auth_client_id를 둔 MCP 서버에는 그 클라이언트의 토큰만 가고, 그 토큰은azp가 허용 목록에 없어 이 API 에서401 invalid_token이다. - 운영 규칙: MCP 서버 전용 클라이언트를
CLI_OIDC_CLIENT_IDS에 넣지 않는다. 넣으면 그 서버가 받은 토큰으로 CLI 키를 받을 수 있게 된다. 허용 목록에는 evo-api 를 직접 부르는 클라이언트(evo-cli, 데스크톱 앱 등)만 둔다. - 남는 위험:
auth_client_id없이"auth": "org"로 지정한 서버에는 여전히evo-cli토큰이 간다. 다른 사람이 운영할 수 있는 서버에는auth_client_id를 쓰도록 Evo CLI 문서가 안내한다(evo #480). evo-dispatch 처럼 우리 쪽 서버로 가는 토큰이 evo-api 에서도 통하는 점은 범위 밖으로 둔다. aud검사(CLI_OIDC_AUDIENCE): 기본으로 꺼 둔다. 모든 evo-cli 토큰에 같은aud가 붙는 지금 설정에서는 위 재사용을 막지 못한다. 더 좁히려고 켤 때는 Keycloak(선택 스코프)·Evo CLI(그 스코프 요청)와 먼저 맞추고, 그 스코프를 요청하지 않는 판(예:original0.23.1)이401이 되므로 evo-cli 에 미리 알린다.
4. 응답과 오류
섹션 제목: “4. 응답과 오류”| 상황 | 응답 |
|---|---|
| 성공 | 200 — user, org(선택), endpoints[{provider:"openai", base_url, api_key, models}], default_model(선택, §4.1), api_key_expires_at (langfuse 필드는 없다) |
| 토큰 없음·서명·발급자·만료·허용 클라이언트 불일치 | 401 {"error":"invalid_token"} + WWW-Authenticate: Bearer error="invalid_token" |
비활성 사용자·user/admin 그룹 소속 없음 |
403 {"error":"forbidden"} |
| 기능 꺼짐·게이트웨이 꺼짐·발급자 미설정 | 403 {"error":"cli_disabled"} |
| 요청 과다 | 429 + Retry-After |
| 월 사용 금액 한도 이상(evo-lite 키 방식, §9) | 429 {"error":"monthly_budget_exceeded"} (키를 만들지 않는다) |
| IdP(Discovery·JWKS·userinfo) 도달 불가 | 503 {"error":"idp_unavailable"} (토큰 문제가 아니므로 다시 시도) |
응답에는 Cache-Control: no-store 가 붙고, 오류 본문과 서버 로그에는 토큰·키 원문이 남지 않는다.
4.1 default_model — 그룹·조직 기본 모델
섹션 제목: “4.1 default_model — 그룹·조직 기본 모델”default_model 은 사용자가 아직 모델을 고르지 않았을 때 쓰는 기본값이다. 사용자가 고른 모델은 그대로 우선한다.
Evo CLI·evo-mac 은 사용자가 고른 모델(목록에 있을 때) → 세션 응답의 default_model → 모델 목록(/models)의 첫 모델 순으로 쓴다.
evo-windows 는 지금 서버 default_model 을 쓰지 않는다(로컬 설정 모델 → 첫 모델).
-
결정 순서(세션을 발급할 때마다 다시 계산한다,
DefaultModelResolver한 곳):- 그 사용자가 속한 그룹들의 그룹 기본 모델 중 적용 가능한 것 — 우선순위(priority)가 높은 그룹 먼저, 같으면 그룹 이름 순
- 없으면 조직 기본 모델(적용 가능할 때)
- 없으면 필드를 생략한다(클라이언트는 사용자가 고른 모델 또는 첫 모델을 쓴다)
적용 가능 = 카탈로그에 활성(모델·연결 모두)으로 있고 그 사용자의 허용 모델 목록(웹·
/v1/models·CLI 키models와 같은 계산, admin 은 활성 모델 전체)에 있는 것. 두 방식(§5 evo-api 키, §7 evo-lite 키) 모두 같다. 세션 발급 로그(CLI 세션 발급 … defaultModel=<id> source=<group:이름|org|none>)에 결과와 출처가 남는다. -
값: 카탈로그의
model_id(게이트웨이/models가 돌려주는 것과 같은 문자열, 모델 등록 FK 아님). 조직 기본은app_settings의models.default, 그룹 기본은group_default_models표(그룹당 하나, 그룹이 지워지면 함께 지워짐)에 둔다. 둘 다 기본값이 없다(코드·마이그레이션에 모델 id 를 넣지 않는다 — 인스턴스마다 모델이 다르다). -
웹 채팅: 같은 계산 결과를
GET /api/models의defaultRef(model:<등록 모델 UUID>— 그model_id의 허용·활성 등록 모델 중 모델 목록 순서의 첫 것, 없으면 생략)로 내준다. 웹 모델 선택기는 사용자가 고른 값이 없거나 목록에 없을 때defaultRef→ 첫 프리셋 → 첫 모델 순으로 기본을 보이고, 고른 값이 있으면 그대로 둔다. 모델을 지정하지 않은 채팅 요청(웹·/v1의model생략)도 관리자 기본 모델을 먼저 쓴 뒤 기존 순서(기본 프리셋 → 첫 활성 모델 → 첫 허용 모델)를 따른다. 명시 모델·auto는 바뀌지 않는다. -
클라이언트에 반영되는 때: Evo CLI 는 시작할 때만 세션을 다시 읽으므로(세션이 유효하면 서버를 다시 부르지 않는다) 기본 모델을 바꾸면 세션이 만료된 뒤 다음 시작부터 반영된다. evo-lite 방식도
endpoints[].models에 허용 모델 목록을 넣는다(§7).
조직 기본 모델
섹션 제목: “조직 기본 모델”- 정하는 곳: 관리자 설정 → 설정 탭 → 기본 모델. 카탈로그의 활성 모델 중 하나를 고르거나 없음(첫 모델) 으로 해제한다.
- API(경로 권한은 roles-groups.md §9):
GET /api/admin/settings/default-model(admin.settings.read) →{"modelId": "<모델 id 또는 null>", "available": <저장값이 지금도 활성 모델인지>, "status": "none|applied|disabled|missing", "options": [{"modelId", "displayName", "connectionName"}]}.status는 미설정(none)·적용됨(applied)·등록돼 있으나 모델·연결이 꺼짐(disabled)·카탈로그에 없음(missing)이며,available은 호환을 위해 남긴다(applied일 때 true).options는 조회자 개인의 모델 권한과 무관한 활성 모델 전체이며(같은modelId는 한 번) 연결 주소·키는 담지 않는다.PUT /api/admin/settings/default-model(admin 전용) — 본문{"modelId": "<모델 id>"}.null·빈 값이면 해제. 활성 모델이 아니면(없는 id·비활성 모델)400. 변경은 감사 로그(setting.change, 대상models.default)에 남는다.
- 저장한 모델을 나중에 비활성화·삭제하면 설정값은 남지만 어떤 사용자에게도 내주지 않는다. 화면에는 “현재 사용할 수 없음”으로 표시되므로 다른 모델을 고르거나 해제한다.
- 개발서버 등 각 인스턴스의 첫 값은 배포 후 관리자 화면에서 정한다(배포만으로는 설정되지 않는다).
그룹 기본 모델
섹션 제목: “그룹 기본 모델”- 정하는 곳: 관리자 설정 → 그룹 탭 → 그룹 선택 → 허용 모델 아래 그룹 기본 모델·우선순위. 허용 모델 체크와 기본 모델·우선순위를 허용 모델·기본 모델 저장 버튼으로 한꺼번에 확정한다(되돌리기 가능). 저장 전에 기본 모델의 체크를 빼면 그 모델은 기본 모델 선택지에서 사라지고(선택이 ’없음’으로 보임) 다시 체크하면 돌아온다. 저장해야 실제로 해제된다.
- 고를 수 있는 값: 그 그룹 허용 모델 중 활성 모델만(그 밖은
400). 기본 그룹admin은 허용 모델 없이 무제한이므로 카탈로그의 활성 모델 전체에서 고른다. - 우선순위: 정수, 높을수록 먼저(생략하면 0, 음수 가능). 여러 그룹에 속한 사용자에게 어느 그룹 값을 쓸지 정한다. 같으면 그룹 이름 순이다. 높은 그룹의 기본 모델이 그 사용자에게 적용 가능하지 않으면 다음 그룹 → 조직 기본 순으로 넘어간다.
- 상태:
none(미설정),applied(그룹 허용 안에 활성 모델이 있어 적용됨),disabled(값은 있으나 그 모델 또는 연결이 꺼져 적용 안 됨 — 화면에 “적용 안 됨: 꺼진 모델”).disabled인 동안 세션은 이 값을 건너뛰고 다음 순위로 간다. 다시 켜면 그대로 적용된다. - 자동 해제(그룹 기본 모델은 항상 그 그룹 허용 모델 안의 값이다, Human 결정 2026-10-21):
- 그룹 허용 모델 저장(
PUT groups/{id}/models, 전체 교체)으로 그model_id가 그룹 허용에서 모두 빠지면 같은 요청·트랜잭션에서 그룹 기본 모델을 해제한다.admin그룹은 대상이 아니다. - 모델 삭제·연결 삭제로 그룹 허용 행이 사라져(CASCADE) 그
model_id가 그룹 허용에 하나도 남지 않으면 같은 트랜잭션에서 해제한다(다른 연결의 같은model_id가 허용에 남아 있으면 유지).admin그룹은 그model_id가 카탈로그에서 아예 사라졌을 때 해제한다. - 모델·연결 비활성은 허용 행이 남으므로 해제하지 않는다(상태
disabled). - 해제는 감사 로그(
group.model, 대상 그룹 id, “허용 모델에서 빠져 그룹 기본 모델 해제(<그룹>): <모델>” 등)에 남는다.
- 그룹 허용 모델 저장(
- API(경로 권한은 roles-groups.md §9):
GET /api/admin/groups/{id}/default-model(admin.groups.read) →{"modelId": "<모델 id 또는 null>", "priority": <정수>, "status": "none|applied|disabled"}PUT /api/admin/groups/{id}/default-model(admin.models.assign) — 본문{"modelId": "<모델 id>", "priority": <정수>}.modelId가null·빈 값이면 해제. 그룹 허용 모델 중 활성 모델이 아니면400,admin그룹은 행위자가 admin 이 아니면403. 변경은 감사 로그(group.model)에 남는다. 허용 범위는 바뀌지 않으므로 evo-lite 키는 갱신하지 않는다.
- 확인: 사용자별 최종 허용 모델 화면(§6,
GET /api/admin/users/<아이디>/models)에 그 사용자에게 적용될 기본 모델(defaultModel)과 출처(defaultModelSource:group:<그룹>·org·none)가 나온다.
5. 키 정책
섹션 제목: “5. 키 정책”- 키는 개인
sk-키와 같은 종류이며CLI_KEY_TTL뒤에 만료된다. 만료된 키는/v1에서401이다. - 이름은
evo-cli또는evo-cli (<device>)이다. 같은 사용자·같은 device 로 다시 받으면 이전 키는 폐기된다. - CLI 세션 키는 사람이 발급하는 키의 상한(
max_keys_per_user)에 포함되지 않는다. - 쿼터는 키 단위가 아니라 사용자·그룹 단위로 적용된다.
- 사용자는 웹의 API 키 화면에서 CLI 키를 보고 폐기할 수 있다. 발급·폐기는 감사 로그(
cli.session.issue,cli.session.revoke)에 남는다(원문 제외).
6. 모델 허용과 업스트림 사용자 식별
섹션 제목: “6. 모델 허용과 업스트림 사용자 식별”- 허용되지 않은 모델: 존재하는 모델이지만 사용자의 그룹 정책으로 허용되지 않았으면
/v1/chat/completions는403(permission_error, 코드model_not_allowed)이고 업스트림(LiteLLM 등)을 호출하지 않는다. 존재하지 않는 모델만404(model_not_found)다. 검사는 요청마다 현재 그룹 정책으로 수행하므로 그룹을 바꾸면 다음 요청부터 반영된다. - 업스트림
user필드: OpenAI 호환 연결(openai·openai_compatible)로 나가는/v1요청 본문의user에<렐름>:<사용자 이름>(예:evo:hong,evo-personal:hong)을 넣는다. 렐름 표기는GATEWAY_USER_PREFIX로 정하고, 비어 있으면 인스턴스 발급자 주소의 마지막 경로를 쓰며, 둘 다 없으면 사용자 이름만 보낸다. Anthropic·Ollama 연결에는 보내지 않는다. 웹 채팅도 같은 값을 보낸다(§9). - 사용자 식별: OIDC 사용자는 발급자(
iss)+sub로 식별한다. 이전 버전에서 만든 계정(발급자 없음)은 같은sub로 이어받아 첫 로그인(웹·CLI)에서 발급자를 채운다. 이미 다른 발급자로 채워진 계정은sub가 같아도 같은 사람으로 보지 않는다.
사용자별 최종 허용 모델 확인
섹션 제목: “사용자별 최종 허용 모델 확인”관리자는 사용자 한 명의 최종 허용 모델을 읽기 전용으로 확인할 수 있다. 웹 채팅 모델 목록·/v1/models·CLI 키 models 가 쓰는 것과 같은 계산 결과이며 허용 근거인 그룹 이름이 함께 나온다.
- 화면: 관리자 설정 → 사용자 탭 → 사용자 행의 허용 모델 버튼.
- API:
GET /api/admin/users/<아이디>/models(관리자 전용). 응답은allModels(admin 역할이면 true — 활성 모델 전체),models[](modelId·displayName·connection·groups),defaultModel·defaultModelSource(모델을 고르지 않았을 때 쓰일 기본 모델과 출처, §4.1)이며 토큰·키는 없다. 없는 사용자는404. - 허용 범위는 이 화면에서 바꾸지 않는다. 그룹 탭에서 그룹의 허용 모델을 지정하고 사용자를 그 그룹에 넣는다(여러 그룹이면 합집합, 한 사람만 다르게 하려면 그 사람 전용 그룹을 만든다). 그룹도 허용 모델도 없으면 빈 목록이다(전체 허용 아님).
admin역할은 항상 활성 모델 전체이고 비활성 모델은 제외된다.
7. evo-lite(LiteLLM) 사람별 키 방식
섹션 제목: “7. evo-lite(LiteLLM) 사람별 키 방식”CLI_LITELLM_CONNECTION·CLI_LITELLM_ADMIN_KEY 가 있으면 세션은 evo-api sk- 키 대신 evo-lite 의 사람별 가상 키를 발급한다(CLI_LITELLM_TEAM_ID 는 선택). 하나라도 비면 이전 방식(§5)을 그대로 쓴다.
evo CLI ──POST /api/cli/session──▶ evo-api ──POST /key/generate──▶ evo-liteevo CLI ◀── provider litellm · base_url=evo-lite · api_key=사람별 키 (모델 목록은 응답에 없다)evo CLI ──키로 evo-lite /v1/models 조회──▶ 키에 허용된 모델만 나온다evo CLI ──Bearer <사람별 키>──▶ evo-lite /v1/* (evo-api 를 거치지 않는다)- 발급 값:
models=그룹 허용 모델,duration=CLI_KEY_TTL,user_id=<렐름>:<사용자 이름>(예evo:hong),team_id=CLI_LITELLM_TEAM_ID(비우면 팀 없는 키),key_alias=evo-cli:<user_id>:<device>,metadata={client, device}. - evo-lite 의 키 alias 는 서버 전체에서 유일하다. 같은 사용자·같은 device 로 다시 받으면 이전 키를 먼저 지우고 새 키를 만든다(
/key/delete, alias 기준). 새 키 발급이 실패하면 그 device 의 이전 키도 이미 없어진 상태이며 다시 받으면 된다. - 허용 범위가 바뀌면 그 사용자(그룹 멤버)의 CLI 키를 evo-lite 에서 갱신한다(허용 모델이 0개이거나 사용자를 비활성화하면 삭제). 키는 유지되므로 CLI 가 다시 로그인할 필요가 없다. 자세한 동작은 아래 “허용 범위가 바뀔 때” 항목을 본다.
- 모델 목록은 표시용으로 응답에 넣는다.
endpoints[0].models는 키 발급에 쓴 것과 같은 허용 모델 목록(카탈로그 순서 — 웹/api/models·/v1/models와 같은 계산)이다. 허용 범위의 기준은 여전히 evo-lite 키의models이며, 그룹 허용 모델이 바뀌면 키의models는 아래처럼 갱신되지만 이미 받은 응답의 목록은 다음 세션 발급 때 바뀐다. 클라이언트는 이 목록을 표시 순서로 쓰고, 실제 사용 가능 여부는 evo-lite/v1/models로 확인한다. 키의models는 사용자가 속한 그룹들의 허용 모델 합집합이다(사용자별 추가·제외는 이후 단계 — 관리 화면이 필요하다). - 허용 모델이 하나도 없는 사용자(그룹이 없거나 그룹에 허용 모델이 없음)에게는 키를 만들지 않고 세션이
403 {"error":"forbidden"}이다. LiteLLM 키의models가 비면 모든 모델을 허용하는 것으로 알려져 있어 막아 둔 것이며, 실제 evo-lite 에서 0개를 표현하는 방법을 확인한 뒤 확정한다. admin 역할은 모든 활성 모델이 허용된다. - **허용 범위가 바뀔 때 키를 갱신한다 **: 대상 이벤트는 그룹의 허용 모델 변경, 그룹 멤버 추가·제거, 그룹 삭제, 사용자 역할 변경·비활성화, 모델의 활성/비활성 전환·삭제·추가(수동 추가·모델 탐지 포함), 연결의 활성 전환·삭제, OIDC 로그인 그룹 동기화다. 표시 이름·라우팅·주소·키 변경처럼 허용 모델 집합이 바뀌지 않는 변경은 키를 건드리지 않는다.
- 변경이 DB 에 반영된 뒤 영향받는 사용자별 최종 허용 모델을 다시 계산하고, evo-lite 에서 그 사용자 키의
models만 바꾼다(/key/list로 alias 의 키를 한 건 찾아/key/update. 확대·축소 모두이며 alias·user_id·팀·만료는 그대로다). 최종 허용 모델이 0개이거나 사용자가 비활성화되면 키를 삭제한다(빈models는 전체 허용으로 해석될 수 있기 때문이다). 이미 만료된 키는 대상이 아니다. 갱신을 모두 먼저 하고 삭제는 마지막에 한다. - 관리 작업(그룹·모델·연결·사용자 화면)에서 evo-lite 갱신이 하나라도 실패하면 요청이
502(“evo-lite 키를 갱신하지 못해 변경하지 않았습니다. 잠시 뒤 다시 시도하세요”)로 실패하고 DB 변경은 롤백된다. 이미 바꾼 키는 변경 전 모델로 되돌린다(결과를 알 수 없는 호출 포함). 갱신 뒤 DB 커밋이 실패해도 같은 방식으로 되돌린다. 일부 키 삭제가 끝난 뒤 실패하면 지워진 키는 되살릴 수 없으므로 그 기록은 폐기 처리하고(해당 사용자는 다시 로그인), 감사 로그에 남긴다. 관리 화면은 오류를 표시하고 화면 값을 변경 전으로 돌려 둔다. - evo-lite 호출 제한: 한 번의 변경에서 evo-lite 로 보내는 호출은 동시에 최대 4건, 호출 하나당 10초, 전체 30초 안에서 처리하고 넘으면 실패로 보고 롤백한다. 값은 코드 기본값이며 evo-api 가 스스로 지키는 제한이다(evo-lite 설정이 아니다). 필요하면 앱 설정
app.security.cli-session.refresh-concurrency·refresh-call-timeout·refresh-total-timeout으로 바꾼다. - OIDC 로그인 그룹 동기화는 예외다: 소속 변경은 DB 에 반영하고 로그인을 허용한다. 그 사용자의 활성 키는 로그인마다 현재 허용 모델로 다시 맞추며(evo-lite 에서 키를 조회해 다르면 갱신), 실패해도 로그인은 막지 않고 감사 로그에 남긴 뒤 다음 로그인에서 다시 시도한다. 갱신이 실패한 동안에는 기존 키에 부여된 모델이 그대로 남는다. 소속에서 빠진 모델은 키가 만료될 때까지(
CLI_KEY_TTL, 기본 12시간) 쓸 수 있고, 새로 생긴 모델은 갱신이 성공할 때까지 쓰지 못한다. - 키 원문과 해시 토큰은 DB·로그·감사 어디에도 남기지 않는다. evo-lite 에 키가 없거나(
/key/list결과 0건) 같은 alias 가 여러 건이면 그 키 갱신은 실패로 본다. 관리자가 폐기한 키와 같은 기기의 재로그인 때 이전 키를 지우는 동작은 바뀌지 않는다.
- 변경이 DB 에 반영된 뒤 영향받는 사용자별 최종 허용 모델을 다시 계산하고, evo-lite 에서 그 사용자 키의
- evo-lite 호출이 실패하면 세션은
503 {"error":"key_service_unavailable"}로 실패하고 키가 반쯤 만들어지지 않는다. - 키 원문은 응답에서 한 번만 내주며 DB·로그·감사에 남기지 않는다. DB(
cli_litellm_keys)에는 alias·device·만료·사용자만 저장한다. - 관리자는
GET /api/admin/cli-keys로 목록(alias·device·사용자·만료)을 보고DELETE /api/admin/cli-keys/<id>로 폐기한다(evo-lite 키 삭제). - **관리자 화면 **: 관리자 설정의 ‘CLI 키’ 탭에서 같은 목록(사용자·기기·alias·발급·만료)을 보고 행마다 ’폐기’로 지운다. 폐기는 확인 대화상자를 거치고 evo-lite 의 키 삭제 + 기록 폐기이며, 해당 CLI 는 다음 호출에서
401을 받아 다시 로그인해야 한다. 이미 폐기된 키(404)는 목록을 새로 고치고, evo-lite 삭제가 실패하면(502) 기록이 남으므로 잠시 뒤 다시 시도한다. 키 원문은 화면·응답 어디에도 없다. 사용자 이름으로 거를 수 있다. - 주소: 관리자 화면에 등록된 연결(
CLI_LITELLM_CONNECTION)의 주소를 그대로 재사용한다. 관리 API 는 그 주소 끝의/v1을 뗀 루트에서 호출한다. 응답base_url은 연결 주소 그대로이며 CLI 가 닿을 수 없는 내부 주소면CLI_LITELLM_PUBLIC_URL로 지정한다. - 조직용·일반용 인스턴스가 같은 evo-lite 를 쓸 수 있다(지금). 팀 ID 와
user_id렐름 표기가 달라 키·사용량이 나뉘고, 나중에 LiteLLM 을 분리하면 인스턴스 설정만 바꾸면 된다. - 운영자가 evo-lite 에 준비할 것: 인스턴스별 팀, 모델 단가(사람별 금액 집계), Langfuse 콜백(Langfuse 기록은 evo-lite 가 한다), 키 발급·삭제 권한이 있는 관리 키( 부터 사용자·최종 사용자 예산 조회·설정 권한, 팀을 쓰면 부터 팀 구성원 예산 설정(
/team/member_update) 권한도 필요하다). 월 사용 금액 한도는 §9 를 본다(토큰·요청 쿼터는 evo-lite 예산에 반영하지 않는다). 일반용 인스턴스는 관리자 화면에서 연결을 등록하고 그룹별 허용 모델을 지정해야 세션이 쓸 수 있는 모델을 준다(임베딩 모델은 허용 목록에서 뺀다 — 코드로 분류하지 않으므로 운영 규칙이다).
key_service_unavailable(503) 점검 순서
섹션 제목: “key_service_unavailable(503) 점검 순서”앱 로그의 evo-lite <경로> ... 경고 한 줄에 연결 이름·호스트·응답 코드(LiteLLM 오류 응답이 JSON 이면 error.type·error.code)가 남는다. 관리 키·헤더·본문·키 원문은 남지 않는다. 이어서 CLI 세션 발급 실패(key_service_unavailable) user=... 한 줄이 남는다.
- 호스트가 기대한 evo-lite 인가 — 연결(
CLI_LITELLM_CONNECTION)의 주소는 관리자 화면(또는GET /api/admin/connections)에서 확인한다. 연결 주소를 바꿨는데 이 로그의 호스트가 이전 서버면 연결 설정이 안 바뀐 것이다. CI 변수OPENAI_COMPATIBLE_BASE_URL은 연결이 하나도 없을 때(최초 기동)만 쓰이므로 이미 있는 연결의 주소는 바꾸지 못한다. - 응답 코드 401/403 — 관리 키(
CLI_LITELLM_ADMIN_KEY)가 그 evo-lite 의 키 발급·삭제 권한을 가진 키인지 확인한다. - 그 밖의 4xx —
error.type·error.code를 본다. 팀 ID(CLI_LITELLM_TEAM_ID)를 쓰면 그 팀이 evo-lite 에 먼저 있어야 한다(없는 팀의 키는 발급돼도 그 키로/v1/models·채팅이 404 이므로 로그에는 나타나지 않고 CLI 쪽 호출에서 드러난다). 호출 실패: <예외 클래스>(응답 코드 없음) — 앱 컨테이너에서 evo-lite 호스트로 닿는지(DNS·방화벽·TLS)를 확인한다.- 로그에
CLI_LITELLM_CONNECTION 에 해당하는 연결이 없습니다가 있으면 변수의 연결 이름이 관리자 화면의 연결 이름과 다른 것이다.
8. 확인 방법
섹션 제목: “8. 확인 방법”.env에 위 값을 넣고 재기동한다(데이터베이스 마이그레이션이 자동 적용된다 — V18 이users.external_issuer, V19 가cli_litellm_keys, V24 가users·groups의monthly_budget을 추가한다).- Evo CLI 로
evo login후 세션을 받거나, 직접POST /api/cli/session에 Keycloak access token 을 Bearer 로 보낸다. 토큰·키 값을 문서·로그·대화에 붙여 넣지 않는다. - 받은 키로
GET /v1/models를 호출해 응답의models와 같은지 확인한다.
9. 월 사용 금액 한도
섹션 제목: “9. 월 사용 금액 한도”사용자별 월 사용 금액이 한도 이상이면 웹 채팅과 CLI 호출을 모두 막는다. 금액은 evo-lite(LiteLLM)가 모델 단가로 계산한 값이며 evo-api 에는 단가표가 없다. 기존 /v1 토큰·요청 쿼터는 그대로이고 별개로 함께 적용된다.
한도 정하기
섹션 제목: “한도 정하기”- 관리자 설정 → 사용자 탭의 행 버튼 금액 한도, 그룹 · 권한 탭 그룹 상세의 월 사용 금액 한도에서 넣는다. API 는
GET/PUT /api/admin/budgets/users/<아이디>,GET/PUT /api/admin/budgets/groups/<그룹 id>(관리자 전용, 본문{"monthlyBudget": 12.5})이다. - 적용 순서: 사용자 개별값 → 그룹 값(여러 그룹이면 가장 좁은 값) → 무제한. 그룹 기본 한도는 없다(Human 결정 2026-10-20). 값을 비우면(
null) 미설정이다.0은 전면 차단이다. - 금액 단위는 evo-lite 에 등록된 모델 단가의 통화(보통 USD)다. 0 이상 10억 이하, 소수 둘째 자리까지 받고 그보다 긴 소수·음수·문자는
400으로 거부한다(반올림하지 않는다, 과 같은 원칙). 변경은 감사 로그(setting.change, 대상budget:user:<아이디>·budget:group:<이름>)에 이전값→새값으로 남는다. - 사용자 조회 응답에는 자기 값(
monthlyBudget), 실제 적용 값(effectiveBudget·source=USER|GROUP|NONE), evo-lite 누적 금액(spent, 조회 실패면null), 적용 여부(enforced)가 있다. - evo-lite 연동(
CLI_LITELLM_CONNECTION·CLI_LITELLM_ADMIN_KEY)이 없으면 금액을 알 수 없어 한도를 적용하지 않는다(enforced=false, 화면에 안내가 나온다). 값은 저장된다.
웹 채팅 ──user=<렐름>:<아이디>──▶ evo-lite (최종 사용자 = end-user 로 금액이 쌓인다)CLI ──사람별 키(user_id=<렐름>:<아이디>)──▶ evo-lite (내부 사용자 = user 로 금액이 쌓인다)웹 채팅 시작 전 ──/user/info + /customer/info──▶ evo-lite : 두 금액의 합 ≥ 한도면 막는다CLI 키 발급 전·한도 변경 때 ──/user/update|new {max_budget, budget_duration}──▶ evo-lite ──/budget/update|new {budget_id, max_budget, budget_duration} (+ /customer/update|new {budget_id})──▶ evo-lite- 웹 채팅: OpenAI 호환 연결로 나가는 웹 채팅 요청에
/v1과 같은user(<렐름>:<사용자 이름>, §6)를 넣는다. 채팅을 시작하기 전에 evo-api 가 evo-lite 에서 그 식별값의 누적 금액(사람별 키 사용분 + 웹 사용분)을 조회해 한도 이상이면 메시지를 저장하지 않고429 {"error":"monthly_budget_exceeded","message":...,"limit":...}로 거부한다. 화면은 “이번 달 사용 금액 한도에 도달해 채팅할 수 없습니다. 관리자에게 문의하세요.“를 보여 준다. 한도 미만이면 그대로 진행한다. /v1(evo-apisk-키, 이전 방식 CLI): 같은 판정으로429(OpenAI 형식type=rate_limit_error,code=monthly_budget_exceeded)를 돌려준다.- CLI(evo-lite 키 방식, §7): 세션 발급 때 이미 한도 이상이면
429 {"error":"monthly_budget_exceeded"}로 키를 만들지 않는다. 발급 전에 evo-lite 의 그 사용자(user_id)와 최종 사용자(end-user)에max_budget=한도,budget_duration=CLI_LITELLM_BUDGET_DURATION을 넣는다(없으면 만든다). 이후 예산을 넘은 CLI 호출은 evo-lite 가 거부한다(evo-lite 의 예산 초과 오류가 CLI 에 그대로 간다).CLI_LITELLM_TEAM_ID를 쓰면(팀 키) evo-lite 는 사용자 개인 예산을 보지 않고 팀 구성원 예산만 검사하므로, 같은 값을/team/member_update(max_budget_in_team·budget_duration, 구성원 행이 없으면 evo-lite 가 만든다)에도 넣는다. 한도를 비우면 구성원 예산도 지운다. 예산 설정이 실패하면 세션은503 key_service_unavailable이고 키는 만들지 않는다. - 한도를 바꾸면: 실제 적용 한도가 바뀐 사용자(사용자 값 변경은 그 사용자, 그룹 값 변경은 그 그룹 구성원)의 evo-lite 예산을 바로 맞춘다. 하나라도 실패하면 이미 맞춘 사용자는 이전 값으로 되돌리고
502(“evo-lite 사용자 예산을 갱신하지 못해 변경하지 않았습니다…”)로 실패하며 DB 변경은 롤백된다. 그룹 소속 변경처럼 한도 화면을 거치지 않은 변경은 다음 CLI 세션 발급이나 그 사용자의 다음 웹 채팅 확인 때 evo-lite 예산이 다르면 맞춘다(실패해도 채팅은 막지 않는다). - 최종 사용자(end-user) 예산은 예산 행으로 넣고 지운다: evo-lite 의
/customer/update는max_budget의null·0을 조용히 버린다(LiteLLMupdate_end_user의 값 필터). 그래서 한도를 비워도 최종 사용자 예산이 남아 웹 채팅이 evo-lite 의ExceededBudget(예:Budget=0.0)으로 끊기고, 이미 있는 최종 사용자에게0이 들어가지 않았다. 지금은/customer/info의litellm_budget_table.budget_id로 연결된 예산 행을/budget/update {budget_id, max_budget, budget_duration}으로 바꾼다(한도를 비우면max_budget·budget_duration모두null→ 예산 없음). 예산 행이 없으면/budget/new로 만들고 그budget_id를/customer/update(있는 최종 사용자) 또는/customer/new(없는 최종 사용자)로 연결한다. 예산 행도 한도도 없으면 쓰지 않는다. 사용자(/user/update)의null은 그대로 지워진다. - 남은 예산 정리: 앱 한도가 무제한인데 evo-lite 에 사용자·최종 사용자 예산이 남아 있으면 다음 CLI 세션 발급(
sync)이나 그 사용자의 다음 웹 채팅 확인 때 지운다. 이를 위해 웹 채팅은 무제한 사용자도(연동이 있을 때만) 누적 금액 조회를 한다 — 한도 확인과 같은 캐시(CLI_LITELLM_SPEND_CACHE_TTL)를 써서 사용자당 TTL 마다 최대 한 번이고, 조회·해제 실패는 경고 로그만 남기고 막지 않는다. 같은 조회 결과로는 해제를 한 번만 시도한다(권한 부족 등으로 실패해도 매 채팅마다 쓰기를 반복하지 않는다). - 조회 실패·시간 초과: 막지 않고 허용하며 경고 로그
월 사용 금액 조회 실패 — 이번 요청은 허용한다 user=<렐름>:<아이디> 원인=<예외 클래스>를 남긴다(Human 결정 2026-10-20). 결과(실패 포함)는CLI_LITELLM_SPEND_CACHE_TTL(기본 30초) 동안 재사용하므로 그동안은 다시 조회하지 않는다. 관리 키·응답 본문은 로그·응답에 남지 않는다. - 웹 채팅이 evo-lite 가 아닌 연결(Anthropic·Ollama·다른 OpenAI 호환 서버)을 쓰면 그 사용량은 금액에 들어가지 않는다(evo-api 단가표는 범위 밖, 후속). 한도 확인 자체는 모델과 상관없이 채팅 전에 한다.
운영자 준비와 확인할 점
섹션 제목: “운영자 준비와 확인할 점”- evo-lite 에 모델 단가를 등록해야 금액이 계산된다(단가가 없으면 금액이 0 으로 쌓여 막히지 않는다).
- 관리 키(
CLI_LITELLM_ADMIN_KEY)에/user/info·/user/new·/user/update·/customer/info·/customer/new·/customer/update·/budget/new·/budget/update권한이 있어야 한다(마스터 키·proxy_admin이면 된다)./budget/*권한이 없으면 최종 사용자 예산을 넣거나 지우지 못해 CLI 세션은503, 한도 저장은502다. - 업그레이드 참고: 이 변경 전에 한도를 비운 사용자는 evo-lite 최종 사용자 예산(예:
0)이 남아 있을 수 있다. 그 사용자의 다음 웹 채팅 확인이나 CLI 세션 발급 때 지워진다. - 예산 주기는 evo-lite 가 관리한다. evo-lite 의 예산 재설정은 달력의 1일이 아니라 예산을 처음 넣은 때부터
budget_duration마다다.1mo표기를 모르는 버전이면CLI_LITELLM_BUDGET_DURATION=30d로 바꾼다. - 개발서버 실측(2026-10-20): 팀 없는 인스턴스는 사용자 예산으로 CLI 호출이 막힌다(422). 팀 키는 사용자 예산만으로는 막히지 않아(evo-lite 의
apply_user_budget_to_team_keys가 꺼진 기본값) 팀 구성원 예산을 함께 넣는다. 아직 실측하지 않은 것: 웹 채팅user사용분이 최종 사용자 금액으로 쌓이는지. - 업그레이드 참고: 이 변경 전에 넣은 한도는 팀 구성원 예산이 비어 있다. 그 사용자가 CLI 세션을 다시 받거나 관리자가 한도를 다시 저장하면 채워진다.
- 알려진 한계: evo-lite 는 CLI 호출을 사람별 키 사용분(내부 사용자 금액)만으로 막는다. 웹 사용분은 최종 사용자 금액으로 따로 쌓이므로, 웹 채팅은 합계로 막히지만 CLI 는 CLI 사용분이 한도에 닿을 때까지 계속 쓸 수 있다. CLI 가 evo-lite 요청에
user를 따로 넣으면 그 사용분이 두 금액에 모두 잡혀 웹 판정에서 두 번 셀 수 있다.
텔레메트리 중계 — 예전 batch 형식
섹션 제목: “텔레메트리 중계 — 예전 batch 형식”주의: Langfuse v4 의 기본 쓰기 모드(
events_only)는 이 형식의trace-create·span-create·generation-create를400 Event type not accepted로 거부한다(evo-api 응답은 207 이고 기록은 남지 않는다. 2026-10-21 개발서버에서 Evo CLI 0.19.0 조직 모드 기록이 모두 이렇게 버려지는 것을 확인했다 —, evo-cli 에 OTLP 전환 요청). Langfuse v4 를 쓰면 아래 OTLP 중계를 쓴다. 이 경로는 전환 기간 동안 남겨 둔다.
CLI 가 보내는 에이전트 명령 단위 기록(Langfuse batch)을 evo-api 가 받아 자기 Langfuse 프로젝트로 넘긴다. Langfuse 주소·키는 사용자 PC 에 주지 않는다(세션 응답에도 langfuse 필드가 없다). 연결 정보는 관리자 설정의 관측성(주소·공개 키·비밀 키)을 그대로 쓰며 새 환경변수는 없다. 조직용·일반용은 각자의 Langfuse 프로젝트를 설정한다.
- 호출:
POST /api/cli/telemetry,Authorization: Bearer <Keycloak access token>, 본문{"batch":[{"id","type","body","timestamp"?}]}. - 인증은 세션 API 와 같은 토큰 검증이다(
401 invalid_token, 기능 꺼짐403 cli_disabled). 사용자는 이미 세션을 받은 적 있는 활성 계정이고user또는admin그룹 소속이어야 한다(아니면403 forbidden). 이 호출은 그룹 동기화를 하지 않는다. - 허용
type:trace-create,span-create,generation-create. 하나라도 다른 type 이면 요청 전체가400 {"error":"unsupported_event"}이고 아무것도 전달하지 않는다. 형식 오류는400 invalid_request. - 서버가 하는 일:
trace-create의body.userId를 토큰 사용자의 evo 계정 이름으로 덮어쓴다(아래 ‘사용자 식별’ 참고), 태그evo-cli와 조직 이름(metadata.org)을 붙인다,timestamp가 없으면 받은 시각으로 채운다. 모든 이벤트id와body.id·traceId·parentObservationId에 사용자 식별 접두(evo-<해시>-)를 일관되게 붙여 전달한다 — 같은 id 로 다시 보내면 갱신이고 다른 사용자의 기록은 가리킬 수 없다. - 입력·출력 내용은 관측성 설정의 캡처 수위를 따른다:
metadata(기본) 는input·output을 빼고,masked는 개인정보를 마스킹해,full은 그대로 전달한다. - 한도(기본값, 코드 설정
app.security.cli-session.telemetry-*): 요청 본문 1MB(413 payload_too_large), 이벤트 200개(413 too_many_events), IP·사용자별 분당 120건(429 rate_limited+Retry-After). 초과는 사용자 이름·종류·크기만 로그에 남기고 내용·토큰은 남기지 않는다. - 응답: Langfuse 가
200/207이면 그 상태와 본문을 그대로 돌려준다.207의errors[]가 비어 있지 않으면 evo-api 가 거부 건수·대표 상태·사유(message, 120자 이내)를 경고 로그(Langfuse 가 CLI 텔레메트리를 거부함)로 남긴다(본문·키는 남기지 않는다). 관측성 설정이 꺼져 있거나 불완전하면503 telemetry_unavailable, Langfuse 호출 실패·오류 응답·시간 초과(10초)는502 telemetry_upstream_error(응답 본문·키는 노출하지 않는다). CLI 는 실패를 무시하고 계속 동작해야 한다.
사용자 식별
섹션 제목: “사용자 식별”- CLI 기록의 Langfuse 사용자(userId)는 토큰(발급자+sub)으로 찾은 evo 앱 계정의 사용자 이름이다. 웹 채팅 기록도 같은 값을 쓰므로 한 사람의 웹·CLI 기록이 같은 사용자로 모인다. 토큰의
email·preferred_username클레임이나 클라이언트가 보낸 값은 쓰지 않는다. - SSO 계정은 같은 이름의 로컬 계정이 있으면
_oidc접미사가 붙은 이름이 된다(web-oidc.md참고) — Langfuse 에서도 그 이름으로 보인다. - 이 규칙을 적용한 배포 이전의 CLI 기록은 토큰의 이메일(없으면
preferred_username)로 남아 있다. Langfuse 기록은 고칠 수 없으므로 이전 기록을 찾을 때는 이메일로 조회한다.
텔레메트리 중계 — OTLP
섹션 제목: “텔레메트리 중계 — OTLP”CLI 가 OpenTelemetry trace 를 OTLP/HTTP JSON 으로 evo-api 에 보내면, evo-api 가 사용자 강제·id 분리·캡처 수위를 적용해 Langfuse 의 OTLP 경로(<관측성 주소>/api/public/otel/v1/traces)로 넘긴다. Langfuse 주소·키는 지금처럼 evo-api 관리자 관측성 설정에만 있고 CLI 에 주지 않는다. Langfuse v4(events_only)에서 동작한다.
POST /api/cli/otel/v1/traces- 헤더:
Authorization: Bearer <Keycloak access token>(세션 API 와 같은 evo-cli 토큰),Content-Type: application/json - 본문: OTLP
ExportTraceServiceRequest의 JSON 표현({"resourceSpans":[{"resource":{"attributes":[…]},"scopeSpans":[{"scope":{…},"spans":[…]}]}]}). 키는 lowerCamelCase,traceId(32자리)·spanId·parentSpanId(16자리)는 16진수 문자열, 시간은startTimeUnixNano·endTimeUnixNano(문자열 숫자). protobuf·gRPC 는 받지 않는다(415). - OpenTelemetry SDK 를 쓰면 OTLP HTTP JSON 익스포터의 traces 주소를
<evo-api>/api/cli/otel/v1/traces로, 헤더에 Bearer 토큰을 넣는다(토큰 갱신 시 헤더도 갱신).
권장 속성 (Langfuse 표시용)
섹션 제목: “권장 속성 (Langfuse 표시용)”| 위치 | 속성 | 뜻 |
|---|---|---|
| resource | service.name = evo-cli, service.version = CLI 버전 |
출처·버전 |
| 루트 span | langfuse.trace.name |
명령 이름(예: evo run) |
| 루트 span | langfuse.session.id |
CLI 세션 id(evo-api 가 사용자 접두를 붙인다) |
| 루트 span | langfuse.trace.tags(문자열 배열) |
추가 태그(evo-cli 는 서버가 붙인다) |
| 모든 span | langfuse.observation.type |
span·generation·tool 등 |
| LLM 호출 span | gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens |
모델·토큰 수 |
| 모든 span | langfuse.observation.metadata.<이름> |
거를 수 있는 메타데이터 |
| 내용(선택) | langfuse.observation.input·langfuse.observation.output, gen_ai.prompt*·gen_ai.completion* |
입력·출력 — 캡처 수위에 따라 서버가 지우거나 가린다 |
서버가 하는 일 (CLI 가 보낸 값보다 우선)
섹션 제목: “서버가 하는 일 (CLI 가 보낸 값보다 우선)”- 사용자:
user.id·langfuse.user.id·enduser.id(resource·span 모두)를 버리고 모든 span 에langfuse.user.id= 토큰 사용자의 evo 계정 이름을 넣는다(아래 ‘사용자 식별’ 참고). 태그evo-cli와 조직 이름(langfuse.trace.metadata.org, 설정 시)을 붙인다. 출처 표시langfuse.observation.metadata.evo_source=cli도 모든 span 에 넣는다(클라이언트 값은 버린다, ’내 기록’의 출처 구분용 —usage-records.md). - id:
traceId·spanId·parentSpanId를 (발급자, 사용자, 원래 값)의 해시로 바꾼다(길이는 그대로). 같은 id 로 다시 보내면 같은 결과이고, 다른 사용자의 trace 를 가리킬 수 없다. 부모·자식 연결은 유지된다. 다른 trace 를 가리키는links는 버린다. 세션 id(langfuse.session.id·session.id)에는 사용자 접두(evo-<해시>-)를 붙인다. - 내용: 캡처 수위
metadata(기본)는 입력·출력 속성(langfuse.trace.input/output,langfuse.observation.input/output,input.value·output.value,gen_ai.prompt*·gen_ai.completion*·gen_ai.input.messages·gen_ai.output.messages·gen_ai.system_instructions·gen_ai.tool.call.arguments/result,llm.input_messages*·llm.output_messages*,mlflow.spanInputs/Outputs)과 spanevents를 지운다.masked는 그 값과 events 의 문자열을 마스킹하고,full은 그대로 둔다.
응답과 오류
섹션 제목: “응답과 오류”| 상태 | 본문 | 뜻·CLI 처리 |
|---|---|---|
| 200 | Langfuse 응답 그대로(예: {"partialSuccess":{}}) |
전달됨. span 이 0개면 Langfuse 를 부르지 않고 200 {} |
| 400 | {"error":"invalid_request"} |
JSON·OTLP 형식 오류, id 형식 오류(재시도하지 않는다) |
| 401 | {"error":"invalid_token"} |
토큰 없음·위조·만료·다른 클라이언트 → 토큰 갱신 후 재시도 |
| 403 | cli_disabled / forbidden |
기능 꺼짐 / 세션을 받은 적 없는 계정·비활성·그룹 밖 |
| 413 | payload_too_large / too_many_events |
본문 1MB·span 200개 초과 → 나눠 보낸다 |
| 415 | unsupported_media_type |
JSON 이 아님 |
| 429 | rate_limited + Retry-After |
분당 120건 초과(예전 경로와 한도 공유) |
| 503 | telemetry_unavailable |
관측성 설정 꺼짐·불완전 |
| 502 | telemetry_upstream_error |
Langfuse 실패·200 외 응답·10초 시간 초과 |
CLI 는 실패해도 명령 실행을 막지 않는다(기록 전송은 부가 기능).
답변은 문서를 근거로 만들지만 틀릴 수 있습니다. 출처 문서를 함께 확인하세요.