콘텐츠로 이동

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 ──디바이스 코드 로그인──▶ Keycloak
evo CLI ──POST /api/cli/session (Authorization: Bearer <Keycloak access token>)──▶ evo-api
evo 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"} 이고 기존 동작은 달라지지 않는다.

  • 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 에 groups claim(전체 경로 아님)을 넣는다. 없으면 userinfo 로 보강한다. 그룹이 없는 사용자는 claim 항목 자체가 빠진다(‘그룹 없음’).
  • 웹과 CLI 가 같은 Keycloak 사용자를 쓰면 같은 계정·그룹으로 묶인다.
이름 기본값 설명
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 발급자만 사용한다. 다중 인스턴스에서는 요청 제한이 인스턴스별로 적용된다.

토큰은 표준 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(그 스코프 요청)와 먼저 맞추고, 그 스코프를 요청하지 않는 판(예: original 0.23.1)이 401 이 되므로 evo-cli 에 미리 알린다.
상황 응답
성공 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 한 곳):

    1. 그 사용자가 속한 그룹들의 그룹 기본 모델 중 적용 가능한 것 — 우선순위(priority)가 높은 그룹 먼저, 같으면 그룹 이름 순
    2. 없으면 조직 기본 모델(적용 가능할 때)
    3. 없으면 필드를 생략한다(클라이언트는 사용자가 고른 모델 또는 첫 모델을 쓴다)

    적용 가능 = 카탈로그에 활성(모델·연결 모두)으로 있고 그 사용자의 허용 모델 목록(웹·/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)가 나온다.
  • 키는 개인 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 역할은 항상 활성 모델 전체이고 비활성 모델은 제외된다.

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-lite
evo 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 가 여러 건이면 그 키 갱신은 실패로 본다. 관리자가 폐기한 키와 같은 기기의 재로그인 때 이전 키를 지우는 동작은 바뀌지 않는다.
  • 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=... 한 줄이 남는다.

  1. 호스트가 기대한 evo-lite 인가 — 연결(CLI_LITELLM_CONNECTION)의 주소는 관리자 화면(또는 GET /api/admin/connections)에서 확인한다. 연결 주소를 바꿨는데 이 로그의 호스트가 이전 서버면 연결 설정이 안 바뀐 것이다. CI 변수 OPENAI_COMPATIBLE_BASE_URL 은 연결이 하나도 없을 때(최초 기동)만 쓰이므로 이미 있는 연결의 주소는 바꾸지 못한다.
  2. 응답 코드 401/403 — 관리 키(CLI_LITELLM_ADMIN_KEY)가 그 evo-lite 의 키 발급·삭제 권한을 가진 키인지 확인한다.
  3. 그 밖의 4xx — error.type·error.code 를 본다. 팀 ID(CLI_LITELLM_TEAM_ID)를 쓰면 그 팀이 evo-lite 에 먼저 있어야 한다(없는 팀의 키는 발급돼도 그 키로 /v1/models·채팅이 404 이므로 로그에는 나타나지 않고 CLI 쪽 호출에서 드러난다).
  4. 호출 실패: <예외 클래스>(응답 코드 없음) — 앱 컨테이너에서 evo-lite 호스트로 닿는지(DNS·방화벽·TLS)를 확인한다.
  5. 로그에 CLI_LITELLM_CONNECTION 에 해당하는 연결이 없습니다 가 있으면 변수의 연결 이름이 관리자 화면의 연결 이름과 다른 것이다.
  1. .env 에 위 값을 넣고 재기동한다(데이터베이스 마이그레이션이 자동 적용된다 — V18 이 users.external_issuer, V19 가 cli_litellm_keys, V24 가 users·groups 의 monthly_budget 을 추가한다).
  2. Evo CLI 로 evo login 후 세션을 받거나, 직접 POST /api/cli/session 에 Keycloak access token 을 Bearer 로 보낸다. 토큰·키 값을 문서·로그·대화에 붙여 넣지 않는다.
  3. 받은 키로 GET /v1/models 를 호출해 응답의 models 와 같은지 확인한다.

사용자별 월 사용 금액이 한도 이상이면 웹 채팅과 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-api sk- 키, 이전 방식 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 을 조용히 버린다(LiteLLM update_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 기록은 고칠 수 없으므로 이전 기록을 찾을 때는 이메일로 조회한다.

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 토큰을 넣는다(토큰 갱신 시 헤더도 갱신).
위치 속성 뜻
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)과 span events 를 지운다. 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 는 실패해도 명령 실행을 막지 않는다(기록 전송은 부가 기능).