변경 내역¶
Continuum Router의 모든 주요 변경 사항은 이 파일에 문서화되어 있습니다.
형식은 Keep a Changelog를 기반으로 하며, 이 프로젝트는 Semantic Versioning을 따릅니다.
v1.18.0 - 2026-08-05¶
아직 등록하지 않은 백엔드 후보를 위한 것과 이미 설정된 백엔드 하나를 위한 것, 두 개의 관리자 인증 디스커버리 엔드포인트를 추가하고, 유지 관리되는 쿠버네티스 배포 프로파일과 릴리스 이미지 공급망 게이트를 함께 제공합니다.
추가됨¶
- 등록 전 백엔드 후보를 검사하는 비변경 프로브
POST /admin/backends/probe를 추가했습니다 (#1252). 백엔드 생성과 동일한 후보 필드에operations: ["health", "models"]를 더해 받고, 유닉스 소켓을 포함한 모든 Admin 전송에서 관리자 인증을 거치며,GET /admin/capabilities가transient_backend_probe_v1로 광고합니다. 후보는 요청 범위 안에만 존재합니다. 활성 설정, 설정 이력, 백엔드 풀, 헬스 상태, 서킷 브레이커, 집계 캐시, 환경 파일, 토큰 스토어 어디에도 기록하지 않습니다. 응답은health.credential_status(valid,invalid,unknown,not_required)와catalog.source(live,curated,configured,unsupported)를 분리해서 돌려주므로, 인증 없이 살아 있는 엔드포인트를 자격 증명이 유효하다는 증거로 잘못 읽을 수 없습니다. 모델 목록을 제공할 수 있는 후보는 등록된 디스커버리가 쓰는 URL 조합, 인증 헤더, 파서, 정규화, 응답 크기 상한, 백엔드별 모델 개수 상한을 그대로 재사용합니다. 요청 본문, 동시 프로브 수, 요청 빈도, 업스트림 시간, 응답 바이트, 반환 모델 개수에 모두 상한이 있고, 자격 증명이 담긴 URL은 거부하며, 검증·헬스·인증·업스트림·타임아웃·파싱·응답 크기 오류는 전부 정제해서 내보냅니다. - 설정된 백엔드 하나의 라이브 모델 카탈로그를
backends[].models허용 목록 적용 전에 돌려주는POST /admin/backends/{name}/models/discover를 추가했습니다 (#1245). 이름으로 정확히 하나의 백엔드를 해석하고 일반 집계가 쓰는 모델 페처를 그대로 재사용하므로 타임아웃, 응답 크기 상한, 백엔드별 모델 개수 상한, 정규화, Codex 계정 플랜 필터링, 중앙화된 OAuth 갱신이 모두 유지됩니다. 아무것도 기록하지 않습니다. 설정, 집계 모델 캐시, 헬스 상태, 서킷 브레이커 상태는 그대로이고 평소의 집계 모델 노출도 영향을 받지 않습니다. 네이티브 Anthropic, Bedrock, Codex가 아닌 OAuth 백엔드는 설정된 모델 이름으로 대체하는 대신model_discovery_unsupported와 함께501을 반환하고, 알 수 없는 이름은backend_not_found와 함께404를 반환하며, 업스트림 실패는backend_authentication_failed,backend_discovery_timeout,backend_discovery_network_error,backend_discovery_parse_error,backend_discovery_http_error,backend_discovery_response_too_large로 기계가 읽을 수 있게 구분되면서 액세스 토큰, 리프레시 토큰, 토큰 스토어 경로, 인증 헤더를 실어 보내지 않습니다. - 유지 관리되는 쿠버네티스 배포 프로파일을
deploy/아래에 추가했습니다 (#13). staging 오버레이가 딸린 Kustomize base와 development, staging, canary, production 값을 갖춘 Helm 차트가 인증된 설정, 강화된 파드, TLS 인그레스, 오토스케일링, 중단 예산, 네트워크 격리를 다룹니다.monitoring/prometheus/에는 Prometheus Operator가 없는 클러스터를 위한 독립형 Prometheus 번들이 10 GiB 퍼시스턴트 볼륨 클레임과 15일 보존 설정으로 들어 있고, 오퍼레이터 CRD가 설치된 환경에서는 Helm 차트가ServiceMonitor를 렌더링할 수 있습니다.
변경됨¶
- 등록된 백엔드와 임시 후보의 라이브 디스커버리 경로를 하나로 합쳤습니다 (#1253, PR #1254). 후보 디스커버리와 등록 디스커버리가 프로바이더·인증 분기를 각자 중복해서 갖고 있었는데, 이제 검증된 백엔드 스냅샷을 기준으로 하나의 공용 함수가 양쪽을 처리하고 토큰 스토어 접근만 등록된 백엔드 전용으로 남겼습니다.
수정됨¶
- 임시 OAuth 후보가 실제 확인 없이 healthy로 보고하던 것을 고쳤습니다 (#1252, PR #1254). 프로브 헬스가 등록된 백엔드의 액티브 체크 우회 경로를 물려받아 네트워크 I/O 없이
healthy를 답했는데, 등록된 OAuth 백엔드가 의존하는 라이프사이클 관리 인증 전략은 등록 전에는 존재하지 않습니다. 이제 임시 OAuth 헬스는health.status: "unknown",credential_status: "unknown",backend_probe_health_unsupported를 반환합니다. - 헬스 전용 프로브에서 인증 오류를 안정적인 코드로 돌려줍니다 (#1252, PR #1254). 헬스 전용 프로브의 401 또는 403이 일반 헬스 오류로 뭉개져서 키가 틀린 경우와 엔드포인트에 닿지 못한 경우를 구분할 수 없었습니다. 이제 업스트림 본문을 노출하지 않고
backend_authentication_failed로 매핑합니다. - 설정 기반 후보 카탈로그에 상한을 적용했습니다 (#1252, PR #1254). 설정 기반 카탈로그가
models만 읽고 백엔드별 상한을 적용하지 않아 프로브 응답이max_models_per_backend를 넘길 수 있었습니다. 이제models를 먼저 쓰고model_configs를 확장 폴백으로 쓰며, 설정 기반과 큐레이션 카탈로그 모두 백엔드별 상한으로 잘라냅니다. 요청이 지정한 설정 기반 Anthropic 모델 id는 기존 info 레벨 카탈로그 로깅에 더 이상 실리지 않습니다. - 지원하지 않는 전송 방식과 잘못된 확장 모델 설정을 디스패치 전에 거부합니다 (#1252, PR #1254). 지원하지 않는 URL 스킴이 검증에서 걸리지 않고 하위 페치 코드까지 내려갔고, 프로브 전용 검증 경로는 일반 백엔드 검증이 거부하는
model_configs를 받아들였습니다. 이제 둘 다 타입이 있고 정제된 후보 오류로 실패합니다.
문서¶
- 두 디스커버리 엔드포인트를 영문과 국문 Admin API 레퍼런스 및 백엔드 설정 가이드에 문서화했습니다. 오류 코드, 비변경 경계, 어느 쪽을 언제 쓰는지를 함께 실었습니다.
- 메트릭 가이드에 Helm
ServiceMonitor객체와 독립형 Prometheus 번들을 추가했습니다. 스토리지 용량 산정, 클러스터 범위 디스커버리 RBAC, NetworkPolicy 주의 사항을 함께 다룹니다. - 새 프로파일에 맞춰 롤링, 블루-그린, 카나리, 설정 롤아웃 절차를 배포 문서에 추가했습니다.
CI¶
- 모든 Helm 프로파일과 Kustomize 오버레이를 렌더링해 kubeconform으로 매니페스트를 검증하고, 렌더링된 라우터 설정마다
continuum-router config validate를 돌리는Deployment Asset Validation잡을 추가했습니다 (#13).deploy/,monitoring/prometheus/, 검증 스크립트, 해당 워크플로가 바뀔 때만 실행되며scripts/local-ci.sh가 같은 스크립트를 로컬에서 돌립니다. - 릴리스 컨테이너 이미지에 게이트를 걸었습니다 (#13). Trivy가 Debian amd64와 arm64 이미지를 스캔해 CRITICAL 또는 HIGH가 나오면 릴리스를 실패시키고, buildx가 SBOM과
provenance: mode=max를 생성하며, 멀티 아키텍처 매니페스트 다이제스트를 검증한 뒤 증명서를 발급합니다. 워크플로 수준의contents: write와packages: write는 잡별 부여로 좁혔습니다. - 보호된 환경 뒤에서 도는 릴리스-투-스테이징 워크플로
deploy-staging.yml을 추가했습니다.
v1.17.0 - 2026-08-03¶
허브가 관장하는 가드레일 정책을 처음으로 실행하고, 허브 코스트 센터 V2 월간 제한을 로컬에서 강제 적용하며, 파일 해석과 허브 엔벨로프 디코드에 상한을 두고, 키별·클라이언트별 레이트 리밋을 실제 트래픽에서 동작시키며, Files API 전송을 버퍼링 대신 스트리밍으로 바꿉니다.
보안¶
- Responses의 파일·세션 권한 판정을 클라이언트가 보낸
X-User-ID헤더가 아니라 인증된 API 키 주체에 묶었습니다 (#1244). 파일 해석, 저장 세션 생성,previous_response_id재생,GET,DELETE가 모두AuthContext::user_id()에서 요청자를 도출하고,X-User-ID는 업스트림으로 넘기기 전에 걸러내며, 위조 가능하던 캐시·세션 폴백 네임스페이스는 제거했습니다. Responses와 Anthropic 양쪽의 소유 파일·세션 검사는 인증된 요청자가 없으면 거부 쪽으로 닫히고, Responses 파일 해석의AccessDenied는 원본file_id를 전달하는 대신403을 반환합니다.X-User-ID로 Responses 상태를 분리하던 배포는 키 단위 분리로 옮겨야 합니다. 영문·국문 가이드에 마이그레이션 절차를 실었습니다. - 클라이언트가 보는 오류 본문에서 백엔드 URL을 비롯한 reqwest 유래 전송 정보를 가렸습니다 (#1235). 응답 경계에서
reqwest::Error::without_url()을 사용하는 공용client_safe_reqwest_error헬퍼를 도입해, 그동안 원본 표시 문자열을 그대로 끼워 넣던 Responses, Messages,count_tokens, 이미지 편집, 멀티모달 임베딩, responses-only 패스스루 응답 생성 경로를 모두 덮었습니다. responses-only 패스스루 매퍼도 Responses 매퍼와 같은 정책을 따라 고정된 백엔드 연결 실패 메시지를 반환합니다. HTTP 상태, 오류 타입, 오류 코드, 서킷 브레이커 집계, 운영자 로그의 정보량은 그대로입니다. -
1161이 범위 밖으로 남겨 둔 신뢰할 수 없는 로그 값들을 이스케이프하고 길이를 제한했습니다 (#1179).
POST /v1/files, 이미지 편집·변형 파서, 관리자 Files API 업로드의 클라이언트 제공 멀티파트 필드 이름은 포맷 문자열 보간으로message에 닿았는데,EscapeGuard는\n과\r을 통과시키고 길이 제한도 없었습니다. #1161이 손대지 않은 Anthropic 하위 트리의 백엔드 제공 값들, 특히/v1/responses파싱 실패 본문 앞부분과 스트리밍 중 업스트림 오류 본문 전체에도 같은 처리를 적용했습니다.¶ - 사고 비활성화 거부 응답 본문에 그대로 실리던 클라이언트 값들에 길이 상한을 두었습니다 (#1183).
disabled_thinking_effort_error_message는 원본effort를 두 번, 원본 모델 id를 한 번 보간했고 세 개 인그레스 모두 이 메시지를 400 본문으로 만듭니다. 두 게이트 어느 쪽도 입력을 먼저 제한하지 않습니다.effort_exceeds_high는 다듬고 소문자로 바꾼 사본으로 비교하면서 보간은 원본으로 하고,claude_family_version은 앞쪽 토큰만 본다는 점 때문입니다. 이제 상한이 공용 빌더 안에 있어 모든 인그레스가 이를 물려받습니다. - Responses의
file_id해석에 요청 단위 상한을 두었습니다 (#1180, #1174 해결). #1169가 Anthropic 경로에서 고친 것과 같은 결함 종류입니다. 이 해석기는 파일별 10 MiBMAX_INJECTION_SIZE하나만 강제했기 때문에 개별적으로는 작은 참조를 몇 개든 집계 상한 없이 해석했고, 참조마다 자기 base64 사본을 디스패치 시점까지 메모리에 붙들고 있었습니다. 이제 첫 메타데이터 조회 전에 개수 사전 검사가 참조 100개를 넘는 요청을 거부하고, 집계 바이트 예산을 콘텐츠를 읽기 전에metadata.bytes에서 참조마다 계산하며(따라서 같은 참조를 반복하면 반복만큼 계산합니다), 해석 단계 전체에 다른 두 경로가 이미 갖고 있던 30초 타임아웃을 겁니다. 세 경로가 실제로 공유하는 상한들은 새src/core/files/limits.rs로 모았고, 중복 선언 두 벌과 값을 맞춰 달라고 부탁하던 주석이 사라졌습니다. - chat-completions의 로컬
file_id해석에 Anthropic Messages 및 Responses와 같은 원본 32 MiB 집계 바이트 예산을 적용했습니다 (#1240). 이제 chat 경로는 내용을 읽기 전에metadata.bytes에서 각 참조를 계산하며, 같은 파일을 반복 참조해도 참조마다 계산하고, 요청 하나가 예산을 넘으면 부분 해석된 페이로드를 전달하지 않고 400invalid_request_error를 반환합니다. 또한 chat-completions는 PDF에도 이미지와 같은 10 MiB 파일별 인라인 상한을 적용합니다. 더 큰 PDF는files.max_file_size아래에서 업로드할 수는 있지만 chat-completions JSON 본문 안으로 확장되지는 않습니다. - 지금까지 크기 상한이 없던 허브 정책 엔벨로프 디코드에 상한을 두었습니다(#1193).
sync_policy는 이제 설정 동기화와 같은 상한 디코더를 거쳐 약 152.6 MiB의 유도된 상한(MAX_POLICY_SYNC_RESPONSE_BYTES) 아래에서 디코드합니다. 이 상한은 허브 자체의 배포 예산을 근거로 항목별로 기록해 유도했습니다(허브가 조직 단위로 제한하지 않는 유일한 멤버인 키 테이블에 대한 10만 항목 허용치, 4 MiBrequest_params예산을 실은 티어 1,024개, 256 KiB 가드레일 캐노니컬 예산, 비용 센터 할당 5,000건과 상한이 걸린 센터 1,024개, 허브가 전혀 제한하지 않는 멤버들에 대한 명시적 허용치). 모든 상한을 동시에 채운 정상 테넌트도 디코드되도록 크기를 잡았고, 새로 추가한 상한 규모 왕복 테스트가 이를 고정합니다. 상한 초과 거부는 조용한 파싱 계열 실패가 아니라 요란하고 타입이 있는 신호입니다. 상한과 관측 크기를 담은 별도의HubError::ResponseTooLarge오류, 두 값을 명시하는error!로그(본문 내용은 절대 싣지 않습니다), 그리고 "엔벨로프가 너무 큼"을 일반 동기화 실패와 구분하는 새control_plane_policy_sync_failure_total{reason="envelope_too_large"|"other"}카운터가 함께 동작합니다. 거부가 지속되면 재시작한 라우터가 영구히 fail-open 상태로 남는 반면 일반 실패는 저절로 회복되기 때문입니다. 두 reason 시리즈는 등록 시점에 0으로 생성되어 아직 아무것도 거부하지 않은 라우터에서도 플릿 알림이 시리즈를 볼 수 있고, 거부 횟수와 마지막 발생 시각은GET /admin/control-plane/status의policy_sync.oversize_refusals/last_oversize_refusal_ms필드로 노출됩니다.last_sync_ms가 null인데 거부 횟수가 0이 아니면 콜드 스타트 fail-open 상태라는 신호입니다. 정책 스토어는 거부를 겪어도 last-known-good 스냅샷을 유지하며 강제 적용을 절대 넓히지 않습니다. 콜드 스타트에서의 거부는 스토어를 정확히 fail-open 상태로 남겨 둡니다. 웹소켓 정책 스트림은 이제 메시지와 프레임 상한을 같은 상수로 설정합니다. 기존의 tungstenite 64 MiB 기본값은 폴링 상한보다 작아서 한쪽 경로에서는 받아들이는 정상적인 대형 엔벨로프를 다른 쪽에서 거부할 수 있었습니다. 남아 있던 무제한 허브 응답 디코드(enroll,push_usage,push_probes)에도 각각 유도된 상한과 같은 타입 거부를 적용했습니다. usage ack 상한은 기본값 500건이 아니라 이 코드베이스가 강제하는 1만 건 클램프(MAX_USAGE_BATCH_MAX_RECORDS)에서 유도했고, 읽을 수 없는 초과 크기 usage ack은 이미 처리된 배치에 대해 종결로 취급해 무한 재시도를 막았습니다.heartbeat,report_config_apply_result,push_batch_status는 응답 본문을 디코드하지 않으므로 변경이 없습니다.
추가됨¶
- 허브 코스트 센터 월간 제한을 교정된 V2 수렴 계약에 따라 로컬에서 강제 적용합니다 (#1162; 허브 이슈 #688, #735, #736, 에픽 #646). 벤더링된
continuum-protocol크레이트가 허브 rev98bb8ac에서 코스트 센터 표면 전체를 동기화하며, 허브가 커밋한 픽스처를 바이트 단위로 복사해 새 와이어 테스트로 고정했습니다. 그래서 V1 멤버(cost_centers,cost_center_policy_digest)는 파싱 호환을 유지하되 강제 적용되지 않고 확인 응답도 하지 않으며, V2 멤버(cost_centers_v2)가 강제 적용되는 계약이 됩니다. 이 계약은 라우터별 안정 할당 명세(허브가 미리 해석한key_id -> cost_center_id소유권, 라우터 폴백, 프로바이더/모델 및 안정 백엔드 폴백, 센터별 상한)와 그것이 참조하는 영속 동적 예산 스냅샷(현재까지 소비된 카운터, 허브가 작성한 차원별 판정, 라우터별included_router_usage_seq워터마크)으로 구성됩니다. 명세는 하나의 원자적 적용 단위로서 크기 상한, 중복, 참조, 라우터 범위 다이제스트, 결정적 커서, 스냅샷 최신성, 워터마크 가능성을 검증받고, 결함이 있으면 타입 코드(invalid_cost_center_policy,digest_mismatch,stale_budget_snapshot,invalid_usage_watermark)와 함께 전체가 거부되며 last-known-good 단위가 계속 강제 적용됩니다. 하트비트 상태의 적용/거부 양쪽에 안정 커서/다이제스트와 동적 예산 커서가 정확히 확인 응답됩니다. 강제 적용은 로컬이며 허브는 요청 경로에 관여하지 않습니다. 승인 게이트는 허브가 계측할 사용량 레코드를 만들 수 있는 요청 경로에서만 작동합니다. 키와 라우터 계층은 승인 미들웨어에서 거부와 원자적 예약(요청 1건 + 결정적 토큰 허용량: 상한이 있는 바이트 기반 프롬프트 추정치와, 티어request_params상한 및 고정 상한으로 잘라낸 요청의 출력 최대값)이 이루어지고, 예약은 응답 본문에 실려 마지막 스트리밍 프레임까지 유지되며 다른 모든 승인의 사용량에 합산되므로 동시의 한계 직전 요청들이 모두 통과할 수 없되, 요청 하나가 자기 추정치만으로 거부되는 일은 없습니다(거부는 상한 도달 시점에만). 하위 계층(안정 백엔드, 그다음 프로바이더/모델, 허브 자신의 우선순위)은 유효 재작성 반영 대상의 모든 후보 서빙 백엔드가 각자 상한을 넘긴 코스트 센터로 귀결될 때 승인 시점에 거부됩니다. 요청이 서빙될 수 있는 신원이 전부 소진된 상태이므로 디스패치 전 결정이 실제로 서빙되고 계측되는 신원과 어긋날 수 없기 때문입니다. 후보들이 하나의 동일한 코스트 센터로 귀결될 필요는 없습니다. 그 조건은 충분하지만 필요하지는 않았고, 소진된 코스트 센터 두 곳에 백엔드가 걸쳐 있는 모델을 지목하는 것만으로 상한을 넘겨 계속 서빙할 수 있는 구멍을 남겼습니다. 모든 계층의 계측은 사용량 이음새에서 정확히 한 번 정산됩니다. 단일 사용량 루프 소비자가 전송 버퍼에 들어가는 레코드마다 영속 단조router_usage_seq를 찍고, 최종 레코드 자체의 신원(허브가 수집 시 할당에 쓰는 것과 동일한 입력)으로 계산한 코스트 센터 아래에 계측 사용량을 원장에 기록합니다. 더 새로운 스냅샷을 수락하면 허브가 포함한 워터마크 이하의 원장 항목만 정확히 폐기하고(반영되지 않은 사용량은 절대 리셋되지 않고 반영된 사용량은 절대 이중 계산되지 않음), 새 런타임이 발급한 적 없는 시퀀스를 가리키는 허브 워터마크(자격 증명은 남고 사이드카만 잃은 경우)는 영구 거부에 갇히는 대신 시퀀스 공간의 내구성 있는 전방 점프로 복구하며, 예산 블록 없이 도착한 명세는 문서화된 예산 불가용 상태(허브 #750)로서 보유 카운터와 허브 작성 판정을 새 상한 아래에 적용하는 대신 폐기하고, 전송 버퍼에서 영구히 버려진 레코드, 분할 불가한 초과 크기 배치, 허브가 레코드 단위로 거부한 레코드는 원장 항목도 함께 은퇴하며, 시퀀스 카운터, last-known-good 단위, 적용된 워터마크, 미반영 델타는control_plane.state_file옆의 소유자 전용 사이드카(<state_file>.cost-center.json,0600, 원자적 rename)에 영속화되어 재시작 시 시퀀스 공간을 처음부터 다시 시작하지 않고 이어갑니다. 판매 지출은 허브 전권입니다. 라우터는 판매 지출을 로컬에서 계산하거나 차감하거나 추정하지 않으며sell_spend_status판정이 유일한 입력입니다(exhausted는 거부, 더 새로운 허브의 알 수 없는 판정은 절대 거부하지 않음).cost_center_limits_v2는 계약 전체가 살아 있을 때만(사이드카 연결, 신원 결속, 시퀀싱과 강제 적용 배선 완료) 광고되고 레거시cost_center_limits_v1문자열은 광고하지 않으며, 문서화된 초과분은 없다고 주장하는 대신 네 개의 명시된 항(스냅샷 주기 곱하기 플릿 크기, 예약 추정 오차, 동시 승인 요청당 허용량 하나, 응답 종료와 레코드의 버퍼 진입 사이의 프로세스 내 전달)으로 상한이 정해집니다. 영문 및 국문 컨트롤 플레인 아키텍처 문서에 반영했습니다. - 허브가 관장하는 가드레일 정책을 추가했습니다. Continuum Hub가 플릿 전체의 콘텐츠 안전 집행을 관장하면서도 실행 권한은 각 라우터가 그대로 가집니다 (#1094, #1105, #1106, #1107, #1108, #1109, #1110, #1111, #1112, #1122 종료). 허브가 관장하는 가드레일 정책을 실제로 실행하는 첫 릴리스는 v1.17.0이며, 허브 호환성 매트릭스(
lablup/continuum-hub#603)가 고정해야 할 버전이 이것입니다. 그보다 오래된 라우터는 이 필드를 무시하고guardrail_policy_v1기능을 광고하지 않으므로, 영원히 pending으로 남는 대신unsupported로 판단됩니다. 거버넌스는 기존 정책 엔벨로프에control_plane.policy.enabled아래로 실려 오며, 닫혀 있고 타입이 정해진 정수 전용 표면입니다.mode,category_thresholds(부호 없는 마이크로유닛으로1000000이 정확히1.0),stages,inspect_reasoning,block_behavior로 구성되고, 해석된 모델 id를 키로 하는 라우트 재정의는 최대 64개입니다. 라우트 범위에서 다루는 것은mode와category_thresholds뿐입니다. 라우트 범위의stages,inspect_reasoning,block_behavior는 로컬 라우트 스키마에 담을 자리가 없고, 전역 설정으로 접어 넣으면 라우트 하나에 대한 결정이 모든 라우트로 번지므로, 해당 라우트와 항목 이름을 담은 경고와 함께 적용하지 않습니다. 배포 배선은 허브에서 오지 않습니다.providers목록 자체, 프로바이더의endpoint/backend:/api_key_env, 프로바이더별 및 전역timeout_ms와on_error, 모든streaming_*항목,bypass_api_keys,audit블록, 라우트별enabled과providers부분집합, 전역 및 라우트별allow/deny매치 리스트는 전부 로컬이 주권을 가집니다. 정책 타입에는 자유 형식 페이로드 필드가 어디에도 없으므로, 프롬프트, 완성 텍스트, 매치된 구간, 마스킹된 값은 어느 방향으로도 실릴 수 없습니다. 합성은 항목별로 더 엄격한 쪽이 이깁니다. 어느 쪽에서 오든enforce가monitor를 이기고, 카테고리별로 더 낮은 임계값 하한이 이기며 로컬에 없는 카테고리는 추가되고,stages는 모든 프로바이더 행에 합집합으로 적용되고,inspect_reasoning은 논리 OR이며, 선언된block_behavior는 엄격도 순서가 아니라 거버넌스 선택으로서 이깁니다. 합성은 두 입력 중 어느 쪽이 바뀌어도 다시 계산되고 원자적으로 적용되므로, 로컬 편집이 허브 계층을 지우는 일도, 허브 갱신이 로컬 편집을 묻는 일도 없으며, 요청은 항상 하나의 완전한 정책 세대에 대해 평가됩니다. 전달은 둘이 아니라 세 상태입니다. 엔벨로프 멤버가 없으면 마지막 정책을 유지하고, 정책이 선언되면 교체하며, 명시적 해제는 라우터를 로컬 전용 설정으로 되돌리고 해제된 정책의 정규 digest로 확인 응답을 보냅니다. "없음"으로 보고하지 않기 때문에 해제된 테넌트가pending이나stale에 머무르지 않고 수렴합니다.guardrails.enabled: false로 시작했거나guardrails:블록이 아예 없는 라우터도, 집행 가능한 정책이 도착하면 시작 시점과 동일한 팩토리 경로로 로컬 프로바이더 배선만 사용해 런타임에 가드레일 서비스를 생성합니다. 이 배포가 실행할 수 없는 집행을 요구하는 정책, 즉 게이팅할 수단이 전혀 없거나 실행 가능한 프로바이더가 담당할 수 없는 스테이지를 요구하는 정책은 타입 코드guardrails_unavailable과 함께 전체가 거부되며 결코 active로 보고되지 않습니다. 덕분에 Fleet 화면이 실제로 일어나지 않는 집행을 보여주는 일이 없습니다. 검증에 실패한 본문은invalid_guardrail_policy로 거부되므로, "라우터가 정책보다 오래되었다"는 상황이 공유 코드인digest_mismatch와 섞이지 않습니다. 거부는 허브 개입 없이 해소됩니다. 와이어 계층이 정책을 적용된 상태로 유지하고 로컬 설정이 바뀔 때마다 재평가하므로, 빠진 프로바이더 행이나deny규칙을 추가하면 같은 정책이 다음 리로드에서 적용됩니다.guardrail_policy_v1은 살아 있는 가드레일 정책 재조정기가 붙은 뒤에야, 그리고 모든 상태 기록 경로가 공유하는 단일 기능 목록에서만 광고됩니다. 이 기능은 빌드 플래그가 아니라 실행에 대한 약속이기 때문입니다. 가드레일 트랙과 티어 트랙은 양방향으로 독립적입니다. 영문 및 국문 가드레일 가이드(거버넌스 범위, 로컬 주권, 실제 예시가 딸린 합성 표, 3상태 전달, 거부 코드, 허브 상태 도출), 컨트롤 플레인 아키텍처 문서, 설정 어시스턴트 스킬 문서(스키마 1.30.0에서 1.31.0)에 반영했습니다. - 요청을 실제로 처리한 백엔드의 안정적인 설정 식별자를 컨트롤 플레인 사용량 레코드와 하트비트 인벤토리에 보고합니다. 이제 백엔드를 코스트 센터로 배정할 수 있습니다 (#1153, 허브
lablup/continuum-hub#686). 새로 추가된 선택 항목backends[].backend_id는 전적으로 운영자 소유이며 무엇으로도 유도하지 않습니다.name은 운영자가 핫 리로드 때 언제든 바꿀 수 있는 표시 레이블이고(provider, model)쌍은 백엔드가 아닙니다. 여러 백엔드가 같은 쌍을 제공할 수 있기 때문입니다. 따라서 유도한 값을 쓰면 같은 쌍을 공유하는 서로 다른 두 비용 주체가 조용히 합쳐지고, 이름을 바꾸는 순간 하나의 비용 주체가 조용히 갈라지며, 둘 다 사용량이 수집된 뒤에는 눈에 띄지 않습니다. 식별자가 없는 백엔드는 아무것도 보고하지 않고 사용량은 기존과 똑같이 provider/model 단위로 귀속되는데, 이는 오류가 아니라 정상 상태입니다. 값은 구분자 안전한 ASCII 집합[A-Za-z0-9._-]에서 1~128자이며,fallback.fallback_chains가 이미 문서화한 것과 같은 문자 집합입니다. 이 값은 허브가 바이트 단위로 대조하고 정규화하지 않는 불투명한 키로 전달되므로, 공백,/,:,,, 제어 문자, 양방향 재정의를 포함한 비 ASCII는 어느 요청이 어느 코스트 센터에 청구되는지를 바꾸도록 두지 않고 원천에서 거부합니다. 고유성은 한 라우터 안에서만 요구합니다. 허브가(authenticated_router_id, backend_id)로 소속을 구분하므로 두 라우터가 같은 로컬 표기를 재사용해도 됩니다. 이 검사는Validate구현뿐 아니라infrastructure::config::validator의 공용 수용 지점에서 실행됩니다. 실제 로드 경로는 섹션을 명시적으로 검증할 뿐Config::validate()를 호출하지 않기 때문입니다. 후보 설정을 통째로 거부하는 것이 핫 리로드를 원자적으로 유지하는 방법이기도 해서, 실행 중인 라우터는 식별자 맵이 일부만 적용된 상태가 아니라 마지막 정상 설정에 그대로 머무릅니다./admin/backends핸들러 계열은propagate_config_change를 통해 핫 리로드 watch 채널로 곧장 발행하므로 이 수용 지점에 도달하지 않습니다. 그래서 자기 경계에서 같은 검사를 수행하고,POST /admin/config/validate도 같은 규칙을 확인하여 저장 전 사전 점검이 통과라고 알린 값을 뒤이은 쓰기가 거부하는 일이 없게 했습니다. 식별자는 각 서빙 지점에서, 실제 선택이 사용한 설정 뷰를 기준으로 확정해 사용량 이벤트에 실어 보내며, 레코드를 푸시할 때 백엔드 이름으로 조회하지 않습니다. 그 사이에 핫 리로드가 백엔드 이름을 바꾸면 나중의 조회는 아무것도 찾지 못하고 이미 서빙된 사용량의 귀속이 사라지기 때문입니다. 폴백 홉은 TTFT 기준 시각을 덮어쓰는 것과 똑같이 식별자도 덮어쓰므로 폴백된 요청은 실제로 응답을 만든 백엔드에 청구되고, 스트림 중간 폴백 릴레이는 요청이 라우팅된 스냅샷을 기준으로 각 홉을 확정하며, 프로바이더 배치는 제출 시점에 확정한 식별자를 들고 있어 몇 시간 뒤 완료되어도 그 배치를 실행한 백엔드를 지목하고,/v1/responses스트리밍은 디스패치 지점이 없으므로 컨텍스트 생성 시점에 확정하며, 로컬 캐시 적중은 백엔드로 나간 적이 없으므로 아무것도 보고하지 않습니다. 식별자가 없을 때 표시 이름으로 대신 채우는 일은 없습니다. 와이어에서UsageRecord.backend_id는 업스트림 필드를 그대로 옮긴 것이고,BackendInfo.backend_id는 업스트림 재동기화보다 앞서 추가한 것입니다. 허브는 자체 백엔드 인벤토리를 갖고 있지 않아서, 운영자가 배정 가능한 식별자를 확인할 수 있는 경로는 하트비트뿐이기 때문입니다. 둘 다 serde 기본값이 있는 선택 필드이므로 식별자를 하나도 설정하지 않은 라우터는 이전 와이어와 바이트 단위로 동일하게 직렬화되고PROTOCOL_VERSION은 0 그대로입니다.stable_backend_identity_v1은 조건 없이 광고합니다. 가드레일 정책과 달리 나중에 붙을 수도, 안 붙을 수도 있는 별도 실행기가 없고 설정 검증, 인벤토리, 사용량 탭이 하나의 단위로 컴파일되고 배선되기 때문입니다. 이 기능 문자열은 데이터가 아니라 구현을 서술하므로, 식별자를 하나도 설정하지 않은 라우터도 광고하고 아무것도 보고하지 않는데, 이것이 허브가 "아예 보고할 수 없는 라우터"와 구분해야 하는 상태입니다. 영문 및 국문 설정/아키텍처 가이드,config.yaml.example, 설정 어시스턴트 스킬 문서(스키마 1.32.0에서 1.33.0)에 반영했습니다. - 하트비트 인벤토리에 메타데이터 전용 가드레일 카운터를 보고합니다. 허브가 라우터마다 스크래핑하지 않고도 플릿 전체의 가드레일 활동을 볼 수 있습니다 (#1120).
RouterInventory.guardrails에는checks_total,blocks_total,blocks_by_category,(stage, mode, result)기준verdicts,(strategy, outcome)기준stream_buffer_cap_trips, 그리고SupplySummary와 동일한 프로세스 시작 값에서 찍는counters_since_ms가 실립니다. 두 카운터 계열이 함께 리셋되므로 허브는 재시작을 한 번으로 읽습니다. 카운터는 레지스트리가 아니라 Prometheus 지표를 내보내는 것과 같은 지점에 기록되는 인프로세스 트래커에서 옵니다.metrics는 선택 사항이고, 이를 빼고 빌드한 배포에서 0을 보고하면 실제로 차단 중인 가드레일이 놀고 있다고 허브에 알리는 셈이 되기 때문입니다. 모든 레이블 공간은 닫혀 있으므로 서드파티 모더레이션 프로바이더가 허브의 저장 키 공간을 무한정 늘리거나 프로바이더가 작성한 텍스트를 실어 보낼 수 없습니다. 알려진 어휘 밖의 카테고리는other로 접히고, 인식되지 않은 stage, mode, result, strategy, outcome은 버려지는 대신unknown슬롯으로 접힙니다. 이 블록은 가드레일 서비스가 존재하기만 하면 첫 검사 전이라도 보고되고 라우터가 가드레일을 전혀 돌리지 않으면 통째로 생략되므로, 블록이 없으면 "가드레일이 꺼져 있음", 값이 0이면 "켜져 있고 조용함"입니다. 담기는 것은 개수와 한정된 id뿐이며 프롬프트 텍스트, 완성 텍스트, 매치된 구간, 매치된 규칙, 마스킹된 값은 들어가지 않습니다. - 요청당 메모리 예산이 프로세스 단위 상한을 실제로 함의하도록, 동시 처리 요청 수 상한을 선택적으로 추가했습니다 (#1173). 라우터가 집행하는 메모리 상한은 전부 요청 단위입니다. 10 MiB
GLOBAL_BODY_LIMIT, 기본 512 MiB이며 메모리에 통째로 버퍼링되는files.max_file_size, #1169와 #1174가 추가한 32 MiB 파일 해석 합계 예산이 그렇습니다. 최악의 상주 메모리는 이 값들과 동시에 떠 있는 요청 수의 곱인데, 두 번째 항을 묶는 것이 아무것도 없었습니다. 속도 제한은 그 역할을 하지 못합니다. 속도 제한이 묶는 것은 단위 시간당 도착이지 상주가 아니며, Little의 법칙에 따르면 정상 상태의 동시 요청 수는 도착 속도 곱하기 평균 지속 시간입니다. 따라서 평균 30초짜리 완성에per_client: 10(초당 10건)이면 한 클라이언트에서만 약 300건이 정책 안에서 동시에 떠 있고, 파일 해석 상한까지 채우면 약 12.6 GiB입니다. 게다가 기본 설정에는 속도 제한 자체가 없으므로, 그 경우 상한은 클라이언트가 열 수 있는 만큼입니다.server.max_concurrent_requests의 기본값은 설정하지 않음이라 기존 배포가 업그레이드만으로 요청을 흘려보내기 시작하는 일은 없습니다. 값을 설정하면 라우터는 그만큼만 동시에 받아들이고 나머지는503과Retry-After, 상한을 명시한 JSON 본문으로 답합니다. 대기열에 넣지 않고 흘려보내는 이유는, 대기열이 메모리 부족 종료를 무한정 늘어나는 지연으로 바꿀 뿐 대기 중인 것들의 메모리는 그대로 붙잡고 있기 때문입니다. 구현은tower의ConcurrencyLimit/LoadShed가 아니라try_acquire_owned로 획득하는tokio::sync::Semaphore입니다. 전자는 기본이 대기열이고, 배경 작업을 가진Buffer가 필요하며, 거절 본문이나 재시도 힌트를 제어할 수 없고, iOS에 부적합한 의존성을 일부러 배제한embed기능 그래프에 새tower기능을 끌어들였을 것입니다. 이번 구현은 의존성을 전혀 추가하지 않습니다. 퍼밋은 응답 본문에 실려 다니므로 스트리밍 완성은 마지막 프레임까지 퍼밋을 쥐고 있습니다. 즉 짧은 비스트리밍 트래픽에 맞춰 잡은 상한은 스트리밍 트래픽을 굶기므로, 가장 긴 요청을 기준으로 잡아야 합니다. 이 레이어는 애플리케이션 전체에 장착되지만 CORS와 레이트 리미터보다는 안쪽입니다. 그래서 흘려보낸503을 브라우저가 읽을 수 있고, 어차피 리미터가 거절할 요청이 퍼밋을 차지하지 않습니다. 요청당 항목이 가장 큰/v1/files그룹은 별도로 중첩되어 있지만 같은 상한의 적용을 받으며,/health,/healthz, 설정된metrics.path는 면제됩니다. 활성 상태 프로브를 차단하는 것은 정상적으로 성능을 낮추고 있는 레플리카를 과부하 한복판에서 재시작시키는 길이기 때문입니다.SupplySummary::max_concurrency와shed_requests는 이제 허브에 "상한이 없다"고 약속하는 대신 실제 상한과 실제 차단 수를 보고합니다. 상한을 설정하지 않았다면0이 아니라 부재로 보고합니다.0은 라우터가 아무 요청도 받지 않는다는 뜻이 되기 때문입니다. 요청을 균일하게 세는 대신 선언된 비용만큼 청구하는 메모리 인식 승인 제어도 검토했으나 보류했습니다. 본문을 읽기 전에 요청당 비용 추정치가 필요한데, 읽기 전에 얻을 수 있는 유일한 신호인Content-Length는 청크 인코딩에서는 존재하지 않고 애초에 비용을 예측하지도 못합니다. #1168의 공격 벡터가 파일 참조 800개를 실은 57 KiB 본문이었습니다. 용량 계산, 설정 키가 딸린 요청당 항목, 속도 제한과의 관계는 영문과 한국어 배포 및 성능 가이드에 운영자용으로 문서화했으며, 기본 크기 업로드 하나만으로도 넘어서는 Kubernetes 예시의512Mi메모리 상한도 함께 올렸습니다. - 항목이 없어 아무 정보도 제공하지 못하던 모델 다섯 개의 메타데이터를 추가해,
/v1/models가 표시 이름, 가격, 능력, 한도를 제공하도록 했습니다 (#1091, #1090 해결). Claude Opus 5(claude-opus-5,claude-opus-5-latest, 네이티브 Anthropic 백엔드의 내장 지원 모델 목록에도 함께 등록), Kimi K3, Qwen 3.7-Max, Qwen 3.8-Max Preview, Solar Open 2입니다. Opus 5는claude_family_version()이 이미 id를(5, 0)으로 파싱하고 있어 능력 게이팅 코드가 필요 없었습니다. 확장 사고, 적응형 사고, 샘플링 파라미터 처리, max effort, 대화 중 시스템 프롬프트, fast 모드 적격성을 새 단위 테스트로 고정해, 과거 substring 게이트가 Sonnet 5를 조용히 망가뜨린 것(#854)과 같은 회귀를 리팩터링이 다시 만들 수 없게 했습니다. Qwen 3.8-Max Preview는 프리뷰 접근이 크레딧 기반이고 토큰당 공시 요율이 없어 0 요율을 유지하며, Kimi K3와 Solar Open 2는 벤더가 공시하지 않아knowledge_cutoff를 아예 넣지 않았습니다.solar-pro-2에는 자체 요약이 이미 함의하고 있던 도구 사용 능력과 일본어 지원을 더했습니다. - 설정할 수는 있고 문서에도 있었지만 실제로는 한 번도 돌지 않던
files.retention_days시작 시 정리를 구현했습니다 (#1234, #1229 해결). 0이 아닌 값은 이제 메타데이터 사이드카의created_at이 설정한 기간 이상 지난 저장 파일을 삭제하고,retention_days: 0은 여전히 파일을 영구 보존합니다.FileService::sweep_retained_files는 메타데이터를 먼저 지우고, 콘텐츠 객체가 없는 경우를 정상으로 취급하며, 콘텐츠 삭제가 실패하면 메타데이터를 복원하고, 정리마다 로그를 남기며file_retention_deletes_total을 보고합니다. 읽기 경합 경계, 시작 시 정리 의미론, 설정 예시는 영문·국문 가이드, 관리자 설정 스키마, 설정 어시스턴트 스킬에 문서화했습니다. BackendConfigBuilder가BackendConfig를 완전히 덮도록 채웠습니다 (#1208).backend_id,internal,role,region,endpoint_type,auth,org_id,model_configs,retry_override,health_check,anthropic_auto_cache_control,anthropic_fast_mode,external_storage세터를 추가했고,timeout과max_retries는 명시적 세터와 같은retry_override필드를 변경하며,backend_id형식은BackendConfigBuilder::build에서, 유일성은ConfigBuilder::build에서 검증합니다. rest 패턴을 쓰지 않는 필드 인벤토리 가드가 있어BackendConfig에 새 필드를 추가하면서 빌더 쪽 결정을 빠뜨리면 컴파일이 실패합니다.
변경됨¶
- Claude Opus 5에서 사고 비활성화 설정과
high를 넘는 effort의 조합을 라우터 경계에서 거부합니다 (#1103). Anthropic은 Opus 5에서output_config.effort가high이하일 때만thinking: {"type": "disabled"}를 받아들이고xhigh나max에서는 HTTP 400을 반환하는데, 그동안은 이 조합을 그대로 전달했기 때문에 클라이언트가 왕복을 다 마친 뒤에야 알 수 있었습니다. 해결 방식은 재작성이 아니라 즉시 실패입니다. effort를 깎거나 사고 설정을 떨어뜨리는 것은 둘 다 명시적인 클라이언트 지시를 조용히 고쳐 쓰는 일이고, Opus 5에서 사고를 끈 상태 자체도 문서화된 실패 양상 두 가지(도구 호출이 구조화된tool_use블록 대신 평문으로 나오는 것,<thinking>태그가 응답에 새는 것)를 갖고 있어 어느 쪽도 중립적인 대안이 아니기 때문입니다. 400 본문은 제약과 두 가지 해결책을 함께 알립니다. 게이트는 모든 인그레스에서 돕니다. 공용 OpenAI-to-Anthropic 변환, Anthropic 형태 스트리밍의 두 진입점(TCP와 유닉스 소켓), 별칭 해석과request_params적용 이후의 타입 있는/anthropic/v1/messages핸들러, 그리고 페이로드를 독립적으로 만드는 Responses 변환기입니다. - 아무도 읽지 않던
Backend::max_concurrent_requests()트레이트 훅과 모든 구현·테스트 더블을 제거했습니다 (#1224). 예전의 하드코딩된 프로바이더 상수를 허브에 보고하는 것은 집행되지 않는 허구를 게시하는 일이었습니다. 백엔드별 컨트롤 플레인 인벤토리는None으로 두되, 주석은 #1173이 추가한 실제 라우터 단위server.max_concurrent_requests상한을 가리키도록 고쳤습니다. - Files API의 업로드와 다운로드를 파일 전체를 메모리에 올리는 대신 스트리밍으로 바꾸고, 다운로드 권한 판정을 콘텐츠를 읽기 전에 내리도록 했습니다 (#1145).
FileStorageBackend의 기본 연산이store_stream과retrieve_stream이 되고 슬라이스 기반store/retrieve는 기본 구현 래퍼로 남았습니다. 그 결과 전송 1건의 최대 상주 메모리가 파일 크기의 약 1.5배에서 64KB 청크 버퍼로 줄었습니다.LocalFileStorage는 업로드를 프로세스별·업로드별 임시 파일로 스트리밍한 뒤 모든 검사를 통과한 다음에만 fsync하고 제자리로 rename합니다. 따라서 읽는 쪽이 절반만 쓰인.bin을 보는 일이 없고, 거부되거나 오류가 났거나 도중에 취소된 업로드는 부분 파일도 유출된 임시 파일도 남기지 않습니다. 콘텐츠 검증은 판정 결과를 그대로 유지한 채 점진적 방식으로 바뀌었습니다. 앞쪽 13바이트가 이미지·실행 파일 매직 바이트 검사를 모두 감당하고, 새로 추가한 할당 없는Utf8Validator가 부분 멀티바이트 시퀀스를 청크 경계 너머로 이어받으므로, 네트워크 청크 두 개로 쪼개진 문자도 단일 버퍼일 때와 똑같이 판정되고 시퀀스 도중에 끝나는 본문은 여전히 거부됩니다.GET /v1/files/{id}/content와GET /admin/files/{id}/content는 이제 파일을 열기 전에 메타데이터만으로 권한을 판정합니다. 파일 ID를 아는 비소유자가 전체 크기의 읽기와 할당을 유발해 놓고 결국403을 받던 경로가 닫혔고, 두 엔드포인트 모두 응답 본문을 버퍼링 대신 스트리밍합니다.413동작,File too large: exceeds maximum N bytes메시지 문구를 비롯해 겉으로 드러나는 Files API 동작은 그대로입니다. 이제 임시 파일이 전송 내내 존재하므로, 갑작스러운 종료(SIGKILL, OOM kill, 노드 축출, 정전)에서는 프로세스 안의 정리 코드가 돌지 못해 임시 파일이 남을 수 있고, 제품의 다른 어떤 것도 그것을 보지 못했습니다. 고아 스캐너는.bin과.meta.json만 찾고, 사용자 데이터 보존 정리는 메타데이터 사이드카의created_at을 기준으로 삼아 임시 업로드 dotfile을 보지 않기 때문입니다. 그래서 라우터를 시작하면 24시간이 지난 임시 파일을 모두 회수합니다. 오래된 임시 파일은 복구 가능성이 남은 데이터가 아니라 명백한 쓰레기이므로cleanup_orphans_on_startup에 걸지 않았고,FileService::detect_orphans가 그 개수를 보고하므로 다음 재시작 전에도 누수를 관측할 수 있습니다. 24시간 기준은 다른 프로세스가 아직 스트리밍 중인 업로드를 청소가 지우지 못하게 하는 장치입니다. 임시 파일의 mtime은 쓰기마다 갱신되므로 진행 중인 전송은 스스로 기준 밖으로 계속 밀려납니다.commit은 부모 디렉토리도 fsync하므로, 가리키는 바이트뿐 아니라 rename 자체가 내구성을 갖습니다.
함께, 업로드 라우트의 바깥쪽 본문 상한이 문서화된 1KB~5GB 범위로 클램프한 files.max_file_size에서 파생됩니다. 더 큰 값을 조용히 512MB로 깎던 하드코딩은 사라졌습니다. 범위를 벗어난 값은 실제 설정 로드 경로에서 시작 경고로 보고합니다(하드 에러가 아닌 이유는 max_file_size: 0이 지금도 받아들여지고, 치명적 검사로 만들면 잘 돌던 라우터가 뜨지 않기 때문입니다. 라우트를 지키는 것은 클램프입니다). 이와 관련해, 전송 계층 상한이 잘라낸 본문은 이제 400 invalid_request가 아니라 413 file_too_large로 응답합니다. 두 핸들러가 multipart 실패를 전부 잘못된 입력으로 분류했는데, 상한이 512MB 고정일 때는 사실상 닿지 않던 경로가 max_file_size에서 파생되면서 닿게 되었기 때문입니다. 그동안 어디에도 연결되지 않았던 file_uploads_total, file_upload_size_bytes, file_upload_duration_seconds, file_downloads_total, file_download_duration_seconds 메트릭이 등록되어 실제로 기록됩니다. 업로드 결과는 세션의 drop에서 기록하는데, 크기 초과 거부, purpose 파트 누락, 본문 도중 클라이언트 연결 끊김까지 볼 수 있는 유일한 지점이기 때문입니다. 다운로드는 시작했다는 이유가 아니라 본문이 오류 없이 끝까지 도달했을 때만 성공으로 셉니다. files.max_file_size가 cgroup v2 메모리 한도의 4분의 1을 넘으면 시작 시 경고가 뜹니다(한도가 없거나 max로 읽히거나 cgroup이 없는 플랫폼에서는 조용히 넘어갑니다).
수정됨¶
- 자체 호스팅 분류기 template 네 개 중 두 개가 인식하지 못한 가드 모델 출력을 깨끗한 허용으로 읽던 문제를 수정했습니다 (#1203, PR #1182 후속).
verdict_llama_guard와verdict_yes_no는 모델 답변의 첫 토큰만 보고 자신의 단일 양성 토큰(unsafe,yes)이 아닌 모든 값에GuardrailVerdict::Allow를 반환했습니다. 그래서 오류 문자열, 응답 거부, 채팅 템플릿 잔여물, 32토큰 상한에 잘린 생성, 예상 밖 언어로 나온 답이 안전한 것으로 서빙됐습니다. 게다가 조용했습니다. 파서가 평범한Ok(Allow)를 내놓으니ProviderOutcome::failure는None으로 남고,mode: enforce에서도 Prometheus 오류 계열과 하트비트의errors_total/fail_open_total/fail_closed_total, 감사 기록이 모두 검사를 마친 깨끗한 요청이라고 보고했습니다. 관대한 파서 쪽이 기본 경로이기도 했습니다.template생략, 알 수 없는template,granite_guardian,shieldgemma가 모두verdict_yes_no로 귀결되므로, 오타 하나로 Llama Guard 배포가unsafe를yes로 볼 수 없는 파서를 쓰게 되고 모든 검사가 허용으로 통과했습니다. 이제 두 파서 모두 양쪽 극을 명시적으로 인식해unsafe와 함께safe를,yes와 함께no를 읽고, 진짜로 인식할 수 없는 출력만 검사 오류로 처리합니다. Qwen3Guard와custom_classifier가 이미 하던 방식이며, 이로써 네 template의 규칙이 일치합니다. 업그레이드 전에 확인할 동작 변경: 가드 모델이 설정된 template으로 읽을 수 없는 출력을 내는 배포는 더 이상 조용히 통과하지 않습니다. 프로바이더의 유효on_error를 따르며, 기본값fail_open에서는 트래픽은 그대로이고guardrail_errors_total과 하트비트의errors_total/fail_open_total에 새 오류가 잡히며,fail_closed에서는 트래픽이 거부됩니다.api_format: completion에서는 분류 대상 텍스트가 지시문 없이 프롬프트 전체가 되므로, completion으로 서빙되는 가드 모델이 그 텍스트를 이어 쓰기만 하는 구성이 이 변화를 가장 먼저 겪게 되고, 예전처럼 조용히 허용하는 대신 이제 눈에 띄게 실패합니다.
동작 중인 배포가 받아들이던 범위를 좁히지 않기 위해 판정 토큰 정규화를 폴백이 아니라 명시적으로 정했습니다. 앞뒤 공백, ASCII 대소문자, 토큰을 감싼 문장부호나 마크다운 강조(**unsafe**, Unsafe:, Yes,), 판정 앞에 붙은 닫힌 <think>...</think> 추론 블록을 모두 허용합니다. 공백과 대소문자는 이전에도 허용됐고, 나머지 두 가지는 모델이 unsafe라고 답했는데도 조용한 허용으로 읽히던 경우라 이제 있는 그대로 양성 판정으로 읽습니다. 닫히기 전에 잘린 추론 블록은 판정을 담고 있지 않으므로 검사 실패가 되며, 이 template들이 쓰는 32토큰 상한에서는 그쪽이 더 흔한 결과입니다. 첫 토큰 뒤쪽은 판정 단어를 찾아 훑지 않습니다. 가드 모델은 설명문 안에서 양쪽 표현을 모두 다시 쓰기 때문에("this is not safe") 훑기 시작하면 설명이나 거부 응답을 판정으로 읽게 되고, 그것이 바로 이 변경이 없애려는 실패이기 때문입니다. 실제 분류에서 나온 허용 두 가지는 그대로입니다. 보고된 위해 코드가 categories에서 모두 제외된 unsafe 판정과, category_thresholds 항목에 억제된 양성 판정이 그렇습니다.
설정 검증 쪽의 두 번째 동작 변경. self_hosted_classifier(및 classifier) 프로바이더의 알 수 없는 options.template 값을 이제 Config::validate가 거부합니다. 예전에는 시작 시 warn!만 남기고 granite_guardian으로 폴백했는데, 바로 그 폴백이 llama-guard나 shield_gemma 같은 오타를 완전한 조용한 우회로 바꿔놓았고, 실패 방향이 항상 허용 쪽이라 운영자가 알아챌 만한 잘못된 차단은 결코 나오지 않았습니다. 따라서 알 수 없는 template 값이 들어간 설정은 이제 로드되지 않고 continuum-router config validate가 이를 보고합니다. 값을 고치거나 키를 지우면 됩니다. template 생략은 여전히 유효하며 granite_guardian을 뜻합니다. 프로바이더 쪽 폴백은 검증을 거치지 않은 생성 경로를 위한 방어선으로 남겨두었습니다. 허용되는 값과 판정을 읽는 규칙은 영문·국문 가드레일 가이드에 문서화했습니다.
- 라우터의 파일시스템 구조가 Files API와 두 개의
file_id해결 경로에서 클라이언트가 보는 오류 본문으로 새어 나가던 문제를 막았습니다 (#1191).FileError::Storage(_)가 담고 있는 문구는 라우터가 직접 만든 것으로,src/services/files/storage.rs와src/services/files/metadata.rs의 스무 곳 남짓에서 모두path.display()를 넣은format!으로 조립되는데,map_file_error가 그 문자열을 그대로 500 응답의message로 넘겼습니다. 저장된 실체 파일을 읽을 수 없게 된 파일을 내려받으면files.storage_path아래의 절대 경로, 2단계 샤딩 하위 디렉터리, 저장된.bin파일 이름, 하부std::io::Error가 그대로 응답에 실렸습니다. 업로드가 실패하면 임시 파일 이름이 실렸는데,temp_file_name은 이를.{id}.{pid}.{sequence}.tmp로 만들기 때문에 라우터 프로세스 id(컨테이너에서는1이며, 그 자체로 배포 형태를 알려줍니다)와 프로세스 시작 이후 시작된 업로드 개수까지 함께 노출됐습니다. 기본값인files.auth.method: api_key에서 이 정보의 수신자는files스코프를 가진 유효한 키의 모든 소유자이며,enforce_ownership으로 자기 파일만 볼 수 있는 테넌트도 포함됩니다. 즉 다른 테넌트의 바이트는 읽지 못하는 호출자가 오류 본문만으로 배포의 스토리지 구조를 읽어낼 수 있었습니다.method: none이면 포트에 닿을 수 있는 누구에게나 열립니다. 치환은 생성 지점이 아니라 각 응답 경계에서 새로 추가한FileError::client_message를 통해 한 번만 이뤄집니다. 경로야말로 스토리지 장애를 조사하는 운영자에게 필요한 정보이고, 로그가 그 자리이기 때문입니다.Display는 손대지 않았으므로 모든error!,warn!지점은 전체 문구를 예전 그대로 기록하며, 테스트는capture_logs로 두 방향을 모두 확인합니다. 상태 코드와type,code는 그대로 유지되므로(500/server_error/storage_error및io_error) 이 필드로 분기하는 클라이언트는 아무 변화를 겪지 않고, 사람이 읽는 메시지만 고정 문구인storage error: the file operation could not be completed로 바뀝니다. 클라이언트 요청이 원인인 변형들은 호출자가 보낸file_id, purpose, 한도를 가리키므로 문구를 그대로 둡니다. 같은 문자열을 실어 나르던 클라이언트 대면 표면은 세 개였고 셋 다 처리했습니다. 공개/v1/files*핸들러, 챗 컴플리션file_id경로(해결 경고들이FileResolutionError::PartialResolutionFailed로 합쳐져 404 본문에 실리므로, 각 경고를 만드는 지점에서 치환하고 전체 오류는 그 자리에서 로그로 남깁니다), 그리고 Anthropic/v1/messages경로(FileResolverError::ReadError가File service error: ...형태로 502에 실립니다)입니다. Responses API는 손댈 필요가 없어 그대로 뒀습니다.try_resolve_files가 한도 위반이 아닌 모든 오류를 원본 요청으로 되돌리기 때문에 클라이언트에 닿는 것은LimitExceeded와Timeout뿐입니다. 변형별 주장을 가정하지 않고 실제로 확인하는 과정에서 찾은 인접 결함 두 가지도 같은 변경에서 고쳤습니다. 스토리지 백엔드가 올리는FileError::NotFound는 파일 id가 아니라{shard}/{id}.bin을 담고 있어서, 실체 파일이 지워진 파일을 내려받으면 404를 통해 샤딩 방식이 드러났습니다. 이제FileService가 호출자가 보낸 id로 다시 이름 붙이며, 이는 더 안전하면서 메시지로서도 더 낫습니다. 그리고 관리자 Files API의build_error_response는 만드는 모든 본문에"type": "invalid_request_error"를 박아 넣었고,map_file_error가 이 함수로 보내는 500들도 예외가 아니었습니다. 그래서 관리자 API는 서버 쪽 스토리지 장애를 클라이언트 잘못으로 표시했고,error.error_type()에서 타입을 가져오는 공개 핸들러와 어긋났습니다. 이제 타입이 오류를 따라가며, 관리자 500 하나와 관리자 4xx 하나를 고정하는 테스트를 두어 두 부류가 다시 조용히 합쳐지지 못하게 했습니다. 관리자 표면은 상세 정보를 관리자 경계 뒤에 남겨두는 대신 공개 표면과 같은 기준으로 가립니다. 어느 쪽이든 운영자가 찾는 답은 로그에 있고, 두 표면에 하나의 정책을 두는 것이 서로 어긋나는 것을 막는 방법이며,type버그가 바로 그렇게 생겼기 때문입니다.src/admin_config/prompts_api/handlers.rs도 같은 조사 범위에 넣었고 더 좁은 형태의 같은 문제가 있었습니다. 상위 디렉터리가 기준 디렉터리 밖일 때 내는PathTraversal거부가 해석된 절대 경로를 실었는데, 몇 줄 아래의 형제 검사는 이미 호출자의 상대 경로를 보고하고 있었으므로 이제 그쪽에 맞췄습니다.PromptFileError::IoError와InvalidPath에도 같은 클라이언트/로그 분리를 적용했습니다. - Azure Content Safety의 한쪽 하위 검사가 실패했을 때 다른 쪽이 내린 차단 판정이 버려지던 문제를 수정했습니다 (#1194, PR #1182 후속).
AzureContentSafetyGuardrail::check_input_text는 입력 단계를 Prompt Shields(text:shieldPrompt)와 Content Safety 텍스트 분석(text:analyze)이라는 서로 독립된 두 엔드포인트로 검사하는데, #1182에서 하위 검사를 보고 가능한GuardrailCheckResult형태로 바꾸면서 둘을?로 묶었습니다.?는 단락 평가를 하므로 Prompt Shields가 실패하면 텍스트 분석은 아예 실행되지 않았고, 검사 전체가 오류가 되었으며, 기본값인on_error: fail_open에서 서비스는 그 오류를Allow로 해석했습니다. 그 결과 Azure 텍스트 분석이라면 거부했을 콘텐츠가 그대로 전달되었습니다. 반대 방향도 마찬가지여서, 텍스트 분석이 실패한 경우에는 Prompt Shields가 잡아낸 탈옥 차단이 버려졌습니다. 이제 두 하위 검사는 항상 실행되며, 느린 엔드포인트가 다른 쪽에 필요한 타임아웃 예산을 먹어치우지 못하도록 순차가 아니라 동시에 호출합니다. 결과는 살아남은 판정이 이미 거부인지로 고릅니다. 살아남은 판정이Block이면 그 판정을 반환해 그대로 적용하고, 거부가 아니면(Allow이거나 차단하지 않는Flag) 여전히 실패를 반환해 fail 정책이 처리를 결정하고 #1182에서 추가한 카운터(errors_total,fail_open_total/fail_closed_total,guardrail_errors_total{kind="error"})도 계속 움직이게 합니다. "무언가를 발견했는지"가 아니라 "이미 거부인지"로 판단하는 이유는, 이 어댑터가 의도적으로on_error를 알 수 없어서 두 정책 모두에서 옳은 값을 반환해야 하기 때문입니다.Block은 fail 정책이 만들어낼 수 있는 가장 엄격한 판정과 정확히 같은 수준이지만,Flag는 게이트에서 허용으로 매핑되므로 그대로 반환하면fail_closed가 거부하도록 설정된 요청이 전달되고 엔드포인트 장애도 집계되지 않습니다. 단순히 "성공한 쪽을 반환"하지 않는 이유는 두 불변식을 동시에 지키기 위해서입니다. 아무것도 거부하지 못한 절반짜리 검사가 깨끗한 통과로 읽히는 일도 없고, 옆 엔드포인트가 죽었다는 이유로 진짜 거부 판정이 삼켜지는 일도 없습니다. 오류와 차단이 모두 요청을 거부하므로fail_closed동작은 그대로입니다. 같은 형태의 호출 지점은 여기뿐이라는 점도 다시 확인했습니다.check_output은 텍스트 분석만 실행하고(Prompt Shields는 입력 전용), Bedrock 프로바이더는 두 단계를 모두 하나의ApplyGuardrail호출로 처리합니다. - 백엔드 인벤토리 신원을 허브의
stable_id와이어 키로 보고해, 백엔드 코스트 센터 계층 전체를 무력화하던 #1172의 조용한 v0 와이어 결함을 수리했습니다 (#1162 리뷰, PR #1190). #1172는BackendInfo.backend_id로 출시했지만 허브가 독립적으로 작성한 대응 필드(허브 #732)는stable_id를 읽습니다. 양쪽 모두 serde 기본값을 쓰고 rename도deny_unknown_fields도 없어서, 허브는 라우터의 키를 없는 것으로 파싱했고 어떤 백엔드도 배정 가능한 코스트 센터 인벤토리가 되지 못했으며backend_assignments는 사실상 항상 비어 있었습니다. 추론이 아니라 고정된 rev의 허브 클론에 대해 검증했습니다. 벤더링된 필드를 허브의 키로 개명하고 업스트림 문서 주석을 채택했으며, 운영자 대상backends[].backend_id설정 키와 별개의(올바른)UsageRecord.backend_id필드는 그대로입니다. 허브가 커밋한heartbeat_stable_backend_identity.json픽스처를 바이트 단위로 복사해 양방향으로 고정했고, 신원이 다시는 로컬 설정 키로 직렬화되지 않도록 가드를 추가했습니다. 같은 재동기화에서 허브의policy_envelope.json을 엔벨로프 수준 값 고정으로 벤더링했는데, 이것이 두 번째 파싱 충실도 결함을 즉시 잡아냈습니다.KeyEntry.project_id(허브 #685)가 재직렬화 시 조용히 사라지던 것을 이제 그대로 실어 나릅니다. 강제 적용은 #1152 전까지 손대지 않습니다. - 부하가 걸린 상태에서 죽은 백엔드가 요청마다 다시 시도되는 상황과, 지정하지 않은 상태 코드가 서킷 브레이커의 성공으로 기록되던 문제를 함께 고쳤습니다 (#1148). 트래픽 도중 주 백엔드를 죽여도 스트리밍 폴백이 모든 요청을 구제하기 때문에 가용성은 유지되지만, 첫 토큰 p95는 장애가 끝날 때까지 저하된 채로 머물렀습니다. 죽은 백엔드를 요청마다 다시 시도했기 때문입니다. 이 준안정 상태를 만든 원인은 서로 다른 세 가지이며, 제보자가 제시한 메커니즘(폴백이 요청을 성공시키므로 실패가 누적되지 않는다)은 그중에 없습니다. 폴백은 이미 모든 단계에서 체인을 넘기기 전에 주 백엔드의 전송 실패를 기록하고 있었습니다. 첫째,
CircuitBreaker::record_failure가failure_status_codes에 없는 상태 코드를record_success로 바꿔 처리했고, Closed 상태에서 이는failure_count를 0으로 되돌리고 슬라이딩 윈도에 성공을 밀어 넣었습니다. 이슈 제목이 지목한 실제 결함이 이것입니다. 429나 404를 진짜 5xx와 섞어 반환하는 백엔드는 임계치에 영영 도달하지 못했고, 429만 반환하는 백엔드는 영원히 건강해 보였습니다. 이제 이런 상태 코드는 half-open 프로브 슬롯만 반납하고 나머지는 아무것도 건드리지 않는 중립 결과로 기록되며, 새OutcomeRecord반환값을 통해 Prometheus 미러도 더 이상 이를 성공으로 세지 않습니다.429는 기본failure_status_codes에서 계속 제외합니다. 일시적 rate limit은 백엔드가 살아 있고 스로틀링 중이라는 뜻이고, 서킷을 열면 스로틀링이 장애로 바뀌며, (#740)/(#742)에서 만든 재시도 경로가 이미 업스트림Retry-After를 존중하고 비일시적 할당량 소진에는 빠르게 실패하기 때문입니다. 둘째, 서킷 브레이커가 기본적으로 꺼져 있어서 모든 기록 지점이 무동작이었고, 제외는 전적으로 헬스 체크에 맡겨졌습니다. 기본값 기준 90초(interval30초 xunhealthy_threshold3)인 그 창은 제보된 저하 구간을 통째로 포함합니다. 브레이커는 계속 옵트인으로 둡니다. 기본으로 켜면 기존 배포 전체의 요청 라우팅이 바뀌기 때문입니다. 대신fallback.fallback_chains가 설정되어 있는데 브레이커가 없으면 시작 시 경고를 남기며, 실패 양상을 설명하고 설정된health_checks값에서 노출 창을 계산해 알려줍니다. 폴백을 설정한 배포 템플릿 세 개에는circuit_breaker섹션을 추가했습니다. 셋째, 브레이커를 전혀 거치지 않던 디스패치 경로가 많았습니다. 이미지 생성과 이미지 편집, Responses 전용 패스스루 브리지, 네이티브 Gemini 멀티모달 임베딩, Anthropic Messages 인그레스 전체(네이티브, OpenAI 호환, Responses 기반, Bedrock Runtime, Unix 소켓, 웹 검색 에뮬레이션), Responses 인그레스(변환 전략 네 가지와 패스스루·compact), Anthropiccount_tokens입니다. 이제 모두 새proxy::circuit::Admission가드를 통해 승인하고 결과를 기록합니다. 이 가드는 승인과 결과 기록을 정확히 하나씩 짝지으며 drop 시 half-open 프로브 슬롯을 반납하므로, 출구가 여럿인 OAuth 재시도 경로가 프로브 용량을 흘리지 못합니다. 중요한 점은 승인을 단독으로 적용하지 않는다는 것입니다.proxy::selection::select_admissible_backend가filter_admissible로 후보를 거르고 선택한 뒤 승인하며, 경합에서 밀리면 다시 선택하고, 백엔드와 가드를 함께 반환합니다. 서킷을 모르는 선택 단계 위에 디스패치 시점 게이트만 얹었다면 건강한 동료가 처리할 수 있는 요청이 라우터의 503으로 바뀌었을 것입니다.HalfOpen은 중립 결과가 여전히 서킷을 닫는 쪽으로 진행시키는 유일한 상태인데, 이 상태가 던지는 질문은 백엔드가 건강한지가 아니라 트래픽 차단을 그만둘지이기 때문입니다. 이 규칙이 없으면 429만 반환하는 백엔드가 종료 시한도 없이 프로브 한도에 묶입니다.services::health_service에 있던, 연결되지 않은 두 번째 서킷 상태 기계는 데이터 경로가 아니라는 점을 문서화해 진짜 브레이커로 오인하지 않게 했습니다. - 하트비트의
guardrails블록을 허브 모양으로 맞췄습니다. 가드레일이 돌고 있는 라우터가 인벤토리를 아예 전달하지 못하던 문제를 고칩니다 (#1150). 라우터의GuardrailSummary와 허브의 것이 각각 따로 만들어져 서로 맞지 않았고,RouterInventory.guardrails가 관대한 blob이 아니라 중첩 구조체라서 serde가 구조체 전체를 실패시켰습니다. 그 결과 가드레일이 활성화되고control_plane.enabled가 켜진 라우터는 백엔드도, 헬스도, 부하도, 공급도,policy_status도, 아무것도 보내지 못했습니다. 추정이 아니라 허브58bb3d0에 대고 확인한 내용입니다. 이제GuardrailSummary는 허브가 필수로 선언한 스칼라 여섯 개(transforms_total,flags_total,errors_total,fail_open_total,fail_closed_total,stream_buffer_cap_trips_total)를 싣고, 카테고리 내역은 이 라우터가 보내던blocks_by_category배열이 아니라 카테고리 id를 키로 하는 허브의by_category객체입니다. 유일하게 진짜로 깨지는 와이어 변경이지만, 예전 모양은 어떤 허브도 파싱할 수 없었기에 안전합니다.transforms_total,flags_total,stream_buffer_cap_trips_total은 따로 세지 않고GuardrailSnapshot::into_summary에서 대응 내역으로부터 유도하므로, 지금 허브가 읽는 스칼라와 나중 허브가 읽을 내역이 어긋날 수 없습니다. 테스트가 각 합계를 대응하는 합과 대조합니다.errors_total,fail_open_total,fail_closed_total은 컨트롤 플레인 트래커에 출처가 아예 없던 값으로, 이제guardrail_errors_total을 기록하는 것과 같은 지점(src/services/guardrail/service.rs)에서GuardrailTracker::record_error가 기록합니다. Prometheus 레지스트리에서 되읽지 않는 것은 의도적입니다.metrics는 선택 사항이고, 이를 빼고 빌드한 배포에서 0을 보고하면 트래픽을 조용히 통과시키고 있는 가드레일이 놀고 있다고 허브에 알리는 셈이 되기 때문입니다. 두 fail 정책 카운터는 설정된on_error가 아니라 실제로 적용된 결과로 나뉩니다. monitor 모드는 아무것도 게이팅하지 않으므로 monitor에서 실패한 검사는 정책과 무관하게 검사 없이 나간 트래픽이며 fail-open으로 셉니다. 이를 fail-closed로 보고하면 강제 적용의 구멍을 드러내는 유일한 카운터가 0이 되어 버립니다.errors_total은 총계가 아니라 하한입니다. 그 지점에 닿는 것은 프로바이더 타임아웃뿐이고, 빠르게 실패하는 프로바이더는 전송·상태 코드·파싱 실패를 자기 안에서 판정으로 바꿔버리기 때문입니다. Prometheus 쪽도 같은 공백을 갖고 있으며, 메우려면ProviderOutcome에 명시적인 실패 표식이 필요합니다. 라우터 전용verdicts와stream_buffer_cap_trips내역은 부가 선택 필드로 남습니다. 지금 허브는deny_unknown_fields를 쓰지 않으므로 그냥 무시합니다.by_category에 키가 들어가는 유일한 경로가GuardrailCategoryId리터럴이므로 카테고리 id는 개수·바이트 길이·문자 집합 모두 구조적으로 한정되며,is_bounded()가 허브의 수집 규칙에 맞춰 셋을 모두 검사합니다. 허브가 커밋한guardrail_summary.json을 바이트 단위로 이 저장소에 고정했고, 가드레일 정책 픽스처와 같은 "허브가 바뀌면 다시 복사할 것, 로컬 타입에서 재생성하지 말 것" 규칙을 함께 적었습니다. 전에는 두 저장소 모두 자기 타입만 왕복 검사했고, 양쪽 다 초록불인 채로 모양이 어긋난 것이 바로 그 때문입니다. guardrails설정 섹션을 재시작 필요가 아니라 즉시 적용 핫 리로드로 보고합니다 (#1127).ConfigSection::Guardrails가HotReloadCapability::RequiresRestart로 선언되어 있었고 관리자 API가 이를 그대로 운영자에게 노출했지만,GuardrailService::update_config가 예전부터 라이브로 적용해 온 mode 전환, 임계값 편집, 라우트 변경, 프로바이더 행에 대해 이 선언은 틀린 값이었습니다.enabled토글에도 더 이상 필드 단위 예외가 필요하지 않습니다. 가드레일 서비스 핸들이 나중에 채울 수 있는 형태가 된 이후로, 비활성 상태로 시작한 라우터는 리로드나 허브 정책이 가드레일을 켜는 첫 순간에 서비스를 생성하고, 런타임 비활성화는 서비스를 파괴하는 대신 라이브 서비스에enabled: false로 표현되므로 양방향 모두 라이브입니다. 이 섹션에는 재시작이 필요한 부분이 없으며, 이제 그에 맞게 분류됩니다./anthropic/v1/messages경계에서, 응답 범위에서만 고유한 백엔드의 도구 호출 id를 되돌릴 수 있는 형태로 요청 범위마다 고유하게 다시 씁니다 (#1143, #1142 해결). Anthropic 프로토콜에서tool_use.id는 대화 전체 범위인데, vLLM은 Kimi-K3에 대해{tool_name}:{index}형태로 id를 만들고 이 인덱스는 매 응답마다 다시 시작합니다. 그래서 같은 도구를 두 턴에 걸쳐 부르면read_file:0을 두 번 받게 됩니다. Claude Code의ensureToolResultPairing은 이 재사용을 손상된 것으로 보고 '도구 사용이 중단됨'이라는 합성 결과로 대체하며 턴을 끝내버렸고, 라우터 쪽에서는 매 요청이 200으로만 보여 아무 문제도 드러나지 않았습니다. 이제 이미 대화 범위에서 고유하다고 알려진 형태(toolu_,call_,fc_,chatcmpl-tool-접두사이면서 id 전체가 문자 집합상 안전한 경우)가 아닌 아웃바운드 id는crt_<nonce:12><base64url_nopad(raw_id)>로 다시 씁니다. 실제로 충돌을 끊는 부분은 12자리 UUID v4 nonce입니다. base64url만으로는 결정적이라 매 턴 같은 id를 클라이언트에 그대로 돌려주기 때문입니다. 원본 id는 들어올 때 바이트 단위 그대로 복원하며 문자만 정리하는 식으로 손대지 않습니다. 어떤 백엔드는 자기가 만든 id 템플릿을 살짝 바꿔서 되돌려 받으면 응답을 아예 멈춘다는 보고가 있어서, 이 정확성이 그대로 동작을 좌우하기 때문입니다. 디코딩은 요청을 절대 실패시키지 않으며, 인코딩된 형태와 맞지 않으면 무엇이든 그대로 통과시킵니다. 새src/http/handlers/anthropic/tool_id.rs모듈이 생겼습니다. 네이티브 Anthropic 백엔드는 패스스루 SSE로 스트리밍되어 이 경로에 닿지 않고, Gemini는 이미 대화 범위에서 고유한toolu_id를 자체적으로 만들어 씁니다.format: "toml"로 요청한POST /admin/config/export가 무조건 실패하던 문제를 고쳤습니다 (#1159, #1138 해결). 대부분의 선택적Config섹션은skip_serializing_if = "Option::is_none"없이#[serde(default)]만 붙어 있어서 값을 설정하지 않은 섹션도 명시적null로 직렬화됐는데toml크레이트는 null 값을 표현할 방법이 없습니다. 기본 설정을 내보내면 이런 null이 19개나 나왔습니다. 이제 남아 있던 최상위 선택 필드 21개가 값을 설정하지 않으면 빠지도록 바뀌었고 기본 내보내기에서 여전히 null을 흘리던 중첩 필드들(ServerConfig.workers,ServerConfig.connection_pool_size,ResponseCacheConfig.{redis,l1,l2,tiered},DisaggregatedServingConfig.default_external_storage,S3CacheLayerConfig.ttl_override,RequestTimeoutConfig.image_generation)도 같이 고쳤습니다. 그래서GET /admin/config/full,POST /admin/config/export, 설정 기록 스냅숏은 이제 설정하지 않은 선택 섹션을null로 담는 대신 아예 빼고 돌려줍니다.get_section_value는 이렇게 빠진 값을 다시Value::Null로 되돌려주므로GET /admin/config/{section}으로 존재하지만 설정하지 않은 섹션을 조회해도 여전히404가 아니라200과 null 바디를 받고PATCH도 이 null을 기준으로 그대로 병합됩니다. WebUI의 구조 비교와 기록 비교 도우미(config.js,history.js)도 이제 키가 없는 상태와 명시적null을 같은 상태로 취급하므로 예전 형태로 내보낸 파일을 새 응답 형태에 붙여 넣어도 사라진 null 키가 가짜 추가나 삭제로 표시되지 않습니다.- 로그 문자열을 고정 바이트 오프셋 대신 UTF-8 문자 경계에서 잘라내도록 고쳐 비ASCII 값 하나가 라우터 프로세스 전체를 중단시키는 일이 없도록 했습니다 (#1158, #1157 해결).
proxy,core::files,http::middleware,http::handlers::anthropic,infrastructure::backends::anthropic,services::smart_routing에 걸친 12곳의 로그 절단 지점에서 바이트 인덱스&str슬라이스 18개가 고정 오프셋이 멀티바이트 문자 중간에 떨어질 때마다 패닉을 일으켰습니다. 릴리스 프로파일은panic = "abort"(Cargo.toml:311)로 설정되어 있어 이 패닉은 요청 하나를 실패시키는 데 그치지 않고 프로세스 자체를 끝냈습니다. 인증된 클라이언트가 특별한 배포 없이도 도달할 수 있는 지점이 둘 있습니다.proxy::utils::sanitize_file_id_for_log는FileId::from_string이 이미 거부한 원본 파일 id를 잘라내고core::files::transformer에 있던 바이트까지 동일한 중복 함수는 검증을 통과한 id를 잘라냈습니다.FileId::from_string의 유니코드 인식 알파뉴메릭 검사가 비ASCII 값도 통과시키기 때문입니다. 두 곳 모두file-일이삼사오육같은 id에서 패닉이 났습니다.infrastructure::backends::anthropic::transform_openai_file_to_anthropic(손상된 멀티바이트file_data미리보기)와http::middleware::admin_audit::mask_username(멀티바이트Basic인증 사용자명)도 같은 방식으로 도달할 수 있습니다. 새src/core/text_utils.rs에truncate_on_char_boundary와truncate_tail_on_char_boundary를 추가했습니다. 무작정 자르는 대신 가장 가까운 유효 경계로 물러나거나 다가갑니다. 영향받는 모든 호출부가 이제 이 함수를 거치며core::files::transformer에 있던 중복 함수는 공용 함수 쪽으로 정리해 지웠습니다. 눈으로 확인하는 대신 네 가지 알파벳으로 0부터 700바이트까지 모든 ASCII 길이에서 기존 출력과 새 출력을 전수 비교했고 기존 로그 형식과 doctest 출력은 바이트 단위까지 그대로였습니다. - 클라이언트가 보낸 값이 Anthropic 핸들러의 로그 필드에 닿기 전에 이스케이프하고 길이를 제한해, 인증된 호출자가 개행 문자를 심어 로그 기록을 위조하던 통로를 막았습니다 (#1161, #1151 해결).
field = %value는 값을DisplayValue로 감싸는데tracing_subscriber의 기본 포매터는message가 아닌 필드를 아무 감싸기 없이{:?}로 그대로 기록하므로Display출력이 로그 줄에 날것으로 남았습니다. 포맷 문자열에 값을 끼워 넣는 방식도 나을 게 없었는데message에 적용되는EscapeGuard는 ANSI와 C1 제어 문자만 다시 쓰고\n과\r은 그대로 통과시켰기 때문입니다.src/http/handlers/anthropic/에 걸친 약 40곳의 클라이언트 영향 지점(도구 이름, 도구 호출 id와 파일 id, effort 문자열, 요청 모델,anthropic-version헤더,AnthropicToolChoice::Simple같은 열거형 값)은 이제%기호 없이 이름 붙은 필드로 값을 기록해Visit::record_str을 거쳐Debug for str로 포맷됩니다. 이 경로는 문자열을 따옴표로 감싸고\n,\r,\t,",\와 출력 불가능한 문자를 이스케이프해 위조 시도를 한 줄짜리 로그 기록 하나에 가둡니다. 이스케이프만으로는 위조를 막을 뿐 값이 부풀어 오르는 것까지는 막지 못하므로, 이 값들은 새로 추가한core::text_utils::cap_client_value_for_log도 함께 거칩니다. 이 함수는 바이트 인덱스 슬라이스 대신 기존의 경계 안전 함수truncate_on_char_boundary로CLIENT_LOG_VALUE_MAX_BYTES(256바이트)까지만 값을 잘라냅니다.backend.name,url,status, 타입이 정해진 열거형, 숫자처럼 라우터가 스스로 정하는 값과, 고정된 허용 목록으로 이미 제한된mime_type필드 두 곳은%기호를 그대로 남겨두었습니다. 클라이언트가 절대 고를 수 없는 바이트까지 이스케이프하면 실제 회귀를 가릴 뿐이기 때문입니다. - 마스킹된 설정을 내보냈다가 그대로 다시 가져오면 자격 증명이 자기 마스크 값으로 덮어써지면서도 성공으로 보고되던 문제를 고쳤습니다 (#1160, #1139 해결). 기본값인
include_sensitive: false로 설정을 내보낸 뒤 그대로 다시 가져오는, WebUI의 Configuration 페이지가 보여주는 바로 그 흐름에서 마스킹된 자격 증명마다 자리표시자 값이 그대로 기록되고도success: true가 돌아왔습니다. 문제는 한참 뒤 모든 백엔드가 401을 반환하면서 드러났고 복구하려면 시크릿을 하나하나 다시 입력해야 했습니다. 예전 자리표시자 형태(긴 값에는sk...(21 chars), 4자 이하 값에는 그대로***MASKED***, 환경 변수 참조에는${***VAR***})는 비밀 값 자체에서 만들어졌기 때문에 마스킹된 값과 같은 모양의 진짜 자격 증명을 구분할 수 없었습니다. 이제 Admin API가 반환하는 모든 마스킹 값은 고정된 센티널***CONTINUUM-MASKED:<힌트>***로 감싸이며(4자 이하 값은 예전처럼***MASKED***만 나오는 대신(N chars)를 표시합니다), 이 센티널만 '지금 적용된 값을 그대로 둔다'는 뜻으로 읽습니다.POST /admin/config/import,config후보를 담은POST /admin/config/apply,PUT/PATCH /admin/config/{section}, 저장 전 예행 검증인POST /admin/config/validate가 모두 같은 방식으로 자리표시자를 복원하므로 예행 검증이 약속한 결과가 실제 저장 결과와 같습니다. 실제로 값을 기록하는 쪽은 설정 변경 락을 잡은 뒤 값을 채워 넣으므로 동시에 들어온 다른 변경이 복원 대상 시크릿을 옮겨 놓을 수 없습니다. 가져오기 응답은 새 필드preserved_secret_paths에 그대로 둔 경로를 모두 담고, 예행 검증을 포함한 모든 기록 경로는 같은 목록을MASKED_SECRET_PRESERVED경고로 알려줍니다. 배열 항목은 위치가 아니라id나name으로 대응하므로backends순서를 바꿔도 제대로 복원되지만,guardrails.bypass_api_keys나rate_limiting.bypass_keys처럼id도name도 없는 목록은 위치가 유일한 단서라서 항목을 추가하거나 빼면 추측 대신 거부합니다. 같은 공급자가 발급한 키는 접두사와 길이가 같을 수 있어서 한 칸 밀린 위치가 엉뚱한 키로 대응해 버리기 때문입니다. 대응할 값을 찾지 못한 자리표시자는 옆에 있던 비밀 값을 빌려 쓰는 대신MASKED_SECRET_UNRESOLVED와 함께 실패한 경로를 알리며 기록 전체를 거부합니다. 센티널이 생기기 전 세 가지 자리표시자 형태도 여전히 인식하지만 오직 거부하기 위해서입니다.LEGACY_MASK_PLACEHOLDER가 뜨면 이 수정 이전 라우터가 내보낸 문서라는 뜻이므로 조용히 믿지 않고 거부하며, 벗어나려면 업그레이드한 라우터에서 다시 내보내거나 지적된 경로에 실제 비밀 값을 직접 입력하면 됩니다. 그대로 둔 경로 목록과 거부된 경로 목록은 각각 500개로 제한되고 초과분은MASKED_SECRET_REPORT_TRUNCATED표시로 요약되므로 자리표시자로 가득한 문서가 응답을 부풀리지 못합니다. 백엔드PUT/PATCH와 WebUI의 Configuration·History 페이지도 새 센티널을 인식합니다. 영문·국문 Admin API 문서와 WebUI 문서에 반영했습니다. - API 키별·클라이언트(IP)별 레이트 리밋 차원이 실제 트래픽에서 집행되도록 고쳤습니다. 그동안 두 차원은 프로덕션에서 한 번도 동작한 적이 없습니다 (#1164, #1147 해결). 배선 결함 세 개가 겹쳐 있었습니다. 리미터가 요청 확장에서 맨
SocketAddr를 읽었지만 axum이 넣는 타입은ConnectInfo<SocketAddr>라서client_ip가 항상None이었고, 그 결과 클라이언트별 차원과 IP 화이트리스트, 신뢰 프록시X-Forwarded-For경로가 모두 죽어 있었습니다. 리미터가AuthContext를 채우는 API 키 인증보다 바깥, 즉 애플리케이션 전체에 장착되어 있어서 키별 차원이 붙을 식별 정보가 없었습니다. 그리고rate_limiting.limits.per_api_key는 아예 읽히지 않았습니다. 결국global차원만 동작했으므로 한 테넌트가 폭주해도 차단되지 않았고, 정상 테넌트의 첫 토큰 지연이 약 22ms에서 5600ms로 무너졌습니다. 이제 리미터는 하나의 저장소를 공유하는 두 장착 지점에 차원별로 나뉘어 있습니다. 전역 차원은 인증과 CORS보다 바깥에 있는 애플리케이션 전체 장착 지점 하나가 집행하므로, 인증이 나중에 401로 거절하는 API 요청(키 추측이 계량됩니다), 실패한 admin 인증, CORS 프리플라이트, 404 폴백까지 모든 요청이limits.global에서 정확히 한 번 차감됩니다. 클라이언트별 차원은 API 라우트에 인증보다 바깥으로, 나머지 식별 차원(키별, 모델별)은AuthContext가 존재하는 인증보다 안쪽으로 장착됩니다(장착 위치는 이후 #1170에서 다시 정리했습니다). 두 장착 지점이 겹치지 않는 차원을 집행하므로 어떤 버킷에서도 요청 하나가 두 번 차감되지 않습니다. 거절 시에는 거절한 차원의 값이 기준이며, 전역 차원과 식별 차원이 동시에 거절할 상황이면 먼저 검사되는 전역 거절이 클라이언트에 전달됩니다. 기존 설정에 영향을 주는 동작 변경이 두 가지 있습니다.rate_limiting.limits.per_api_key는 이제 자체api_keys[].rate_limit이 없는 모든 인식된 키에 적용되는 기본값으로 집행됩니다. 따라서 이미 이 블록을 설정해 둔 환경에서는 지금까지 제한이 없던 키들이 차단되기 시작합니다. 명시적rate_limit은 여전히 이 기본값을 대체하고,rate_limit: 0은 해당 키를 제외합니다. 그리고429본문이 이제 하나의 올바른 JSON 문서입니다(error.message,error.type,error.code,error.details.limit_type). 이전에는 JSON 오류 문서가 이스케이프 없이 다른 엔벨로프 안에 다시 박혀 있어 파싱이 불가능했습니다. 거절 응답에는retry-after와x-ratelimit-*도 함께 실리며,Retry-After는 #1146에 따라 최소 1초로 고정됩니다. 식별 정보가 없어 건너뛴 차원(유닉스 소켓 리스너의 클라이언트별 차원, permissive 인증 모드의 익명 호출자에 대한 키별 차원)은 새rate_limit_skipped_total{dimension}카운터와 디버그 로그에 기록됩니다. - 레이트 리미터가 응답 헤더로 호출자 식별 정보와 공유 상태를 흘리지 않게 하고, 미인증 폭주를 그것을 위해 존재하는 차원이 실제로 묶게 하며, 버킷 맵이 가득 찼을 때 트래픽을 거절하지 않게 고쳤습니다 (#1170). 집행 자체는 그대로입니다. 설정된 모든 차원이 이전과 똑같이 차감하고 똑같이 거절하며, 거절 귀속과
Retry-After,429본문도 건드리지 않았습니다. 달라진 것은 라우터가 무엇을 보고하는지, 그리고 두 장착 지점이 어디에 있는지입니다. 이제x-ratelimit-*는 호출자 본인의 할당량만 설명합니다. 이전 계약은 남은 토큰이 가장 적은 차원을 보고했는데, 그러면 그 값이 호출자가 누구인지에 따라 달라집니다.limits.per_api_key버스트 7과limits.global버스트 100을 설정한 상태에서 유효한 키는7/6을, 인식되지 않은 키는100/99를 읽었고, 이것이 permissive 인증 모드에서 라우터가 내보내는 유일한 키 인식 신호였습니다. 같은 헤더는 미인증 호출자에게 공유 전역 버킷을 탐침 하나당 토큰 하나 비용으로 실시간 조회하는 수단도 함께 넘겼습니다.401, 폴백404,/version같은 미인증200이 모두 해당되며, 그 결과 전역 고갈 공격이 감으로 하는 폭주에서 시점을 정확히 맞춘 공격으로 바뀝니다. 또한bypass_keys보유자는 면제 센티널 때문에 헤더를 아예 받지 않고 다른 토큰은 받았는데 두 응답 모두401이었으므로, 리미터 혼자서 우회 목록 등재 여부를 확인해 주고 있었습니다. 이제 규칙은 인증된 식별 정보에 묶인 할당량만 보고한다는 것이고, 현재로서는per_api_key입니다. 호출자는 그 버킷의 키가 되는 API 키를 가지고 있음을 이미 증명했으므로, 그 값은 본인이 이미 가진 정보 외에는 알려 주지 않습니다. 나머지는 허용된 응답에서 모두 감춥니다.global,per_model,per_backend는 플릿 전체가,per_client는 같은 출발 주소 뒤의 모두가 함께 소비하므로 어느 공유 잔량도 호출자의 할당량이 아닙니다. 게다가per_client는 인증보다 먼저 평가되어 라우터가 누구에게 보고하는지조차 알 수 없는 시점이고, 이를 보고했다면 발견 3의 오라클이 그대로 되살아납니다. 겉보기에 똑같은401에서bypass_keys보유자는 면제 센티널 때문에 헤더를 받지 못하고 다른 토큰은 실제 값을 받게 되기 때문입니다. 그 차원이 평가하지 않은 응답에는x-ratelimit-*가 예외 없이 실리지 않으므로,401,404, 미인증200, 화이트리스트, 우회 키 경로에서 구분 신호가 한꺼번에 사라집니다. 거절 응답은 공유 차원이라도 거절한 차원의 값을 그대로 보고합니다. 클라이언트에게 재시도 힌트가 필요하고, "버킷이 비었다"는 사실은429자체가 이미 말해 주기 때문입니다. 업그레이드 전에 확인할 결과가 두 가지 있습니다. 이제 허용된 응답에 할당량 헤더가 실리는 것은per_api_key가 요청을 평가한 경우뿐이므로, 클라이언트가 이 헤더를 읽는다면 그 차원을 설정하십시오. 그리고 permissive 모드의 잔여 항목은 고치지 않고 받아들였습니다. 인식된 키의 응답에는 키별 헤더가 실리고 인식되지 않은 토큰의 응답에는 실리지 않으므로, 헤더의 존재 여부로 둘이 여전히 구분됩니다. 인식되지 않은 호출자에게 헤더를 숨기면 신호가 부재 쪽으로 옮겨 갈 뿐이므로, permissive 모드가 인증 경계가 아니라 개발 및 신뢰 네트워크용 편의 장치임을 분명히 밝히기로 했습니다. 이 모드는 이미 유효한 키와 무효한 키를 똑같이 처리합니다. 키 유효성이 민감한 환경이라면api_keys.mode: blocking을 설정하십시오.per_client가 인증 안쪽에서 바깥으로 옮겨 갔습니다 (API 라우트 기준). 이 차원은 피어 주소를 키로 쓰고 그 주소는 인증 실행 전에 연결이 제공하므로,AuthContext가 실제로 필요한 차원들과 함께 묶어 두면 정작 이 차원이 존재하는 이유인 패턴을 막지 못합니다. blocking 모드에서 주소 하나가 보낸 잘못된 키 요청 열두 개는 모두401을 받았고 하나도 차단되지 않았으며, 그 폭주는 공유 전역 버킷만으로 계량되면서 버킷이 마르는 동안 다른 테넌트를 전부 함께 차단했습니다. 같은 라우트 그룹에 인증보다 바깥으로 장착하면 그 폭주를 자기 버킷에서 묶으면서도 WebUI, admin, 정적 트래픽으로 적용 범위가 넓어지지 않습니다. 이제 API 라우트의 요청 경로는전역 -> 클라이언트별 -> 인증 -> 키별 -> 핸들러순이며, 세 장착 지점은 여전히 서로 겹치지 않는 차원을 집행하므로 이중 차감은 없습니다. 메트릭 면제가 고정 경로가 아니라 설정에서 유도됩니다. 문자열/metrics를 그대로 비교하는 방식은 양쪽 다 반대였습니다. 메트릭을 껐을 때 존재하지도 않는 라우트를 면제해 계량되지 않는404배출구를 남겼고, 운영자가 사용자 지정metrics.path로 옮겨 둔 스크레이프는 제한해서 정작 장애 시점에 필요한 모니터링을 차단했습니다. 이제 면제는 실제metrics.path를 따르고metrics.enabled가 참인 동안에만 적용됩니다. 스크레이프를 차단 대상에서 빼는 것은 의도한 선택입니다. 부하 상황에서 모니터링을 굶기는 리미터가 계량되지 않는 스크레이프 엔드포인트보다 나쁘기 때문이며, 그래서 남는 위험은 받아들이고 명시합니다. 스크레이프 비용은 메트릭 카디널리티에 비례해 커지고 리미터는 그 폭주를 막아 주지 않으므로metrics.auth와 네트워크 정책이 중요합니다./health와/healthz는 조건 없이 면제로 남습니다. 포화된 라우터도 자기 활성 상태 검사에 계속 답해야 과부하 도중 재시작당하지 않기 때문입니다. 버킷 맵이 가득 찼을 때 이제 닫히는 쪽이 아니라 열리는 쪽으로 실패합니다.per_client,per_api_key,per_backend는 각각 600초 TTL과 함께 항목 100,000개에서 상한에 걸리는데, 상한에 도달하면Capacity거절을 돌려주고 있었습니다. 클라이언트와 백엔드 검사에는 이미 존재하는 항목인지 확인하는 조건이 없어서, 맵이 가득 차면 이미 자기 버킷을 가진 호출자까지 거절했습니다. 거절해도 메모리를 아끼지 못합니다. 거절 경로 역시 버킷을 만들지 않기 때문입니다. 반면 서로 다른 출발 주소 100,000개는 IPv6/64하나로도 쉽게 만들 수 있으므로, 닫히는 쪽으로 실패하면 공격자가 한정된 맵을 그 차원의 모든 호출자에 대한 10분짜리 장애로 바꿀 수 있었습니다. 이제 가득 찬 맵은 해당 요청에서 그 차원만 건너뛰고 나머지 차원은 그대로 집행하며, 건너뛴 사실을rate_limit_skipped_total{dimension}과 차원별로 분당 한 줄로 제한된 경고 로그에 남깁니다. 인라인 만료 정리도 맵당 초당 한 번으로 제한해, 고유 키 폭주가 요청마다 10만 항목 스캔을 강제하지 못하게 했습니다. 정상 상태의 정리는 60초 주기 배경 작업이 계속 담당합니다. 다만 클라이언트별 버킷은 여전히 주소 전체를 키로 쓰므로, 하위 64비트를 바꾸는 IPv6 출처는 이 차원을 빠져나가면서 맵까지 포화시킵니다. 그런 트래픽은 앞단에서 막으십시오. 어느 장착 지점이 만든429든 브라우저에서 읽을 수 있습니다. 전역 장착 지점은 프리플라이트를 계량하기 위해 CORS 레이어 바깥에 있고, 그래서 그 거절에는Access-Control-Allow-Origin이 아예 실리지 않아 브라우저가 클라이언트에게 가장 중요한 거절을 정체 불명의 네트워크 오류로 만들어 버렸습니다. 장착 지점 자체는 옮기지 않았고, 대신 즉시 반환하는 거절 응답에cors.allow_origins에 등재된 오리진에 대한 allow-origin 반사,Vary: Origin, 설정된 경우 자격 증명 플래그를 싣습니다. 여기에 더해retry-after와x-ratelimit-*헤더를 CORS 레이어의Access-Control-Expose-Headers에 항상 추가했습니다. 이 목록이 없으면 교차 출처 스크립트가 거절의 상태 코드는 볼 수 있어도 정작 행동에 필요한 재시도 힌트는 읽지 못했고, 이는 바깥 장착 지점만이 아니라 모든 장착 지점에 해당하는 문제였습니다. 영문과 한국어 레이트 리밋 가이드에 문서화했습니다. - 요청 단위로 Anthropic
file_id해석에 상한을 두어, 업로드된 파일 하나를 등에 업은 작은 요청 하나가 수 기가바이트의 상주 메모리로 부풀지 못하게 했습니다 (#1169, #1168 해결).src/http/handlers/anthropic/file_resolver.rs는 메시지, 콘텐츠 블록,tool_result안에 중첩된 블록의 세 단계를 상한 없는try_join_all세 개로 펼쳤고, 10 MiBMAX_INJECTION_SIZE를 요청이 아니라 파일 단위로만 검사했습니다. 그 결과 10 MiB 파일 하나를 800번 참조하는 약 57 KiB짜리 요청 본문이 약 10.7 GiB의 base64를 동시에 상주시켰습니다. 같은file_id를 중복 제거해도 이 문제는 해결되지 않습니다.resolve_image_source와resolve_document_source가 참조마다BASE64_STANDARD.encode를 호출하므로, 바이트를 한 번 읽었든 800번 읽었든 참조마다 인코딩된 사본이 따로 상주합니다. 그래서 이번에 추가한 합계 예산은 고유 파일을 합산하지 않고 중복을 포함한 모든 참조를 계산합니다. 이제 서로 다른 항을 각각 막는 세 가지 상한이 적용됩니다.MAX_TOTAL_RESOLVED_BYTES(요청당 원본 32 MiB, base64 확장 후 약 43 MiB)는 콘텐츠를 읽기 전에load_file에서metadata.bytes로 청구되며, 리졸버가 소유한AtomicUsize에fetch_add가 돌려준 값을 검사하는 방식이라 동시에 진행 중인 다른 해석과 경합하는 확인 후 증가 방식이 아니고, 이 상한은 근사치가 아니라 정확합니다. read-modify-write는 원자 변수의 단일 수정 순서에서 바로 앞에 기록된 값을 항상 읽으므로 관측되는 누적값은 그 순서상의 접두 합이 되고, 검사를 통과하는 참조는 정확히 그 순서의 접두 구간이므로 어떤 인터리빙에서도 통과한 참조들의 크기 합이 예산을 넘지 못합니다.MAX_FILE_REFERENCES_PER_REQUEST(100)는 첫 메타데이터 조회보다 앞선 단일 사전 순회에서 세 단계를 모두 세어 적용합니다. chat-completions의 20(src/proxy/files.rs)보다 의도적으로 다섯 배 높게 잡았는데, Messages API는 매 턴마다 대화 전체를 다시 보내므로 긴 멀티모달 세션에서는 단일 chat-completions 호출에서는 나올 수 없는 개수의 참조가 정상적으로 쌓이기 때문이며, 개수 상한은 메모리 상한이 아니라 값싼 사전 필터입니다.MAX_CONCURRENT_FILE_LOADS(8)는 리졸버가 소유한tokio::sync::Semaphore로 강제합니다. 세 펼침 지점이 중첩되므로 단계별buffer_unordered(8)는 512로 곱해지기 때문입니다. 퍼밋은 스토리지 읽기 구간에서만 유지되고 그 위의 재귀 해석 전체에는 걸리지 않으므로,tool_result가 자기 조상이 쥔 퍼밋을 기다리는 교착은 발생하지 않습니다. 해석 단계에는 chat-completions 경로에 이미 있던 30초 타임아웃도 추가했으며,proxy::files가crate::proxy내부 전용이라 상수를 import하지 않고 같은 값으로 복제했습니다. 새 상한 두 가지는400 invalid_request_error로, 타임아웃은504로 전달되며, 스트리밍과 비스트리밍 핸들러가 각자 들고 있던 중복 match 블록 대신 새로운FileResolutionResult::into_request_or_response를 공유하므로 한쪽 경로에서만 처리되고 다른 쪽에서 조용히 누락되는 일이 생길 수 없습니다. 기존 파일 단위FileTooLarge는 그대로 502를 반환하며 이번 변경에서 건드리지 않았습니다. 영문과 한국어 API 가이드에 문서화했습니다. - 닫는 추론 태그 앞에 놓인 분류기 판정을 살려 둡니다 (#1241, PR #1214 후속).
strip_reasoning_prefix는 추론 블록이 판정 앞에 온다는 규칙에 따라 마지막</think>뒤를 남기는데, 태그가 답 뒤에 오는 출력(unsafe\nS1</think>)은 앞부분이 빈 문자열로 잘렸습니다. #1203이 읽을 수 없는 출력을 실패한 검사로 만든 뒤로, 실제 거부가 실패한 검사가 되어 기본값인on_error: fail_open아래에서 그대로 서빙됐습니다. #1203이 막으려던 바로 그 결과가 없어진 게 아니라 자리를 옮긴 셈이었습니다. - 빠르게 실패하는 가드레일 프로바이더를 깨끗한 허용이 아니라 실패로 보고합니다 (#1182). 전송 오류, 비정상 HTTP 상태, 파싱 불가 본문은 각 어댑터 안에서 실패 정책 판정으로 해소됐고,
outcome.timed_out에서만 발화하던 텔레메트리 이음매는 이를 전혀 보지 못했습니다. 그래서fail_open아래에서는 모더레이션 장애가 깨끗한 트래픽 위의 정상 가드레일과 바이트 단위로 같은 하트비트를 만들었고,fail_closed아래에서는 범주 없는 안전 차단이 쏟아졌습니다. 이제Guardrail트레이트가Result<GuardrailVerdict, GuardrailCheckError>를 반환하므로 새 어댑터가 물리적으로 실패를 삼킬 수 없고, 실패 정책은 버퍼링 경로와 스트리밍 경로가 공유하는 단일 서비스 이음매에서 적용되며, 생성 시점에 고정되는 대신 검사마다 살아 있는 설정 스냅샷에서 해소됩니다. 트레이트 변경은 트리 밖 어댑터에 대해 호환성을 깨는 변경입니다. PII 프로바이더만 문서화된 저하 경로를 위해 로컬on_error를 유지하는데, fail-open에서 외부 인식기 실패는 내장 탐지로 검사를 마치는 정밀도 저하이지 실패한 검사가 아니기 때문입니다. - 파싱되고, 시작 시 정규식 검증을 거치고, 관리자 API로 설정할 수 있고, 집행 기능으로 문서화까지 되어 있으면서 정작 요청 경로의 어떤 코드도 읽지 않던
guardrails.allow/guardrails.deny매치 목록을 실제로 집행합니다 (#1104). 새matchlist모듈이 전역·라우트별 목록을 설정 스냅샷마다 한 번 컴파일하므로 요청 경로에서는 정규식을 컴파일하지 않고, 설정 스냅샷은 설정과 컴파일된 규칙을 한 락 아래 담는PolicySnapshot이 되어 한 검사가 서로 다른 세대의 설정과 규칙을 읽을 수 없습니다. 우선순위는 deny, allow, 프로바이더 순입니다. deny 매치는 프로바이더가 돌기 전에 차단하고, allow 매치는 프로바이더 호출 없이 곧바로 허용하며, 라우트 목록은 전역 목록을 대체하는 것이 아니라 확장하므로 라우트 재정의가 전역 deny 규칙을 약화시킬 수 없습니다.exact항목은 대소문자를 구분하지 않는 부분 문자열 일치입니다. 대소문자만 바꾼 변형이 보안 통제를 우회해서는 안 되기 때문입니다. 빈 리터럴과 빈 패턴은 모든 요청에 매치되는 대신 경고와 함께 버려집니다. 매치는 예약된 의사 프로바이더 라벨match_list_deny/match_list_allow로 기록되어 집행 전에 모니터 모드에서 측정할 수 있고, 차단 사유는 매치된 텍스트나 규칙을 절대 그대로 싣지 않습니다. enforce 모드는 이제 프로바이더 하나 또는 deny 규칙 하나를 요구합니다. 예전에는 실제로 차단하는 deny 전용 정책을 거부했습니다. - GPT-5.2에서 멈춰 있어 그 이후 등급을 전부 이름 휴리스틱에 맡기던 내장 OpenAI 모델 카탈로그를 교정하고 채웠습니다 (#1177). 발견된
gpt-5.3-codex는 400K/128K와 \(1.75/\)14 대신 256K 컨텍스트, 4096 토큰 최대 출력, 그리고 꾸며낸 100만 토큰당 \(10/\)30으로 광고됐습니다. 내장 테이블에 GPT-5.5, GPT-5.4, GPT-5.3 계열과gpt-5.2-codex를,model-metadata.yaml에gpt-5.3-codex-spark와 이전 codex 등급 세 개를 추가했습니다.gpt-5.1은 두 카탈로그에서 서로 다른 방식으로 틀려 있었고, 이제 2024-09 컷오프에 400K/128K, \(1.25/\)10이며 애초에 없던audio능력도 뗐습니다.gpt-5.1-codex와gpt-5.1-codex-mini도 같은 방식으로 교정했습니다. 모든 codex 등급은 Responses API 전용으로 문서화되어 있어 네 개 모두responses_only를 달았고, 라우터는 이들에 대한 Chat Completions 요청을/v1/responses로 연결합니다. 폐기 처리는 OpenAI 폐기 문서와 Azure Foundry 퇴역 일정을 교차 확인해 결정했습니다. solar-pro-3의 가격, 능력, 별칭을 교정했습니다 (#1097, #1095 해결). 이 항목은 0 요율을 광고하고 있었는데, 이 파일에서 0은 호스팅 API 비용이 없는 오픈 웨이트 모델의 관례이므로/v1/models가 유료 호스팅 모델을 무료로 보고했고 하류의 비용 추정은 Solar Pro 3 지출을 0으로 축소했습니다. Upstage는 100만 입력 토큰당 $0.15, 출력 토큰당 $0.60을 청구합니다. 항목의 요약이 추론 정확도 향상을 이야기하는데도reasoning능력이 빠져 있었습니다. Upstage가 실제로 공시하는solar-pro3와solar-pro3-260323은 어디에도 해석되지 않았습니다. 4자리도 6자리도 8자리 날짜 접미사 정규화 대상이 아니기 때문이며, 이제 둘 다 명시적으로 등재했습니다. 같은 이유로solar-pro-2도solar-pro2와solar-pro2-251215별칭을 얻었습니다 (#1099, #1098 해결).- 설정 쓰기가 떨어뜨리던 런타임 상태를 다시 만들어, 관리자 API 변경이 백엔드 조회와 모델 메타데이터가 깨진 설정을 게시하지 않도록 했습니다 (#1100은 #1089 해결, #1136은 #1116 해결).
put_config_section과patch_config_section은 실행 중인 설정을 JSON으로 왕복시키는데, 이 과정에서#[serde(skip)]런타임 상태(backends_index,model_metadata_cache,response_defaults_cache)가 조용히 타입 기본값으로 돌아가고 아무도 복구하지 않았습니다. 그래서 섹션PUT/PATCH가 성공하면 다음 전체 설정 로드나 파일 기반 핫 리로드 전까지 백엔드 조회와 메타데이터 해석이 깨진 채였습니다. 이제 두 핸들러가reload_config_from_file과 같은 순서로 인덱스를 다시 만들고 메타데이터를 다시 읽으며, 후속 변경이 전체 설정 apply·import·rollback 게시 경로까지 같은 처리를 확장하고 create·update·delete·weight·models 변경의 인덱스 재구축을 한곳으로 모았습니다. - 모델 메타데이터 조립이 실패하면 경고 후 빈 캐시로 리비전을 게시하는 대신 핫 리로드를 실패시킵니다 (#1102).
publish_config_revision은Config전체를 대입하므로model_metadata_cache와response_defaults_cache가 통째로 교체됐고, 라우터는 가격, 컨텍스트 창, 능력,/v1/models응답 기본값이 조용히 백엔드 보고값으로 되돌아간 채 계속 서빙했습니다. 재시작이 없으니 설명할 단서도 없었습니다. 경로 검증, 크기 초과 검사, 파싱 실패, 환경 변수 오버라이드, 시크릿 처리, 설정 검증은 모두Err를 반환해 이전 리비전을 그대로 두는데, 메타데이터 조립만 경고하고 계속 진행하던 유일한 단계였고 이제 같은 규칙을 따릅니다. 정말 치명적인 조건만 이 경로에 도달합니다. 읽을 수 없거나 파싱할 수 없는 기본 파일, 메타데이터 스키마에 맞지 않는 병합 문서, 그리고MAX_METADATA_LAYERS(64)와MAX_TOTAL_METADATA_BYTES(8 MiB) 상한입니다. 기본 파일 부재와 읽기·파싱·검증에 실패한 드롭인은 여전히 경고 후 건너뜁니다. 이 문제가 새로 도달 가능해진 이유는 상한 위반입니다.model-metadata.d/디렉터리에 쓰기 권한이 있으면 기존 파일을 건드리지 않고 65번째 파일을 추가할 수 있기 때문입니다. /admin/backends의 모든 핫 리로드 후보를 라이브 watch 채널에 게시하기 전에 공용 설정 검증기로 검증합니다 (#1206). 이로써 백엔드 전용 관리자 API가 파일·설정 수용 경로와 같은 판단을 하게 됩니다. 검증 실패는400으로, watch 채널 전송 실패는 기존 서버 오류로 매핑되며, 형식과 유일성이 이제 나머지BackendConfig와 같은 전체 설정 게이트를 지나므로backend_id전용 관리자 API 검증 헬퍼는 제거했습니다. create, update, delete, weight, models에 대한 회귀 테스트가 거부된 후보는 게시되지도, 설정 이력에 기록되지도 않음을 증명합니다.- 두 팩토리 경로 모두에서 시작 시 빈 백엔드 풀을 허용해 기존의 백엔드 0개 배포 계약을 지키면서, 비어 있지 않은 설정의 백엔드가 전부 실패하는 경우의 즉시 실패 동작은 그대로 유지합니다 (#1134, #1133 해결).
- half-open 서킷의 승인을 디스패치 이음매로 미뤄, 로컬 사전 처리·캐시·가드레일 작업을 위해 백엔드를 고르는 것만으로 프로브 슬롯이 소비되지 않게 했습니다 (#1236, #1148 후속). 서킷 필터를 거친 선택만이 만들어 내는 지연된
SelectedBackend토큰이 업스트림 I/O 직전에Admission으로 소비되고, 늦은 승인 경합은 남은 적격 후보에서 다시 선택합니다. Anthropiccount_tokens, Anthropic Messages, Responses, compact Responses, responses-only Chat-to-Responses 브리지가 모두 승인 전에 선택된 백엔드 메타데이터를 확인하는 방식으로 바뀌었습니다. half-open의 진행도 실제 성공 근거와 분리해, 중립 결과나 타임아웃만 담긴 프로브 구간은 서킷을 새 쿨다운과 함께Open으로 돌리고, 중립과 실제 성공이 섞인 구간은 여전히 닫힐 수 있습니다. - CORS 노출 응답 헤더를 첫 번째로 파싱된 이름만 남기는 방식으로 중복 제거해, 운영자가 설정한 내장 헤더의 중복이 두 번 나타나지 않게 했습니다 (#1220). 기존
Vec::dedup은 인접성에 의존해서, 내장 짝이 결합된 목록에서 인접하지 않은retry-after설정을 놓쳤습니다. - Prometheus 메트릭이 꺼져 있어도 키별 인메모리 통계 귀속이 유지되도록 했습니다 (#1209, #1196 해결). API 키 귀속과 라벨 정리를 항상 컴파일되는 코어 모듈로 옮겼고, 로컬과 호스팅 CI 매트릭스 모두 메트릭 비활성 회귀 검증을 추가했습니다.
POST /admin/guardrails/test와 가드레일 WebUI 테스트 콘솔에서 dry-run 프로바이더 실패를 드러내, fail-open 대체값을 진짜 허용과 구분할 수 있게 했습니다 (#1221). 정리된 실패 메타데이터가GuardrailService::dry_run을 거쳐 전달되고, 콘솔은 fail-open·fail-closed 결과 칩과 실패 상세를 렌더링합니다. 영문·국문 WebUI 가이드에 문서화했습니다.- PII 외부 인식기의 저하를 로컬 텔레메트리로 드러냅니다 (#1223). 새
guardrail_degraded_total{provider,kind}는 fail-open에서 선택적 외부 인식기에 닿지 못해 PII 프로바이더가 내장 탐지로 되돌아갈 때kind="external_unavailable"을 기록합니다. 이는 정밀도가 낮아진 검사이지 실패한 검사가 아니므로guardrail_errors_total은 의도적으로 세지 않습니다. 내장 전용 처리와 fail-closed 오류 동작은 그대로입니다. -
1172가 깨뜨린
BackendConfig구조체 리터럴 네 개를 복구하고, 그것을 가리고 있던 CI 구멍을 막았습니다 (#1200).#[serde(default)]는 역직렬화만 덮고 Rust 구조체 리터럴에는 아무 일도 하지 않으므로, 필수backend_id필드를 추가할 때 완전한 리터럴을 전부 같이 고쳐야 했습니다.get_health_check_configrustdoc 예제 안의 두 개는 CI가 doctest를 컴파일하지 않아서, Bedrock 통합 테스트의 두 개는bedrock-sigv4를 빌드하는 잡도 릴리스 바이너리도 없어서 살아남았습니다..github/workflows/ci.yml과scripts/local-ci.sh가 각각cargo test --doc과 전체 기능cargo check --all-targets --keep-going두 단계를 서로 대칭으로 얻었습니다. 두 번째를 좁은--features bedrock-sigv4대신 고른 이유는, 고정되지 않은 기능을 한 번에 전부 덮고 나중에 추가되는 기능도 매트릭스 수정 없이 계속 덮기 때문입니다. 매트릭스 수정 누락이 바로 이 버그를 만든 실패 양상입니다.¶ - 메트릭 비활성 호환 표면에 no-op
FileMetrics::record_retention_delete를 추가해 (#1238), 항상 컴파일되는 Files 서비스를 포함하는 모든 no-default 기능 그래프의 컴파일 정합성을 복구했습니다. 이 구멍은 #1234와 #1233이main에서 합쳐지고 나서야 드러났습니다. - 가드레일 하트비트의
verdicts와errors_total이 무엇을 세는지 분명히 했습니다 (#1227). 프로바이더가 여럿인 단계와 fail-closed 대체 차단에서 올바른 단위로 읽을 수 있도록,continuum-protocolrustdoc과 영문·국문 가드레일 가이드가 이제 실패한 검사를 명시적으로 설명하고, fail-open 해석을 검사되지 않은 허용 트래픽의 상한으로 한정하며, fail-closed에서verdicts와blocks_total이 갈리는 지점을 문서화합니다.
의존성¶
- 스트리밍 Files API 경로를 위해
tokio-util의io기능을 켜고 (#1189), 가드레일 런타임 서비스 구체화 경로가 무조건 의존하게 되어arc-swap을 선택적 의존성에서 필수 의존성으로 바꿨습니다 (#1126). 이번 릴리스에서 의존성 그래프에 추가되거나 제거된 크레이트는 없습니다.
v1.16.0 - 2026-07-28¶
스트리밍 출력 guardrail 게이트를 모든 스트리밍 경로에서 실행하고, 차단만 하고 마스킹은 못 하던 문제를 고치고, 4 MiB 버퍼 상한을 넘어서도 검사를 이어가게 하고, monitor 모드가 실제로 관찰할 것을 갖게 했습니다. metadata download 명령과 계층형 모델 메타데이터 드롭인 디렉터리도 추가했습니다.
추가됨¶
- 벤더 기준 파일과 운영자 커스터마이즈가 같은 파일 하나를 두고 다투지 않도록, 계층형 모델 메타데이터 드롭인 디렉터리를 추가했습니다(#1069).
model_metadata_file은 슬롯이 하나뿐이라 배포된 사본을 영영 갱신하지 않거나 업스트림 변경을 매번 손으로 병합하거나 둘 중 하나를 고를 수밖에 없었고, 그 파일을 통째로 갈아치우는metadata download(#1067)를 로컬 수정과 함께 쓰기도 위험했습니다. 이제 메타데이터는 우선순위가 낮은 것부터 기본model_metadata_file,/etc/continuum-router/model-metadata.d/,<사용자 설정 디렉터리>/continuum-router/model-metadata.d/,./model-metadata.d/, 그리고 새 설정 키model_metadata_dirs에 적은 모든 디렉터리를 적은 순서대로 쌓아 조립합니다. 이는find_config_file의 먼저 찾은 것이 이기는 순서를 뒤집은 것이고 의도한 결과입니다. 계층으로 표현하면 더 구체적인 위치가 나중에 적용되어 이깁니다.model_metadata_dirs는 관례 위치를 대체하지 않고 보강하며 항목을 그대로 쓰기 때문에, 쿠버네티스 ConfigMap을/etc/router/metadata/에 마운트했다면 그 경로를 그대로 적으면 됩니다. 기본값이 비어 있으므로 기존 설정은 예전과 똑같이 역직렬화되고 똑같이 동작합니다. 한 디렉터리 안에서는 확장자가.yaml·.yml인 일반 파일만 파일 이름 사전순으로 읽어10-/50-/90-관례가 동작하게 하고, 하위 디렉터리로 재귀하지 않으며, 점으로 시작하는 파일과*~·*.swp는 건너뛰고, 심볼릭 링크는 따라가되 최종 대상이 일반 파일이어야 하며, 디렉터리가 없으면 오류가 아니라 빈 것으로 봅니다. 병합은 역직렬화 이전 단계인 파싱된serde_yaml::Value에서 이뤄지는데 이는 우연이 아니라 핵심입니다. 매핑은 키 단위로 재귀 병합하므로 오버라이드 파일이 바꾸는 필드만 담으면 되고, 시퀀스와 스칼라는 통째로 교체하므로 물려받은aliases나capabilities항목을 지울 수 있으며,models항목은id를 키로 삼아 이미 있는 id는 제자리에서 병합하고 새 id는 뒤에 덧붙입니다.responses_only가 제대로 동작하는 이유도 같습니다.#[serde(default)]가 붙은 평범한bool은 역직렬화 후에 값이 없었는지false였는지 구분할 수 없어서, 타입 단위 병합이라면 그 키를 아예 언급하지 않은 오버라이드 파일이 물려받은true를 조용히 지워버렸을 것입니다. 같은 이유로ModelMetadata에 필드가 새로 생겨도 병합 코드를 추가하지 않아도 됩니다. 병합된 문서는 한 번만 역직렬화·검증하며, 백엔드model_configs가 여전히 그보다 우선하고 병합 결과는 여전히 내장 OpenAI 레지스트리보다 우선합니다. 읽기·파싱·검증에 실패한 드롭인은 눈에 띄는 경고와 함께 건너뛰고 나머지 탐색 경로는 그대로 적용되는데, 이는 시작 시점과 리로드 시점에 이미 쓰이던 경고 후 계속 진행 방침과 같으며 잘못된 운영자 파일 하나가 실행 중인 라우터를 내려앉히지 못하게 합니다. 기본model_metadata_file이 잘못된 경우는 종전과 똑같이 치명적입니다. 책임은 한 번만 묻습니다. 병합 결과가 스키마를 만족하지 못하게 된 순간에 적용 중이던 레이어만 원인으로 지목하므로, 기본 파일이 이미 잘못된 경우 그 위에 얹힌 멀쩡한 드롭인이 아니라 기본 파일이 원인으로 보고됩니다.--resolved도 이제response_defaults가 유효하지 않다는 이유로 1로 종료하지 않습니다. 그 경우 경고만 남기고 기본값으로 돌아가는 라우터 자신의 동작을 그대로 따라서, 실제로 라우터가 서비스하는 문서를 출력하지 못하고 거부하던 문제를 없앴습니다. 두 보고 형식 모두 설정 파일을 찾긴 했지만 불러오지 못했거나 애초에 찾지 못한 경우를# WARNING:머리글 줄과config_warningJSON 필드로 드러내므로, 그 설정의model_metadata_file과model_metadata_dirs가 빠진 탐색 경로를 아무 말 없이 보여주던 예전과 다릅니다. 파일은 최대 64개, 합계 최대 8 MiB까지 조립하고 한도를 넘으면 잘라내지 않고 오류로 처리합니다. 오버라이드 집합을 일부만 조용히 적용하는 쪽이 조립을 거부하는 쪽보다 나쁘기 때문입니다. 건너뛴 파일은 클라이언트 쪽에서 보이지 않으므로, 새 명령continuum-router metadata show [--resolved] [--json]이 탐색 경로, 적용 순서대로의 레이어 목록(나중 레이어가 가져간 모델 id와 그 이전 파일 포함), 건너뛴 파일과 사유를 출력합니다. 기본 모드는 원본 병합 결과를 그대로 출력해 오타가 보이게 하고,--resolved는response_defaults검증·정제를 거친 실제 적용 타입 문서를 출력하며, 사람이 읽는 머리말은 YAML 주석으로 쓰므로 출력을 그대로 저장해도 읽을 수 있는 파일이 됩니다. 적용된 파일 목록과 모델 id 오버라이드는 시작 시 로그로도 남습니다. 레이어를 읽을 때는 기본 파일을 포함해 경로를 유닉스에서O_NONBLOCK으로 한 번만 열고, 그 동일한 디스크립터를fstat으로 검사해 일반 파일인지 확인한 다음 남은 바이트 한도만큼만take로 읽습니다. 그래서 타입 검사와 한도 계산과 실제로 읽는 바이트가 모두 같은 대상을 가리키며, 같은 경로를 세 번 따로 조회하던 예전 방식은 사라졌습니다. 나열된 드롭인 디렉터리 안에 fifo나 디바이스 노드가 끼어들거나 기본 파일 자리에 fifo가 바꿔 들어가도 시간 제한 없이open이 멈춰버리는 일이 이제는 없고,fstat이후에 파일이 커지거나 실제 길이를 속여도 크기 한도를 피해갈 수 없습니다.models목록 병합은 레이어마다 누적된 항목을id로 한 번만 인덱싱하므로, 오버레이 항목마다 전체 목록을 다시 훑던 이차 시간 대신 모델 수에 선형으로 걸립니다.ConfigSection::ModelMetadataDirs도FromStr에 연결해, 섹션 목록과 admin JSON 스키마에는 광고되면서 섹션별 admin GET/PUT 핸들러에서는 알 수 없는 섹션으로 거부당하던 틈을 막았습니다.metadata download는 여전히 기본 파일만 쓰고 드롭인 디렉터리는 읽지도 쓰지도 않으므로 반복 실행해도 안전하며,--model-metadata플래그의 의미도 그대로입니다. 이 플래그는 기본 파일을 지정할 뿐 드롭인을 끄지 않습니다.model_metadata_file과model_metadata_dirs는 모두requires_restart로 남습니다.libc가O_NONBLOCK용으로cfg(unix)의존성에 새로 들어갔지만 이미 lock 파일에 있던 crate라서 실제로 새로 추가되는 crate는 없습니다. man 페이지, 국문·영문 configuration 가이드, 예제 설정, configuration-assistant 안내(스키마 1.27.0에서 1.28.0)에 문서화했습니다. - 저장소에서 정식
model-metadata.yaml을 받아 라우터가 실제로 읽는 경로에 설치하는continuum-router metadata download(표시 별칭metadata update)를 추가했습니다(#1067). 릴리스 아카이브·Debian 패키지·컨테이너에는 이 파일이 들어 있지 않고, 메타데이터만 바꾸는 커밋으로 새 프런티어 모델이 계속 들어오는 동안 기존 사본은 조용히 낡아갑니다. 저장소를 통째로 복제하지 않고서는 이를 받아올 방법이 없었습니다. 대상 경로는 문서화된 순서(--output, 전역--model-metadata, 불러온 설정의model_metadata_file로 로더와 동일한 틸드 확장 적용, 발견한 설정 파일 옆의model-metadata.yaml,~/.config/continuum-router/model-metadata.yaml)로 정해지며 성공 경로뿐 아니라 실패 경로에서도 출력됩니다.--model-metadata가 설정 필드보다 위에 있는 이유는 라우터 본체에서 하는 일이 정확히 그것이기 때문입니다. 그래서--model-metadata /etc/cr/meta.yaml metadata download는 해당 인스턴스가 실제로 읽을 경로에 파일을 설치합니다. 내려받은 내용은 디스크를 건드리기 전에 파싱해 로더의response_defaults검증을 거치는데, 경고만 남기고 기본값으로 되돌리는 로더와 달리 이 명령은 검증 실패를 치명적으로 취급합니다. 형식이 깨졌거나 스키마에 맞지 않으면 1로 종료하고 동작 중인 배포는 바이트 단위로 그대로 둡니다. 교체는 대상 디렉터리 안의 임시 파일을 거쳐 원자적으로 이뤄집니다. 임시 파일은 실행마다 다른 이름으로 배타 생성하므로, 다른 사용자가 쓸 수 있는 디렉터리에 예측 가능한 이름으로 미리 심어둔 심볼릭 링크가 쓰기를 엉뚱한 곳으로 돌릴 수 없고 동시에 실행된 두 작업이 같은 버퍼 파일에 섞여 들어가지도 않습니다. 기존 파일의 권한 모드는 유지하며(새 파일은 토큰 저장소의0600이 아니라0644),--no-backup을 주지 않으면 이전 파일을<file>.bak으로 남깁니다. 내용은 SHA-256으로 비교하므로 바뀐 것이 없으면--force없이는 쓰지 않고 건너뜁니다.--check는 아무것도 쓰지 않고 업데이트가 있으면 2로 종료하므로, cron과 CI가 낡은 사본을 오류로 취급하지 않고 분기할 수 있습니다. HTTPS 소스만 받아들이고, HTTPS를 벗어나는 리다이렉트는 reqwest 기본 정책처럼 따라가지 않고 거부하며, 참고용에 불과한Content-Length를 믿는 대신 스트리밍 본문을 8 MiB로 제한합니다. 저장소가 비공개인 동안에는 GitHub 토큰이 필요합니다. 인증 없는 raw 요청은 모든 ref에 대해 404를 돌려주기 때문입니다. 토큰은CONTINUUM_GITHUB_TOKEN,GITHUB_TOKEN,GH_TOKEN순으로 읽어 비어 있지 않은 첫 값을 쓰고 앞뒤 공백은 잘라냅니다.--token플래그는 일부러 두지 않았습니다. argv에 담긴 비밀은 셸 히스토리와ps출력으로 새어 나갑니다. 자격 증명은 요청 호스트가 정확히raw.githubusercontent.com또는api.github.com일 때만 붙으며, 접미사 비교가 아니라 대소문자 구분 없는 완전 일치로 판단하므로--url로 지정한 미러나raw.githubusercontent.com.evil.test같은 유사 호스트에는 절대 전달되지 않습니다. 토큰은 로그·출력·--json보고서 어디에도 남지 않고, 보고서에는 값 대신 어느 변수에서 읽었는지만 담깁니다.--url에 담긴 basic 인증 정보도 같은 방식으로 소스를 출력하는 모든 지점에서 제거하므로, 비공개 미러의 비밀번호가 사람이 읽는 보고서나--json의source_url필드, 오류 메시지에 남지 않습니다. 404는 이제 두 원인을 뭉뚱그리지 않습니다. 자격 증명이 없으면 세 변수 이름과 함께 저장소가 비공개일 수 있다고 알리고, 자격 증명이 있으면 ref나 경로가 없거나 토큰에 접근 권한이 없을 수 있다고 알립니다.--ref로 브랜치나 태그에 고정하고,--url로 미러나 폐쇄망 설치용 소스를 지정하며,--json은 같은 보고서를 객체로 냅니다. 보고서에는 소스 URL, 해석된 ref, 대상 경로, 이전과 이후 모델 수, 추가·제거된 모델 id, 백업 경로, 두 해시가 담기고, 파일을 갱신해도 실행 중인 라우터가 핫 리로드하지 않는다는 안내가 함께 나옵니다. 새로 추가한 crate 의존성은 없습니다. man 페이지, 국문·영문 configuration 가이드, configuration-assistant CLI 빠른 참조에 문서화했습니다.
변경됨¶
- 릴리스 컨테이너 이미지를
ghcr.io와 함께cr.backend.ai/product/continuum-router에도 올립니다(#1068). Debian·Alpine 멀티아치 태그를 한 번의 빌드 단계에서 두 레지스트리에 모두 푸시하고 마지막imagetools create는 Harbor에 있는 매니페스트를 소스로 삼습니다. 그래서 ghcr 쪽 상태와 무관하게 Harbor가 확실히 가지고 있는 매니페스트에서만 태그가 만들어집니다. 다운스트림 배포는 ghcr 가용성이나 사용자별 PAT에 기대지 않는 소스를 얻게 됩니다. ghcr 쪽 동작은 그대로입니다.
수정됨¶
- 스트리밍 출력 게이트를 모든 스트리밍 경로에서, 각 경로의 wire 형식 그대로 실행합니다(#1087, #1076 해결).
StreamingOutputGate를 실제로 만드는 지점은 OpenAI chat 스트리밍 핸들러 하나뿐이었고, 그래서 나머지 스트리밍 표면에서는 출력 guardrail이 조용히 동작하지 않았습니다. 도달 가능한 다섯 갈래 전부의/anthropic/v1/messages, SSE 파이프라인을 지나는 Gemini, 미드스트림 폴백,/v1/responses의 네 가지 스트리밍 전략, thinking 패턴 변환, Bedrockruntime·converse, Unix 소켓 스트리밍 생성자 전부,responses_onlychat 브리지가 그랬습니다. 이제는 각 경로마다 클라이언트가 실제로 받는 것을 검사합니다. 새 Anthropic wire 드라이버는 차단 시 형식에 맞는 Anthropic 종료 시퀀스를 만들고(열린 블록 닫기, 거부 텍스트 블록,message_delta,message_stop,block_behavior: error에서는error이벤트 하나), SSE 파이프라인에는 관측 탭과 캐싱 탭 사이에 출력 게이트 단계가 들어가면서 캐싱 탭에 공유 거부권이 생겨 차단·마스킹된 스트림은 저장되지 않습니다.MidStreamFallbackContext는 폴백을 몇 번 거치든 클라이언트 스트림당 게이트 하나를 들고 다니므로, 백엔드가 바뀌는 지점을 가로질러 보유한 텍스트도 하나의 완성으로 검사합니다. 이때 정책 차단은 폴백 재시도를 부르지 않고 스트림을 완료 처리합니다. Responses 패스스루 갈래는response.output_text.done과response.completed미러 이벤트를 종료 시점 검사 뒤로 미루고 게이트가 보는 클라이언트 노출 텍스트로 다시 씁니다. 그래서 델타에서 가린 내용이 미러 이벤트로 새지 않습니다. 게이트를 만들 때 전역guardrails.mode를 그대로 박아 쓰던 것도 고쳤습니다. 공용build_streaming_gate가 경로별mode와 경로별enabled,bypass_api_keys허용 목록을 함께 해석하므로, 전역이monitor인데 경로에서enforce로 덮어쓴 경우 이제 실제로 차단하는 게이트를 받습니다. 예전에는 기록만 남고 차단은 되지 않았습니다. 비활성 경로나 우회 키에는 게이트를 아예 만들지 않으므로, 모든 검사를Allow로 단축하면서 스트림 전체를 지연시키던 게이트도 사라졌습니다.stream_with_auto_backend_selection하나만 범위 밖으로 남겼고(라이브러리 전용이라 어떤 HTTP 라우트도 등록하지 않습니다), 그 판단을 rustdoc에 적고 임베더가 guardrail을 켠 채 게이트 없는 갈래에 도달하면 프로세스당 한 번 경고합니다. 새로 게이팅된 표면에서도 monitor 모드는 스트림당 출력 provider 호출 한 번을 쓰며, 이 비용은 guardrail 가이드에 적었습니다. - 스트리밍 출력 경로에서
Transform판정을 실제로 적용해, 기본 트래픽 형태에서 출력 PII 마스킹이 조용히 무력화되던 문제를 고쳤습니다(#1075, #1074 해결).finalize와run_window_check가 모든 판정을is_block()으로 뭉개는 바람에,mask액션이 내놓는Transform이 허용 갈래로 떨어져 원본 평문 청크가 그대로 나가고 마스킹된 텍스트는 버려졌습니다. 같은 요청도 비스트리밍이면 제대로 마스킹됐습니다. 이제 보유 중인 청크를 가려진 텍스트로 다시 써서 내보냅니다. 소스 형태는 청크 단위로 판별하므로 게이트 하나가 OpenAIchoices[].delta.content와 네이티브 Anthropiccontent_block_delta페이로드를 모두 처리하고 프레이밍 이벤트(role 시작, 도구 호출 델타,finish_reason, usage,message_start,ping,message_stop,[DONE], 파싱되지 않는 것 전부)는 바이트 단위로 그대로, 순서도 그대로 지나갑니다.buffer_full에서는 완성 전체를 들고 있으므로 완전한 가림이 됩니다.chunked에서는 마스킹된 윈도우에서 이미 내보낸 컨텍스트와의 최장 공통 접두사를 떼고 내보낸 뒤, 누적 버퍼의 검사 완료 구간에도 그 결과를 덮어씁니다. 이 덮어쓰기가 없으면 다음 윈도우의 컨텍스트에 원본이 다시 들어가 provider가 두 번 가리고, 클라이언트는 자리표시자가 중복된 텍스트를 보게 됩니다. 내장 PII provider는 이제 trait 기본 구현 대신check_streaming_chunk를 직접 구현합니다. 기본 구현은 모든 청크에Allow를 돌려주므로streaming_mode: chunked에서는 provider가 사실상 놀고 있었습니다.streaming_stream_first: true와 enforce +streaming_mode: passthrough는 검사 전에 내보내므로 마스킹 판정이 앉을 자리가 없습니다. 이때는 이미 보낸 텍스트를 그대로 두고 스트림당 한 번 경고하며 가린 사본을 다시 보내지는 않습니다. OpenAIn > 1에서는 가려진 텍스트가 choice 0으로 모입니다. 문서에 적어둔 대로 다중 choice 스트림에서는 형태가 망가지지만 유출되지는 않습니다. - 4 MiB 게이트 버퍼 상한을 넘어서도 스트리밍 출력 검사를 계속합니다(#1082, #1078 해결). 상한에 닿으면 게이트가 열린 채로 실패했습니다.
passthrough로 바꾸고, 검사를 한 번도 돌리지 않은 채 들고 있던 것을 전부 흘려보내고, 나머지 스트림도 검사 없이 내보냈습니다. 긴 완성 하나가 그 응답 전체에서 출력 차단과 PII 마스킹을 꺼버렸고, 흔적은warn!한 줄이 전부였습니다. 이제 상한에서는passthrough가 아니라chunked로 낮춥니다. 들고 있던 전부에 대해 전체 텍스트 검사를 돌리고 판정을 온전히 적용하며(차단이면 스트림을 끊고,Transform이면 보유 청크를 가린 뒤 내보내고, 깨끗한 판정에서만 그대로 내보냅니다), 나머지 구간에서는stream_first를 강제로 끕니다.buffer_full은 출력을 내보내기 전에 검사한다는 약속 위에서 설정하는 값이기 때문입니다. 검사가 끝난 이력은 뒤쪽streaming_context_size윈도우만 남기고 압축하므로, 메모리는 유계로 유지되면서 검사도 계속됩니다.chunked스트림은 낮추는 대신 압축만 하므로 나머지 동작은 그대로입니다. 새guardrail_stream_buffer_cap_trips_total{strategy,outcome}카운터와 경고는 스트림당 한 번만 발생합니다. 나머지 두 사본의 크기를 재려고만 들고 있던 세 번째 전체 사본도 없앴습니다. - monitor 모드가 스트리밍 출력을 실제로 관찰합니다(#1086, #1079 해결). monitor는 스트리밍 응답에서 출력 단계 평가를 전혀 하지 않았습니다.
StreamingOutputGate::new가 monitor에서 모든streaming_mode를Passthrough로 접고(monitor는 보유하거나 끊으면 안 되니 이 자체는 옳습니다), 그 갈래는 기록 없이 전달만 하고finalize의 passthrough 갈래는 누적 버퍼가 비어 있지 않을 때만 도는 구조였습니다. monitor는 어떤 정책을 강제해도 되는지 판단하는 수단이고 챗 UI와 에이전트에서는 스트리밍이 기본이므로, 한 주 내내 monitor로 돌려 출력 단계 판정이 하나도 없는 것을 보고 트래픽이 깨끗하다고 결론 내릴 수 있었습니다. 이제 monitor일 때는 passthrough 갈래도 누적합니다. 청크를 들고 있는 것이 아니라 추출한 텍스트만 남기므로 보유·지연·절단·재작성이 없고, 종료 시점 검사가 rustdoc과 guardrail 가이드가 이미 약속하던 판정을 냅니다. enforce +streaming_mode: passthrough는 검사도 provider 호출도 없는 경로로 그대로 남습니다. monitor는 상한에서chunked로 낮출 수 없습니다. 윈도우를 내보내기 전에 검사한다는 것이 곧 스트림을 붙잡는다는 뜻이기 때문입니다. 그래서 4 MiB에서 관찰을 멈추고, 잘린 사실은guardrail_stream_buffer_cap_trips_total{strategy="monitor",outcome="truncated"}로 셉니다.is_active()는 더 이상 건너뛰기 신호로 안전하지 않으며 rustdoc에도 그렇게 적었습니다. monitor 게이트는false를 돌려주면서도 모든 청크가 필요합니다. 이전 릴리스가 기록한 monitor 결과에는 아무 정보가 없습니다. - 배열 형태의 스트리밍
delta.content를 검사하고 추론 텍스트를 새 옵트인 뒤에서 출력 guardrail 범위로 들입니다(#1083, #1080 해결). 스트리밍 추출기는 비스트리밍 짝이 읽던 배열 형태delta.content를 건너뛰었습니다. 백엔드를 조사해 보니 지금 그 형태를 내보내는 지원 백엔드는 없으므로, 실제 유출을 막았다기보다 검사의 비대칭을 심층 방어 차원에서 닫은 것입니다. 다만 앞으로 content 파트를 스트리밍하는 백엔드나 프록시가 생기면 출력 단계를 우회하지 않고 검사·마스킹됩니다. 추론과 확장 사고 텍스트는 결정이 아니라 사고로 출력 단계에서 보이지 않았고, 이제guardrails.inspect_reasoning(bool, 기본false)로 검사할 수 있습니다. OpenAI 형태 경로의 스트리밍·비스트리밍reasoning_content와 네이티브 Anthropic 경로의thinking_delta·thinking블록이 대상입니다. 기본값을 끈 이유는 추론이 provider가 보는 텍스트 양을 대략 두 배로 늘려 지연이 늘고 네트워크 기반 provider에서는 비용이 늘기 때문이고 비스트리밍 경로까지 함께 덮는 이유는 스트리밍에만 켜면 이 수정이 없애려는 바로 그 비대칭이 되살아나기 때문입니다. 범위는 명시적인TextScope로 두 경로의 모든 추출기·재작성기·transform 적용 지점에 전달되며 두 범위 각각에서 모든 청크 형태에 대해 추출 가능성과 재작성 가능성이 일치하는지 표 기반 테스트가 검사하므로 양쪽이 다시 어긋날 수 없습니다. configuration-assistant 스키마는 1.28.0에서 1.29.0으로 올렸습니다. - guardrail debug 로그에서 판정 페이로드를 가립니다(#1081, #1077 해결).
GuardrailService::evaluate가 집계된 판정을?aggregated로 남기면서GuardrailVerdict를 Debug 포맷으로 출력했고,Transform::new_content와 차단의Block::reason이 통째로 찍혔습니다. 텍스트를 일부러 남기지 않도록 설계한 audit 모듈을 우회하는 셈이었습니다. 이제 두debug!지점 모두redacted()뷰를 남기며, 여기에는 판정의 형태(variant 이름, 카테고리, 점수)만 있고 페이로드는 없습니다. 내장 PII provider는 변환된 텍스트가 이미 가려진 상태이므로 이 변경은 로그 위생과 심층 방어에 해당하고 실제로 닫는 노출은 두 필드에 원본을 담는 커스텀·자체 호스팅 provider 쪽입니다. test_legacy_routing_section_is_ignored_on_load를ENV_TEST_MUTEX로 감쌌습니다(#1085, #1084 해결). 이 모듈에서 잠금 없이 환경 민감 config를 읽는 테스트는 이것 하나였고,CONTINUUM_BACKEND_URLS를 설정하는 열두 개 남짓한 형제 테스트와 경합해 CI에서 간헐적으로 실패했습니다. guardrail 작업이 병렬 테스트 스케줄을 흔들면서 드러났습니다. 테스트 전용이고 #1027에서 이 테스트가 들어온 때부터 잠복해 있던 문제입니다.
의존성¶
- minor·patch 그룹 6건을 갱신했습니다(#1073).
tokio1.53.0에서 1.53.1,tokio-util0.7.18에서 0.7.19,clap4.6.3에서 4.6.4,tokio-stream0.1.18에서 0.1.19,aws-config1.9.0에서 1.10.1,libc0.2.186에서 0.2.189입니다. - CI 워크플로의
actions/setup-python을 6에서 7로 올렸습니다(#1072).
v1.15.5 - 2026-07-24¶
모델이 낸 출력 항목을 그대로 되돌려 보내는 클라이언트에서 /v1/responses의 무상태 멀티턴 도구 호출이 다시 동작하도록 복구하고, 시작 시 리스너 바인딩이 시간 제한 없는 백엔드 프로브에 발목 잡히지 않도록 했습니다.
추가됨¶
- 백엔드를 프로브하기 전에 리스너를 바인딩할 수 있도록
health_checks.block_startup(bool, 기본값true)을 추가했습니다(#1066, #1065 해결). 기본값에서는 시작 절차가 이전과 바이트 단위로 동일합니다. 연결 예열과 첫 헬스체크 라운드가 인라인으로 실행되며 리스너 바인딩을 막습니다.false로 두면 리스너가 먼저 바인딩되고 두 단계는 완료를 로그로 남기는 백그라운드tokio::spawn태스크에서 실행되므로,/health와/v1/models가 즉시 응답하는 동안 백엔드별 상태는 admin·모델 엔드포인트를 통해 수렴합니다. 오케스트레이터가 준비 상태 제한 시간(Backend.AI GO는 30초) 안에서/health를 폴링하는데 도달 불가능하거나 응답이 멈춘 백엔드 하나가 바인딩 전 대기 구간을 그 한계 너머로 밀어낼 때 문제가 되던 상황입니다./health는 백엔드 상태를 보고한 적이 없는 라우터 생존 확인용 프로브이므로 백그라운드 예열 중에도 의미가 유지됩니다. 국문·영문 health-and-caching·configuration·deployment 가이드, 예제 config, configuration-assistant 안내(스키마 1.26.0에서 1.27.0)에 문서화했습니다.
수정됨¶
- 되돌려 보낸 Responses 입력 항목이 변환 가능한 상태로 유지되어, 변환 백엔드에서 무상태(
store: false) 멀티턴 도구 호출이 다시 동작합니다(#1064, #1063 수정). v1.15.0 회귀(#1051)가 평범한id와status를 베타 필드phase·agent·caller와 함께 네이티브 전용 메타데이터로 취급했기 때문에, 모든 OpenAI 호환 클라이언트가 (모델이 발급한 항목id까지 포함해) 그대로 되돌려 보내는message·function_call·function_call_output항목이 불투명한 passthrough 값이 되었고,convert_to_anthropic과convert_to_gemini가 요청 전체를Request field 'native Responses input item' ... would lose it로 거부했습니다. 이제 정확히 보존하는 판정은 실제로 손실이 발생하는 베타 필드에서만 발동하며, 길이가 제한된 표준id문자열과 인식되는status값만 버려도 되는 재전송 메타데이터로 간주합니다. 지나치게 크거나 형식이 잘못됐거나 알 수 없는 메타데이터는 바이트 단위로 보존되고 네이티브 전용으로 묶이며 요청 크기 검증에 계산되므로, 변환은 여전히 fail-closed로 동작합니다. - 되돌려 보낸 reasoning 항목이 바이트 단위로 충실하게 직렬화됩니다(#1064). reasoning 입력 항목의
encrypted_content·content·status는 값이 없으면 직렬화를 건너뛰므로, 전달되는 항목에 OpenAI가Unknown parameter: 'input[N].status'로 거부하는 명시적null이 붙지 않습니다. - 시작 시 헬스체크 경로에서 설정된
health_checks.timeout을 요청별 타임아웃으로 적용합니다(#1066).HealthChecker::with_shared_client는 라우터의 공유 reqwest 클라이언트를 재사용하는데 그 전역 타임아웃은 600초 스트리밍 총합이므로, 연결은 수락하고 응답이 멈춘 백엔드가 헬스체크 라운드를 최대 10분까지 붙잡을 수 있었습니다. 이제 공유 클라이언트와 전용 클라이언트 두 생성 경로 모두에서 모든 검사에 시간 제한이 걸립니다.Duration은 설정 락에서 읽은 뒤 요청 전에 가드를 해제하므로 await 구간에 락을 잡고 있지 않습니다. - 일반 연결 예열 HEAD 요청에 10초 상한을 적용해(#1066) 공유 클라이언트의 600초 천장을 물려받지 않도록 했습니다. Anthropic 분기가 이미 쓰던 상한과 같은 값입니다.
v1.15.4 - 2026-07-23¶
표준 blocking 모드 인증에서 Anthropic 고유의 x-api-key 헤더를 수용하고, hub 정책 해시·guardrail 우회 허용 목록·키별 통계가 모두 동일한 제시 자격증명을 기준으로 동작하도록 했습니다.
수정됨¶
- 표준 blocking 모드에서
x-api-key를 수용합니다(#1062, #1061 해결). blocking 모드에api_keys가 설정된 상태에서 동적 인증 미들웨어의 표준(비 AppProxy) 경로는Authorization: Bearer만 읽었기 때문에, 유효한 키를x-api-key로 제시하는 Anthropic 네이티브 클라이언트(Anthropic SDK,ANTHROPIC_API_KEY만 설정한 Claude Code)가 401을 받았습니다. 공유 헬퍼extract_presented_api_key(Bearer 우선,x-api-key폴백, 공백 제거, 원본 자격증명은 로깅하지 않음)가 blocking·permissive 두 모드 모두에서 미들웨어에 자격증명을 공급하고, AppProxy ROUTER 키 추출은 이 헬퍼에 위임하며, Anthropic messages·count-tokens·models 핸들러는x-api-key를 다시 요구하는 대신 미들웨어가 붙인 인증 컨텍스트에서 인증된 사용자를 얻습니다. 핸들러의 조기 반환이 제거되면서, 인증된 요청이라도anthropic-version헤더 형식이 잘못됐으면 조용히 통과하는 대신 400을 받습니다. - 모든 자격증명 소비자가 인증 미들웨어가 검증한 제시 자격증명을 기준으로 동작합니다(#1062). control-plane hub 키 해시는 공백이 덧붙은 Bearer 자격증명을 인증 경로와 동일하게 트리밍하므로, 공백을 덧붙인
Authorization헤더로 인증은 통과하면서 hub의 rate·예산·티어 집행과 사용량 귀속을 회피하는 일이 불가능해졌습니다. guardrail 우회 허용 목록(비스트리밍·스트리밍·Responses 전용 경로)과 키별 통계 귀속도 같은 제시 자격증명을 읽으므로,x-api-key만 쓰는 클라이언트가 우회와 통계를 올바르게 적용받고, 요청이 다른 Bearer 키로 인증됐을 때 우회 키와 우연히 일치하는 미인증x-api-key가 우회를 발동시킬 수 없습니다.
v1.15.3 - 2026-07-22¶
자체 호스팅 guardrail 분류기에서 Qwen3Guard 판정을 실제로 강제하고, AppProxy 기능 그래프를 Admin API에서 분리하며, Chat-to-Responses 브리지에서 도구 호출 인자가 비는 문제를 고치고, rmcp 2.x 마이그레이션을 포함해 의존성 다섯 개를 갱신했습니다.
추가됨¶
- 자체 호스팅 분류기에 네이티브 Qwen3Guard 템플릿을 추가해, 호환되지 않는 파서 탓에 조용히 허용되던 구조화 평문 판정을 실제로 강제하도록 했습니다(#1060, #1059 해결). 이 템플릿은 Safe·Unsafe·Controversial 결과에 대해 줄 단위로 제한된
Safety:·Categories:출력을 파싱하고, Qwen의 공식 분류 체계를 대소문자 구분 없이 매핑된 임계값으로 대응시키며(인젝션 양성은 jailbreak로 분류), 차단하지 않는flag를 기본값으로 하는controversial_action: flag | block | allow를 제공합니다. 잘못된 출력은 적용 중인 fail-open/fail-closed 정책을 따릅니다. 분류기 옵션·Qwen 파싱·테스트는 각각 500줄 미만의 모듈로 분리했고, 설정은 국문·영문 guardrail 가이드와 예제 config, configuration-assistant 안내에 문서화했습니다.
변경됨¶
- AppProxy 기능 그래프가 Admin API를 전이적으로 활성화하지 않고 독립적으로 컴파일되도록 했습니다(#1058, #1053 해결). 프로세스 전역 config 변경 잠금은
infrastructure::config로, CIDR 기반 IP 허용 목록 평가는 기능 독립적인 HTTP 미들웨어 헬퍼로 옮기면서 둘 다 호환 재익스포트를 남겼고,appproxy-common은 더 이상admin을 활성화하지 않습니다. 그래서--no-default-featuresAppProxy 빌드는 Admin 라우트 트리를 컴파일하지도 마운트하지도 않으며, 공식 릴리스 빌드는 여전히 기본full기능 집합으로 Admin을 받으므로 동작이 바뀌지 않습니다. 독립appproxy-common·appproxy-router·control-plane,appproxy-router검사를 CI에 추가했습니다.
수정됨¶
- Responses 스트림이 완전한 JSON을
response.output_item.added나done이벤트에서만 제공할 때 함수 호출 인자를 보존해, Chat Completions 도구 호출이 빈arguments문자열로 클라이언트에 도달하지 않도록 했습니다(#1057, #1056 해결). 변환기는 출력 인덱스별로output_item.added에서 관측한 인자를 버퍼링하고, 각 도구 호출에 이미 전달한 델타를 추적하며, 종료 이벤트를 조정할 때 접두사가 호환되는 가장 긴 누락 접미사만 내보냅니다.
의존성¶
rmcp를 1.8에서 2.2로 올리고 MCP 리소스 목록을 주석 없는Resource·ResourceTemplate빌더로 마이그레이션했습니다.RawResource·RawResourceTemplate·AnnotateAble·no_annotation()이 2.x API에서 제거됐기 때문입니다.tower-http는 0.7(cors 레이어),object_store는 0.14(s3-cache),aws-smithy-eventstream은 0.61(bedrock-sigv4),tokio-tungstenite는 0.30(control-plane 정책 스트림)으로 올렸습니다.
v1.15.2 - 2026-07-21¶
Windows 릴리스 바이너리를 MSVC C 런타임을 정적 링크한 상태로 배포해 깨끗한 Windows 설치본에서도 실행되도록 하고, 의존성 트리를 갱신했습니다.
수정됨¶
x86_64-pc-windows-msvc릴리스 빌드에서 MSVC C 런타임을 정적 링크하여, 배포되는continuum-router.exe가 더 이상 Visual C++ 재배포 패키지를 필요로 하지 않도록 했습니다(#1055). v0.23.1 이후 모든 Windows 릴리스는 CRT를 동적 링크했기 때문에, VC++ 2015-2022 재배포 패키지가 없는 Windows 머신에서는VCRUNTIME140.dll누락 오류로 실행되지 않았습니다. 이제 릴리스 워크플로가 Windows 타깃을-C target-feature=+crt-static로 빌드하며, 이 잡은 명시적--target으로 빌드하므로 해당 플래그는 최종 바이너리에만 적용되고 호스트 빌드 스크립트나 proc-macro에는 적용되지 않습니다. macOS와 Linux 빌드는 변경되지 않았습니다.
의존성¶
- 의존성 12개를 한 그룹으로 마이너·패치 업데이트했습니다(#1052).
tokio1.52.3→1.53.0,redis1.3.0→1.4.1,uuid1.23.5→1.24.0,serde1.0.228→1.0.229,serde_json1.0.150→1.0.151,thiserror2.0.18→2.0.19,async-trait0.1.89→0.1.91,clap4.6.1→4.6.2,futures-util0.3.32→0.3.33,fastrand2.4.1→2.5.0,regex1.13.0→1.13.1,toml1.1.2→1.1.3을 포함합니다.
v1.15.1 - 2026-07-20¶
OpenAI Chat Completions·Responses API 전반에서 GPT-5 계열을 정합했습니다. 이번 릴리스는 GPT-5.6 모델과 GPT-5 Pro를 등록하고, reasoning_effort를 모델별 정확한 매트릭스에 맞춰 조용한 하향 없이 검증하며, Chat-to-Responses 브리지를 지날 때 호환 가능한 요청 필드를 버리지 않고 보존합니다.
추가됨¶
- GPT-5.6 Sol, Terra, Luna 모델과 계열 별칭(alias)을 등록하고 OpenAI 모델 정규화·reasoning-effort 처리에 연결했습니다(#1051). 모델 메타데이터가 새 세대를 다루고, 계열 별칭은 구체 모델로 해석됩니다.
- GPT-5 Pro를 Responses 전용 모델로 추가하여
/v1/chat/completions가/v1/responses로 투명하게 브리지되도록 했습니다(#1051). GPT-5 Pro는 업스트림에서 Responses 전용이지만, 라우터는 기존 Chat-to-Responses 브리지를 통해 Chat Completions 호환을 유지합니다. - Responses API 스키마를 reasoning context·mode, 텍스트 verbosity, 프롬프트 캐싱, 프로그래매틱 도구 호출, 멀티 에이전트 아이템, 네이티브 replay 메타데이터, safety identifier, context management, 도구 호출 한계까지 확장했습니다(#1051).
변경됨¶
- 스트리밍·비스트리밍 Chat Completions 요청 모두에서 OpenAI
reasoning_effort를 GPT-5 세대·모델의 정확한 매트릭스에 맞춰 검증합니다(#1051). 원래gpt-5계열은minimal을 받고, GPT-5.1은none/low에서 시작하며, GPT-5.2부터 GPT-5.5는xhigh를 추가하고, GPT-5.6은max를 추가하며,gpt-5-pro는high만 받습니다. 라우터는auto,xhigh,max를 다른 effort로 재작성하지 않으며, OpenAI 모델은 더 이상 기존 Gemini 정규화 간섭을 거치지 않습니다. - Chat-to-Responses 브리지를 지날 때 flat
verbosity, 메타데이터, safety identifier, 프롬프트 캐시 제어를 포함한 호환 Chat 필드를 보존하고, 유효하지 않은 값은 버리는 대신 거부합니다(#1051). 네이티브 OpenAI persisted-response 상태와 beta 헤더는 그대로 유지되고, 로컬 세션 상태는 변환 전략에서만 해석되며 네이티브previous_response_id값은 변경 없이 전달됩니다. 프롬프트 캐시 TTL 검증은 문서화된30m값을 받습니다.
수정됨¶
- 네이티브 전용 Responses 제어가 비네이티브 변환 경로에서 조용히 버려질 상황이면
unsupported_request_parameter로 fail-closed 처리합니다(#1051). GPT-5.6 전용 제어를 담은 네이티브 Responses 요청은 응답 캐싱을 우회하고, 조용히 성능이 떨어지는 대신 변환 전용 백엔드에서 거부됩니다.
호환성 변경 사항¶
- OpenAI Chat Completions는 이제 지원되지 않는 모델·effort 조합을 조용히 재작성하는 대신
400 invalid_request_error로 거부합니다(#1051). 이전에 라우터가 범위를 벗어난reasoning_effort를 조정해 주는 데 의존하던 클라이언트는 명시적 오류를 받으며, 대상 모델이 지원하는 값을 보내야 합니다.
v1.15.0 - 2026-07-20¶
설정 진실성 점검(configuration truth pass). v1.14.0 트리를 수동 코드 감사한 결과, 스키마가 받아들이고 매뉴얼이 문서화했지만 어떤 프로덕션 코드 경로도 존중하지 않는 설정 knob과 엔드포인트가 발견되었습니다. 이번 릴리스는 각각을 런타임에 연결해 문서화된 동작이 실제로 적용되도록 하고, 매뉴얼이 이미 설명하던 환경 오버라이드와 보간(interpolation)을 추가하며, 문서 정합 감사와 머지 후 하드닝 패스로 마무리합니다.
추가됨¶
- 하드코딩된 secret/URL/경로 집합뿐 아니라 모든 문자열 값 config 필드에서
${VAR}환경 참조를 보간합니다(#1039, closes #1032). 로더가 파싱된 YAML/TOML 트리를 순회하며 typed 역직렬화 전에${VAR},${VAR:-default},$$리터럴 달러 이스케이프를 해석하고, 변수 이름 검증과 과도한 값·치환에 대한 경계를 둡니다..env파일은 파싱 전에 로드되어 그 값이 보간에 반영되고, 기본값 없는 미설정 필수 참조는 시작과 핫리로드를 경로 인식 오류로 중단시키며,config validate는 미설정 참조를 경고로 보고하고,config show --resolved와config diff는 보간하며(diff는 secret 이름 필드를 여전히 마스킹),config show와 MCP 표면은 참조를 리터럴로 유지하고 환경 값을 읽지 않습니다. - 런타임에 연결된 검증형
model_aggregationconfig 섹션을 추가했습니다(#1042, closes #1022). 이 섹션은 집계 서비스, 캐시 TTL, fetcher·엔트리 한계, 중복 제거 전략, force-refresh 게이트, 백그라운드 새로고침 작업에 매핑되어allow_force_refresh와background_refresh를 기본값에 고정하지 않고 운영자가 제어할 수 있게 합니다. 캐시와 서비스가 시작 시점에 구성되므로 리로드 클래스는 restart입니다. 스키마 카운트가 35로 늘어납니다. - 설정 로더가
CONTINUUM_SELECTION_STRATEGY환경 오버라이드를 읽도록 했습니다(#1037, closes #1023). 문서화됐지만 읽히지 않던 값으로, 이제 로더가--selection-strategy와 동일한 표기 규칙으로 여섯 전략을 모두 파싱하고, 인식되지 않는 값은 조용히 무시하는 대신 명확한 오류로 설정 로드를 실패시키며, 우선순위는--selection-strategy>CONTINUUM_SELECTION_STRATEGY> 파일 값 >RoundRobin기본값입니다. - 설정 로더가 네 개의
CONTINUUM_FILES_AUTH_*Files API 인증 환경 오버라이드를 읽도록 했습니다(#1046, closes #1033).CONTINUUM_FILES_AUTH_METHOD,CONTINUUM_FILES_AUTH_SCOPE,CONTINUUM_FILES_ENFORCE_OWNERSHIP,CONTINUUM_FILES_ADMIN_ACCESS_ALL는 문서화됐지만 무동작이었습니다. 이제files.auth에 적용되고, 파일에files:섹션이 없으면 기본FilesConfig를 구성하며, 두 불리언은 리터럴 소문자true/false만 받아들이고1,0,yes,no, 대소문자 혼용은 명확한 오류로 거부합니다. - 라우터가 이미 광고하던
X-Fallback-Attempts응답 헤더를 실제로 내보냅니다(#1035, closes #1024). 폴백 경로가 응답을 구성하기 전에 시도 횟수를 버렸습니다. 이제 헤더는 primary를 포함한 총 모델 시도 횟수를 보고하고, 폴백 백엔드가 응답을 제공했을 때만 나타나며(최소값 2), 모델별notify_on_fallback설정으로 게이팅됩니다. 스트리밍 응답은 본문 전에 헤더를 flush하므로 스트림 중간 복구는 헤더 대신streaming_fallback_*메트릭으로 관측 가능합니다.
변경됨¶
- 영문·국문 매뉴얼, CLI
--help, 생성된 설정 안내, Debian 패키징 텍스트, Docker 예제, 매뉴얼 페이지를 구현된 라우터 표면에 맞춰 정합했습니다(#1034). 사용자 대상 문서에서 역사적·희망적·존재하지 않는 기능 주장을 제거하고 실제 엔드포인트, 기능 게이트, 인증, 폴백, 서킷 브레이커, 환경 확장, model-aggregation, 핫리로드 경계를 문서화했으며, Admin·MCP 핫리로드 메타데이터와 회귀 테스트를 실제 배선에 맞췄습니다. 이 감사가 이번 릴리스의 나머지가 닫는 후속 발견들을 드러냈습니다. - Debian Build-Depends Rust 하한을 lablup PPA에 맞춰 1.75에서 1.96으로 올리고 패키징 문서를 정합했습니다(#1021, closes #1020). 잘못된 edition-2024 MSRV 주장(크레이트는 edition 2021), 틀린 Noble 코드명 매핑, 그리고 stock Ubuntu 시리즈는 Rust 1.96을 제공하지 않으니 lablup PPA를 활성화해야 한다는 배포판 안내를 바로잡았습니다.
수정됨¶
- 서킷 브레이커 상태 머신을 일반 프록시 트래픽으로 구동합니다(#1044, closes #1028). 브레이커는 구현·설정·핫리로드·Admin 노출까지 되어 있었으나 어떤 LLM 프록시 요청도 이를 구동하거나 참조하지 않아 활성화해도 라우팅 경로를 보호하지 못했습니다. 이제 선택 단계에서 열린 서킷을 제외하고, 디스패치 전 게이트가 승인/거부하며, 종료 결과를 Chat, Completions, Responses, Embeddings, Images, Rerank와 모든 폴백 홉 및 표준 스트리밍에 걸쳐 기록합니다. 업스트림 5xx와 설정된
failure_status_codes는 서킷을 열고, 타임아웃은timeout_as_failure를 존중하며, 전송 오류는 무조건 실패이고, 클라이언트 측 4xx는 기록하지 않으며, 승인 거부는 재시도 가능한 오류가 되어 선택과 폴백이 표준 503을 반환하기 전에 다른 백엔드로 진행합니다. retry.*와timeouts.*핫리로드 개정을 요청 실행에 적용합니다(#1045, closes #1029). 둘 다 핫리로드 가능으로 분류됐지만 요청 경로가 시작 스냅샷만 읽어, 리로드가 설정을 바꾸고 성공을 보고하면서도 실효 동작은 시작 값에 머물렀습니다. 이제 각 요청 표면이 진입 시점에 라이브 정책과 타임아웃 예산을 스냅샷해 진행 중 요청마다 일관된 정책 하나를 유지합니다. reqwest 클라이언트의 연결·전체 타임아웃은 클라이언트 구성 시점에 고정되므로 restart 전용으로 남습니다.- 라이브
selection_strategy와prefix_routing.load_factor_epsilon변경을 실행 중인BackendPool에 적용합니다(#1038, closes #1025). watcher가 감지한 전략 변경이 적용됐다고 광고됐지만HotReloadService가 라이브 풀에 밀어넣지 않아 라우팅이 시작 전략을 유지했습니다. 이제 둘 다 lock-free 셀로서 백엔드 멤버십, 백엔드별 통계, in-flight 회계, 해시 링 안전성을 보존하며 원자적으로 교체되고, Admin·MCP·문서 표면이 이를 즉시(immediate)로 보고합니다. - 받아들여지지만 무동작이던
prefix_routing.virtual_nodes와anthropic_cache_control_injectionknob을 런타임에 연결합니다(#1041, closes #1026). 이제virtual_nodes가 consistent-hash 링 크기를 정하고(변경 시 캐시 재구성),anthropic_cache_control_injection이 fleet 전역 마스터 스위치로 자동 Anthropic cache-control 주입을 게이팅하며, 둘 다 핫리로드 가능하고/admin/prefix-routing/stats가 활성 풀 값을 보고합니다. - 공유 API 키 저장소를 Admin 인증에 주입하여
admin.auth.method: api_key가 동작하도록 수정 (#1036, closes #1030). Admin 라우트가AdminAuthState::without_api_key_store(...)로 마운트되어 모든api_key요청이 빈 저장소 분기에 걸려500 Internal Server Error를 반환했습니다. 이제 Admin API 키 인증은 일반/v1인증 및 Admin 키 관리와 동일한 저장소로 검증하고(Authorization: Bearer와X-API-Key모두 허용),required_scope를 강제하며, 선택적allowed_ips를 존중합니다. 키를 비활성화, 회전, 만료, 삭제하면 재시작 없이 즉시 인증에서 차단됩니다. POST /admin/config/apply를 정직하게 수정 (#1047, closes #1031). 이 엔드포인트는 자리표시자였습니다. 변경 없는 현재 설정을 새 기록 버전으로 기록하고,config_sender로 발행하지 않았으며, 아무것도 트리거하지 않고도hot_reload_triggered: true를 반환할 수 있었습니다. 이제 선택적 전체config후보를 받습니다. 후보가 없으면 명시적 no-op(기록 버전 없음,hot_reload_triggered: false)이고, 후보가 있으면 검증한 뒤 실행 중인 설정과 비교하여 차이가 있을 때만 수정 잠금 아래에서 원자적으로 발행합니다.hot_reload_triggered는 변경된 설정을 성공적으로 발행한 뒤에만true이고,updated_sections/requires_restart는 실제 diff를 반영하며,hot_reload: false는 적용 없이 diff를 미리보기하고, 후보 없는 반복 호출은 빈 기록 버전을 만들지 않습니다.--features embed에서src/errors.rs가 빌드되도록metrics::security스텁을 추가했습니다(#1048). 항상 컴파일되는 오류 경로가metrics기능 뒤에 있는crate::metrics::security::truncate_at_char_boundary를 호출하는데, embed 스텁 모듈에 실제 UTF-8 경계 안전 절단 로직을 담은 서브모듈을 추가했습니다.embed기능은 CI 매트릭스에 없어서 (#1017이 유발한) 이 파손은 off-feature 빌드 매트릭스를 로컬에서 돌릴 때에만 잡혔습니다.- capacity 정수 전용 하트비트 단언을 capacity 필드로 한정했습니다(#1040). 릴리스 feature-set CI 잡이 1.14.0 이후 결정적으로 실패했는데, 직렬화된 하트비트 전체에서
.0을 검사하다router_version"1.14.0"의 정당한 dot-zero에 걸렸기 때문입니다. 이제 세 개의observed_*_capacity값이 JSON 정수로 직렬화되는지만 확인합니다. - 위 설정·라우팅 변경들에 대한 머지 후 교정을 통합했습니다(#1050). 서킷 브레이커 승인·half-open 회계·핫리로드를 일관되게 만들고, 재시도 정책을 스냅샷하고 해시 링 세대를 하드닝하며, 완전한 Admin config 카탈로그를 진실한 리로드 메타데이터와 함께 노출하고, 모든 스트리밍 전송을 다루면서 설정 문서를 동기화했습니다.
제거됨¶
- 무동작이던 레거시
routingconfig 섹션을 제거했습니다(#1043, closes #1027). 스키마가 이를 받아들이고 문서가 고급 라우팅 오버라이드로 설명했지만 어떤 프로덕션 코드도 읽지 않아,routing을 설정해도 검증은 통과하되 그 값 중 어느 것도 트래픽에 영향을 주지 않았습니다. 라이브 선택은 최상위selection_strategy, 백엔드별weight/models,smart_routing을 씁니다. 최상위routing:키를 여전히 가진 설정은 계속 로드되고(알 수 없는 키는 무시), 로더와config validate는 이제 조용히 무동작하는 대신 라이브 설정을 가리키는 마이그레이션 경고를 내보냅니다. 스키마 카운트는 34로 돌아가고schema_version은 1.26.0이 됩니다.
v1.14.0 - 2026-07-19¶
추가됨¶
- 임베디드 WebUI를 완전한 관리 콘솔로 개편했습니다(에픽 #931). 단일 파일 SPA에 Alpine.js, uPlot, 사전 빌드한 Tailwind 스타일시트를 벤더링하고 CDN 의존성을 모두 제거해 엄격한
default-src 'self'CSP 아래 에어갭 환경에서도 동작하며, 페이지별 모듈 레지스트리로 재구성했습니다(#948). 자격증명 검증과 세션 UX를 갖춘 로그인 흐름이 콘솔을 게이팅하고(#949),GET /admin/capabilities엔드포인트가 페이지 단위 기능 게이팅을 구동해 비활성 서브시스템 패널이 깨지는 대신 자연스럽게 비활성화됩니다(#950). 새 관리 페이지는 가드레일(#951), 캐시 운영(#952), 스마트 라우팅(#953), 사용량 분석(#954), 프롬프트(#955), 파일(#956), 카탈로그·별칭·폴백 체인을 담은 모델·라우팅(#957), 온디맨드 헬스 프로브를 갖춘 백엔드 상세 편집 폼(#958), diff 미리보기와 핫리로드 표시를 갖춘 스키마 기반 config 폼(#960), 컨트롤 플레인·ACP·AppProxy 통합 상태(#961)를 다룹니다. diff에 표시된 삭제 항목을 소리 없이 누락시키는 config PATCH 저장을 차단하고(#962), API 키에 허용 목록·주석·사용량 드릴다운을 더했으며(#963), 대시보드에 추세·업타임·서킷 상세를 추가하고(#964), 커맨드 팔레트·키보드 단축키·반응형 레이아웃·접근성 개선을 반영했습니다(#965, #966, #1004). - 모든 LLM API에 걸쳐 typed 운영자 요청 파라미터 정책을 추가했습니다(에픽 #990). 프로토콜 독립 정책 엔진(#996)이 일곱 canonical 파라미터(
temperature,top_p,max_tokens,presence_penalty,frequency_penalty,top_k,min_p)에 대해 운영자가 정의한 기본값·오버라이드·최소/최대 클램프를 typed 값과 검증된 범위로 적용하며, config로 노출하고(#1006) Chat Completions·Completions 프록시 경로(#1007), 네이티브 Anthropic Messages 경로(#1008), Responses API(#1009)에서 강제합니다. 변형은 값이 없는 결정적 메타데이터를 내보내고 잘못된 정책과 잘못된 요청 필드를 구분합니다. 감사 후 런타임 경로와 로컬 경계 보존 보장을 하드닝해 Hub 티어 제한이 로컬에 설정된 경계를 더 좁힐 수는 있어도 넓힐 수는 없도록 했습니다(#1013, #1014, #1015). - Continuum Hub 티어 요청 파라미터 제한을 Router 데이터 플레인에서 강제합니다(#1002, Hub #299 / PR #314 및 #301 / PR #319). 버전 0을 유지하는 추가형 와이어 계약은 일곱 canonical 파라미터의 정수 마이크로단위 경계, 로컬 별칭 해석 후 정확한 모델 범위, canonical 공개 티어 digest, typed capability/active/rejection 증거를 전달합니다. 전체 엔벨로프는 하나의 원자적 last-known-good 교체 전에 검증·변환되며 잘못되거나 오래됐거나 digest가 맞지 않는 후보는 부분 적용되지 않습니다. 인증된 Hub 키는 Completions, Chat Completions, Responses, Anthropic Messages에서 캐시 identity·치환·arbitrage·백엔드 선택·재시도·폴백 전에 Hub와 배포 로컬 제한의 가장 좁은 교집합을 한 번 고정합니다. 권위 있는 clear, 혼합 버전 Hub, feature-off/default 빌드, Hub 장애의 기존 동작을 보존하며 로그·메트릭·하트비트 상태는 값 없이 유지됩니다. vendored protocol은 Hub PR #319 merge
59387265e755bf02a9dbbb238da369e36d566d49에 고정됩니다. - 아웃바운드 전용 컨트롤 플레인 에이전트를 통해 Continuum Hub 플릿 요청 파라미터 설정을 적용합니다(#1001, Hub #302 / PR #317). 별도 옵트인인
control_plane.config_sync.enabled는 값이 제거되고 상한이 정해진 structuredrequest_params기능을 광고하고, 리비전에 묶인 공개 scalar content를 가져와 Hub 호환 canonical digest와 전체 effective config를 검증하며, 명시적인 로컬 pin 아래에 Hub leaf를 계층화한 다음 하나의 원자적 hot-reload snapshot을 게시합니다. Dry run, rollback revision, 중복 전달, ack 유실, 재연결, 재시작은 비공개 last-known-good 상태/outbox를 통해 멱등적으로 처리됩니다. 일시적인 Hub 실패는 terminal rejection이 되지 않고, 진행 중 요청은 이미 잡은 config를 유지하며, snapshot·로그·apply result에는 값이 들어가지 않습니다. vendored protocol은 버전 0에서 추가형 호환성을 유지하고 Hub PR #317 head771591f에 고정됩니다. - Continuum Hub 인벤토리에 백엔드별 관측 RPM, 입력 TPM, 출력 TPM 추정치를 추가했습니다(#972). 컨트롤 플레인 에이전트는 확인된 일시적 업스트림
429가 있을 때만 60초 롤링 처리량 관측치를 용량으로 승격하고, 최소 두 건의 성공 완료를 포함한 온전한 윈도를 요구하며, 각 토큰 차원은 모든 완료 건의 토큰 계측이 갖춰져야 내보냅니다. 추정치는 15분 뒤 만료되는 최고 관측값으로 유지됩니다. 콜드 스타트, 유휴 상태, 가벼운 부하, 불완전한 토큰 계측, 할당량 소진, 오래된 백엔드는0을 보고하는 대신 필드를 비워 두며, 관측 용량은 허브에 저장된 운영자 선언 용량을 읽거나 합치지 않습니다. 같은 선택적 추정치는 컨트롤 플레인 빌드의/admin/stats/backends에서도 확인할 수 있습니다. - 라우터 관리 Gemini 컨텍스트 캐싱을 추가했습니다(#929, #928 종료).
gemini_context_cache.enabled가 켜지면type: gemini백엔드로 보내는 크고 안정적인 시스템 프롬프트 접두사를 GooglecachedContents리소스로 캐싱하고 요청 간 재사용해, OpenAI 호환 클라이언트에는 투명하게 캐시 토큰 할인을 보장하고 큰 고정 시스템 프롬프트를 반복 전송하는 워크로드의 prefill 지연을 줄입니다. 기존 Anthropiccache_control주입을 compat·네이티브 Gemini 양쪽 표면에서 그대로 따르며, (백엔드, 모델, 접두사 digest)로 키잉하고 single-flight 생성, API가 거부한 접두사의 네거티브 캐싱, LRU 축출, 히트 시 TTL 연장을 갖춥니다. 기본값은 꺼짐입니다. 명시적 캐싱은 토큰-시간당 저장 비용이 청구되므로 비활성 시에는 맵 할당·백그라운드 작업·요청당 지연이 전혀 없는 완전한 no-op입니다.continuum_gemini_context_cache_*메트릭 계열이 요청·생성·실패·엔트리·축출·캐시 토큰을 보고합니다. - 비용 상한이 걸린 Hub 동기화 합성 provider/model 프로브를 실행합니다(#969, #1019).
control-plane피처가 컴파일되고control_plane.enabled,control_plane.policy.enabled, 그리고 대상이 있는 활성 HubProbePolicy가 모두 성립할 때, 에이전트는 나열된(provider, model)대상마다 정책 주기에 맞춰 내부Backend::execute_probe_chat_completion지점을 통해 비스트리밍 프로브 호출 한 건을 실행합니다(정상 변환과 실효 인증, 구조화된 결과, 치환·폴백·캐시·재시도 없음,UsageEvent없음). 이는control_plane.state_file옆의 소유자 전용 사이드카에 지속되는 라우터별 월간 예산 상한으로 제한됩니다(0은 무제한, 양수 상한은 디스패치 전에 예약하고 가격·상태가 없으면 fail-closed). 결과는 안정적인report_id/probe_id재전송과 구형 허브를 위한 비치명적 rate-limited 404 경로로POST /api/agent/v1/probes에 게시됩니다. config 필드는 추가되지 않으며 게이트 하나라도 꺼지면 프로브 동작·비용·와이어 트래픽이 전혀 없습니다. - Continuum Hub 인벤토리에 백엔드별 서빙 모델(#920), 백엔드별 인증·할당량 거부 카운터(#925), 토큰 공급·포화 텔레메트리(#968)를 보고하고, 인식되지 않은 키에 대해 fail-open 대신 fail-closed하는 옵트인
policy.unmatched_key거부 strict 모드를 추가했습니다(#923). 모두control-plane피처 뒤에 있으며PROTOCOL_VERSION은 그대로입니다. - 컴파일 타임 Cargo 피처 플래그와 런타임 활성 서브시스템을 하나의 읽기 전용 응답으로 보고하는
GET /admin/capabilities엔드포인트를 추가했으며, 기존 관리자 인증·감사 미들웨어가 게이팅합니다(#950).
변경됨¶
- 의존성 12개를 한 그룹으로 마이너·패치 업데이트했습니다(#976).
regex1.12.4→1.13.0,aws-config1.8.18→1.9.0,http-body1.0.1→1.1.0,rust-embed8.11.0→8.12.0과uuid,bytes,lru,socket2를 포함합니다.
수정됨¶
- 라우터 관측성 에픽을 위해 실시간 Prometheus 메트릭 소스를 연결하고 레거시 컬렉터를 정리했습니다(#973; #982, #986, #993). 등록만 되고 기록되지 않던 메트릭 계열에 프로덕션 기록 지점을 붙이고, 레거시
lazy_static카운터를 최신 메트릭 소스에서 동기화하며, 영문·국문 메트릭 레퍼런스가 내보내는 메트릭과 레이블 이름을 문서화합니다. - 컨트롤 플레인 인벤토리를 통해 관측된 백엔드별 용량을 보고하되(#972, #983), 병렬 카운터가 아니라 단일 인플라이트 게이지를 읽습니다.
- 내부 백엔드를 모든 사용자 대상 API 표면에서 숨겨
backend:로 참조되는 내부 모델이 모델 목록이나 라우팅 후보 집합에 새어 나가지 않도록 했습니다(#985). - 에픽 #990 응답 캐시 identity를 하드닝해 서로 다른 실효 요청 간에 캐시 키가 충돌하지 않도록 했습니다(#1018).
- Unix 소켓 스트리밍 경로에서 업스트림 오류 본문을 노출합니다(#1017, #1016 종료). plain·thinking·Anthropic Unix 소켓 스트림의 비-2xx 백엔드 응답이 이제 남은 요청 타임아웃으로 제한된 8 KiB 상한과 2초 예산 안에서 오류 본문을 드레인해, 백엔드 설명을 버리는 대신 TCP 스트리밍 경로와 동일한 오류 본문 동등성을 확보합니다.
- 스트리밍 응답에서 Unix 소켓 사용량을 계측해 해당 전송에서 컨트롤 플레인 사용량 레코드가 누락되지 않도록 했습니다(#927).
- 컨트롤 플레인 관측성과 strict 인증을 하드닝하고(#926), 헬스 동기화 중 이름이 바뀐 백엔드를 정리하며(#921), fail-open 사용량 귀속에 unattributed 센티널을 찍고(#922), 사용량 provider를 오래된 스냅숏이 아니라 이벤트별 실시간 config에서 해석하도록 했습니다(#919).
- 병합된 구현 감사 후 에픽 #973의 관측성 상태를 하드닝했습니다. 동시 요청의 완료 시각이 순서와 다르게 기록되더라도 오래된 처리량이 남거나 미래 관측값이 노출되지 않도록 용량 샘플을 타임스탬프 순서로 유지합니다. 헬스 체크가 비활성화된 경우에도 백엔드 제거와 같은 이름의 엔드포인트 교체가 이름 기반 용량 및 자격증명 상태를 항상 정리하며, 등록되어 있던 집계 모델 사용량·토큰·최종 타임아웃 카운터에 실제 프로덕션 기록 지점을 연결했습니다. 영문·국문 메트릭 레퍼런스도 실제로 내보내는 메트릭과 레이블 이름에 맞췄습니다.
- 실제 HTTP 트래픽이 설정된
selection_strategy를 통해 디스패치되도록 수정했습니다(#975). 이전에는 어떤 HTTP 요청 경로도 풀 셀렉터를 호출하지 않았습니다. 채팅 컴플리션, 스트리밍 채팅, Responses API(비스트리밍·스트리밍·compact), Anthropic Messages와 count_tokens, 이미지 생성이 모두 모델 조회 순서상 첫 번째 정상 후보를 골랐고 그 결과 RoundRobin, WeightedRoundRobin, Random, LeastLatency, ConsistentHash, PrefixAwareHash가 실제 트래픽에는 전혀 작동하지 않았습니다. 이제 이 경로들은 하나의 선택 지점을 공유합니다. 각 경로는 자체 사전 필터와 오류 의미론(모델 조회, 내부 백엔드 필터, 키별 허용 목록, 재시도 상태 제외, 그리고 model-not-found / 403 / all-unhealthy 구분)을 그대로 유지하고 공유 지점이 살아남은 후보를 헬스 필터링하므로 비정상 백엔드는 결코 선택되지 않으며, 최종 선택은 새 후보 제한 풀 진입점BackendPool::select_from_candidates를 거칩니다. 이 진입점은select_backend_with_context와 전략 코어를 공유하고 컨시스턴트 해시 링 캐시를 참여 백엔드 이름으로 키잉하므로 모델별 후보 부분집합이 각각 올바른 링으로 해싱됩니다. 접두사 인지 라우팅도 끝까지 연결되었습니다.prefix_routing.enabled가 켜지면 요청 본문(OpenAImessages[]와 Anthropic 최상위system형태)에서 접두사 키를 추출해 #971의 실제 인플라이트 게이지 위에서 PrefixAwareHash CHWBL 배치를 구동하며, KV 캐시 인덱스가 함께 구성된 경우 시작 시KvOverlapScorer가 등록됩니다. 트래픽 분배 동작 변경: 기본값이 아닌selection_strategy를 설정한 배포에는 이제 그 전략이 실제로 적용되고, 기본 배포는 '항상 첫 번째 후보 백엔드'에서 모델별 정상 후보들에 대한 실제 라운드로빈으로 바뀝니다. - 백엔드별 인플라이트 추적을 실제 요청 경로에 연결하여 CHWBL 부하 상한이 더 이상 무력하지 않도록 수정했습니다(#971).
BackendStats.in_flight_requests에는 프로덕션 기록 지점이 없어PrefixAwareHash셀렉터의 bounded-load 절반이 항상 0인 값으로 상한을 계산했고, 한 번도 바인딩될 수 없었습니다. 이제 백엔드 풀이 항상 컴파일되는 RAII 인플라이트 트래커를 소유하고 모든 디스패치 지점(채팅 프록시, 스트리밍 채팅, Responses API, Anthropic Messages와 count_tokens, 이미지 생성, 이미지 편집)에서 기록하며, 스트리밍 가드는 응답 본문에 실려 마지막 프레임 또는 클라이언트 연결 종료까지 유지됩니다. 상한 공식은 배치하려는 요청까지 포함하는ceil((total_in_flight + 1) * (1 + epsilon) / backend_count)로 바뀌었고,prefix_routing.load_factor_epsilon(기본값 0.25)이 마침내 풀까지 전달되어 문서에 적힌 설정값이 실제로 적용됩니다. 이 게이지가 단일 기준이 되어 관리자prefix-routing/stats엔드포인트와 컨트롤 플레인 인벤토리의 백엔드별active_requests가 같은 값을 읽으며, #968이 만들었던 병렬 컨트롤 플레인 게이지를 대체하고 0 대신 실제 부하를 보고합니다. - 적용 범위 참고: 이번 변경은 CHWBL 셀렉터가 읽는 게이지를 고친 것이지, 어떤 코드 경로가 그 셀렉터를 호출하는지를 바꾼 것은 아닙니다. 라우터의 HTTP 요청 경로는 여전히 해당 모델을 서빙하는 첫 번째 정상 백엔드로 디스패치하므로,
selection_strategy(CHWBL 포함)는 아직 실제 트래픽 분배에 관여하지 않으며 이 변경으로 라우팅이 달라지는 요청은 없습니다. 풀 셀렉터를 요청 퍼널에 연결하는 작업은 #975로 반영되었습니다(위 항목 참고). - Unix 소켓 통합 테스트가 멈추지 않도록 하고 CI에서 복구했습니다(#984, #992, #995).
v1.13.1 - 2026-07-10¶
추가됨¶
- Converse API를 통한 Amazon Bedrock 멀티 프로바이더 지원(#615).
type: bedrock백엔드의 새endpoint_type: converse는POST /model/{modelId}/converse[-stream]으로 요청을 보내 Bedrock의 모든 채팅 모델(Claude, Nova, Llama, Mistral, Cohere, AI21 Jamba, DeepSeek)을 하나의 통합 와이어 형식으로 사용합니다. OpenAI 형식 요청을 Converse 콘텐츠 블록으로 변환하고(시스템 프롬프트 추출,toolConfig/toolUse/toolResult,inferenceConfig샘플링 파라미터), #614의 공유 SigV4 서명 경로로 서명하며, 이진 ConverseStream 이벤트 스트림을 도구 호출/추론 델타를 포함한 OpenAI SSE로 다시 디코딩합니다. 하드코딩된 모델별 기능 매트릭스(채팅, 스트리밍, 도구, 비전, 문서, 가드레일, 프롬프트 캐싱, 추론)가 지원되지 않는 기능 요청을 서명 이전에 기능 이름을 담은 4xx로 거절하고, 벤더 전용 옵션은extra_body/additionalModelRequestFields로 그대로 전달되며, 원격 이미지 URL은 SSRF 검증과 크기 제한을 거쳐 base64로 인라인되고, Converse 네이티브 본문은 감지되어 이중 변환 없이 전달됩니다. 기존bedrock-sigv4Cargo 피처 뒤에 위치합니다. - 백엔드 수준
internal가시성 플래그(#908, #911). 백엔드에internal: true를 지정하면 그 백엔드가 서비스하는 인프라 전용 모델(가드레일 분류기, 내부 요약 모델, 임베딩 모델)이 사용자 대상 표면에서 빠집니다. 내부 백엔드 이름은 OpenAI/v1/models,/v1/models/extended, 단일 모델, Anthropic/anthropic/v1/models목록에서 제거되고, 키별 허용 목록보다 먼저 라우팅 후보군에서도 빠지므로 내부 전용 모델을 요청하면 오해를 부르는 Forbidden 대신 model-not-found(404)가 반환됩니다. 라우터 내부의backend:참조와 관리자 표면은 여전히 내부 백엔드를 해석합니다. 이 필드는 기본값이false이고 직렬화 시 생략되므로 기존 설정은 그대로입니다. - 컨트롤플레인 키 회전 겹침 구간에서 물러나는 허브 키 해시 수용(#906).
KeyEntry에 추가형 필드previous_key_hash와previous_retires_at_ms가 생겨, 라우터가 겹침 구간 동안 현재 해시와 이전 해시를 모두 받아들입니다. 이전 해시는 로컬 타임스탬프 시점이나 허브가 다음 동기화에서 해시를 생략하는 시점 중 먼저 오는 쪽에서 물러나며, 현재 해시가 이전 해시를 가리는 일은 없고 폐기(revocation)는 여전히 두 해시 모두를 거부합니다.control-plane피처 뒤에 위치하며, 예전 봉투(envelope)도 그대로 파싱되고PROTOCOL_VERSION은 0으로 유지됩니다. - 컨트롤플레인 usage 레코드에 스트리밍 TTFT(첫 토큰까지 시간) 기록, 부하 상황에서도 하트비트 인벤토리 신선도 유지(#912).
UsageRecord에 추가형ttft_ms가 생겼습니다.latency_ms와 동일한 요청 시작 시점부터 첫 스트리밍 바이트까지를 chat, thinking, Anthropic, Gemini SSE 루프에서 측정해latency_ms로 클램프하며, 비스트리밍·로컬 캐시 재생·배치 응답에서는 지어내지 않고 생략합니다. 하트비트 루프는 각 비트의 구성·전송 시간을 간격에서 빼므로 부하로 비트가 느려져도 허브의 부하 샘플 간격을 넘어 주기가 밀리지 않고, 모든 비트는 여전히 전체 라우터 인벤토리를 담습니다.control-plane피처 뒤에 위치하며PROTOCOL_VERSION은 0으로 유지됩니다.
변경됨¶
- 공식 릴리스 바이너리에
control-plane과appproxy-router피처가 컴파일되어 함께 배포됩니다(#905). Release 워크플로가 6개 타깃 모두를--features control-plane,appproxy-router로 빌드하고 Debian 패키지와 Docker 이미지가 그 바이너리를 재포장하므로 모든 배포 채널이 이 피처를 담습니다. 두 피처 모두 크레이트default/full세트에는 들어가지 않으므로 소스에서cargo build로 빌드하면 바이트 단위로 동일하고, 패키지 바이너리의 경우 옵트인은 런타임 설정(control_plane.enabled, 기본값false, 그리고 기본적으로 없는appproxy_router섹션)으로 옮겨가므로 기존 배포의 동작은 바뀌지 않습니다. 레거시appproxy/appproxy-legacy워커는 소스 빌드 옵트인으로 남습니다.
수정됨¶
- 기본 피처
cargo clippy -- -D warningsCI 단계를 모든 브랜치에서 붉게 실패시키던 clippy 린트 드리프트를 해소했습니다(#907). Anthropic Gemini 변환과 Bedrock reasoning 변환에서 clippy가 제안한 기계적 재작성 두 건이며 동작 변화는 없습니다.
v1.13.0 - 2026-07-09¶
추가됨¶
- Continuum Hub 컨트롤 플레인의 정책 집행과 최적화를 추가하여, opt-in 에이전트를 아웃바운드 텔레메트리(v1.12.0)에서 완전한 정책 계층으로 승격했습니다. 모든 기능은
control-plane피처 뒤에 있고 정책이 로드되지 않으면 fail-open하므로, 기본 빌드는 의존성도 런타임 비용도 늘지 않고 바이트 단위로 동일합니다. - 허브와 동기화된 키 테이블, 티어 레이트 리밋(rpm과 캐시 인식 입력/출력 TPM), 월간 예산, 모델 허용 목록을 post-auth 미들웨어로 집행하고
x-continuum-ratelimit-*헤더를 내보냅니다. 추가적인KeyUsageSnapshot프로토콜 타입과 poll/WebSocket 정책 동기화를 ArcSwap 스토어로 받습니다(#877). - 정확 일치 응답 캐시를 티어
cache_enabled와 조직 최적화 정책으로 게이트하고,x-continuum-cache·x-continuum-batch요청별 오버라이드 헤더를 티어 범위 안에서 강제하며(off가 항상 우선, on은 티어를 넘지 않음),x-continuum-cachehit/miss/bypass 결과 헤더를 내보내고, 로컬 캐시 히트를 토큰 예산을 차감하지 않는 메타데이터 전용 이벤트로 계측합니다(#878). - usage 레코드에
CacheHitType(exact/prefix/semantic과 개방형 unknown)을 이미 계측 중인cache_hit·cached_input_tokens·batched·provider_batch_id필드와 함께 기록합니다. serde 기본값을 갖는 추가 필드이며PROTOCOL_VERSION은 그대로입니다(#879). - 배치 가능한 트래픽을 OpenAI 방식과 Anthropic Message Batches 엔드포인트로, 동기 rpm/tpm 한도를 절대 소비하지 않는 별도의 배치 레이트 풀(
batch_rpm과 인플라이트batch_queue_depth게이지)에서 디스패치하고, 각 provider 작업의 수명주기를 at-least-once·404 허용BatchStatusUpdate메시지로 허브에 보고하며, per-key 소유권 게이트와 나이 기반 회수 백스톱을 갖춘 완전한 키 범위/v1/batches표면(create·retrieve·list·cancel·results)을 노출합니다.control_plane.batch를 켜지 않으면 비활성입니다(#880, #895). - OpenAI chat과 Anthropic Messages 비스트리밍에서 정확 일치 캐시와 분리된 키스페이스로 프롬프트 접두어 응답 캐시를 실행합니다. 프롬프트를 결정하는 필드는 유지하고 샘플링에 묶인 꼬리는 버리며, 자연 종료된 응답만 저장하고 저장 항목이 새 요청의
max_tokens에 맞을 때만 서빙하도록 보호합니다(#895). - 가격 차익(
x-continuum-arbitrage, 티어arbitrage_enabled, tri-stateequivalence_classes)으로 가장 저렴한 동등 provider를, 헬스·서킷 브레이커·지연·티어·요청별 헤더 게이트를 모두 통과한 뒤에만 선택하고, 이후 선택 패스가 정책 결정을 뒤집지 못하도록 디스패치를 선택된 백엔드로 제약합니다(#896, closes #891). - 관리 대상 요청의 컨텍스트를 라우터 측에서 압축합니다(
x-continuum-compression,TierLimits.compression_enabled,CompressionPolicy). responses-only 라우팅, 스트리밍, 캐시 조회/저장, 디스패치 전에 채팅 이력을 결정적으로 줄이되 선두의 system/developer와 최근 턴은 보존하며, 내용을 허브에 노출하지 않고compressed·uncompressed_input_tokens를 보고합니다(#898, closes #892). - 배치 가능한
/v1/batches제출을 다음으로 일치하는 UTC 라우팅 윈도(PolicyEnvelope.routing_windowstri-state, 요일 마스크, 티어 범위, 끝 배타적·자정 넘김 경계)가 열릴 때까지 지연시키되 배치 승인은 디스패치 시점에 확인하여 요청이 대기하는 동안 풀 슬롯을 잡아두지 않으며, 라우터 인벤토리 heartbeat에 정수 전용LoadSummary요청/오류율/p50/p99 카운터와 자문용OptimizationPolicy.load임계값을 추가합니다(#900, closes #893). - 컨트롤 플레인 경로에서 허브 정책에 따라 요청 모델을 치환하며, 치환과 차익 메타데이터는 상호 배타로 유지합니다(#894).
- 허브가 전달한 조직 범위 provider 자격증명(
PolicyEnvelope.provider_credentials, 허브 M3 vault)을 로컬 설정api_key대신 사용하여, 재시작이나 인플라이트 요청 중단 없이 회전을 적용합니다. 비밀값은 메모리에만 있고 모든 Debug 출력에서 마스킹되며, 모든 provider 디스패치 전송 지점(proxy, streaming, images, Responses, batch, 네이티브 Anthropic, count_tokens, embeddings)에서 값만 해석되므로 auth 헤더 존재 여부와 OAuth/SigV4/Bedrock 게이트는 바뀌지 않습니다(#901). - 허브 정책 집행과 usage 귀속을 네이티브 Anthropic Messages 경로(스트리밍·비스트리밍, native·OpenAI-bridge·Responses-bridge·Bedrock-runtime 하위 경로)와 스트리밍
/v1/responses로 확장하여, Anthropic 네이티브x-api-key자격증명을 받아들이고 캐시 인식 토큰 사용량을anonymous가 아닌 허브key_id로 과금합니다(#882, #889).
수정됨¶
- 계층형 KV 캐시 이벤트 라우팅을 하드닝했습니다. 설정된 KV 이벤트 소스를 시작 시 인덱스에 연결하고, 이벤트가 스토리지 티어만 바꿀 때 기존 어피니티 점수를 보존하며, 오프로드/리로드/퍼지 이벤트 전체를 메트릭과 admin 상태로 노출하고, 분리형 fast-decode 실행을 KV 인덱스에서 선택된 decode 백엔드로 제약하며, 오케스트레이션 중 per-client 백엔드 허용 목록을 준수하고, 2단계 워커 프로토콜을 사용할 수 없을 때 거짓 성공 prefill/decode 헤더 대신 service-unavailable 오류를 반환합니다(#903).
- 에픽 864 가드레일 핫 리로드 동작을 설정 validator와 가드레일 서비스 전반에서 하드닝했습니다(#864).
- 프리픽스 캐시 히트 계측 호출 지점에 모델 치환 인자를 연결하여, #894와 #895가 서로 다른 베이스에서 깨끗하게 병합된 뒤
--features control-plane로 트리가 컴파일되도록 했습니다(#897).
문서¶
- AppProxy 워커 모드 레퍼런스(영문·국문)를 새 ROUTER 프론트엔드 중심으로 재설계했습니다. 워커당 단일 바인딩 주소, 클러스터 수준 모델 추상화, 2단계 계층 라우팅, 매핑의 event+pull 전파와 해시 전용 키 관리, BEP-1053 상호 참조를 담았습니다(#748).
- 두 릴리스만큼 뒤처져 있던
docs/en·docs/ko변경 내역 미러에 v1.11.0과 v1.12.0을 소급 반영했습니다. - 완화된 drift 가드에 맞춰 config-assistant 동기화 노트를 바로잡고 CHANGELOG en/ko 미러 요구 사항을 명시했습니다.
의존성¶
- minor-and-patch 그룹을 2건 갱신했습니다(#883):
rand0.10.1 → 0.10.2,arc-swap1.9.1 → 1.9.2.
CI¶
- 스킬 문서 버전 drift 가드를 정확한
CARGO_PKG_VERSION일치에서 호환 범위 검사로 완화하여, 범위 안의 통상적 버전 상승은 손수 편집이 필요 없고 범위 상한을 넘는 메이저 상승은 여전히 빌드를 실패시켜 재검토하도록 했습니다.
v1.12.0 - 2026-07-05¶
추가됨¶
config.yaml을 작성하고 검증하는 설정 어시스턴트를 추가했습니다(에픽 #530).validate·generate·diff·show --resolved동사를 갖춘configCLI 하위 명령과configuration-assistant스킬, 설정 템플릿(#829).claude mcp add continuum-router-config -- continuum-router mcp-serve가 바로 동작하는mcp-servestdio MCP 서버 모드입니다.mcp피처 뒤에 있고default/full에 포함됩니다(#830).- IDE 규칙 생성기(
scripts/generate-ide-rules.sh)와 문서(#831), 그리고@lablup/continuum-router-mcpnpm 래퍼 패키지(#832). - 스키마 변경이 어시스턴트 문서나 템플릿 갱신을 빠뜨리면 빌드를 실패시키는 검증·CI drift 가드(#833)와 후속 하드닝(#834).
- opt-in
control-plane피처 뒤에 Continuum Hub 컨트롤 플레인 에이전트를 추가했습니다(#875, closes #874). 라우터를 허브에 등록하고 liveness와 라우터 인벤토리를 heartbeat하며, 메타데이터만 담은 usage 레코드를 배치로 밀어 올리는 아웃바운드 전용 작업입니다.default/full에 없으므로 기본 빌드는 의존성도 런타임 비용도 늘지 않습니다. 유일한 새 의존성은 피처를 켰을 때만 컴파일되는 vendored serde 전용continuum-protocol크레이트입니다. - Backend.AI AppProxy ROUTER 워커 모드를 추가했습니다(에픽). ROUTER wire 타입·이벤트·워커 설정(#815), 노드별 applied-ack를 갖춘 ROUTER 이벤트 오버레이(#816), circuit과 매핑을 백엔드로 조정하는 로직(#817), 라우터 설정을 당겨오는 coordinator 클라이언트(#818),
allowed_models기반 모델 단위 API 키 게이트(#819), ROUTER 워커 수명주기와 설정 연결·피처 플래그(#820)를 포함합니다. - Claude Sonnet 5를 지원합니다(#857).
streamGenerateContent를 통한 네이티브 스트리밍 Gemini Responses 변환을 추가했습니다(#856).- 가드레일 provider 두 개를 추가했습니다.
backend:접두어로 서비스 중인 백엔드를 가드레일 모델로 참조하는 provider(#871)와, 프롬프트로 정의한 JSON 판정을 반환하는custom_classifierprovider(#870)입니다. - 내부 미러를 위해 web-search provider의 base URL을 설정할 수 있게 했습니다(#836).
- 인프로세스 iOS 임베딩을 위한 cli 없는 라이브러리 빌드를
embed피처 뒤에 추가했습니다(#850).continuum_router::serve_embedded(config, addr)를 노출합니다.
변경됨¶
- 두 백엔드 연결 풀을 단일 트레이트 기반 풀로 통합하고(#851, refs #812) 레거시
Backend구조체의 이름을PooledBackend로 바꿨습니다(#843, refs #511). - 이중 스트리밍 경로를 하나로 모았습니다. SSE 파이프라인 관심사를 조합 가능한 stream 미들웨어로 추출했고(#839), Gemini 스트리밍을
Backend트레이트로 라우팅했으며(#840), 중복된 Anthropic 스트리밍 변환을 합치고(#841), OpenAI reasoning 정규화를 스트리밍 경로에 적용했습니다(#842). - 가드레일용 공유 LLM-백엔드 전송 코어를 추출하고(#869), alloc 없는 family 조회를 갖춘 네이티브 Anthropic system 게이트를 추가했습니다(#861).
- 현재 AppProxy 워커를
legacy(appproxy v3)로 옮기고 공유 common 코어를 추출했습니다(#813).
수정됨¶
- 컨트롤 플레인 usage 푸시에서 usage attribution 메타데이터를 보존했습니다(#876).
/v1/responses스트리밍 요청을file_id해석 전에 검증하여, 잘못된 요청이 해석 도중 실패하는 대신 올바른 오류로 거부되게 했습니다(#873). 스트리밍 경로에서도 키별 allow-list를 존중합니다(#863).- Anthropic capability 게이팅을 위해 Bedrock 모델 접두어를 정규화했습니다(#859).
- 쿼리 문자열이 붙은 Brave
web_searchbase_url을 거부합니다(#852). - 네이티브 Gemini Responses 오디오·이미지 변환을 강화하고(#853, #780 후속)
input_audio에 네이티브 Gemini Responses 변환을 연결했습니다(#835). - 에픽 505 스트리밍 프롬프트 패리티(#846)와 에픽 530 설정 어시스턴트(#834), AppProxy ROUTER 요청 게이팅(#823)을 강화했습니다.
- 일별 series 정리·읽기 테스트를 벽시계에 고정하여 더 이상 흔들리지 않게 했습니다(#814).
문서¶
- SSE 스트리밍 파이프라인 오버헤드를 재현 가능한 벤치마크로 문서화하고(#848) 두 스트리밍 경로 사이의 차이를 감사했습니다(#837).
- 백엔드 변환 일관성 통합 테스트를 추가했습니다(#838).
의존성¶
- minor-and-patch 그룹을 1개 디렉터리에서 3건(#845), 그리고 2건(#802) 업데이트했습니다.
actions/cache를 5에서 6으로(#844),actions/checkout을 6에서 7로(#801) 올렸습니다.
v1.11.0 - 2026-06-21¶
추가됨¶
- 콘텐츠 안전 가드레일 서브시스템을 추가했습니다(에픽 #644). 기본값이 off인
Option<GuardrailsConfig>로 설정을 게이트하므로guardrails키가 없으면 기능이 꺼진 채로 남고 기존 설정은 영향을 받지 않습니다. - 기반: 입력·출력·스트리밍 검사 단계를 갖춘 object-safe async
Guardrail트레이트, 가장 위험한 결과가 이기는 심각도 순서의GuardrailVerdict(Allow/Block/Transform/Flag), 빌림 기반GuardrailContext, 그리고 전역monitor/enforce모드와 env로 자격 증명을 받는 provider·경로별 override·API 키별 우회·가드레일별timeout_ms와on_errorfail-open/fail-closed·차단 동작·스트리밍 모드·exact/regex allow/deny 목록을 담은GuardrailsConfig스키마를 정의했습니다. 검증기는 provider 없는enforce,[0,1]을 벗어난 임계값, 컴파일되지 않는 regex, 0인 timeout 같은 잘못된 조합을 거부합니다(#779, closes #644). 이 검증기는 실제 설정 로드 경로에 연결되어 잘못된guardrails블록을 로드 시점에 거부합니다(#781). - 서비스: 가드레일별 timeout과 fail-open/fail-closed 처리, hot-reload, 차단 응답 빌더를 갖춘
GuardrailService파이프라인을 추가했습니다(#782). - factory 뒤의 provider: OpenAI Moderation(#783), self-host 분류기(#785), PII 탐지·삭제(#786), AWS Bedrock과 Azure 클라우드 가드레일(#787).
- 게이팅: chat·Anthropic·responses 디스패치 경로에 입력 게이팅을 연결하고(#788), 비스트리밍 응답 경로에 출력 게이팅을(#789), buffer/chunked/passthru 모드로 스트리밍 출력 게이팅을(#790) 붙였으며, 디스패치 핸들러에 경로별 정책을 연결해 요청 모델 id를 키로
guardrails.routes[<model>]override(모드, enable/disable, provider 부분집합, 임계값)가 끝까지 적용되게 했습니다(#796). - 제어·관측: 가드레일 정책 런타임 제어(#784), Prometheus 지표와 결정별 감사 로깅(#792), 입력·출력 게이팅 종단 통합 스위트(#791), 아키텍처와 지표를 다루는 가드레일 가이드와 사이트 내비게이션(#793)을 추가했습니다.
- admin stats API에 모델별·일별 usage-series 차원을 추가했습니다(#799, closes #798).
GET /admin/stats/api-keys/{id}/models와GET /admin/stats/users/{user_id}/models는 모델별 토큰·요청 분해를 반환하고,.../{id}/series와.../{user_id}/series는 UTC 완료 날짜를 키로total_requests·prompt_tokens·completion_tokens·total_tokens의 오름차순 일별 시계열을 반환합니다(from/to는 Unix 밀리초 또는 RFC 3339를 받고,interval기본값은day이며 다른 값은400을 반환합니다). id·모델·날짜에 나타날 수 없는U+001F구분자로 합성 키를 만든 카디널리티 제한DashMap네 개가 이 차원을 뒷받침하고,series_retention_days옵션(기본 30, serde 기본값)이 컷오프보다 오래된 일별 버킷을 정리하며, 네 맵은#[serde(default)]로 스냅샷에 추가되어 이 변경 이전에 기록된 스냅샷도 그대로 로드됩니다.
수정됨¶
- 일별 series 정리 수명주기를 강화했습니다(#800, refs #799). 이제 restore가 런타임 삽입 경로에서 비예약으로 취급하는
anonymous일별·모델 버킷을 포함해 구분자를 가진 모든 합성 키를 세므로, 재시작 후 만료 버킷을 정리할 때 예약된 적 없는 슬롯을 감소시켜AtomicUsize를 언더플로하고 새 실버킷을 오버플로 행으로 접어 넣던 문제가 사라졌습니다. 스냅샷 작업이 정리를 돌리지 않는 stats 영속화 off 상태에서는 전용 시간별 series 정리기가 생성되므로, 기본 설정은 만료된 일별 버킷을 카디널리티 상한까지 쌓지 않고 물리적으로 버립니다. /v1/responses트래픽의 admin stats를 기록했습니다(#794). 변환 실패의 stats 성공·실패 경계를 바로잡고 등록된 API 키별·사용자별 admin stats 엔드포인트의 회귀 커버리지를 강화했습니다.- 네이티브 Gemini·Anthropic 변환에서 OpenAI
input_audio콘텐츠 블록을 처리했습니다(#778). Gemini Responses 변환기는 이제 오디오를 형식에서 유도한mime_type(wav는audio/wav,mp3과 그 외는audio/mp3)의inline_data파트로 매핑하고data:URL은 기존 inline-data 경로로 보냅니다. Anthropic Messages API에는 오디오 콘텐츠 타입이 없으므로, 네이티브 변환은 블록을 조용히 버리거나 그대로 전달하는 대신 비스트리밍·스트리밍 진입점 모두에서ValidationError를 HTTP 400으로 표면화합니다.
의존성¶
- minor-and-patch 그룹을 5건 업데이트했습니다(#769):
uuid1.23.2에서 1.23.3로,regex1.12.3에서 1.12.4로,redis1.2.2에서 1.2.3로,aws-smithy-eventstream0.60.20에서 0.60.21로,aws-smithy-types1.4.9에서 1.5.0로.
v1.10.2 - 2026-06-15¶
추가됨¶
- Kimi K2.7-Code와 GLM-5.2 모델 메타데이터를 추가했습니다 (#775). Kimi K2.7-Code는 MoonViT 비전 인코더와 256K 컨텍스트를 갖춘 1조 파라미터 MoE(활성 32B)입니다. thinking 모드로만 동작하고 추론을 네이티브
reasoning_content필드로 반환하므로<think>마커 설정을 두지 않습니다. GLM-5.2는 GLM-5 계열의 코딩 플래그십으로 1M 컨텍스트 윈도우와 131K 최대 출력, 두 단계의 thinking-effort(High, Max)를 지원합니다. GLM 계열과 마찬가지로 표준<think>/</think>마커 설정을 둡니다. GLM-5.2 단독 API 가격은 출시 시점에 공개되지 않아 입력·출력 요금을 GLM-5/GLM-5.1 티어에서 추정했습니다.
수정됨¶
- vLLM/OpenAI 호환
reasoning필드를/v1/chat/completions에서 정규reasoning_content로 정규화했습니다 (#776, closes #774). 최신 vLLM이 추론 출력 필드를reasoning_content에서reasoning으로 바꾸었습니다(스트리밍delta.reasoning, 비스트리밍message.reasoning). 라우터가 이를 그대로 중계하면서reasoning_content를 읽는 클라이언트는 self-host vLLM 추론 모델의 추론 텍스트를 조용히 잃었습니다. 정규화는 모든 OpenAI 호환 중계 경로에서 동작합니다. 스트리밍 기본·thinking 변환기, unix-socket 중계, mid-stream fallback 중계, 비스트리밍 프록시 본문이 대상입니다. 같은 객체에reasoning_content가 없을 때만 적용하므로 정규 이름을 이미 쓰는 업스트림은 덮어쓰지 않습니다. Gemini·Anthropic 핸들러와 Responses API에는 적용되지 않습니다.
문서¶
- reasoning-effort 아키텍처 문서(영문·국문)에 라우터가 업스트림 vLLM
reasoning필드를reasoning_content로 정규화한다는 설명을 추가했습니다.
v1.10.1 - 2026-06-15¶
추가됨¶
- API 키별·사용자별 사용량 통계 REST API를 추가했습니다. 관리자 인증 라우터 아래에 기존
/admin/stats/models와 같은 형태로GET /admin/stats/api-keys,GET /admin/stats/api-keys/{id},GET /admin/stats/users,GET /admin/stats/users/{user_id}네 개 엔드포인트를 둡니다 (#772, closes #770).StatsCollector에per_api_key와per_user차원이 추가됩니다. Prometheusllm_tokens_total카운터를 갱신하는 바로 그 스폰 태스크에서 함께 기록하므로 이 인메모리 차원은 더 이상metrics피처에 의존하지 않으며 실패한 요청이나 토큰이 0인 요청까지 집계됩니다.api_key_id는 요청 핫 패스 밖에서 한 번만 해석하는 파생된 비가역 식별자입니다. 인증되지 않은 요청은"anonymous"버킷으로 묶이고 차원당 1000개 상한을 넘는 식별자는"unknown"오버플로 버킷으로 묶여 전체 합계에는 그대로 반영됩니다.GET /admin/stats/.../{id}는 기록된 사용량이 없는 식별자에404를 반환합니다. 선택적window쿼리 파라미터는 응답에 그대로 돌려주지만 필터링하지는 않습니다. 집계가/admin/stats/models처럼 전체 기간 누적 카운터이기 때문입니다. 스냅샷·영속 형식은 두 차원을 모두#[serde(default)]로 추가하므로 이 변경 이전에 기록된 스냅샷도 형식 버전을 올리지 않고 그대로 로드됩니다.
변경됨¶
- 루트 디렉터리의
.sh스크립트를.gitignore로 무시해 로컬 헬퍼 스크립트가 실수로 커밋되지 않도록 했습니다.
문서¶
- 관리자 API 키 관리 엔드포인트와 API 키별·사용자별 사용량 통계를 영문과 국문 양쪽 관리자 REST API 레퍼런스에 문서화했습니다 (#773, closes #770). 새로 추가한 'API 키 관리 API' 절은 여덟 개
/admin/api-keys엔드포인트(생성·목록·조회·수정·삭제·회전·활성화·비활성화)를 다룹니다. 여기에는sk-***abcd마스킹과 생성 시 전체 값을 한 번만 노출하는 동작을 반영한 요청·응답 스키마,ApiKeyConfig필드,permissive와blockingapi_keys.mode동작,persistence_file을 통한 런타임 키 영속과 핫 리로드 설명이 들어갑니다. 통계 절에는 네 개의 새 통계 엔드포인트,window에코,anonymous와unknown버킷, 차원당 상한,api_key_id와 발급된 키id의 연결을 추가했습니다.
v1.10.0 - 2026-06-12¶
추가됨¶
- Codex(ChatGPT OAuth) 백엔드의 동적 모델 열거를 추가했습니다 (#753, closes #752). Codex 백엔드에는 표준
/v1/models가 없으므로, 라우터가 모델 검색 시 로드된 OAuth 토큰으로 플랜에 따라 게이팅되는GET <base>/models?client_version=<ver>엔드포인트를 조회하고, 사용자에게 노출되는 항목(visibility: list이며available_in_plans가 비어 있거나 계정의chatgpt_plan_type클레임과 일치)만 남겨/v1/models응답과 라우팅을 채웁니다. 네트워크 오류, 2xx 아닌 응답, 빈 목록 등 어떤 실패에도 설정된models:목록이 폴백으로 남으며, 이 동작은 Codex OAuth 백엔드에만 적용됩니다.
변경됨¶
src/services/responses/sse.rs에 공유parse_responses_body를,src/http/streaming/handler.rs에 공유build_openai_chat_request_core를 추출해, Codex, 패스스루, Anthropic 핸들러 변형에 걸쳐 중복돼 있던/responsesSSE 집계 파싱 경로와 스트리밍 요청 구성 로직을 제거했습니다 (#766, closes #762). 동작 변화는 없으며, 한 사본의 수정이 다른 사본에 전파되지 않는 드리프트 위험을 없앴습니다.
수정됨¶
- Codex(ChatGPT OAuth) 채팅 완성을 백엔드의
/responses엔드포인트로 라우팅합니다 (#755, closes #754).build_responses_url이.../backend-api/codex루트에/v1/responses를 덧붙였는데, 업스트림 엣지가 그 경로를 403 HTML 페이지로 거부했고 라우터는 OAuth 토큰이 유효한데도 이를 불투명한 인증 오류로 노출했습니다. 이제 Codex 루트는 내부/v1을 제거하는 공유 URL 컴포저를 거칩니다. 경로가 고쳐진 뒤에는 Codex가 문자열input을 HTTP 400 "Input must be a list"로 거부하므로,sanitize_codex_responses_request가 단일 메시지의 문자열input을 한 개짜리 아이템 리스트로 변환합니다. - OAuth 토큰 저장소에서 RFC3339 형식의
expires_at을 받아들입니다 (#756, closes #751). 이 필드가 단순u64였기 때문에 날짜 문자열로 기록된 토큰 저장소는 역직렬화에 실패했고, 백엔드가 조용히 OAuth 전략을 잃은 채 인증 없이 요청을 보내 실제 원인이 가려진 불투명한 403으로 드러났습니다.expires_at은 이제 정수·실수 epoch 초, 숫자 문자열, 오프셋 포함 RFC3339 날짜를 모두 epoch 초로 정규화해 받아들이고, 그 외 값은 허용 형식을 명시한 오류로 거부합니다. 디스크의 표준 형태는 JSON 숫자 그대로이므로 기존 저장소와 저장/로드 왕복은 변하지 않습니다. - 클라우드 OpenAI로 향하는
/v1/chat/completions요청에서 로컬 엔진 전용 필드(chat_template_kwargs,thinking_budget_tokens,enable_thinking,preserve_thinking,top_k,min_p,repeat_penalty)를 제거합니다 (#760, closes #758). 클라우드 OpenAI는 알 수 없는 최상위 키를 HTTP 400 "Unknown parameter"로 거부합니다. 이 제거는api.openai.com을 게이트로 하는 공유field_filter모듈을 통해 기존 클라우드 Gemini 동작을 그대로 따르고, 클라우드 OpenAI로 채팅을 보내는 모든 지점(비스트리밍, 스트리밍, 폴백 루프의 각 홉)에서 실행되며,extra_body와reasoning_effort는 절대 건드리지 않고, 로컬 OpenAI 호환 엔진에는 아무 동작도 하지 않아 백엔드 패스스루 계약을 보존합니다. - Codex 동적 열거에서 설정된
models:선택을 존중하고 열거된 모델에 정리된owned_by를 기록합니다 (#765, closes #763). 라이브 열거가 성공하면 운영자가 설정한models:허용 목록이 무시되어/v1/models가 열거된 Codex 모델을 전부 노출했는데, 이제 후처리 필터가 일관되게 적용되어 비어 있지 않은 목록은 열거 결과를 선택한 부분집합으로 좁히고 빈 목록은 전체를 노출합니다. 열거된 Codex 항목의owned_by에 원시 백엔드 이름이 들어가던 문제도 백엔드 타입에서 해소한 값(openai)으로 바로잡아 다른 모든 백엔드와 일치시켰습니다. - Codex(ChatGPT OAuth) 백엔드에서
stream:false로 들어온/v1/chat/completions요청에 올바른 비스트리밍 응답을 반환합니다 (#764, closes #761). Codex의/responses엔드포인트는stream:true+store:false만 받아들이므로stream:false를 보내면 HTTP 400 "Stream must be set to true"가 발생했습니다.sanitize_codex_responses_request가 이제stream:true와store:false를 무조건 강제합니다. 두 번째 수정으로PassthroughService::execute_request의 SSE 감지가Content-Type: text/event-stream없이 SSE 본문을 보내는 Codex 응답에도 대응합니다.looks_like_sse스니퍼가 첫 비어 있지 않은 줄의data:/event:/:접두어를 확인하고, JSON 파싱이 실패하면 원래 파싱 오류를 드러내기 전에 SSE 집계를 한 번 재시도합니다. - Codex
store:false응답에서response.output_text.delta이벤트로부터 어시스턴트 메시지를 재구성합니다 (#768, closes #767). Codex가store:false로 동작하면 종료response.completed이벤트의output배열이 비어 있고 어시스턴트 텍스트는 증분response.output_text.delta이벤트로만 전달됩니다. 이제 SSE 집계가 그 델타를 누적하고, 완료 이벤트에 어시스턴트 텍스트가 없으면 메시지 아이템을 합성해, 비스트리밍 클라이언트(제목/요약 유틸리티 경로 포함)가 빈 응답 대신 텍스트를 받습니다.
문서¶
- v1.9.1 이후의 Codex 및 전송 계층 변경을 영문·국문 매뉴얼에 문서화했습니다. Codex Chat→Responses 요청 처리와 느슨한
expires_at파싱(backends.md), 클라우드 OpenAI 필드 제거(backend-passthrough.md), Responses API의item_reference해소(api.md)를 다루고, 남은 국문 페이지를 정중한-습니다체로 통일해 모든 페이지가 한 목소리로 읽히게 했습니다.
v1.9.1 - 2026-06-11¶
추가됨¶
{ "version": "<CARGO_PKG_VERSION>" }를 반환하는GET /version엔드포인트를 추가했습니다.appproxy를 비롯한 어떤 Cargo 피처에도 묶이지 않고 베이스 라우터에 무조건 등록되므로 표준 릴리스 바이너리에 항상 포함됩니다. 기존GET /health응답에도version필드를 더했습니다 (#750, closes #749). 두 엔드포인트 모두/health와 마찬가지로 API 인증 경계 밖에 있어 다운스트림 소비자가 404에 막혀 fail-open으로 넘어가는 대신 실행 중인 라우터 버전을 확인해 기능을 게이팅할 수 있습니다.
v1.9.0 - 2026-06-10¶
추가됨¶
- 옵트인
appproxyCargo 피처로 AppProxy 워커 모드를 추가해, Continuum Router가 AppProxy 코디네이터의 제어를 받는 Backend.AI AppProxy 추론 워커로 동작할 수 있게 했습니다 (에픽 #709). 이 피처는 일부러full에서 제외해 기본 빌드에는 영향이 없습니다. - 기반: 타입이 정의된
AppProxyWorkerConfig섹션(코디네이터 URL, 공유api_secret/jwt_secret,redis_url, 와일드카드 프런트엔드 파라미터, heartbeat/reconcile 주기, 이벤트 토글로 구성하며 두 bearer 시크릿은Debug에서 가려지고${ENV_VAR}참조는backends[].api_key와 같은 경로로 해소됩니다),SerializableCircuit/RouteInfo와이어 타입과ProxyProtocol/AppMode/FrontendModeenum(snake_case와 kebab-case를 모두 허용하고 알 수 없는 필드는 무시), 그리고 모듈 골격을 마련했습니다 (#716). - 코디네이터 REST 클라이언트
CoordinatorClient를 추가했습니다.register,heartbeat,deregister,list_circuits,get_circuit를 제공하며 각 호출마다X-BackendAI-Token과 매번 새로 발급한X-BackendAI-RequestID를 싣고, 재시도 가능 오류(연결, 타임아웃)와 치명적 오류(HTTP 4xx)를 구분하는 오류 타입을 갖습니다 (#717). - 서킷-백엔드 변환과 reconcile:
circuit_to_backends가 레플리카마다BackendConfig하나를 만들고(이름은appproxy-<circuit_id>-r<route_key>, traffic_ratio는 라우트를 0으로 떨어뜨리지 않는1..=1000가중치로 매핑,runtime_variant로 vLLM 감지),apply_circuits가 변환된 백엔드를 기존 핫리로드config_sender로 주입합니다.appproxy-접두사로 네임스페이스를 두어 정적 설정과 Admin API 백엔드는 보존됩니다 (#718). - 워커 수명주기 서비스와
/status엔드포인트:run_worker가 백오프와 함께 등록한 뒤 초기 서킷 풀을 수행하고, 이어 heartbeat 루프(코디네이터의 30초 LOST 타임아웃 안으로 유지)와 pull-reconcile 루프(놓친 이벤트를 받쳐 주는 상시 백스톱)를 돌립니다. 각 서킷의 모델은 레플리카의GET /v1/models에서 자동 발견하고, 종료 시 등록을 해제합니다. 공유 인메모리AppProxyRegistry는 서브도메인과 서킷 id로 색인됩니다 (#719). - 매니저가 발급한 엔드포인트 서브도메인을 구체적인 서킷으로 해소하고 요청 모델을 고정해, 기존 선택 경로가 그 서킷의 레플리카를 그대로 서빙하게 하는 Host/서브도메인 인그레스 리졸버를 추가했습니다. 디코드된
id를 서킷 id와 대조하고alg:none다운그레이드를 거부하는 HS256 서킷 bearer 검증, 교차 서킷 모델 집계를 위한 선택적aggregation_hosts필드, 그리고wildcard_domain이 설정되지 않았을 때의 순수 통과 동작을 포함합니다 (#720). - 레거시 모드 코디네이터에 1초 미만의 서킷 갱신을 주는 Redis Pub/Sub 서킷-이벤트 오버레이: 지수 백오프 재연결을 갖춘 구독 루프, base64 + msgpack 봉투 코덱,
circuit_created/circuit_removed/circuit_route_updated핸들러, 그리고 코디네이터의 E10001 "Proxy worker not responding" 오류를 막는 ack 봉투를 추가했습니다 (#721). - Claude Fable 5(
claude-fable-5)와 Mythos 5(claude-mythos-5) 모델을 지원합니다 (#747). 둘 다 1M 컨텍스트, 최대 출력 128K, MTok당 \(10/\)50 가격이며, Mythos 5는 안전 분류기를 제거한 동일 모델로 한정 공개인 Project Glasswing 릴리스로만 제공됩니다. 새is_mythos_class헬퍼가 두 id를 Anthropic 능력 게이트로 라우팅합니다. adaptive thinking이 필수이고(레거시budget_tokens는 HTTP 400으로 거부되며 adaptive로 정규화),temperature/top_p/top_k는 제거되며,maxeffort 레벨을 지원하고(xhigh는max로 매핑), 대화 중간 system 메시지를 보존합니다. 두 모델 모두 명시적thinking.type == "disabled"를 거부하므로,explicit_thinking_for_model이 이제Option을 반환해 400을 유발할 값을 전달하는 대신 thinking 파라미터를 아예 생략합니다.maxeffort 레벨이 더 이상 Opus 전용이 아니므로opus_supports_max_effort는supports_max_effort로 이름을 바꿨습니다. 같은 처리가 OpenAI Responses API 변환 경로에도 적용됩니다. - Gemma 4 QAT 다섯 개 양자화 인식 학습(quantization-aware-training) 체크포인트(E2B, E4B, 12B Unified, 26B-A4B MoE, 31B dense)의 모델 메타데이터를 추가했습니다. 동작에 필요한
-it-qat별칭과 해소/드리프트 가드 테스트를 포함합니다 (#723, #722 종료). - Gemma 4 12B Unified 모델 메타데이터를 추가했습니다 (#705).
변경됨¶
- 요청 본문 추출기의 거부를
warn수준으로 로깅해, 핸들러 실행 전에 Axum이 거부하는 잘못되거나 과대한 본문이 조용히 실패하지 않고 로그에 드러나도록 했습니다 (#707).
수정됨¶
- 일시적인 속도 제한과 비일시적인 쿼터/크레딧 소진을 구분하고 업스트림
Retry-After힌트를 존중해, HTTP 429에서 재시도 루프가 같은 업스트림을 연타하지 않게 했습니다 (#742, #740 종료). 이전에는 모든 429가 공급자의Retry-After를 무시한 고정 지수 백오프로 최대max_attempts(기본값 3)까지 재시도되어, 단일 업스트림이 제공하는 모델에서는 라우터가 이미 소진된 같은 엔드포인트를 다시 때리며 부하를 키우고 피할 수 없는 실패 앞에 지연만 더했습니다.RouterError::RateLimited변형이 이제retryable플래그를 가집니다. 비일시적 429(OpenAIinsufficient_quota/billing_hard_limit_reached, 그리고 "prepayment credits are depleted" 같은 명확한 크레딧 소진 문구)는 재시도 불가로 분류되어 라우터가 호출 한 번 만에 빠르게 실패하고 공급자의 상태 코드와 본문을 그대로 전달하며, 일시적 신호(순수RESOURCE_EXHAUSTED/RPM 스로틀링,rate_limit_exceeded,rate_limit_error)는 재시도 가능으로 남습니다. 분류기는 일부러 좁게 두었습니다. Google이 일시적 스로틀링에 그대로 재사용하는 지나치게 광범위한 "billing"과 "exceeded your current quota" 마커를 제거해, 회복 가능한 Google 429가 더 이상 빠른 실패로 뒤집히지 않습니다. 재시도되는 429는 백오프에 업스트림Retry-After를 사용하고(max_delay로 캡), 요청된 간격이 남은 전체 타임아웃 예산을 넘으면 잠들지 않고 즉시 실패합니다. 힌트는 끝까지 보존됩니다(Google의RetryInfo.retryDelay와 정수 초 형식Retry-After헤더에서 파싱해 클라이언트로 향하는Retry-After헤더에 반영). 예산 프로브는 이제saturating_add를 쓰고 파싱된 힌트는 24시간(MAX_RETRY_AFTER_SECS)으로 클램프해, 적대적 업스트림이u64::MAX에 가까운Retry-After로Duration덧셈을 오버플로시켜 요청 태스크를 중단시키던 원격 트리거 가능 패닉을 막습니다. - 스트리밍 변환 경로(Anthropic, Chat-Completions/Gemini 폴백)에서 누적된
/v1/responses응답을 저장해, 스트리밍된 출력 항목을{"type":"item_reference","id":"item_..."}로 참조하는 후속 요청(OpenAI 및 Vercel AI SDK의 기본 동작)이 1단계에서store:true를 썼더라도 HTTP 400 대신 정상 해소되게 했습니다 (#746, #745 종료). 완료된 응답은 첫response.completed이벤트가 클라이언트에 도달하기 전에 저장됩니다.response.completed를 내보내지 않는 오류 종료 스트림은 비스트리밍 오류 경로와 마찬가지로 저장하지 않으며, 패스스루 스트리밍은 업스트림이 저장을 소유하므로 변경 없습니다. - 전략 dispatch 전에
item_referenceinput item을 해소해, 인라인 항목 대신item_reference를 보내는 멀티-스텝 도구 호출에서/v1/responses가 Anthropic/Claude 백엔드에 더 이상 HTTP 400을 반환하지 않게 했습니다 (#743, #741 참조). 참조는 인라인FunctionCall/Message/FunctionCallOutput항목으로 재작성되고(같은call_id는 first-wins로 중복 제거),build_context_for_user는 저장된 함수 호출 출력 항목을 올바른 tool_use/tool_result 쌍으로 재구성하며, OpenAI/Azure 패스스루 경로는 참조를 그대로 전달합니다. 해소할 수 없는 참조는 id를 명시한 설명적400을 반환하고, 256개 상한(MAX_ITEM_REFERENCES)이 요청당 세션 스토어 스캔 비용을 제한합니다. server.workers를 Tokio 런타임에 전달합니다 (#736, #734 참조). 이 값은config.yaml.example에 문서화되고 출하됐지만,main이 인자 없는#[tokio::main]을 써서 런타임이 항상num_cpus::get()워커 스레드로 돌았기에 아무 효과가 없었습니다. 이제main은 동기 함수입니다. 설정 파일에서server.workers를 미리 읽어 기존RuntimeConfig::build_runtime경로로 적절한 크기의 멀티스레드 런타임을 만들고 그 위에서 비동기 본문을 실행하며, 값이 설정되지 않았거나 0이면 CPU 개수로 폴백합니다.- 패스스루가 아닌 공급자(Anthropic 및 chat-completions 기반 경로)에서
/v1/responses스트리밍이 규격을 따르는function_call출력 항목과 인자 이벤트를 내보내도록 했습니다. 텍스트 출력 페이로드를 보존하고, 교차된 병렬 도구 호출 인자를 업스트림 인덱스로 추적합니다 (#725). - Files API 라우트가 별도 스토어를 만드는 대신 공유
ApiKeyStore를 재사용하게 해, 런타임에서 관리하는 API 키가 Files API와 라우터의 나머지 부분에서 일관되게 인식되도록 했습니다 (#706). - AppProxy: 단일 서킷 이벤트에서 형제 서킷을 보존합니다 (#737, #731 종료). 둘 이상의 서킷을 서빙하는 워커가 단일 서킷 이벤트마다 영향받지 않는 모든 형제의
appproxy-*백엔드를 지워(다음 pull-reconcile까지, 최대 15초 동안 404/502) 버리던 문제가 있었습니다.RegistryEntry에 route 정보가 없어 재구성된 집합이 델타 서킷만 담았기 때문입니다. 이제 각RegistryEntry에 전체 서킷을 캐시하고 변경되지 않은 형제는 그로부터 재구성하므로, 델타 서킷의 백엔드만 바뀝니다. - AppProxy: 와일드카드 서브도메인 인그레스에서 폴백 체인에 도달하게 했습니다 (#738, #735 종료). 레플리카가 모두 죽은 등록 서킷이 더 이상 막다른 길이 아닙니다. 서킷별 인가 후 그 서킷의 정규 모델로 고정되어 일반 파이프라인으로 넘어가고, 거기서
FallbackService가 이어받습니다(운영자가 기대하는 "배포가 죽으면 교차 공급자 모델로 트래픽이 넘어간다" 동작). 이 통과는 등록됐지만 죽은 서킷으로 한정되고, 정말 알 수 없는 서브도메인은 404로 남으며, open-to-public / bearer 토큰 / IP 허용 목록 게이트가 먼저 실행됩니다. - AppProxy: 주기적 reconcile에서 이벤트로 알게 된 모델을 보존합니다 (#739). Redis 이벤트 오버레이가 성공적인 pull 프로브보다 먼저 서킷의 모델을 알게 된 경우, reconcile이 등록됐지만 죽은 레지스트리 항목을 evict 하고 스코프드 폴백을 깨뜨릴 수 있었습니다. 이제 reconcile은 레플리카를 프로브하기 전에 공유 레지스트리가 이미 아는 모델을 재사용합니다.
- AppProxy:
WorkerRegisterResponse역직렬화를 Backend.AI 코디네이터의 실제 응답 형태(slots는 배열,available_slots는 그 개수)와 맞췄습니다 (#726).
문서¶
- Zensical 사용자 문서를 현재 상태 기준 매뉴얼로 다시 썼습니다. 개발 로그식 서술, 로드맵, "coming soon" 항목을 걷어내고, 실제 config 구조체와 어긋난 설정 레퍼런스(존재하지 않는 섹션과 키, retry 필드 이름, admin 인증 형태, 환경 변수 표, 설정 탐색 순서)를 바로잡았으며, 그동안 빠져 있던 출하 동작(CLI 플래그 7종, auth login 하위 명령, Windows AF_UNIX 지원과 Unix 소켓 위 SSE, Windows/musl 및 .deb 릴리스 산출물)을 문서화하고, 한국어 문서를 영어와 같은 수준으로 맞췄습니다.
- AppProxy 워커 모드 설계 문서를 추가했습니다 (#708).
- README "Recent Updates" 목록을 릴리스별 한 줄로 압축했습니다.
의존성¶
validatorderive 의존성과 그 전이 의존성proc-macro-error2를 제거해,validator에 안전한 업그레이드 경로가 없어 임시cargo-denyignore가 필요했던 RUSTSEC-2026-0173 권고를 해소했습니다 (#733, #732).- Rust 패키지 버전을 업데이트했습니다 (#732).
v1.8.2 - 2026-06-02¶
수정됨¶
/v1/responses경로에서 클라이언트의Accept-Encoding헤더 전달을 중단했습니다 (#702). 클라이언트가Accept-Encoding: gzip, deflate, br을 보내면 responses 경로의 헤더 필터가 차단 목록에서accept-encoding을 빠뜨려 업스트림 백엔드로 그대로 전달했고, 업스트림은 gzip을 협상해 압축된 바이트를 반환했습니다. reqwest는Accept-Encoding헤더를 수동으로 설정하면 자동 압축 해제를 비활성화하기 때문에(명시적인.header("Accept-Encoding", "identity")호출은 전달된 값을 교체하지 않고 두 번째 값으로 덧붙기만 했습니다), SSE 변환기가 raw gzip 바이트를 받아 텍스트로 파싱하면서 선두의response.created,output_item.added,content_part.added,output_text.delta이벤트를 떨어뜨리고item_id와text가 비어 있는 꼬리 조각만 남겼습니다. 이제"accept-encoding"이 기본 convert 경로(src/http/handlers/responses.rs)와 responses 네이티브 패스스루 경로(src/proxy/responses_only.rs) 모두의FILTERED_HEADERS에 포함되어 chat-completions 프록시(src/proxy/backend.rs)와 동등해졌고, 업스트림은 항상Accept-Encoding: identity만 받습니다.- Responses SSE 라인의 이중 래핑을 중단해, 비 GPT
/v1/responses스트리밍 변환이 변환된 Anthropic 및 Chat-Completions 경로에서 중첩 레코드 대신 단일 레이어의 OpenAI 호환 SSE 레코드를 내보내도록 수정했습니다 (#701).
의존성¶
uuid1.23.1 → 1.23.2,redis1.2.1 → 1.2.2,socket20.6.3 → 0.6.4,serial_test3.4.0 → 3.5.0으로 bump (#699).
테스트¶
- 프로덕션 StreamService 변환 프로세서에 대한 회귀 커버리지를 강화해, 변환된 Anthropic 및 Chat-Completions 경로에서 단일 레이어 SSE 출력을 단언합니다 (#701).
- Anthropic 백엔드와 gzip을 요청하는 클라이언트로
/v1/responses스트리밍 경로를 검증하는 통합 회귀 테스트. 업스트림 요청이Accept-Encoding: identity만 받는 것과, 변환된 Responses SSE 스트림이 텍스트와 item id가 채워진 전체 이벤트 시퀀스를 유지하는 것을 단언합니다 (#702).
v1.8.1 - 2026-05-29¶
추가됨¶
- 하드코딩된
opus-4-7/opus-4-6부분 문자열 게이트를 대체하는claude_family_version파서와 함께 Claude Opus 4.8 인식 추가 (#693, #687의 일부). 네 개의 Anthropic capability 술어가 이제 파싱된(major, minor)버전을 비교합니다:uses_adaptive_thinking_api≥ (4,6),model_requires_adaptive_thinking/model_forbids_sampling_params≥ (4,7),opus_supports_max_effort= Opus이면서 ≥ (4,6). 파서는 첫 번째 정수 토큰을 major로, 그다음 버전 모양 토큰(1~2자리, 값 < 100)을 minor로 취급하므로,20250514같은 8자리 날짜 접미사는 minor 0이 되어 버전으로 오인되지 않고, 새 minor 릴리스는 버전별 수정 없이 인식됩니다.claude-opus-4-8메타데이터 항목(1M 컨텍스트, 128K 출력, \(5/\)25 가격, 적응형 사고, 2026년 1월 cutoff)을 추가하고claude-opus-4-8/claude-opus-4-8-latest를 내장 지원 모델 목록과 설정 샘플에 등록합니다. 4.5/4.6/4.7과 Sonnet 변형의 동작은 보존됩니다. - 백엔드별
anthropic_fast_modeopt-in(기본값 off) 뒤의 Anthropic fast mode (#694, #687의 일부).is_fast_mode_eligible은 Opus 4.6/4.7/4.8과 이후 Opus minor에서만 true를 반환하고,merge_beta_header는 클라이언트가 보낸anthropic-beta를 보존하면서 beta 토큰을 쉼표로 결합하고 중복을 제거합니다. 네이티브/anthropic/v1/messages경로에서resolve_fast_mode_beta는 요청이speed: "fast"이고, 모델이 적합하며, 백엔드가 네이티브 Anthropic(Bedrock은 절대 아님)이고, opt-in이 켜진 경우에만 병합된fast-mode-2026-02-01beta 헤더를 주입합니다. fast mode가 적용되지 않으면 나가는 본문에서speed를 제거해 의도치 않은 업스트림 400을 유발하지 못하게 합니다. OpenAI 호환 경로는 적합하고 opt-in된 네이티브 Anthropic 대상에만speed: "fast"를 전달하고 beta 헤더를 주입합니다.Anthropic -> OpenAI와Anthropic -> Google폴백 파라미터 매핑은speed를 제거해, fast mode 요청이 비 Anthropic 백엔드로 폴백할 때 네이티브 전용 필드가 누출되지 않게 합니다. 응답의usage.speed는 보존됩니다. - Claude Opus 4.8+ 대화 중간 시스템 메시지 (#695, #687의 일부). 이전 Claude 패밀리가 HTTP 400으로 거부하는
messages배열 내부의role:"system"항목이 이제 허용되며,claude_family_version을 재사용해 패밀리 버전 ≥ (4,8)에 매칭되는 새supports_mid_conversation_system(model_id)술어로 게이트됩니다. 네이티브 핸들러는 해당 항목을 네이티브 Anthropic 백엔드로 변경 없이 왕복시키고, 교차 공급자 변환은System역할을 OpenAIsystem역할로 매핑하며 Gemini와 Responses에서는 user 역할 텍스트로 보존합니다. OpenAI 호환 변환은 지원 모델에 대해 대화 중간system/developer메시지(첫 user 턴 이후)를 배열 내role:"system"항목으로 내보내고, 선두 시스템 메시지는 여전히 최상위system필드를 채웁니다. 비지원 모델(Opus 4.7 이하, 모든 Sonnet/Haiku, Bedrock 접두사 id, 비 Claude id)은 단일 최상위system으로 평탄화하는 기존 동작을 유지합니다. - 거부 응답의
stop_details와refusalstop reason이 전체 Anthropic 응답 파이프라인을 통해 전파됩니다 (#696, #687의 일부).map_anthropic_finish_reason은"refusal"을"content_filter"로 매핑하고, 비스트리밍 변환과 스트리밍message_delta핸들러는stop_reason이"refusal"일 때 choice에stop_details객체를 붙이며, 업스트림이 명시적 null을 보내면 null을 전달하는 대신 키를 생략합니다.
수정됨¶
POST /v1/responses에서 인라인image_url대신 Files API 업로드를file_id로 참조하는input_image콘텐츠 파트를 허용합니다 (#686, refs #681). 부모 enum들이#[serde(untagged)]라서{"type":"input_image","file_id":"file-..."}파트는 이전에 일반적인 untagged-enum 오류로 역직렬화에 실패했고 Axum이 HTTP 422를 반환했습니다. 이제image_url은 선택 사항이 되고input_file을 따라file_id가 추가되었습니다. 공유 헬퍼resolve_local_file_to_data_url이 동일한 메타데이터, 소유권, 크기, 로드, base64 시퀀스를 거쳐 로컬file_id를 인라인 base64 image_url 데이터 URL로 해석하며, 소유권과 10MB 크기 제한을 준수합니다. OpenAI/Anthropic/Gemini 변환기는 선택적image_url을 처리해, 해석된 경우 이미지를 내보내고 해석되지 않은file_id는 경고 후 건너뜁니다.validate_request는 메시지 콘텐츠를 순회해image_url도file_id도 없는input_image를 파일 해석 전에 명확한 400으로 거부합니다.- Claude Opus 4.8 라우팅 게이트를 강화해, fast-mode speed는 전송 계층이 네이티브 Anthropic opt-in과 beta 헤더 주입을 확인한 경우에만 전달되고, 비 Opus Claude 패밀리는 대화 중간 시스템 메시지를 계속 평탄화하며, OpenAI 호환 Anthropic 응답은
usage.speed를 보존합니다 (#698, refs #687).
문서¶
- Claude Opus 4.8 지원을 영어와 한국어로 문서화 (#697, closes #692, #687의 일부):
reasoning-effort.md의 적응형 사고 모델 목록과 샘플링 파라미터 deprecated 경고에claude-opus-4-8-*추가;backends.md에 Claude Opus 4.8 모델 상세, Anthropic Fast Mode 섹션, 대화 중간 시스템 메시지 섹션, 거부stop_reason->content_filter매핑 추가; 모델 인식, fast mode, 대화 중간 시스템 메시지, 거부stop_details를 다루는 changelog 항목 추가. api.md(영어와 한국어)에input_image의file_id지원을 문서화.image_url과file_id중 정확히 하나가 필요하고,file_id는input_file경로와 같은 소유권 및 10MB 크기 제한 아래 백엔드에 도달하기 전에 인라인 base64 데이터 URL로 해석된다는 점을 명시합니다 (#686, refs #681).
테스트¶
claude_family_version과supports_mid_conversation_system경계 테스트(4.7 false, 4.8 true, Sonnet/Haiku/구버전 false, Bedrock 접두사와 교차 리전/ARN id false), fast-mode 적합성, beta 헤더 병합/중복 제거,speed/usage.speed(역)직렬화, 클라이언트 beta 병합을 포함한resolve_fast_mode_beta게이팅, 폴백 공급자 간speed비누출 대 보존 (#693, #694, #695).- 거부 응답 커버리지:
stop_details가 있는 경우와 없는 경우의 비스트리밍 및 스트리밍 거부, 명시적 null 생략 경로,end_turn/max_tokens/stop_sequence/tool_use회귀 테스트 (#696). input_image의file_id단독 및image_url단독 역직렬화, 회귀용으로 실패하던 전체 요청 페이로드, 소유권과 크기 제한을 준수하는 FileResolver 해석, 세 백엔드 전체의 변환기 출력, 두 필드 모두 없는 검증 사례 (#686).
v1.8.0 - 2026-05-28¶
추가됨¶
- 클라이언트 API 키의 선택적
allowed_backends허용 목록을 통한 API 키별 백엔드 접근 제어 (#677, closes #674). 목록이 비어 있지 않으면 해당 키로 인증된 요청은 명시된 백엔드로만 라우팅될 수 있고, 빈 목록이나 누락된 목록은 기존의 무제한 동작을 유지합니다. 이 필드는 엔드 투 엔드로 통합됩니다: 설정 파일과 핫 리로드, 런타임ApiKey와AuthContext, 백엔드 선택 chokepoint와 Responses / Anthropic 선택 경로, 교차 공급자 폴백, Admin REST API(create/update/get/list), 런타임 키 영속화, 모델 목록 엔드포인트. select_backend_with_retry와 Responses(StreamService), Anthropic 네이티브,count_tokens, 이미지 핸들러가 키의 허용 목록으로 후보를 필터링합니다. 모델은 존재하지만 허용 목록이 모든 후보를 거부하면,403에error_type = "permission_error"(재시도 불가)로 매핑되는 새RouterError::Forbidden변형으로 요청이 거부되며, 이는401 AuthError와 구분됩니다./v1/models,/v1/models/extended,/anthropic/v1/models는 제한된 키로 인증된 경우 허용된 백엔드 중 적어도 하나가 제공하는 모델로 필터링되고,GET /v1/models/{model}은 키가 도달할 수 없는 모델에404를 반환합니다. 비인증 또는 무제한 호출자는 전체 목록을 봅니다.- 새
api_optional_auth_middleware가 허용(permissive) 모드로 레이어링됩니다. 제시된 bearer 토큰을 최선 노력으로 검증하고 절대 거부하지 않으면서AuthContext를 첨부하므로, 키별 제한은 인증된 호출자에게 적용되고 익명 및 잘못된 토큰 호출자는 제한 없이 통과합니다. 기존 차단 모드api_auth_middleware는 변경되지 않으며, 둘은 절대 함께 레이어링되지 않습니다. - 키의
allowed_backends가 알 수 없는 백엔드 이름을 참조하면 설정 검증이 하드 실패 대신 경고만 하므로, 운영자가 키를 업데이트하기 전에 백엔드 이름 변경이 라우터를 먹통으로 만들지 않습니다. fallback.mid_stream_enabled설정 필드(기본값true). 운영자가 초기 연결의 저렴한 pre-stream 백엔드 재선택은 유지하면서 스트림별 mid-stream 버퍼링은 끌 수 있습니다 (#680, closes #676). 이전에는fallback.enabled가 all-or-nothing이었습니다. 켜면 pre-stream과 mid-stream 폴백 모두(스트림당 약 100~200KB를 버퍼링하는StreamAccumulator포함), 끄면 폴백 전체가 비활성화됐습니다. 메모리가 제한되거나 동시성이 높은 호스트에 중간 지점이 생겼습니다.- 스트리밍 디스패치가 순수 헬퍼
decide_streaming_fallback_dispatch로 분리된 3-way 결정이 됩니다.fallback.enabled가 true이고 체인이 설정된 상태에서mid_stream_enabled = true는 버퍼링 경로(handle_streaming_with_mid_stream_fallback)를 유지하고,mid_stream_enabled = false는 되살아난handle_streaming_with_pre_stream_fallback으로 라우팅하며(StreamAccumulator나MidStreamFallbackContext할당 없음, mid-stream 실패는 일반 스트림 오류로 표면화), 그 외에는 표준 무폴백 경로가 실행됩니다. - 이전에 dead code였던
handle_streaming_with_pre_stream_fallback과advance_to_next_fallback이 이제 살아 있습니다.#[allow(dead_code)]마커가 제거되었고, 키별 허용 목록이 폴백 재선택에 연결되어 새로 살아난 경로가 API 키별 접근 제어를 우회하지 않습니다.
변경됨¶
src/http/streaming/handler.rs의 스트리밍 로컬 복사본transform_payload_for_openai와 그requires_max_completion_tokens헬퍼를 제거했습니다. 두 호출 사이트는 이제crate::proxy::utils의 정식 구현으로 해석됩니다 (#679, closes #660). 두 복사본은 byte-for-byte 동일해서, 한쪽 변경이 다른 쪽에 반영되지 않는 drift 위험을 만들고 있었습니다.src/proxy/utils.rs의 정식 테스트가 같은 계약을 이미 커버하므로 스트리밍 로컬 중복 단위 테스트는 제거되었습니다.
수정됨¶
- 기본 mid-stream 폴백 핸들러가 mid-stream 실패 후 백엔드를 재선택할 때 API 키별 백엔드 허용 목록을 강제합니다 (#683).
fallback.mid_stream_enabled = true(기본값)에서, 폴백 체인이 비허용 백엔드로 매핑된 제한 키는 mid-stream 실패 시 그 백엔드로 투명하게 전환되었는데, 이는 PR #680의 새mid_stream_enabled작업이 pre-stream 경로에서만 닫았던 접근 제어 우회였습니다.handle_streaming_with_mid_stream_fallback은 이제 소유된allowed_backends: Option<Vec<String>>을 받고(spawn된 스트리밍 태스크로 이동), 새 헬퍼resolve_allowed_backend_name_for_model이resolve_backend_name_for_model을 감싸 모듈의 다른 모든 곳(try_get_healthy_backend_for_model,get_backend_for_model_streaming,get_healthy_backend_for_streaming)에서 쓰는 것과 같은allowed_backends.filter(|l| !l.is_empty())+ 정확한 이름 멤버십 의미론을 적용합니다. 세 폴백 재선택 사이트 모두 이 헬퍼를 통해 해석합니다. 비허용이거나 해석 불가능한 후보는None을 반환하므로 기존warn + continuearm이 건너뛰고 체인 인덱스가 전진합니다. 남은 후보가 모두 필터링되면 루프는 기존 체인 소진 경로로 종료되고 클라이언트에 오류를 표면화합니다. 빈 목록이나None허용 목록은 이전의 무제한 동작과 byte-for-byte 동일합니다. - mid-stream 폴백에서 폴백 페이로드를 재구성하기 전에 허용된 폴백 백엔드를 해석하므로, 비허용 체인 항목이 건너뛰어지기 전에
current_payload를 변형할 수 없습니다 (#684). - Anthropic 네이티브
x-api-key호출자가AuthContext가 없을 때Authorization: Bearer인증 호출자와 동일한allowed_backends정책을 적용받으며, Messages,count_tokens, 모델 목록을 커버합니다 (#684). 키별 허용 목록이 네이티브 Anthropic surface에서 강제되지 않는다고 문서화돼 있던 기존 제한이 닫혔습니다. -
677 머지 후 보안 감사에서 발견된
POST /v1/responses/compact의 HIGH 심각도 인가 우회를 닫았습니다. 이 엔드포인트는 요청 extensions에서AuthContext를 전혀 읽지 않는 유일한 클라이언트 대상 모델 라우팅 핸들러여서, 백엔드 집합 A로 범위가 제한된 키가 요청한 모델을 B가 제공하기만 하면 압축(compaction)을 통해 비허용 패스스루 백엔드 B(OpenAI / Azure)에 도달할 수 있었습니다.compact_response는 이제create_response를 정확히 미러링합니다:allow_list_from_auth로 키별 허용 목록을 도출하고, 헬스 체크나 패스스루 전에 모델의 후보 백엔드를 그 목록으로 필터링하며, 모델은 존재하지만 키가 그 모델을 제공하는 어떤 백엔드로도 라우팅할 수 없을 때 결정론적403 permission_error를 반환합니다. 필터가 백엔드 전달보다 먼저 실행되므로 업스트림 접촉 없이 거부됩니다.¶
문서¶
config.yaml.example,src/services/streaming/mid_stream_config.rsrustdoc,docs/en/configuration/advanced.md에서mid_stream_fallback.enabled가 mid-stream 버퍼링을 비활성화하거나 메모리를 줄이지 않는다는 점을 명확히 했습니다. 값과 무관하게StreamAccumulator는 여전히 생성되어 스트림당 약 100KB까지 버퍼링하고, mid-stream 폴백은 여전히 백엔드 실패 시 활성화됩니다 (#678, closes #675). 이 플래그는 폴백 요청의 continuation 모드와 restart 모드 중 하나를 선택할 뿐입니다. 버퍼링과 메모리의 실제 kill-switch는fallback.enabled: false이거나fallback.fallback_chains에서 모델을 빼는 것입니다. 한국어 문서(docs/ko/configuration/advanced.md)에는 대응하는 Mid-Stream Fallback 섹션이 없어 이 명확화에 대한 한국어 변경은 없지만, #680이 새 토글을 다루는 번역된 "미드스트림 버퍼링 비활성화" 하위 섹션을 별도로 추가했습니다.- API 키별
allowed_backends허용 목록을 보안 문서(영어와 한국어)에 문서화했으며, 이제 닫힌x-api-keyAnthropic Messages 제한 사항도 포함합니다 (#677, #684).
테스트¶
resolve_allowed_backend_name_for_model의 결정론적 단위 테스트: 허용 목록이 해석된 백엔드를 제외하면None반환, 포함하면Some반환, 빈 목록은Some반환,None은 필터 없는 해석기와 일치, 해석 불가능한 모델은None반환 (#683).- 차단 인증 뒤에서
/v1/responses/compact를 구동하는tests/per_key_backend_access_test.rs통합 테스트: 비허용 백엔드만 제공하는 모델을 요청한 제한 키는403 permission_error를 받고(보안 사례), 허용된 백엔드의 모델을 요청한 제한 키는 필터를 통과합니다 (#677). fallback.enabled,mid_stream_enabled, 체인 존재 여부 사이의 3-way 결정 매트릭스를 커버하는 새decide_streaming_fallback_dispatch헬퍼 단위 테스트 (#680).
v1.7.1 - 2026-05-28¶
수정됨¶
- Anthropic
web_search_20250305서버 도구 에뮬레이션이 더 이상 공급자 실패를 빈 결과 집합으로 가리지 않습니다. API 키 누락, HTTP 401/403/429, 타임아웃, 파싱 오류는 이제 매핑된error_code(HTTP 429는"too_many_requests", 그 외 모든 실패는"unavailable")를 가진web_search_tool_result_error콘텐츠 블록으로 클라이언트에 표면화됩니다. 진짜로 비어 있지만 성공한 검색은 계속 빈content: []배열을 내보내므로 두 결과는 여전히 구분 가능합니다. 비스트리밍과 스트리밍 SSE 코드 경로 모두 커버되며, 스트리밍 오류 경로를 지키는 명시적 SSE 이벤트 순서 단언이 포함됩니다. Serper 응답 형태 drift(깨끗이 파싱되지만organic키가 없는 본문)는 이제 감지되어 공급자 태그가 붙은warn!으로 로깅되며, 관찰된 최상위 키만 기록하고 사용자 쿼리 텍스트는 절대 기록하지 않습니다. (#671, #672, #673)
의존성¶
lru0.16 → 0.18,reqwest0.13.3 → 0.13.4,rusqlite0.39 → 0.40(libsqlite3-sys0.37 → 0.38, CI가 Rust 1.95를 실행하게 되어 핀 해제)으로 bump, 그리고 전이적 lockfile 갱신(aws-lc-rs1.16.3 → 1.17.0,h20.4.13 → 0.4.14,http1.4.0 → 1.4.1,hyper/axum/reqwest연쇄 개정).cargo audit은 의존성 그래프 전체에서 취약점 0건을 보고합니다 (#669, #670).
v1.7.0 - 2026-05-26¶
추가됨¶
- Bearer 토큰 기반 AWS Bedrock Claude 백엔드 Phase 1 (bedrock-mantle) (#616, closes #613)
- serde 별칭(
aws-bedrock,bedrock-anthropic,AwsBedrock, ...)을 가진 새type: bedrock백엔드.is_commercial()은 true를 반환하고owned_by()는Some("anthropic")을 반환해, OpenAI 형식 클라이언트가 기대하는 모델 계보를 보게 됩니다. endpoint_type: mantle(기본값)은 리전 템플릿https://bedrock-mantle.{region}.api.aws에서 네이티브 Anthropic Messages API를 사용하고,/anthropic/v1/messages로 라우팅하며,Authorization: Bearer를 사용하고,anthropic-version을 생략합니다(있으면 Bedrock이 HTTP 400을 반환). 명시적url:필드가 프록시와 테스트를 위해 템플릿을 덮어쓰고, 빈 리전이나 대문자 리전은 로드 시 거부됩니다.model_ids.rs는 일반(anthropic.<family>), 지리적(us./eu./jp./au.), 글로벌(global.anthropic.<family>), 전체 ARN 식별자를 인식하며, 지역 접두사가 실제 청구와 레지던시 결과를 수반하므로 자동 별칭 매핑 없이 변경 없이 전달합니다.-
기존 OpenAI/Anthropic 본문 변환과 Anthropic SSE 스트림 변환기를 변경 없이 재사용하므로, 모델별 특이 동작(Opus 4.7 샘플링 파라미터 금지, 적응형 사고)이 중복 없이 Bedrock에 동일하게 적용됩니다.
endpoint_type: runtime은 여기서는 예약만 되어 있고 아래 Phase 2에서 구현됩니다. -
요청 경로 속도 제한이 이제 실제로 강제되고, Redis 스토리지 백엔드가 추가되었습니다 (#635, #632, closes #626)
state.rs가 이전에initialize_rate_limiting이 반환한MiddlewareLayer를 버려서rate_limiting.*설정이 조용한 no-op였습니다. 이제 레이어가ServiceHandles→ContinuumRouterBuilder::build_router→Router::layer를 거쳐 조립된 Axum 앱에 부착되므로, 설정된 예산이 실제로429를 반환합니다.- 다섯 개
rate_limiting차원이 모두 선택 사항이 되었습니다: 이미 선택 사항이던per_api_key/per_model에per_client,per_backend,global이 합류합니다. 운영자는 어떤 차원이든 생략해 비활성화 상태로 로드할 수 있고,Defaultimpl은 세 차원을 그대로 유지하므로 기존 배포는 영향받지 않습니다 (#632). -
새
redis-cachefeature 게이트의rate_limit_v2::redis_backend는 토큰 버킷과 슬라이딩 윈도우 Lua 스크립트를 각각 단일 원자적EVAL로 실행하고,cr:rl:per_client:10.0.0.1같은 키로 공유create_redis_pool헬퍼를 재사용합니다. 어떤 Redis 실패(풀 사용 불가, 타임아웃, Lua 오류)에서든 백엔드는BackendUnavailable을 보고하고 호출자는 인프로세스 토큰 버킷으로 폴백해, 요청을 떨구는 대신 레플리카별 강제로 degrade합니다. - 완성된
src/core/fallback/모듈(약 4,900줄, 단위 테스트 36개)이 요청 경로에서 한 번도 호출되지 않아, 설정된fallback.fallback_chains가 조용한 no-op였습니다.FallbackService가 이제chat_completions(웹 검색 제외),completions,embeddings,rerank,sparse_embeddings, 이미지 생성에 대해 실행되며,execute_with_optional_fallback래퍼(폴백 미설정 시 오버헤드 없음),X-Fallback-*응답 헤더, executor bound를 충족하는From<RouterError>→TriggerReason매핑을 포함합니다 (#631). -
fallback.fallback_chains와fallback.fallback_policy변경이 이제 핫 리로드 구독자를 통해FallbackService::update_config로 런타임에 적용됩니다.fallback.enabled토글은 여전히 재시작 전용이며,config.yaml.example에 그렇게 문서화되어 있습니다 (#637, #665). -
인터랙티브 데스크톱 사용을 위한
POST /v1/models/refresh강제 새로고침 엔드포인트 (#593, #664) ModelCache(all_models키)를 비우고 응답 전에 설정된 모든 백엔드에서 동기적으로 재집계해,GET /v1/models와 같은{"object":"list","data":[...]}형태를 반환합니다. 데스크톱 클라이언트(예: backend.ai-go "Refresh models" 버튼)는 두 번째 왕복 없이 응답을 즉시 사용할 수 있습니다.- 검증된 API 키별로 속도 제한되며, 익명 또는 잘못된 토큰 호출자는 하나의 글로벌 익명 버킷을 공유합니다: 5초 버스트 윈도우당 3회, 분당 12회. 제한을 초과한 호출자는
429 Too Many Requests를 받습니다. 각 호출이 설정된 모든 백엔드에서 업스트림 fetch를 유발하므로 일반 목록 엔드포인트보다 의도적으로 엄격합니다. - 새
model_aggregation.allow_force_refresh: bool설정 필드(기본값true)로 게이트됩니다.false로 설정하면 엔드포인트가403 Forbidden을 반환하므로, 클라이언트가 TTL 기반 만료에 의존해야 하는 강화된 배포에 적합합니다. - 각 새로고침은 가능하면 검증된 API 키 ID를, 아니면
anonymous를INFO레벨로 로깅해 감사 상관관계를 지원합니다. config.yaml.example이 데스크톱 임베디드 가이드로 확장되었습니다: backend.ai-go 스타일 임베디드 프록시를 위한cache_ttl: 10,soft_ttl_ratio: 0.5,allow_force_refresh: true.-
ModelAggregationService의 새force_refresh(state)헬퍼가 비우고 다시 집계하는 흐름을 캡슐화합니다.allow_force_refresh()접근자가 설정 플래그를 핸들러에 노출합니다. -
AWS Bedrock Claude 백엔드 Phase 2: SigV4 + AWS 바이너리 event-stream을 사용하는
bedrock-runtime(#614) type: bedrock백엔드의 새endpoint_type: runtime값은https://bedrock-runtime.{region}.amazonaws.com/model/{modelId}/invoke[-with-response-stream]을 대상으로 합니다. 라우터는 각 요청을 AWS Signature V4(service: "bedrock")로 서명하고, OpenAI → Anthropic 본문을"anthropic_version": "bedrock-2023-05-31"로 감싸고, 최상위"model"필드를 제거하며(Bedrock은 URL 경로에서 모델 ID를 취함), 모델 식별자를 경로에 퍼센트 인코딩해 버전 있는 파운데이션 ID(anthropic.claude-3-5-sonnet-20240620-v1:0)와 전체 ARN이 깔끔하게 왕복합니다.- 스트리밍 응답은 AWS
application/vnd.amazon.eventstream바이너리 프레임 형식으로 도착합니다. 새bedrock::event_stream::EventStreamDecoder가 여러 TCP read에 걸친 프레임을 재조립하고, 각chunk페이로드를 base64 디코딩하며, 기존AnthropicStreamTransformer가 OpenAI 형태 SSE로 번역할 수 있도록 합성event: <type>\ndata: <json>\n\nSSE 바이트를 내보냅니다. 예외 프레임(ThrottlingException,ValidationException, ...)은 조용히 버려지는 대신 합성event: errorSSE 청크로 표면화됩니다. - AWS 자격 증명은 이 순서로 해석됩니다: 인라인
auth.aws.access_key_id+auth.aws.secret_access_key(+ 선택적session_token), 그다음auth.aws.profile을 통한 명명된 프로파일, 그다음 표준 AWS 체인(환경 변수, 공유 설정, IMDS, IRSA / EKS pod identity, ECS task role). 해석기 앞에aws_credential_types::provider::SharedCredentialsProvider를 두어 임시 자격 증명이 요청 사이에 투명하게 갱신됩니다. BackendAuthConfig의 새BackendAuthType::Sigv4변형과auth.aws아래의AwsAuthConfig서브 블록이 추가되었습니다. 둘 다Debugredaction을 거치므로 정적 자격 증명이 로그로 새지 않습니다.BackendAuthType은 YAML 표기로sigv4,aws_sigv4,aws-sigv4를 받습니다.- runtime 헬스 체크는 단일 토큰 본문으로
POST /model/{probe_model}/invoke를 프로브합니다. HTTP 2xx, 400, 401, 403, 429 모두 정상으로 간주되는데, AWS surface가 도달 가능함을 증명하고 인증/과금 문제는 운영자가 별도로 해결할 수 있기 때문입니다. - 모든 AWS SDK 크레이트(
aws-sigv4,aws-smithy-eventstream,aws-credential-types,aws-config)는 새 선택적bedrock-sigv4Cargo feature 뒤에 있습니다. 기본 빌드는 이들을 끌어오지 않으며, feature 없이endpoint_type: runtime을 설정하면 AWS 경계에서 실패하는 대신 리빌드 플래그를 가리키는 명확한 오류를 반환합니다. Phase 1 mantle 경로는 영향받지 않고 feature 유무와 무관하게 동작합니다. src/proxy/backend.rs와src/http/handlers/anthropic/handler.rs의 프록시 헤더 정책이endpoint_type에 따라 분기합니다: Bedrock-mantle은Authorization: Bearer를 유지하고, Bedrock-runtime은 Backend trait 구현이 각 요청을 SigV4로 서명하므로 정적 Bearer 주입을 억제합니다.-
docs/{en,ko}/configuration/backends.md의 영어와 한국어 문서가 새endpoint_type: runtime설정, 빌드 요구 사항, 자격 증명 체인, IAM 정책 스니펫, 지리/글로벌 프로파일 동작, 스트리밍 파이프라인 개요로 제자리에서 확장되었습니다. -
요청 게이트 하이브리드 모델 thinking 변환과 함께 EXAONE 4.0 (vLLM) 등록 (#640, refs #639)
- EXAONE 4.0(예:
EXAONE-4.0-32B-FP8-RNGD)은 하이브리드 reasoning 모델입니다. reasoning 모드에서는 단독</think>로 끝나는 chain-of-thought를content에 인라인으로 스트리밍하고, 비 reasoning 모드에서는</think>없이 일반 답변을 내보냅니다. assume_reasoning_first(unterminated_start) 변환이 이제 HTTP와 Unix 소켓 스트리밍 결정 지점 모두에서 요청이 실제로 thinking을 켰는지(chat_template_kwargs.enable_thinking또는 최상위enable_thinking, 보수적 기본값 false)에 게이트되므로, 비 reasoning 모드가 더 이상 전체 답변을 빈content와 함께reasoning_content로 내보내지 않습니다. 실제<think>마커를 키로 쓰는 표준 패턴 모델은 영향받지 않습니다.-
unterminated_start설정으로exaone-4.0-32b를 등록합니다. 서빙되는-RNGD이름은 아래의 하드웨어 접미사 peel로 해석됩니다. -
모델 ID 매칭의 NPU/가속기 하드웨어 변형 접미사 정규화 (#662)
is_recognized_format_token()에 HARDWARE/ACCELERATOR 카테고리(rngd,warboy,atom,atommax,rebel)를 추가해, FuriosaAI(RNGD/WARBOY)와 Rebellions(ATOM/ATOMMAX/REBEL) 서빙 타깃 접미사가 모델별 alias 없이 기존 계층적 peel 체인을 통해 정규 base 메타데이터 항목으로 정규화됩니다.layered_format_strip()이 peel 전에 소문자화하므로 런타임이 내보내는 대문자 이름도 정확히 해석됩니다.- 정확한 id와 정확한 alias phase가 peel보다 먼저 실행되므로,
*-atom이나*-rebel로 정당하게 등록된 모델은 먼저 정확한 매칭으로 승리합니다. 배포된model-metadata.yaml의 grep으로 현재 충돌이 0건임을 확인했습니다.EXAONE-4.0-32B-FP8-RNGD는 이제 peel(-rngd다음-fp8)로exaone-4.0-32b에 정규화됩니다.
변경됨¶
- 폴백 핸들러의 LM Studio 호환 shim을 좁혀
/와/v1/models만200을 반환하고, 그 외 매칭되지 않은 모든 라우트는 이제404를 반환합니다. JSON 오류 본문 형태는 변경 없이 보존되므로, 본문을 이미 읽고 있는 소비자는 계속 동작합니다 (#628).
수정됨¶
- 알 수 없는 키에 HTTP 400
INVALID_ARGUMENT를 반환하는 Gemini/v1beta/openai/chat/completions엔드포인트로 전달하기 전에 비 OpenAI 최상위 필드 7개(chat_template_kwargs,thinking_budget_tokens,enable_thinking,preserve_thinking,top_k,min_p,repeat_penalty)를 제거합니다.extra_body탈출구는 건드리지 않고reasoning_effort는 유지됩니다(Google이thinking_level로 매핑). 또한3-flash가3.5-flash에 부분 문자열 매칭되지 않았으므로3.5-flashthinking 비활성화 및 is-thinking 매처도 확장했습니다 (#642). - 모든 Anthropic 핸들러 코드 경로(네이티브 HTTP/Unix, Bedrock mantle/runtime, OpenAI 호환, Responses API)의 스트리밍과 비스트리밍 모두에 요청 통계 기록을 연결하고, raw 패스스루 SSE에서 입력/출력 토큰을 누적하는
AnthropicStreamUsageTracker를 추가했습니다 (#627, #634). - mid-stream 비활성 감지에 하드코딩된 60초 대신 설정된
timeouts.request.streaming.chunk_interval을 사용하고 제한된 keep-alive를 내보내, 침묵하는 백엔드가 keep-alive 주석을 영원히 내보내는 대신StreamOutcome::Failed를 통해 다음 폴백 모델로 전진하게 했습니다 (#633). Option<String>필드가 base 설정 위로 병합되는 새StreamingTimeoutOverride/StandardTimeoutOverride구조체로 부분적인model_overrides.<model>.streaming/standard블록을 허용해,--generate-config출력 경로의 YAML 파싱 실패를 수정했습니다 (#630).admin/metrics/metrics-persistence/webuiimport와 함수를 해당 Cargo feature 뒤로 게이트하고, 항상 컴파일되는 호출자가 쓰는 공개 surface를 미러링하는#[cfg(not(feature = "metrics"))]no-op 메트릭 스텁을 추가해, feature를 줄인 빌드가 깔끔하게 컴파일됩니다 (#629, #666, closes #636).- 강제 새로고침 속도 제한을 강화해 익명과 잘못된 토큰 호출자가 하나의 글로벌 버킷을 공유하므로, 스푸핑된
Authorization/X-Forwarded-For/X-Real-IP헤더로 예산을 우회할 수 없습니다. - 메트릭 히스토리 쿼리 제한, UTF-8 안전 메트릭 레이블 절단, 타입 있는 SigV4 구현을 통한 Bedrock runtime 라우팅 (#608, #609, #613, #614 후속).
문서¶
docs/{en,ko}/configuration/backends.md에 Bedrock 백엔드(리전 선택, 지리적 vs 글로벌 인퍼런스 프로파일, 모델 ID 형식, 자격 증명 체인, IAM 정책 스니펫, 스트리밍 파이프라인)를 문서화하고,docs/en/api.md에 Force-Refresh Models 섹션을 추가하고,config.yaml.example을 데스크톱 임베디드 모델 집계 가이드와 폴백 핫 리로드 주석으로 확장했습니다.
테스트¶
transform_payload_for_openai의 negative 및 positive 사례 커버리지 (#661).- 문서화된 버킷 리셋 동작을 포함한 속도 제한 미들웨어 핫 리로드 테스트와, 실제
ContinuumRouter를 빌드해 버스트 소진 시429가 발생함을 단언하는router_wiring_tests(#635, #638, #667). web_search주입이 패스스루 계약과 올바르게 상호작용하는지 검증 (#663).- MLxcel 스트리밍 패스스루 통합 테스트 (#659).
- Bedrock 단위 및 통합 커버리지: serde 별칭, URL 템플릿, 헤더 정책, 모델 ID 파싱(지리/글로벌/ARN), runtime SigV4, wiremock 서버로 구동되는 event-stream 프레임 디코딩 (#616, #614).
의존성¶
- tokio 1.52.1 → 1.52.3, tower-http 0.6.8 → 0.6.11, dashmap 6.1.0 → 6.2.1, serde_json 1.0.149 → 1.0.150, aws-config 1.8.16 → 1.8.17, aws-sigv4 1.4.3 → 1.4.4, aws-smithy-types 1.4.7 → 1.4.8로 bump (#619, #658).
v1.6.3 - 2026-05-12¶
추가됨¶
- API 키별 LLM 토큰 사용량 메트릭 (#608, #610)
- API 키, 모델, 백엔드, 토큰 종류별로 실제 프롬프트와 컴플리션 토큰 소비를 기록하는 새 Prometheus
llm_tokens_total{api_key_id, model, backend, kind}카운터. 핫 패스 카운터의 레이블 집합은 의도적으로 최소한으로 유지되며, 추가 차원은 아래의 동반 info-metric에 있습니다. - 동반
api_key_info{api_key_id, ...}info-metric은 API 키별 주석 레이블(예:email,team,environment)의 설정 가능한 allowlist를 노출하므로, 대시보드가 핫 패스 카운터의 레이블 집합을 부풀리지 않고 표준 PromQL* on(api_key_id) group_left(...)조인으로 토큰 카운터를 그룹화/필터링할 수 있습니다. derive_api_key_id는 설정된id(인증 레이어가 요청을 매칭한 경우) 또는 raw bearer 토큰의 SHA-256 첫 12 hex 접두사k_<hex>를 반환합니다. raw 키는 절대 레이블로 사용되지 않습니다. 전용ApiKeyCardinalityTracker(기본 캡: 고유 키 ID 1000개)가 레이블 카디널리티 폭발을 방지합니다.ApiKeyConfig와 인메모리ApiKey에annotations: HashMap<String, String>필드가 추가됩니다.MetricsConfig에는annotation_labels: Vec<String>이 추가되는데,api_key_info의 레이블로 구체화되는 allowlist입니다. 예약된 정규 주석 키(email,uuid,owner,team,environment)가 문서화되어 있고, 운영자는 커스텀 키를 추가할 수 있습니다.- 스트리밍과 비스트리밍 경로는
StreamTransformConfig의 새StreamObservabilityContext필드를 통해 기존usage파싱 지점에서 기록합니다. 이 필드는handle_anthropic_streaming/handle_gemini_streaming/handle_successful_backend_response를 관통해, OpenAI 호환 / Anthropic / Gemini / thinking-pattern 스트리밍 응답 빌더가 모두 중복 파싱 없이 카운터를 내보냅니다. 라우터는 OpenAI 호환 백엔드에 이미stream_options.include_usage=true를 주입하므로, 클라이언트 opt-in과 무관하게 스트리밍 메트릭이 균일하게 동작합니다. api_key_info는 시작 시metrics.annotation_labels에서 한 번 초기화됩니다. 레이블 이름은 등록 시점에 고정됩니다(Prometheus는 레이블 이름 변경을 허용하지 않음). 주석 값은 기존 config-watch 경로를 통해 핫 리로드되며, admin 작업이 동기화를 유지하도록load_from_config,add_key,remove_key_by_id에서ApiKeyStore::refresh_info_metric이 호출됩니다.- 모든 레이블 값은
CardinalityManager/sanitize_label_value를 거칩니다. 주석 값은@,+,:를 보존하는 약간 덜 엄격한sanitize_annotation_value를 사용해, 이메일과 네임스페이스 식별자가 깔끔하게 왕복합니다. - 설정 가능한 보존 기간을 가진 SQLite 기반 영속 로컬 메트릭 로그 (#609, #611)
- WAL 모드, prepared-statement 캐시,
PRAGMA user_version스키마 버저닝을 갖춘src/metrics/persistence/아래의 새MetricsStoreasync trait + 번들rusqlitev1 구현(store.rs/sqlite.rs/snapshot.rs/snapshot_task.rs). 히스토그램과 서머리는 샘플별 행 형태로 풀어집니다. - 카운터와 게이지는 시작 시 절대 복원되지 않습니다. 영속 로그는 별도의 읽기 경로이므로, 라이브
/metrics엔드포인트는 Prometheus의 단조 카운터 의미론을 유지합니다. 과거 샘플은 새GET /admin/metrics/history?metric=...&from=...&to=...surface(src/admin_metrics_history.rs)로 읽으며, 런타임에 영속화가 비활성화되면 404를, feature가 컴파일되지 않았으면 503을 반환합니다. v1에는 PromQL이 없습니다. src/server/serve.rs의 핫 리로드 파이프라인이 설정 변경을PersistenceCommand::{SetSnapshotInterval, SetRetentionDays, SetCompaction}메시지로 번역해, 진행 중인 스냅샷을 떨구지 않고 ticker와 prune cutoff를 원자적으로 재구성합니다.- 사실상 일일 타이머에 불과한 작업에 전체 cron 크레이트를 끌어오지 않도록, 압축 스케줄은
minute hour * * *cron 서브셋을 따릅니다. - 기본값은
enabled: true이며metrics.persistence.enabled: false로 끕니다.redb와duckdb변형은 YAML 스키마의 예약 키워드로, 구현이 생기기 전까지 시작 시NotImplemented를 반환합니다. - 디스크 사용량: 합성 100-series × 10-snapshot 워크로드에서 샘플당 약 119바이트 측정(
tests/metrics_persistence_test::disk_usage_smoke_check_under_synthetic_load참조). 공식은docs/en/persistent-metrics.md와config.yaml.example에 문서화되어 있습니다.
수정됨¶
- release 빌드의 타입 추론 실패를 막기 위해
with_label_values의 토큰 사용량 레이블 값을&str로 강제합니다.&String레이블 변수를&str리터럴("prompt"/"completion")과 섞으면 컴파일러가&[&String]을 선택해 리터럴을 거부했습니다. #610에서 도입된 회귀입니다.
문서¶
- 두 메트릭 기능의 한국어 번역 (#612)
docs/ko/metrics.md:llm_tokens_total,api_key_id도출,annotation_labelsallowlist,api_key_infoinfo-metric, PromQL 예시, Grafana 패널, 검증 단계를 다루는 새### API 키별 LLM 토큰 사용량섹션.docs/ko/persistent-metrics.md:docs/en/persistent-metrics.md를 번역한 새 페이지(SQLite 기반 스냅샷 의미론, 설정 필드, 디스크 사용량 공식,/admin/metrics/historysurface, 스키마 레이아웃, 운영 노트).docs/ko/admin-api.md: Stats와 Response Cache 사이에## 지속 메트릭 로그 API섹션을 삽입하고 대응하는 TOC 항목 추가.zensical.ko.toml: 한국어 사이드바에서 페이지에 도달할 수 있도록 운영 아래에지속 메트릭 로그nav 항목 추가.- 메트릭 정의,
api_key_id도출 규칙, 주석 설정 스키마, 카디널리티와 핫 리로드 의미론, 예시 PromQL(이메일별 토큰, top-10 키, 팀별 롤업), Grafana 패널 예시, 검증 단계를 다루는 새docs/en/metrics.md### Per-API-Key LLM Token Usage섹션.config.yaml.example에는 문서화된metrics.annotation_labels블록과 각 API 키 항목 아래의annotations:예시가 추가되었습니다.
테스트¶
- API 키별 토큰 사용량 단위 커버리지:
derive_api_key_id우선순위(설정된 id 우선, 없으면 해시, 없으면 anonymous), 결정론, 해시 형식^k_[0-9a-f]{12}$, 주석 레이블 정규화, info 게이지 1회 초기화, refresh 원자성, 카디널리티 경계, 이메일 보존 주석 sanitizer; 스트리밍 변환기 write-through; 미들웨어 주석 스냅샷 노출;tests/metrics_integration_test.rs의 통합 커버리지(두 kind를 모두 가진 4-label 카운터, 익명 폴백, 해시 정규식). (#610) - 영속 메트릭 SQLite 스토어 단위 커버리지(insert, 시간 범위 쿼리, 보존 삭제, 멱등 open, unknown-kind 왕복)와
tests/metrics_persistence_test.rs의 통합 커버리지(스냅샷 태스크가 SQLite에 행을 기록, 보존이 오래된 샘플만 prune, 보존 핫 리로드가 진행 중 스냅샷을 보존, 디스크 사용량 스모크 체크). (#611)
v1.6.2 - 2026-05-10¶
수정됨¶
/v1/responses와/v1/chat/completions가 이제 OpenAI reasoning-API의developer역할을 허용합니다 (#603, #605, #606)- serde lowercase rename을 가진
MessageRole::Developer를 추가해"developer"가 일급 변형으로 역직렬화됩니다. 이전 실패는 알 수 없는 역할 이름을 밝히는 대신 오해를 부르는did not match any variant of untagged enum ResponseInput으로 표면화됐는데, 암묵적 메시지 역직렬화 오류가 이제 문제의 역할 문자열을 명시하고 유효한 역할 목록을 나열합니다. - 백엔드별 번역: OpenAI 호환 서버에는
developer그대로 패스스루; Anthropic은 최상위system파라미터로 병합(system과 developer 텍스트가 모두 있으면\n\n으로 연결되어 기존 덮어쓰기 버그 수정); Gemini는system_instruction으로 병합; Ollama는system으로 매핑(구버전 빌드가developer를 거부). - Chat Completions → Responses 변환기가
developer를 instruction을 담는 역할로 인식합니다: 첫 번째 등장은 최상위instructions가 되고, 이후 등장은 원래 역할을 와이어에 유지한 input 항목으로 남습니다. - 문자열 기반 인식 사이트 전반에서
developer와system을 동등하게 취급합니다: prefix-cache 키 추출, 교차 공급자 폴백 번역, OpenAI-to-Anthropic 변환의 시스템 콘텐츠 추출, global-prompt injector의 기존 시스템 메시지 lookup, smart-routing 분류기 / LLM 프롬프트 빌더.
문서¶
- 문서 사이트를 MkDocs에서 Zensical로 마이그레이션하고 브랜드 스타일 복원 (#602)
mkdocs.yml과mkdocs.ko.yml을 제거하고 네이티브zensical.toml과zensical.ko.toml로 대체. 둘 다 Zensical의 TOML 스키마에 따라[project]네임스페이스 아래에 루트를 두고, 확장별 옵션은 dict로[project.markdown_extensions]안에 둡니다(Zensical 설정 로더는 별도의mdx_configs테이블을 무시).- Zensical이 에셋 디렉터리의 심볼릭 링크를 따라가지 않으므로,
docs/en/shared와docs/ko/shared심볼릭 링크를 각 빌드 전에 실행되는rsync -a --delete docs/shared/ docs/{en,ko}/shared/로 교체. - Zensical이 문서화한
primary = "custom"메커니즘과, 오렌지 CSS 변수를 정의하는docs/shared/stylesheets/extra.css의[data-md-color-scheme="default"][data-md-color-primary="custom"]셀렉터로 lablup 브랜드 컬러 등록. - Mermaid는 이제 호환되지 않는
mermaid2플러그인에 의존하는 대신pymdownx.superfences커스텀 펜스로 등록. favicon이 없으면logo.png로 폴백. - 아이콘, 다이어그램, 브랜드 컬러의 Zensical 렌더 출력 복원 (#604)
- 제거된
materialx를 대체하는zensical.extensions.emojitwemoji 인덱스/생성기로pymdownx.emoji를 다시 활성화해,:material-*:아이콘 문법이 리터럴 텍스트로 렌더링되지 않게 함. <!-- diagram: PATH --> ... <!-- /diagram -->ASCII 대체를 Python-Markdown 확장(docs/hooks/diagram_extension.py)으로 재구현. Zensical이 MkDocs hook 라이프사이클을 노출하지 않아 기존 MkDocson_page_contenthook이 실행되지 않기 때문입니다. Zensical의 콘솔 스크립트 엔트리 포인트에서 확장을 import할 수 있도록docs/__init__.py를 추가하고 빌드 앞에PYTHONPATH=.를 붙임.- 커스텀 팔레트에
--md-primary-bg-color를 설정하고.md-header/.md-tabs를 오버라이드해, Zensical의 modern 레이아웃 위에 오렌지 브랜드 밴드가 칠해지게 함. - 두 TOML 모두에서
nav테이블을 첫[project.X]서브 테이블 위로 이동해[[project.extra.social]]아래로 조용히 파싱되지 않게 함(알파벳순 폴백이 정렬되지 않은 상단 메뉴와 잘못된 prev/next 푸터 이웃을 만들고 있었음).
테스트¶
- 스트리밍과 비스트리밍 경로 모두에서 Anthropic 변환의 system/developer 연결에 대한 회귀 커버리지, 그리고 다섯 백엔드 전체에 대한
developer역할의 백엔드별 변환기 매핑과 Chat Completions → Responses 변환기의 developer-then-system 순서 (#605, #606).
의존성¶
redis1.2.0 → 1.2.1로 bump (#598).
v1.6.1 - 2026-05-07¶
수정됨¶
- Claude Opus 4.7(
claude-opus-4-7)이 이제 Anthropic 백엔드를 통해 올바르게 라우팅됩니다 (#599, #600, #601) - 적응형 사고 API 게이트(
uses_adaptive_thinking_api)를 4.7 계열 모델 ID로 확장. Claude Opus 4.7은thinking.type == "adaptive"+output_config.effort를 요구하며, 레거시budget_tokens형태를 보내면 HTTP 400이 발생합니다. - 4.7 계열 요청 형태 규칙을 위한
model_requires_adaptive_thinking과model_forbids_sampling_params술어 추가: 명시적 수동 thinking은 적응형 사고로 정규화되고,temperature,top_p,top_k는 전달 전에 무조건 제거됩니다. opus_supports_max_effort를 Opus 4.7로 확장해,xhighreasoning effort가 Opus 4.7에서output_config.effort = "max"로 매핑됩니다.claude-opus-4-7과claude-opus-4-7-latest를 내장 지원 모델 목록과model-metadata.yaml에 추가. 추측성claude-sonnet-4-7항목은 Anthropic이 공개하기 전까지 의도적으로 광고하지 않습니다(사용자 제공 설정을 위한 방어적 요청 형태 매칭은 유지).
문서¶
- reasoning-effort 문서(EN + KO)와
backends.md를 업데이트해, Claude 4.7 패밀리의 적응형 사고 요구 사항과 무조건적인 샘플링 파라미터 deprecation을 다룹니다 (#600).
테스트¶
- Opus 4.7 적응형 사고와 무조건적 샘플링 파라미터 제거에 대한 Responses API 회귀 커버리지; Opus 4.6 / Sonnet 4.6 / Haiku 4.5 / Haiku 3.5에 대한 negative 회귀를 포함한 4.7 패밀리의 두 변환 경로(Chat Completions와 Responses) (#600, #601).
v1.6.0 - 2026-05-04¶
추가됨¶
- ChatGPT 구독 / Codex 백엔드 OAuth 디바이스 플로우 인증 (#551, #592)
continuum-router auth login --backend <name>명령이 OpenAI Codex 3단계 헤드리스 디바이스 코드 플로우를 실행합니다.POST /api/accounts/deviceauth/usercode로 일회성user_code를 발급받고,POST /api/accounts/deviceauth/token을 폴링한 뒤,/oauth/token에서 PKCE 교환을 진행합니다. 표준 RFC 8628 디바이스 플로우는 향후 다른 공급자가 구현할 경우 그대로 쓸 수 있도록 남겨두었고, 새OpenAICodexDeviceFlowClient는provider: openai에서 자동으로 선택됩니다.- 토큰은
SecretString으로 감싸져 설정된token_store에 Unix0600모드로 기록되며,O_CREAT|O_EXCL오픈 + 원자적 rename을 사용합니다. tempfile 접미사에 무작위 성분을 포함해 동시 저장 시 충돌을 방지하고, 중간 실패 시 부분 작성된 파일을 unlink해 비밀 정보가 디스크에 남지 않게 합니다. - 액세스 토큰 만료는 JWT
exp클레임에서 파싱하며(JWT가 아닌 토큰은 1시간 폴백), 공급자가 비정상적인expires_in을 보내도 갱신 폭주를 막도록 사용 가능한 최소값으로 클램프됩니다. - 만료 60초 전에 사전 갱신이 발생하며,
tokio::sync::Mutex로 single-flight 처리됩니다. 업스트림 백엔드가401을 반환하면 정확히 한 번 강제 갱신 후 한 번만 재시도합니다. 공급자가 갱신 응답에서refresh_token을 생략해도 이전 refresh token이 race-free로 보존됩니다. - 스트래티지가
identity_fingerprint()(백엔드 이름,client_id,token_store)를 보고하므로, 핫 리로드에서 이 값이 변경되면 이전 인메모리 상태를 조용히 유지하는 대신 스트래티지를 다시 만듭니다. - CLI는
verification_uri_complete와user_code에서 C0/C1 제어 문자를 출력 전에 제거해, 적대적인 공급자 응답이 ANSI 이스케이프로 터미널을 다시 그리지 못하게 막습니다. auth.openai.com과chatgpt.com/backend-api/codex로 향하는 모든 디바이스 플로우 및 런타임 요청에originator: codex_cli_rs(auth.oauth.originator로 변경 가능)와codex_cli_rs/<version>User-Agent(auth.oauth.user_agent로 변경 가능)가 들어가, 공식 Codex CLI와 일치시켜 Cloudflare가 403 JS 챌린지를 반환하는 대신 트래픽을 받아들이게 합니다.- YAML에서
auth.type: oauth가 받아들여지며, 레거시 snake_case 표기o_auth도 함께 인식됩니다.client_id와scope는 공개된 Codex CLI 값으로 기본 설정되어, ChatGPT 구독 사용 시token_store만 지정하면 됩니다. - Anthropic Messages와 Chat Completions 양쪽 surface 모두 ChatGPT Codex 백엔드로 투명하게 라우팅됩니다 (#592)
auth.type이oauth이고 공급자가 Codex 플로우(현재openai)를 사용하는 백엔드는 모델별responses_only메타데이터와 무관하게 모든 요청을 Responses API로 강제합니다.chatgpt.com/backend-api/codex는/responses만 노출하고/chat/completions이 없기 때문에, chat 모양 모델(예:gpt-5.5, alias 매핑된claude-haiku-4-5)이나 알 수 없는 모델 ID도 모두/v1/responses→…/backend-api/codex/responses경로로 dispatch됩니다. OAuth가 아닌 OpenAI 백엔드는 모델별responses_only플래그를 그대로 따릅니다.- 새
core::url_utils::compose_backend_url이/v1,/openai,/backend-api/codex세 OpenAI 호환 루트의 백엔드 URL 합성을 한 곳으로 모았습니다.proxy/backend.rs,http/handlers/responses.rs,http/streaming/handler.rs,services/responses/stream_service.rs, Anthropic 핸들러에 흩어져 있던ends_with("/v1") || ends_with("/openai")체크를 대체해,/backend-api/codex규칙이 일관되게 적용됩니다. - 프록시 핫 패스(
proxy/backend.rs,proxy/responses_only.rs,proxy/image_gen.rs,proxy/image_edit.rs)는 이제src/proxy/oauth_helper.rs를 통해AppState에 노출된 백엔드 이름 키 기반AuthStrategyRegistry를 거칩니다. 헬퍼가 스트래티지를 조회하고, 전송 직전에refresh_if_needed()를 호출하며, 정적 bearer 헤더를 스트래티지가 만든 헤더로 교체하고, 401 응답에 한 번만 강제 갱신 + 재시도합니다. 스트래티지가 등록되지 않은 백엔드는 정적api_key인증이 변경 없이 계속 동작합니다. - Anthropic 호환 핸들러(
src/http/handlers/anthropic/handler.rs)도 같은 레지스트리를 조회합니다. OAuth 스트래티지가 있는 백엔드에서는 클라이언트가 보낸Authorization: sk-ant-…와x-api-key헤더를 OpenAI로 bearer로 전달하지 않고 떨어뜨립니다. - 모델 fetcher가 OAuth 인증 백엔드를 감지해
/v1/models프로브 대신 설정된models목록으로 폴백합니다.chatgpt.com/backend-api/codex가 모델 엔드포인트를 노출하지 않기 때문입니다. - Codex 호환 Responses API 확장 (#536, #537)
- 컨텍스트 압축용
POST /v1/responses/compact엔드포인트. OpenAI / Azure OpenAI 네이티브/v1/responses/compact로 패스스루하며, 다른 백엔드 유형은501을 반환합니다. ResponsesRequest의store필드(기본값true)가 업스트림 세션 영속화를 제어합니다. Codex는 임시(ephemeral) 요청에store: false를 보냅니다.- input 항목에서 어시스턴트 콘텐츠와 사용자 콘텐츠를 구분할 수 있도록
input_text옆에output_text콘텐츠 파트 타입이 추가되었습니다. 모든 변환기(OpenAI, Anthropic, Gemini)가 새 변형을 처리합니다.
문서¶
- 루트
CHANGELOG.md와 한국어 문서(ko/configuration/backends.md,ko/configuration/advanced.md,ko/api.md,ko/architecture.md) 전반에 Codex / Responses-API 확장을 동기화. EN과 KO 빌드 양쪽의 zensical 빌드 경고를 모두 해결하고,pymdownx.slugs.slugify로 toc 앵커 slug의 유니코드를 보존합니다 (#596). - 영어와 한국어 mkdocs 소스 전반의 AI-slop 패턴 정리: 산문의 em dash 교체, filler/slop 단어 제거, 끝에 붙는 분사구와 과장된 동사 재작성, 콜론+불릿 AI 스타일 도입부 정리, 마무리 요약 slop을 구체적인 next-action 링크로 교체 (#597).
CI/CD¶
apple-actions/import-codesign-certs를 6에서 7로 bump (#590).
의존성¶
tokio1.51.0 → 1.52.1,axum0.8.8 → 0.8.9,reqwest0.13.2 → 0.13.3,clap4.6.0 → 4.6.1,fastrand2.4.0 → 2.4.1,uuid1.23.0 → 1.23.1,rand0.10.0 → 0.10.1,lru0.16.3 → 0.16.4로 bump (#595).
v1.5.6 - 2026-04-29¶
수정됨¶
responses_onlyreasoning 모델(gpt-5.4-pro, gpt-5.5-pro)에 대한/v1/chat/completions요청이 HTTP 502responses_parse_failed로 실패하던 문제 수정. OpenAI의/v1/responses응답에{ "id": "rs_...", "type": "reasoning", "summary": [] }모양의 출력 항목이 들어 있는데,OutputItem::Reasoning이content와status를 필수로 요구하던 탓에 serde가missing field 'content'로 거절했습니다. Anthropic Messages surface는 다른 변환 경로로 우회하고 있어서 직접 테스트하기 전까지 버그가 가려져 있었습니다. 이제OutputItem::Reasoning의content와status가 모두 선택 사항이며, reasoning 항목은 기존 정책에 따라 Chat Completions 클라이언트에 도달하기 전에 드롭됩니다. 즉 본문 모양은 deserialize에 성공하기만 하면 됩니다 (#594)
변경됨¶
model-metadata.yaml에서 Gemini 3.1 Pro 패밀리의 정규 ID를gemini-3.1-pro-preview로 재정렬하고,gemini-3.1-pro(와 기존-latest/-customtools형태)는 alias로 강등.generativelanguage.googleapis.com이 실제로 제공하는 ID와 맞춘 결과로, 정규 형태의gemini-3.1-pro는 업스트림에서 404를 돌려주기 때문에 GA 가용성이 있는 것처럼 보이지 않게 하려는 조정입니다. 메타데이터 캐시는 두 형태 모두 같은 항목으로 해석합니다. 참고로 업스트림으로 보내는 페이로드에서 alias를 정규 ID로 다시 쓰는 작업은 이번 릴리스 범위 밖이라,gemini-3.1-proalias로 호출하는 클라이언트는 후속 작업이 들어오기 전까지 업스트림 404를 그대로 받게 됩니다 (#594)- 샘플
config.yaml에 새로 사용 가능해진 pro / 5.5 패밀리 모델(gpt-5.4-pro,gpt-5.2-pro,gpt-5.5,gpt-5.5-pro,claude-opus-4-7,gemini-3.1-pro,gemini-3.1-pro-preview)을 등록해responses_onlydispatch 경로를 실제 업스트림으로 end-to-end 검증할 수 있게 했고, 중복된claude-haiku-4-5항목 제거
v1.5.5 - 2026-04-27¶
추가됨¶
- OpenAI Pro 모델용 투명한 Responses-API 라우팅 (epic #581)
model-metadata.yaml과 내장 OpenAI 레지스트리에responses_only: truecapability 플래그를 추가.gpt-5.2-pro,gpt-5.4-pro,gpt-5.5-pro가 업스트림/v1/responses로만 제공되는 모델로 표시됨 (#574, #582)responses_only모델에 대한/v1/chat/completions요청이 업스트림/v1/responses엔드포인트로 dispatch되고, 응답이 strict 모드의chat.completion(스트리밍은chat.completion.chunk) 봉투로 다시 변환됩니다. 클라이언트에는 투명합니다. 스트림usage는stream_options.include_usage로 제어되며, responses_only 경로에 대한 모델별 latency / success 카운터가 기록됩니다 (#578, #584)responses_only모델에 대한/anthropic/v1/messages요청은 Responses API 모양으로 변환되어/v1/responses로 dispatch되고, Anthropic Messages JSON(스트리밍은 Anthropic SSE 이벤트 시퀀스)으로 다시 변환됩니다. 도구 호출 왕복, 웹 검색 에뮬레이션, Unix 소켓 전송 모두 같은 플래그에서 분기합니다 (#575, #577, #583, #585, #586)- Anthropic Messages ↔ Responses 요청 변환기는 system → instructions, tools, tool_choice(
disable_parallel_tool_use→parallel_tool_calls: false포함),max_tokens→max_output_tokens, reasoning effort 추론, 멀티턴 도구 왕복을 모두 다룹니다. 응답 변환기는 thinking/text/tool_use 순서와 stop-reason 충실도를 그대로 보존합니다 (#575, #583) - SSE 스트리밍 브리지(
AnthropicResponsesStreamTranslator)는 Responses API 이벤트를 Anthropic Messages 이벤트로 매핑하면서 Anthropic의 엄격한 이벤트 순서 불변량(단일message_start, 짝지어진content_block_start/content_block_stop, 종결message_stop)을 유지합니다. 중간error/response.failed/response.cancelled,response.incomplete→stop_reason: max_tokens, 지연된 input 토큰, 그레이스풀한 조기 종료 합성도 처리합니다 (#576, #585) /v1/responses는 OpenAI와 Azure OpenAI 백엔드만 제공합니다.responses_only모델이 다른 백엔드 유형과 짝지어지면 업스트림 호출 전에400 invalid_request_error가 반환됩니다(/v1/chat/completions와/anthropic/v1/messages양쪽에서 거절) (#577, #589)(backend, model)쌍별 첫 dispatch가info레벨로 로깅되므로, 디버그 로그를 켜지 않고도 운영자가 Responses-API 라우팅 여부를 확인할 수 있습니다- Anthropic Messages → Responses 요청은 명시적으로
store: false를 보내 업스트림 부작용을 피합니다 (#589) - {Anthropic, Chat} × {gpt-5.4-pro, gpt-5.2-pro} × {non-streaming, streaming} × {plain, tool-call, reasoning} 매트릭스, 양쪽 surface의 중간 백엔드 실패 negatives, 업스트림 바이트 단편화 회귀 가드를 다루는 22개 결정론적 in-process 통합 테스트 (#579, #588)
docs/en/configuration/advanced.md(Responses-API-only Models 섹션을 Models-marked-out-of-the-box, Marking-a-new-model, Dispatch-behavior, Backend-type-constraint 하위 섹션으로 분리),docs/en/architecture.md(Responses-API Routing 데이터 흐름 다이어그램),docs/en/api.mdChat Completions 및 Anthropic Messages surface 노트(투명한 Responses-API 라우팅 하위 섹션 포함)에 문서화 (#580, #587)
수정됨¶
- Chat Completions responses-only 라우팅이 업스트림 dispatch 전에 호환되지 않는 백엔드 설정을 거절하고, 가능한 경우 호환되는 OpenAI/Azure Responses 백엔드를 선택하도록 수정 (#589)
- Chat assistant
tool_calls[]가/v1/chat/completions의 stateless 도구 결과 턴에 대해 Responsesfunction_callinput 항목으로 보존되도록 수정 (#589)
v1.5.4 - 2026-04-25¶
변경됨¶
- 2026년 4월 말 frontier 모델 출시에 맞춰
model-metadata.yaml갱신 (#572, #573) - GPT-5.5 추가 (\(5/\)30 per 1M, 1M 컨텍스트, 지식 cutoff 2025-12, omnimodal, Terminal-Bench 2.0에서 82.7%로 1위)와 GPT-5.5 Pro 추가 (\(30/\)180 per 1M, Responses API 전용, 심층 reasoning) — 2026-04-23 출시
- DeepSeek V4 Pro 추가 (1.6T total / 49B active MoE, 1M 컨텍스트, 384K 최대 출력, 3가지 reasoning effort 모드)와 DeepSeek V4 Flash 추가 (284B total / 13B active MoE, 1M 컨텍스트, 384K 최대 출력). 공식 API 문서에 따라
deepseek-chat과deepseek-reasoner는 deprecated alias로 유지 — 2026-04-24 출시 gpt-image-2추가 (이미지당 과금 대신 토큰 단위 과금: 텍스트 \(5/\)30, 이미지 \(8/\)30 per 1M 토큰; 1K/2K/4K 해상도 단계; 거의 모든 언어에서 ~99% 텍스트 정확도; 생성 전 내장 reasoning; 컨텍스트 인식 멀티턴 편집;gpt-image-2-latestalias) — 2026-04-21 출시- Claude Opus 4.7 추가 (\(5/\)25 per 1M, 1M 컨텍스트, 128K 최대 출력, 지식 cutoff 2026-01, 최대 2576px / 3.75MP 고해상도 이미지 지원, 새 토크나이저로 이전 모델 대비 ~1.0–1.35× 토큰 사용, 새
xhigheffort 레벨) — 2026-04-16 출시 - Gemini 3.1 시리즈를 preview에서 GA로 승격하고, fallback 호환을 위해
-preview접미사를 alias로 유지 (#573) gemini-3.1-pro-preview→gemini-3.1-pro(alias:gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-pro-latest)gemini-3.1-flash-image-preview→gemini-3.1-flash-image(alias:gemini-3.1-flash-image-preview,nano-banana-2,gemini-3.1-flash-image-latest)gemini-3.1-flash-lite-preview→gemini-3.1-flash-lite(alias:gemini-3.1-flash-lite-preview,gemini-3.1-flash-lite-latest)gemini-3-flash-preview의 deprecation 노트를 새 GAgemini-3.1-proID로 가리키도록 업데이트
v1.5.3 - 2026-04-23¶
추가됨¶
src/models/pattern_matching.rs의 새 매칭 phase 5로 HuggingFace repo 접두사 제거 (#555)try_strip_hf_repo_prefix()가vendor/repo(또는org/team/repo) 접두사를MAX_PREFIX_SEGMENTS = 3한계 안에서 검증하고, 빈 segment(/repo,vendor/,vendor//repo)를 거절하며, ASCII 공백이 들어가 있으면 거절한 뒤 잔류 문자열을 반환합니다- phase 5는 잔류 문자열로 phase 1-4에 재진입하는데, 재귀 깊이가 구조적으로 정확히 1로 보장됩니다(재진입 호출이
allow_prefix_strip게이트를 닫습니다). 따라서 접두사 제거가 기존 계층적 접미사 peel과 단일 lookup 안에서 합성됩니다 — 동기 사례인unsloth/Qwen3.6-35B-A3B-GGUF가 별도 alias 등록 없이도qwen3.6-35b-a3b로 해결됩니다 - phase 5는 wildcard phase 앞에서 실행됩니다. blast-radius 감사 결과
model-metadata.yaml에*가 포함된 alias 중/를 가진 게 없어, 순서 변경이 기존 라우팅 동작에 영향을 주지 않습니다 - tracing 출력의 phase 번호를 문서화된 phase 체인과 일치시킴(이전 코드는 namespace fallback에
phase = 7을 출력하면서 주석은 phase 6이라고 부르고 있었음) - 표준 HF 형태, suffix peel과의 합성, 대소문자 구분 vendor, 등록된 alias 우선순위, 해결 불가능한 잔류, 3-segment 형태, segment-cap 거절, no-slash 입력, 공백 거절, 빈 segment, 재진입 한계, alias-phase 우선순위를 다루는 12개 단위 테스트
tests/format_suffix_normalization_test.rs의 9개 통합 테스트가 phase 5를 거치는 전체RouterConfig/BackendConfig공개 API를 검증docs/en/configuration/advanced.md(과 한국어 카운터파트)의 파이프라인 문서에 합성 의미, 보안 한계, out-of-scope 항목(하이픈 접두사, HF API discovery)을 다루는 새 "HuggingFace repo-prefix stripping (phase 5)" 섹션 추가
변경됨¶
- 이전 phase-6 namespace fallback을 새 phase-5 HuggingFace 접두사 제거 계층으로 교체. 이전 phase는 대소문자를 구분했고 suffix peel과 합성되지 않았는데, 새 phase는 더 엄격한 입력 검증(segment cap, 빈 segment 거절, 공백 거절)을 적용하면서 phase 4의 대소문자 무시 peel과 bounded re-entry로 합성됩니다.
MAX_PREFIX_SEGMENTS(3)를 넘는 병적인 입력(provider/deep/nested/model등)은 이제 재귀적인rsplit_oncefallback으로 조용히 매칭되는 대신 phase 5에서 거절됩니다 (#555) -
560 감사에서
vendor-prefix로 분류되었던 alias(Qwen/Qwen3.6-35B-A3B,MiniMaxAI/MiniMax-M2.5등)가 #555 이후로 peel-coverable-adjacent가 되었습니다. phase 2가 명시적 alias에서 여전히 먼저 승리하지만, phase 5 + phase 4 조합으로도 같은 메타데이터에 도달합니다. 소급 제거는 #555 디자인 섹션 7에 따라 후속 감사로 미룹니다¶
수정됨¶
- 선택된 백엔드가
unix://URL로 설정되었을 때POST /anthropic/v1/messages가 동작하도록 수정 (#567) - 네이티브 Anthropic 백엔드와 OpenAI 호환 백엔드 모두 비스트리밍과 스트리밍 요청에서 Unix 소켓 위로 동작
- 공백이 포함된 소켓 경로(macOS
~/Library/Application Support/...등)도 정확히 처리 - Auth 헤더 선택(Anthropic 백엔드는
x-api-key, OpenAI 호환 백엔드는Authorization: Bearer)이 Unix 소켓 경로에서 정확히 동작 anthropic-version헤더가 Unix 소켓 경로의 Anthropic 백엔드에 자동으로 추가되어, HTTP 경로 동작과 일치
v1.5.2 - 2026-04-21¶
추가됨¶
- llama.cpp와 MLxcel 백엔드의 transport-layer passthrough 계약을 잠그는 회귀 테스트 (#562)
- 새
tests/llamacpp_passthrough_test.rs와tests/mlxcel_passthrough_test.rs가 4개 passthrough 호출 사이트를 모두 다룸: 직접 백엔드execute_chat_completion, factory로 빌드된 백엔드(BackendFactory -> LlamaCppBackend),proxy/backend.rsHTTP 핸들러, 스트리밍 핸들러 - 새
test_mlxcel_factory_backend_passthrough_nonstandard_fields가BackendFactory -> LlamaCppBackend::execute_chat_completion이 비표준 필드를 transport 시점에 byte-for-byte 보존함을 단언 - Anthropic input 테스트(
tests/anthropic_input_test.rs)에 명시적 passthrough 커버리지 추가 - passthrough 계약과 4개 보호된 호출 사이트, transport 직전에 실행되는 라우터 측 변환 목록(
global_prompts, o1/o3/gpt-5*용transform_payload_for_openai,web_search주입)을 문서화한docs/en/architecture/backend-passthrough.md와 한국어 카운터파트docs/ko/architecture/backend-passthrough.md(#562, #563) model-metadata.yaml의 모든 alias를 peel-redundant, peel-redundant-but-kept, peel-independent로 분류한docs/reports/alias-audit-2026-04.md.docs/en/configuration/advanced.md(과 한국어 카운터파트)에 두 메커니즘 중 무엇을 선호할지 설명하는 "aliases vs peel" 정책 섹션 추가 (#560)
변경됨¶
- passthrough 계약을 암묵적인 "byte-equivalent" 전역 보장에서 transport-layer 범위로 좁힘. 라우터는 transport 직전에
global_prompts주입, o1/o3/gpt-5* 페이로드 변환,web_search도구 주입을 여전히 수행할 수 있지만, transport 경계에서는 공급자별 재작성이 없음 (#563) src/http/streaming/handler.rs,src/infrastructure/backends/factory/backend_factory.rs,src/infrastructure/backends/llamacpp/backend.rs,src/proxy/backend.rs에 주석 수준의 명확화model-metadata.yamlalias를 peel-normalization 중복 여부로 감사: 정규 ID와 계층적 peel이 이미 처리하는 접미사(-4bit,-q4_k_m,-fp8,-gguf,-mlx,-awq등)만 다른 alias는 제거하고, 정규 flavor 변형(-qat,-instruct)을 인코딩하거나 파라미터 카운트를 구분하는(-1.5bvs-7b) alias는 유지 (#557)- 새
tests/alias_audit_helper.rs와tests/format_suffix_normalization_test.rs가 앞으로 peel-vs-alias 경계를 강제
CI¶
- Debian 빌드 워크플로에서 25.10 (Questing) 대신 Ubuntu 26.04 LTS (Resolute) 타겟으로 변경
debian/update-changelog.sh가 최신 릴리스가 아직 draft 상태일 때 changelog가 회귀하지 않도록 releasepublishedAt이 null이면createdAt로 폴백
v1.5.1 - 2026-04-20¶
추가됨¶
- 자체 호스팅 LLM 백엔드용 내장
web_search도구 (#553) - vLLM, Ollama, llama.cpp, MLxcel, LM Studio, Continuum Router, Generic 백엔드의 chat completion 요청에 라우터 단에서 투명하게 주입되는 도구
src/services/search/에 플러거블SearchProvider트레이트와SerperProvider구현. Exa와 Brave는 같은 트레이트 뒤에 스캐폴드 완료- 백엔드별 오버라이드가 가능한
inject_policy(auto/always/never). 상용 백엔드(OpenAI, Azure, Gemini, Anthropic)는 손대지 않아, 네이티브web_search가 그대로 흐름 - bounded 비스트리밍 도구 실행 루프가
web_search도구 호출을 파싱해 공급자를 실행하고, tool-role 결과를 추가한 뒤 백엔드를 최대max_tool_iterations라운드까지 다시 호출 - 상용/자체 호스팅 분할 불변량을 강제하는 단위 테스트가 포함된 새
BackendTypeConfig::is_self_hosted/is_commercial헬퍼 - API 키는 Debug 출력에서 마스킹되고 로그에 남지 않음.
${ENV}치환을 지원하는 핫 리로드 친화적WebSearchConfig - 도구 호출, 주입, iteration-cap 도달에 대한 Prometheus 카운터를
src/metrics/web_search에 추가 - 모델 메타데이터 lookup용 계층적 양자화 및 형식 접미사 정규화 (#549)
src/models/pattern_matching.rs의 새layered_format_strip()이 모델 ID 오른쪽에서 allowlist된 양자화/형식/flavor 토큰을 반복적으로 peel하며, peel마다 정확한 ID/alias/날짜 접미사 매칭을 다시 시도- 토큰 카테고리:
BIT_WIDTH,GGUF_QUANT,FP_FORMAT,INT_FORMAT,LIBRARY,IMATRIX,UNSLOTH,CONTAINER,FLAVOR(전부 대소문자 무시) - 파라미터 카운트 접미사 보존:
-Nbit은 양자화로 제거되고,-Nb,-aNb,-eNb,-0.6b는 파라미터 카운트로 유지 - allowlist된 flavor로 끝나는 정규 base ID(
gemma-3-12b-qat등)는 peel 실행 전 정확한 ID 매칭으로 승리 - 정규화 파이프라인이
find_matching_config,BackendConfig::get_model_metadata,RouterConfig::get_model_metadata,RouterConfig::get_thinking_pattern_config,resolve_model_tier(라우팅),get_model_profile(admin)에 연결됨 - GLM 5.1, Qwen 3.6, MiniMax M2.7 모델 메타데이터 (#548)
- 빌드와 Docker 작업이 끝난 뒤 Power Automate 웹훅을 통해 Microsoft Teams로 릴리스 알림 게시
변경됨¶
- 문서 도구 체인을 MkDocs + Material for MkDocs에서 Zensical로 마이그레이션.
mkdocs.yml을 네이티브로 읽고 필요한 확장을 번들링
수정됨¶
- 보안: 계층적 peel phase에
MAX_MODEL_ID_LEN=256과MAX_PEEL_ITERATIONS=8캡을 적용해,-4bit-4bit-4bit-...같은 병적 모델 ID로 인한 DoS(이전엔 O(n²) 할당)를 제거 - 보안:
/v1/chat/completions,/v1/completions,/v1/embeddings,/v1/embeddings/sparse에 256자 model 필드 길이 제한 적용 (기존/v1/responses체크와 동등) - 7-phase 메타데이터 매칭 파이프라인을 단일 구현(
find_matching_config_slice)에 통합하고 호출 사이트마다 얇은 어댑터를 두어,BackendConfig,Config::get_model_metadata,Config::get_thinking_pattern_config,find_matching_config사이의 drift 제거 - hot path에서
cfg.to_ascii_lowercase() == peel을str::eq_ignore_ascii_case로 교체 (요청당 String 할당 약 4000개 감소) - MkDocs 빌드 실패를 막기 위해 Pygments <2.20 핀 (Zensical 마이그레이션으로 무효화)
CI¶
- softprops/action-gh-release를 2에서 3으로 bump (#544)
- actions/github-script를 8에서 9로 bump (#545)
- actions/upload-pages-artifact를 4에서 5로 bump (#554)
문서¶
docs/en/configuration/advanced.md에 접미사 순서 모호성(-qat-4bitvs-4bit-qat)과 내부 peel phase 한계 문서화docs/en/architecture.md의 Model Aggregation Service 모듈 목록에pattern_matching.rs추가하고, suffix normalization 섹션으로 상호 참조 링크- 새
docs/en/web-search.md기능 문서.config.yaml.example에web_search섹션 확장
v1.5.0 - 2026-04-11¶
추가됨¶
- 모델 tier 및 capability profile 레지스트리를 갖춘 smart routing 시스템 (#525, #531)
- 규칙 기반 요청 분류기 및 smart routing 정책 엔진 (#526, #532)
- 부하 인식 동적 tier 조정 (#527, #533)
- 하이브리드 모드를 갖춘 LLM 기반 요청 분류기 (#528, #534)
- Smart routing 관측, admin API, 문서 (#529, #535)
- Codex 호환 Responses API 확장 (#536, #537)
변경됨¶
수정됨¶
- Smart routing 통합 갭 보완
- DefaultTransformer PDF 크기 제한을 20MB에서 32MB로 증가 (#542)
CI¶
- actions/deploy-pages를 4에서 5로 bump (#521)
의존성¶
- minor-and-patch 의존성 그룹 4개 업데이트 bump (#539)
문서¶
- Codex 호환 Responses API 갭 분석 보고서 추가
v1.4.5 - 2026-03-27¶
수정됨¶
- 파일 서비스가 설정되지 않은 상태에서 파일 참조가 사용되면 400 오류를 반환하도록 수정 (#519)
변경됨¶
- GLM-5-Turbo 모델 메타데이터 추가 (#516)
문서¶
- ko/ 문서의 한국어 anti-AI-slop 위반 수정
- api.md의 슬롭 단어와 전환어 수정
v1.4.4 - 2026-03-18¶
수정됨¶
- high/xhigh reasoning effort에서 Anthropic thinking이 실패하던 문제 수정.
budget_tokens(32768)가 기본max_tokens(16384)를 초과해 API가 거절했음 (#514) - thinking이 켜져 있고 budget이 max를 초과하면
max_tokens를budget_tokens + 4096으로 자동 조정
변경됨¶
- GPT-5.4 모델 패밀리 추가: 1M 컨텍스트 윈도우의
gpt-5.4,gpt-5.4-pro,gpt-5.4-mini,gpt-5.4-nano(#515) - Gemini 3 시리즈 업데이트:
gemini-3.1-pro-preview,gemini-3-flash-preview,gemini-3.1-flash-lite-preview추가,gemini-3-pro-previewdeprecation 표시 include_thoughts자동 주입을 위해 Gemini 3 Flash와 3.1 Flash-Lite를 thinking 모델로 인식- Claude 4.6 모델 업데이트: 컨텍스트 윈도우 1M(GA), Sonnet 4.6 max_output을 64K로 수정, 지식 cutoff 보정
- 8개 파일에 걸쳐 설정 예시와 문서를 최신 모델 이름으로 업데이트
v1.4.3 - 2026-03-18¶
수정됨¶
- 라우터를 거친 스트리밍 응답에서 Gemini thinking 모델(2.5 Pro, 3 Pro 등)이
reasoning_content를 반환하지 않던 문제 수정 (#513) transform_payload_for_gemini()를transform_request_gemini()로 3개 Gemini 스트리밍 경로 모두에서 교체해include_thoughts: true자동 주입을 보장
v1.4.2 - 2026-03-17¶
변경됨¶
- 스트리밍 안정성 개선을 위해 mid-stream fallback 기본값을 활성화로 변경 (#504)
- Breaking: mid-stream fallback이 이제 기본 활성화. 이전 동작으로 복원하려면
mid_stream_fallback.enabled: false를 설정
문서¶
- fallback 동작을 최적화하기 위한 failover latency 튜닝 가이드 추가
v1.4.1 - 2026-03-17¶
추가됨¶
- 스트리밍 추론용 mid-stream fallback (#497) — SSE 스트리밍 중에 백엔드가 실패하면 라우터가 fallback 백엔드로 투명하게 재시도
변경됨¶
- pre-stream fallback과 mid-stream fallback을 분리 (#500) — 각각 독립적으로 활성화/비활성화 가능
- 의존성 버전을 최신 메이저로 bump
수정됨¶
- 스트리밍 설정 변경이 핫 리로드에서 감지되지 않던 문제 수정 (#503)
- fallback 중 mid-stream 연결 오류가 클라이언트로 누설되던 문제 수정 (#502)
- 사용하지 않는 config crate 의존성 제거
CI¶
v1.4.0 - 2026-03-14¶
추가됨¶
- Prefix-aware routing: PrefixAwareHash 선택 전략과 Bounded Loads를 적용한 Consistent Hash (CHWBL) (#455, #457, #461)
- Response caching: SHA256 기반 캐시 키 계산, 스트리밍 응답 버퍼링과 완료 후 캐싱 (#456, #459, #462)
- 다단계 CacheStore: 인메모리 백엔드 (#466), 연결 풀링이 있는 Redis/Valkey 백엔드 (#467), S3 기반 L1/L2 다단계 캐시 (#483)
- KV 캐시 인덱스: 공유 데이터 구조 (#470), vLLM 백엔드 스트림용 KV 이벤트 컨슈머 (#471), 백엔드 선택에 통합된 prefix overlap 점수 (#473), 설정/메트릭/admin 엔드포인트 (#474)
- 스토리지 tier 인식이 있는 Tiered KV cache (GPU hot / external warm) (#484)
- 외부 KV 텐서 전송을 사용하는 disaggregated prefill/decode 오케스트레이션 (#485)
- Anthropic cache_control breakpoint 자동 주입 (#460)
- Gemini Embedding 2용 멀티모달 임베딩 지원 (#492)
- 공유 캐시 설정 및 운영 메트릭 (#468)
- model-metadata.yaml에 30개 신규 모델 추가 (#472)
변경됨¶
- VAST 전용 식별자를 일반 S3/외부 스토리지 이름으로 변경 (#490) — VAST 전용 필드 이름을 사용 중이라면 설정 파일을 업데이트하세요
수정됨¶
- 공백이 포함된 Unix 소켓 경로용으로 RequestExecutor를 transport-aware로 변경 (#488)
- 문서의 상대 source tree 링크를 GitHub URL로 교체
CI¶
- docker/setup-qemu-action을 3에서 4로 bump (#428)
- docker/metadata-action을 5에서 6으로 bump (#426)
- docker/setup-buildx-action을 3에서 4로 bump (#429)
- docker/build-push-action을 6에서 7로 bump (#430)
- docker/login-action을 3에서 4로 bump (#427)
문서¶
- 종합적인 KV cache 기능 문서, 벤치마크, 설정 예시 (#477)
- VAST Data 연결 가이드 및 통합 예시 (#486)
- 한국어 문서를 영문 카운터파트와 동기화
- 단일 configuration.md를 6개 파일로 분할
v1.3.0 - 2026-03-12¶
추가됨¶
- Agent Communication Protocol (ACP) 지원: JSON-RPC 2.0 프로토콜 계층과 stdio transport (#414, #420)
- ACP 세션 관리: 프로토콜 라이프사이클, initialize/shutdown handshake (#415, #421)
- ACP-to-LLM 추론 파이프라인: 스트리밍 지원 (#416, #422)
- ACP 도구 호출 보고와 권한 위임 (#417, #423)
- MCP 서버 터널링용 MCP-over-ACP 브리지 (#418, #424)
- 메타데이터와 설정 지원이 있는 ACP 에이전트 레지스트리 (#419, #425)
- 프로토콜 라이프사이클과 세션 관리용 ACP 통합 테스트
수정됨¶
- ACP 통합 테스트의 clippy
field_reassign_with_default경고 해결
CI¶
- actions/upload-artifact를 6에서 7로 bump (#398)
문서¶
- MkDocs 통합이 있는 ACP 아키텍처 문서
- IDE 통합 예시가 있는 ACP 실용 사용 가이드
- 라우터 단 캐싱 전략용 KV 캐시 통합 계획
v1.2.1 - 2026-03-07¶
추가됨¶
- MLX 기반 모델 서빙용 MLxcel 백엔드 유형 지원 (#412, #413) — llama-server와 API가 완전히 호환되며, 헬스 체크/모델 디스커버리/프록싱에 같은 백엔드 구현을 재사용
v1.2.0 - 2026-03-06¶
추가됨¶
-
관리자 통계 API - 관리자 API 엔드포인트를 통한 포괄적인 요청 수준 통계 수집 및 보고 추가 (#409)
GET /admin/stats- 전체, 모델별, 백엔드별 통계GET /admin/stats/models- 모델별 통계만GET /admin/stats/backends- 백엔드별 통계만POST /admin/stats/reset- 수집된 모든 통계 초기화?window=1h,?window=24h등을 통한 시간 윈도우 쿼리- OpenAI 호환 API 응답에서의 토큰 사용량 추적
- 링 버퍼에서 계산되는 지연 백분위수 (p50, p95, p99)
- 최소 성능 영향을 위한 핫 패스에서의 락프리 원자적 카운터
- 통계 설정 변경에 대한 핫 리로드 지원
-
통계 영속화 - 관리자 통계를 위한 영속적 스토리지 추가 (#410, #411)
- 설정 가능한 스냅샷 경로, 간격, 비활성 검사를 위한 최대 수명
- 데이터 안전을 위한 원자적 쓰기 (임시 파일 + 이름 변경)
- 누락, 손상 또는 비활성 파일의 그레이스풀 처리로 시작 시 디스크에서 통계 복원
- 그레이스풀 셧다운 (SIGTERM/SIGINT) 시 최종 스냅샷 저장
문서¶
- 설정 가이드에 관리자 통계 및 영속화 추가
- v1.1.0 리팩토링 후 벤치마크 보고서 추가 (#407)
- admin-api.md에 통계 API 문서화 (영어 및 한국어)
v1.1.1 - 2026-03-04¶
추가됨¶
-
임베딩 가능한 라이브러리 크레이트 - continuum-router를 임베딩 가능한 라이브러리 크레이트로 활성화 (1단계) (#394)
-
영속적 런타임 API 키 - 런타임 API 키를 위한 영속적 스토리지 추가 (#405)
-
모델 메타데이터 - 새로운 LLM 모델에 대한 모델 메타데이터 항목 추가 (#403)
수정됨¶
- Anthropic 핸들러에서 Gemini 전용 변환 적용 (#404)
v1.1.0 - 2026-03-01¶
추가됨¶
-
임베디드 WebUI - 설정 관리 및 API 키 관리를 위한 임베디드 WebUI 추가 (#388)
- 라우터 바이너리에서 직접 제공되는 브라우저 기반 관리 인터페이스
- 외부 도구 없이 설정 편집 및 관리
- 그래픽 인터페이스를 통한 API 키 관리
-
Windows AF_UNIX 소켓 지원 - socket2 크레이트를 통한 Windows AF_UNIX 소켓 지원 추가 (#390)
socket2크레이트를 사용한 Windows에서의 완전한 Unix 도메인 소켓 지원- Windows 전용 소켓 주소 처리 및 전송 파싱
- Windows 플랫폼에서 로컬 IPC 활성화
-
Nano Banana 2 (Gemini 이미지 생성) - Nano Banana 2 이미지 생성 모델 지원 추가
- 최신 모델 세대에 대한 업데이트된 Gemini 이미지 생성 지원
수정됨¶
ClientAddr::is_unix에서 튜플 변형 매칭 컴파일 오류 해결- Windows AF_UNIX 소켓 accept 실패 및 설정 검증 해결
- Unix 소켓 설정 검증에서 Windows 절대 경로 허용 (#393)
- Unix 소켓 테스트 및 전송 파싱에서 Windows 컴파일 오류 해결 (#392)
v1.0.0 - 2026-02-19¶
추가됨¶
-
Continuum Router / Backend.AI GO 백엔드 타입 - 연합 LLM 라우팅을 위한
continuum-router전용 백엔드 타입 추가 (#385)- 원격 Continuum Router 인스턴스 또는 Backend.AI GO 배포에 연결
- 다중 serde 별칭:
continuum-router,continuum_router,ContinuumRouter,backendai,backend-ai,backend_ai - Bearer 토큰 인증 및 자동 헤더 주입
- 실시간 응답을 위한 SSE 스트리밍 지원
/v1/models엔드포인트를 통한 자동 모델 탐색/health를 기본으로/v1/models폴백을 사용하는 헬스 체크- 사용 사례: 연합 라우팅, 다중 리전 로드 밸런싱, 계층적 페일오버
-
LM Studio 백엔드 타입 -
lmstudio전용 백엔드 타입 추가 (#381)- LM Studio 로컬 서버에 대한 네이티브 지원
- 기본 URL:
http://localhost:1234(LM Studio 기본 포트) - Serde 별칭:
LMStudio,lm-studio,lm_studio /v1/models기본,/api/v1/models폴백을 사용하는 헬스 체크- LM Studio 모델 목록에서 자동 모델 탐색
/v1/models응답에서owned_by: lmstudio적절한 귀속
-
Claude 4.6 적응형 사고와
output_config.effort- Claude 4.6 모델을 위한 Anthropic의output_config.effort파라미터 지원 추가 (#384)- Claude 4.6 모델(
claude-opus-4-6-*,claude-sonnet-4-6-*)은 이제budget_tokens대신thinking: {"type": "adaptive"}+output_config: {"effort": "<level>"}사용 - 새로운
xhigh→"max"effort 매핑: Opus 4.6용, Sonnet 4.6은 자동으로"high"로 다운그레이드 (max는 Opus 4.6 전용) minimalreasoning effort는"low"effort로 매핑 (Anthropic API에 minimal 레벨 없음)autoeffort는output_config를 생략하여 Anthropic 기본값(high) 사용- 4.6 이전 모델은 기존
budget_tokens방식 유지 - 역변환: Anthropic 요청을 OpenAI 형식으로 변환 시
output_config.effort를reasoning_effort로 매핑 - Responses API 변환기가 Claude 4.6 모델에서 적응형 사고를 사용하도록 업데이트
- Claude 4.6 모델(
-
적응형 사고 및
autoReasoning Effort 레벨 - Claude Opus 4.6과 함께 도입된 Anthropic의 적응형 사고({"type": "adaptive"}) 지원 추가 (#377)- Anthropic 백엔드에서
{"type": "adaptive"}로 매핑되는 새로운autoreasoning effort 레벨. 모델이 사고의 시점과 정도를 동적으로 결정 - 모든 백엔드에서
auto허용: Anthropic에서는 네이티브 적응형 사고, OpenAI 및 Gemini에서는 자동으로medium으로 다운그레이드 - 적응형 사고(고정 예산 없음)를 지원하기 위해
AnthropicThinking.budget_tokens가Option<u32>로 변경 - 평면 형식(
reasoning_effort: "auto")과 중첩 형식(reasoning: {effort: "auto"}) 모두 지원 - 적응형 사고 활성화 시 temperature 파라미터 자동 제거
- Anthropic 백엔드에서
-
Cohere/Jina 호환 리랭킹 및 희소 임베딩 엔드포인트 - 고급 검색 API 지원 추가 (#374)
- 2단계 검색을 위한 새로운
/v1/rerank엔드포인트 (Cohere 호환) - 희소 임베딩 (SPLADE 형식)을 위한 새로운
/embed_sparse엔드포인트 (TEI/Jina 호환) - 리랭킹을 위한 단순 문자열 문서 및 text 필드가 있는 구조화된 문서 모두 지원
- model, query, documents 필드에 대한 포괄적인 검증이 포함된 요청/응답 타입
- 새로운 기능 매핑:
rerank->rerank메서드,sparse_embedding->embed_sparse메서드 - model-metadata.yaml에 예제 모델 추가: BGE Reranker, Jina Reranker, SPLADE 모델
- 2단계 검색을 위한 새로운
-
BGE-M3 및 다국어 임베딩 모델 지원 - BGE-M3 및 동등한 다국어 임베딩 모델에 대한 모델 메타데이터 및 설정 예제 추가 (#373)
- BGE-M3: 568M 파라미터, 1024 차원, 100개 이상 언어, 8192 컨텍스트. Dense, sparse(어휘), ColBERT 멀티벡터 검색 지원
- BGE-Large-EN-v1.5: 335M 파라미터, 1024 차원, 영어 전용, 512 컨텍스트
- Multilingual-E5-Large: 560M 파라미터, 1024 차원, 100개 이상 언어, 514 컨텍스트
- vLLM, Ollama, Text Embeddings Inference (TEI) 배포를 위한 예제 백엔드 설정
- RAG 시스템의 교차 언어 검색 요구 사항 해결
-
Anthropic 파일 변환기에 일반 텍스트 지원 추가 - Anthropic 파일 변환기에 text/plain 지원 추가 (#342)
- 텍스트 파일은 base64 데이터가 포함된
document블록으로 변환 (PDF와 동일한 형식) - 최대 텍스트 파일 크기: 32MB (PDF와 동일)
- 텍스트 파일은 매직 바이트 검증 없음 (모든 콘텐츠 허용)
- SUPPORTED_DOCUMENT_TYPES에
application/pdf와 함께text/plain추가 - 오류 메시지에 일반 텍스트 지원 언급 추가
- 텍스트 파일은 base64 데이터가 포함된
-
OpenAI 및 Anthropic 파일 변환기에 PDF 지원 추가 - 파일 변환기에 PDF 파일 지원 추가 (#340)
- OpenAI 변환기: PDF를 base64 데이터 또는 파일 ID가 포함된
file블록으로 변환 - Anthropic 변환기: PDF를 base64 데이터가 포함된
document블록으로 변환 - 보안을 위한 PDF 매직 바이트 검증 (
%PDF-시그니처) - 최대 PDF 크기: 32MB (백엔드에서 100페이지 제한 적용)
- 이미지는 20MB 제한 유지
- OpenAI 변환기: PDF를 base64 데이터 또는 파일 ID가 포함된
-
네이티브 Anthropic Responses API 지원 - Responses API에 대한 네이티브 Anthropic Messages API 변환 추가 (#332)
- Responses API 요청을 네이티브 Anthropic Messages 형식으로 변환하는 새로운
AnthropicConverter - Anthropic 문서 이해 기능을 통한 전체 PDF 파일 지원 (
file_data가 포함된input_file) - 자동 미디어 타입 감지를 통한 이미지 파일 지원
- Claude 3+ 모델을 위한 확장 사고(Extended Thinking) 콘텐츠 지원
- Anthropic 형식의 적절한 SSE 이벤트 변환을 통한 스트리밍 지원
- 완전한 응답 변환을 통한 비스트리밍 지원
- Responses API 요청을 네이티브 Anthropic Messages 형식으로 변환하는 새로운
수정됨¶
-
외부 파일 URL에 대한 SSRF 검증 - 외부 파일 가져오기 시 SSRF 보호 추가 (#332)
- 사설 IP 주소 검증 (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x 차단)
- Localhost 및 링크 로컬 주소 차단
- IPv6 루프백 및 링크 로컬 주소 차단
- 해석 후 IP 검증을 통한 DNS 리바인딩 보호
-
파일 입력에 대한 미디어 타입 화이트리스트 - 허용된 파일 유형에 대한 보안 화이트리스트 추가 (#332)
- PDF:
application/pdf - 이미지:
image/jpeg,image/png,image/gif,image/webp - 지원되지 않는 미디어 타입은 명확한 오류 메시지와 함께 거부
- PDF:
-
AI SDK Responses API 스트리밍 호환성 - Responses API 스트리밍에서 Vercel AI SDK 호환성 문제 수정 (#334)
- OpenAI 스펙에 맞춰
ResponseStreamEvent직렬화가 점으로 구분된 타입 이름 사용 (예:"type": "output_text_done"대신"type": "response.output_text.done") OutputItemAdded,OutputItemInProgress,OutputItemDone및 기타 스트리밍 이벤트에item_id필드 추가- 이벤트 순서 추적을 위한
sequence_number필드 추가 (긴 스트리밍 세션에서 오버플로우 방지를 위해u64사용) ResponseStreamEvent에 대한 커스텀Serialize구현으로 올바른 JSON 출력 형식 보장- 새 필드가 올바르게 직렬화되는지 확인하기 위해 모든 기존 스트리밍 테스트 업데이트
- OpenAI 스펙에 맞춰
-
핫 리로드 백엔드 동기화 후 즉시 헬스 체크 - 핫 리로드로 백엔드 추가 시 즉시 헬스 체크 실행 (#367)
- 설정 핫 리로드로 추가된 새 백엔드가 이제 즉시 헬스 체크됨
- 이전에는 새 백엔드가 최대 30초(기본 헬스 체크 간격) 동안 사용 불가 상태로 유지됨
- 외부 호출을 위해
HealthChecker::perform_health_checks()공개 설정 - Backend.AI GO 및 기타 클라이언트에서 모델 가용성 응답성 개선
v0.36.1 - 2026-01-30¶
수정됨¶
- 핫 리로드 중 sync_backends 직후 즉시 헬스 체크가 트리거되도록 수정 (#368) — 새 백엔드를 최대 30초 대기 대신 1-2초 안에 사용 가능
- 핫 리로드 중 health_check_info 동기화와 URL 기반 업데이트 사용 (#369) — 새 백엔드가 API 키 인증을 정확히 수신
- 최근 추가된 백엔드의 헬스 체크 가속화 — 추가 후 5분 동안 1초 간격으로 체크
- 백엔드가 정상 상태로 전환될 때 5초 디바운스로 모델 캐시 갱신 트리거
v0.36.0 - 2026-01-27¶
추가됨¶
- 엔드포인트 라우팅이 포함된 네이티브 Anthropic Messages API 핸들러 (#355)
- Anthropic ↔ OpenAI 요청/응답 변환 (#356, #357)
- Anthropic 스트리밍 응답 형식 (#358)
- Anthropic ↔ Gemini 직접 요청/응답 변환 (#359)
- Anthropic 입력의 file_id 소스 타입과 파일 해석 (#360)
- Anthropic 핸들러용 Claude Code 호환성 (#365)
- 모든 백엔드 유형에 대한 계층적 토큰 카운팅
- 성능 향상을 위한 병렬 파일 참조 해석
- anthropic-version 헤더 형식 검증
수정됨¶
- SSRF 방지를 위해 이미지 및 문서 URL에 HTTPS 요구
- 백엔드 세부 정보 대신 일반 오류 메시지를 클라이언트에 반환
- 파일 소유권 체크에 API 키로 인증된 user_id 사용
- 안전한 메시지/도구 ID 생성을 위한 UUID v4 사용
- Anthropic-to-OpenAI 변환에서 도구 메시지를 사용자 텍스트 앞에 배치
- tool_use 블록이 있을 때 stop_reason을 tool_use로 오버라이드
- OpenAI로 라우팅되는 Anthropic 요청에 max_completion_tokens 변환 적용
- 파일 접근 거부 및 not found 오류를 클라이언트로 전파
- 일관된 동작을 위해 요청당 current_config()를 한 번만 호출
리팩터¶
- 공통 SSE 이벤트 타입과 데이터 추출 로직 분리
- 정확한 UTF-8 처리를 위해 SseParser에 parse_bytes 메서드 추가
- AnthropicFileResolver의 불필요한 Arc 래퍼 제거
- enum 크기를 줄이기 위해 FileResolutionResult::Resolved를 Box 처리
v0.35.0 - 2026-01-23¶
추가됨¶
- 함수 호출에서 Gemini 3 thoughtSignature 지원 (#354)
- OpenAI와 Anthropic 파일 변환기에 PDF 지원 (#340)
- AnthropicFileTransformer의 text/plain 지원 (#342)
수정됨¶
- DefaultTransformer와 파일 해석에 PDF 지원 추가 (#343)
- 비스트리밍 Anthropic 요청에 도구 메시지 변환 추가 (#344)
- DefaultTransformer가 이미지가 아닌 파일을 명확한 오류 메시지로 거절 (#338)
- AI SDK가 Responses API 스트리밍 형식과 호환되지 않던 문제 수정 (#335)
v0.34.0 - 2026-01-16¶
추가됨¶
- 자동 품질 파라미터 변환 - DALL-E와 GPT Image 모델 간 자동 품질 파라미터 변환 추가 (#330)
- GPT Image 품질 값을 DALL-E 동등 값으로 변환하는
ImageQuality열거형의to_dalle_quality()메서드 handle_openai_image_generation()및handle_streaming_image_generation()에서 투명하게 적용되는 품질 변환- 품질 변환 매핑:
- DALL-E 3: low/medium/auto → standard, high → hd
- GPT Image: standard → medium, hd → high
- Gemini 모델: 품질 파라미터 무시 (변경 불필요)
- 정확한 DALL-E 3 모델 매칭을 위한
is_dalle3_model()헬퍼 - 코드 중복 제거를 위한
convert_quality_for_model()헬퍼 - 변환은 디버깅을 위해 로그되며 사용자 대면 경고 없이 투명하게 수행
- GPT Image 품질 값을 DALL-E 동등 값으로 변환하는
v0.33.0 - 2026-01-13¶
추가됨¶
- Responses API 로컬 파일 해석 - Responses API 요청에서 로컬
file_id참조 해석 (#325)- Files API를 통해 업로드된 파일을 이제 Responses API 요청에서
file_id로 참조 가능 - FileResolver 서비스가 요청에서
file_id참조를 스캔하고 로컬 스토리지에서 콘텐츠 로드 - 파일 콘텐츠가 백엔드로 전송되기 전에 base64
file_data형식으로 변환 - 보안 기능: 파일 소유권 검증(user_id 확인) 및 주입을 위한 10MB 크기 제한
- 그레이스풀 디그레이드: 해석 실패 시 경고 로그와 함께 원본 요청으로 폴백
- Files API를 통해 업로드된 파일을 이제 Responses API 요청에서
수정됨¶
- Responses API 플랫 도구 형식 - Responses API
/v1/responses엔드포인트가 플랫 도구 형식을 허용하도록 수정 (#323)- 함수 도구는 이제 플랫 형식 사용:
{"type": "function", "name": "...", "parameters": {...}} - OpenAI Responses API 사양에 맞춤
- 중첩 형식(
function래퍼 객체 포함)은 더 이상 Responses API에서 허용되지 않음 - 플랫 도구 형식 예제로 문서 업데이트
- 함수 도구는 이제 플랫 형식 사용:
v0.32.0 - 2026-01-09¶
추가됨¶
- Reasoning Effort 문서 및 xhigh 폴백 로깅 - 포괄적인 reasoning effort 문서 추가 및 xhigh effort 레벨의 폴백 로깅 개선 (#317)
- reasoning effort 파라미터 사용법을 설명하는 새로운 문서
- 비 GPT-5.2 모델에서 xhigh effort 레벨이 high로 폴백될 때의 개선된 로깅
수정됨¶
- Responses API InputItem의 암시적 메시지 타입 추론 - role 필드가 없을 때 암시적 메시지 타입 추론 지원 (#316)
- 더 나은 성능을 위한 최적화된 InputItem 역직렬화기
- 잘못된 role 테스트 커버리지 추가
- Responses API에서 더 유연한 입력 처리 활성화
v0.31.5 - 2026-01-09¶
추가됨¶
-
네이티브 OpenAI 백엔드용 Responses API 패스스루 - 백엔드 유형에 따른
/v1/responsesAPI 스마트 라우팅 (#313)- OpenAI 및 Azure OpenAI 백엔드는 이제 패스스루 모드를 사용하여
/v1/responses엔드포인트로 요청을 직접 전달 - 다른 백엔드(Anthropic, Gemini, vLLM, Ollama, LlamaCpp, Generic)는 자동으로 네이티브 형식으로 변환
- 패스스루 모드 이점: 네이티브 PDF 지원, 보존된 추론 상태, 내장 도구(web_search, file_search) 접근, 향상된 캐시 활용
- 라우팅 결정을 위한
ResponsesApiStrategy열거형이 포함된 새로운router.rs모듈 - 직접 요청 전달을 위한
PassthroughService가 포함된 새로운passthrough.rs모듈 - DoS 방지를 위한 요청 페이로드 크기 검증 (16MB 제한)
- 라우팅 전략, 오류 처리, 요청 검증에 대한 포괄적인 테스트 커버리지
- OpenAI 및 Azure OpenAI 백엔드는 이제 패스스루 모드를 사용하여
-
OpenAI Responses API 파일 입력 유형 - Responses API에서 멀티모달 파일 입력 지원 추가 (#311)
- 새로운
input_text,input_file,input_image콘텐츠 파트 유형 - base64 데이터 URL(
file_data)을 통한 PDF 문서 및 이미지 지원 - SSRF 검증이 포함된 외부 파일 URL(
file_url) 지원 - 지원되지 않는
file_id참조에 대한 경고 로그 (Files API 통합 보류 중) - Anthropic(document/image 블록) 및 Gemini(inline_data/file_data)용 백엔드별 변환기
- 모든 입력 유형에 대한 포괄적인 테스트 커버리지
- 새로운
수정됨¶
- 패스스루 원시 오류 응답 - 더 나은 오류 디버깅을 위해 패스스루 모드에서 백엔드 원시 오류 응답 전달
v0.31.4 - 2026-01-07¶
수정됨¶
- API 키 전달을 위한 핫 리로드 지원 - 프록시 및 스트리밍 핸들러에서 핫 리로드 지원 수정 (#310)
- 프록시 및 스트리밍 핸들러에서 캡처된 설정 스냅샷 대신
current_config()사용 - 핫 리로드를 통한 API 키 및 기타 설정 변경이 이제 새 요청에 올바르게 적용됨
- 런타임 설정 업데이트가 백엔드 요청 전달에 영향을 미치도록 보장
- 핫 리로드 api_key 적용을 위한 포괄적인 엔드투엔드 테스트 추가
- 프록시 및 스트리밍 핸들러에서 캡처된 설정 스냅샷 대신
v0.31.3 - 2026-01-06¶
수정됨¶
-
Unix 소켓 Anthropic 요청/응답 변환 - 변환 누락으로 인한 Unix 소켓을 통한 Anthropic 백엔드 실패 수정 (#307, #308)
- Unix 소켓 전송이 이제 HTTP 전송과 동일한 요청 변환을 Anthropic 백엔드에 적용
- OpenAI 형식 요청이 전송 전에 Anthropic 형식으로 올바르게 변환됨
- Anthropic 응답이 OpenAI 형식으로 다시 변환됨
- 엔드포인트가
/v1/chat/completions에서/v1/messages로 올바르게 재작성됨 - Unix 소켓 Anthropic 변환을 위한 포괄적인 통합 테스트 추가
-
Anthropic 비스트리밍 스트림 파라미터 - Anthropic 비스트리밍 요청에 대한 스트림 파라미터 보존 (#305, #306)
- 비스트리밍 경로에서
transform_openai_to_anthropic_request(stream: true 강제) 대신transform_openai_to_anthropic_with_global_prompt사용 stream: false인 요청이 잘못stream: true로 Anthropic API에 전송되는 문제 수정- 명확성을 위해
transform_openai_to_anthropic_request를transform_openai_to_anthropic_streaming으로 이름 변경
- 비스트리밍 경로에서
문서¶
- Jinja2 구문 이스케이프 - mkdocs-macros-plugin 오류를 방지하기 위해 한국어 설정 문서에서 Jinja2 구문 이스케이프
v0.31.2 - 2026-01-05¶
추가됨¶
-
Anthropic 백엔드의 비스트리밍 지원 - 비스트리밍 채팅 완료 호출에 대해 OpenAI 형식 요청을 Anthropic 형식으로 변환하고 Anthropic 응답을 OpenAI 형식으로 다시 변환
- Anthropic 백엔드로의 비스트리밍 요청이 이제 요청/응답 형식을 올바르게 변환
- 적절한 도구 호출 처리를 위해 transform_str을 사용하도록 스트리밍 핸들러 업데이트
-
Anthropic 백엔드의 도구 호출 및 도구 결과 변환 - Anthropic 모델로 라우팅 시 적절한 도구 사용 워크플로우 활성화
- 어시스턴트 메시지의 OpenAI 스타일 tool_calls를 Anthropic의 tool_use 형식으로 변환
- 도구 결과 메시지를 Anthropic의 tool_result 형식으로 변환
- Anthropic 모델과의 멀티턴 도구 사용 대화 활성화
의존성¶
- rustls, tokio-stream, syn을 포함한 12개 패키지를 최신 버전으로 업데이트
v0.31.1 - 2026-01-04¶
수정됨¶
- Anthropic 비스트리밍 인증 헤더 수정 - 잘못된 인증 헤더로 인한 Anthropic 비스트리밍 요청 실패 문제 수정 (#300, #301)
- Anthropic 백엔드로의 비스트리밍 요청이 이제
Authorization: Bearer대신x-api-key헤더를 올바르게 사용 - 모든 Anthropic 백엔드 요청에
anthropic-version헤더 추가 - HTTP 및 Unix 소켓 전송 경로 간 일관된 헤더 처리 적용
- Anthropic API가 "Invalid Anthropic API Key" 오류 (HTTP 400)를 반환하던 문제 수정
- Anthropic 백엔드로의 비스트리밍 요청이 이제
v0.31.0 - 2026-01-04¶
추가됨¶
-
Unix 소켓 서버 바인딩 - TCP와 함께 Unix 소켓 바인딩 지원 추가 (#298)
- 서버가 이제 로컬 통신을 위해 Unix 도메인 소켓에 바인딩 가능
- 설정 파일에서
server.unix_socket으로 설정 - TCP와 Unix 소켓 동시 바인딩 지원
-
Responses API의 Reasoning 파라미터 지원 -
/v1/responses엔드포인트에reasoning파라미터 지원 추가 (#296)- 중첩 형식 지원:
{"reasoning": {"effort": "high"}} - 유효한 노력 수준:
low,medium,high,xhigh(GPT-5.2 전용) ReasoningEffortLevel열거형을 사용한 타입 안전 검증- 백엔드를 위한 평면
reasoning_effort형식으로 자동 변환 - 역직렬화 시 잘못된 노력 값은 명확한 오류 메시지와 함께 거부
ResponsesRequest에with_reasoning()빌더 메서드 추가
- 중첩 형식 지원:
-
설정 가능한 헬스 체크 엔드포인트 - 백엔드 타입별 설정 가능한 헬스 체크 엔드포인트 추가
- 백엔드 타입에 따른 헬스 체크 경로 커스터마이징
- 백엔드별 헬스 검증 지원
v0.30.0 - 2026-01-01¶
추가됨¶
- 와일드카드 패턴 및 모델 별칭의 날짜 접미사 처리 - 모델 별칭에서 와일드카드 패턴 및 자동 날짜 접미사 처리 지원 (#286)
- 자동 날짜 접미사 정규화: 날짜 접미사가 있는 모델 (예:
claude-opus-4-5-20251130)이 기본 모델 (예:claude-opus-4-5-20251101)의 메타데이터와 자동으로 매칭 - 지원되는 날짜 형식:
-YYYYMMDD,-YYYY-MM-DD,-YYMM,@YYYYMMDD *문자를 사용한 별칭의 와일드카드 패턴 매칭- 접두사 패턴:
claude-*가claude-opus,claude-sonnet등과 매칭 - 접미사 패턴:
*-preview가gpt-4o-preview,o1-preview등과 매칭 - 중위 패턴:
gpt-*-turbo가gpt-4-turbo,gpt-3.5-turbo등과 매칭 - 제로 설정 날짜 처리: 설정 변경 없이 자동으로 작동
- 매칭 우선순위: 정확한 ID > 정확한 별칭 > 날짜 접미사 > 와일드카드 > 기본 이름 폴백
- 자동 날짜 접미사 정규화: 날짜 접미사가 있는 모델 (예:
수정됨¶
- Anthropic 백엔드 기본 URL - 지정되지 않은 경우 Anthropic 백엔드에 기본 URL 적용 (#288)
- 백엔드별 owned_by 값 - owned_by 플레이스홀더를 백엔드 타입별 값으로 교체 (#287)
문서¶
- 와일드카드 패턴 및 날짜 접미사 처리 문서를 한국어로 번역 (#289)
v0.29.0 - 2026-01-01¶
추가됨¶
- 백엔드 웜업 중 가속 헬스 체크 - 백엔드 웜업 시 가속 헬스 체크 구현 (#282)
- 백엔드가 HTTP 503 (Service Unavailable)을 반환하면 "웜업 중" 상태로 진입
- 웜업 중 헬스 체크가 가속된 간격(기본: 1초)으로 수행
- 모델 가용성 감지 지연을 최대 30초에서 약 1초로 감소
warmup_check_interval(기본: 1초) 및max_warmup_duration(기본: 300초)으로 설정 가능- 모델 로딩 중 HTTP 503을 반환하는 llama.cpp 같은 백엔드에 특히 유용
- 모델 메타데이터 CLI 옵션 - 모델 메타데이터 파일 경로 지정을 위한 --model-metadata 옵션 추가 (#281)
- 런타임에 모델 메타데이터 YAML 파일을 지정하는 새로운
--model-metadataCLI 인자 - 설정 파일의
model_metadata_file설정을 오버라이드 - 절대 경로, 상대 경로, 틸다 확장(~) 지원
- 런타임에 모델 메타데이터 YAML 파일을 지정하는 새로운
수정됨¶
- OpenAI owned_by 필드 - OpenAI owned_by 플레이스홀더를 'openai'로 교체 (#280)
- OpenAI 백엔드의 모델이 이제 플레이스홀더 텍스트 대신
owned_by: openai를 올바르게 표시
- OpenAI 백엔드의 모델이 이제 플레이스홀더 텍스트 대신
- 관리자 API 경쟁 조건 - 관리자 API 동시 백엔드 생성에서 경쟁 조건 방지 (#278)
- 동시 백엔드 생성 요청이 데이터 손상을 일으킬 수 있는 문제 수정
- 백엔드 관리 작업에 적절한 동기화 추가
- 핫 리로드 처리 단계 - 핫 리로드에 누락된 처리 단계 추가 (#277)
- 일부 설정 변경이 핫 리로드 중 제대로 적용되지 않는 문제 수정
- 설정 리로드 시 모든 처리 단계가 실행되도록 보장
- 클라우드 백엔드 가용성 상태 - 클라우드 백엔드가 이제 /v1/models/{model_id}에서 available:true 표시 (#272)
- 클라우드 백엔드(OpenAI, Anthropic, Gemini)가 잘못 사용 불가로 표시되는 문제 수정
- 클라우드 백엔드가 건강할 때 사용 가능으로 올바르게 표시
문서¶
- v0.29.0 기능에 대한 테스트 및 문서 추가 (#459da6a)
v0.28.0 - 2025-12-31¶
추가됨¶
- 도구 호출을 위한 SSE 스트리밍 지원 - 도구 호출에 SSE 스트리밍 지원 추가 (#258)
- Server-Sent Events를 통한 도구 호출 응답의 실시간 스트리밍
- 함수 호출 시나리오에서 효율적인 스트리밍 응답 활성화
- llama.cpp 도구 호출 자동 감지 - /props 엔드포인트를 통한 도구 호출 지원 자동 감지 (#263)
- 모델 검색 시
/props엔드포인트를 쿼리하여chat_template분석 - 도구 관련 키워드 감지 (
tool,tools,tool_call,function등) - 감지 시
function_calling기능 자동 활성화 /props엔드포인트를 사용할 수 없는 경우 우아한 폴백- HTTP 및 Unix 소켓 백엔드 모두에서 작동
- 모델 검색 시
- 확장된 /v1/models/{model_id} 엔드포인트 - 풍부한 메타데이터 필드로 확장 (#262)
- 기능 및 가격을 포함한 종합적인 모델 메타데이터 반환
- 추가 모델 정보가 포함된 향상된 응답 형식
- 도구 결과 메시지 변환 - 멀티턴 대화에서 도구 결과 메시지 변환 구현 (#265)
- 도구 결과 메시지 (
role: "tool")를 백엔드 네이티브 형식으로 변환 - Anthropic:
user역할로tool_result콘텐츠 블록과 함께 변환 - Gemini:
function역할로functionResponse파트와 함께 변환 - Anthropic용으로 연속 도구 결과 결합 (병렬 도구 호출)
- Gemini 변환을 위한 자동 함수 이름 조회
- 오류 응답을 위한
is_error표시자 보존
- 도구 결과 메시지 (
- 백엔드별 owned_by 플레이스홀더 - llamacpp, vllm, ollama, http 백엔드에 owned_by 플레이스홀더 추가 (#267)
개선됨¶
- CLI 도움말 출력 포맷팅 - 제목 헤더 및 프로젝트 귀속을 포함한 --help 출력 포맷팅 개선 (#269)
- 향상된 커맨드 라인 도움말 시각적 외관
- 도움말 출력에 프로젝트 귀속 추가
수정됨¶
- 모델 메타데이터 캐시 동기화 - 모델 메타데이터 캐시를 ConfigManager와 동기화 (#270)
- 모델 캐시가 설정 변경을 적절히 반영하도록 보장
CI/CD¶
- 도구 호출에 대한 포괄적인 통합 테스트 (#264)
의존성¶
- minor-and-patch 그룹에서 3개 업데이트 (#257)
기술 개선¶
- Windows 빌드에서 Unix 전용 항목에 대한 dead_code 경고 수정
v0.27.0 - 2025-12-29¶
추가됨¶
- llama.cpp 도구 호출 자동 감지 - llama.cpp 백엔드의 도구 호출 지원 자동 감지 (#260)
- 모델 검색 시
/props엔드포인트를 쿼리하여chat_template분석 - 도구 관련 키워드 감지 (
tool,tools,tool_call,function등) - 감지 시
function_calling기능 자동 활성화 /props엔드포인트를 사용할 수 없는 경우 우아한 폴백- HTTP 및 Unix 소켓 백엔드 모두에서 작동
- 모델 검색 시
- 완전한 Unix 소켓 지원 - 모델 검색 및 스트리밍을 위한 완전한 Unix 소켓 지원 (#248, #252, #253, #254, #256)
- Unix 소켓 백엔드의 SSE/스트리밍 지원으로 로컬 소켓을 통한 실시간 응답 가능
- Unix 소켓 연결을 위한 백엔드 타입 자동 감지
- Unix 소켓을 통한 vLLM 모델 검색 지원
- Unix 소켓을 통한 llama.cpp 모델 검색 지원
- 모델 페처가 Unix 소켓 백엔드를 완전히 지원
- 도구 호출 변환 (Tool Call Transformation) - 모든 백엔드에서 OpenAI 도구 호출 변환 구현 (#244, #245, #246)
- Anthropic, Gemini, llama.cpp 백엔드에 대한 도구 정의 변환
auto,none,required, 특정 함수 선택을 지원하는 도구 선택 변환- 프로바이더 간 통합 응답 형식을 위한 도구 호출 응답 변환
- 멀티턴 도구 대화 지원 - 멀티턴 대화에서 도구 호출을 위한 메시지 변환 (#241)
- 도구 결과 메시지 (
role: "tool")를 백엔드 네이티브 형식으로 변환 - Anthropic:
user역할로tool_result콘텐츠 블록과 함께 변환 - Gemini:
function역할로functionResponse파트와 함께 변환 - Anthropic용으로 연속 도구 결과 결합 (병렬 도구 호출)
- Gemini 변환을 위한 자동 함수 이름 조회
- 오류 응답을 위한
is_error표시자 보존
- 도구 결과 메시지 (
v0.26.0 - 2025-12-27¶
추가됨¶
- 도구 선택 변환 (Tool Choice Transformation) - 백엔드 간
tool_choice파라미터 자동 변환 (#239)auto,none,required, 특정 함수 선택 지원- Anthropic:
{"type": "auto|any|tool"}형식으로 변환, "none"은 도구 제거로 처리 - Gemini:
tool_config.function_calling_config구조로 변환 - llama.cpp: 병렬 함수 호출을 위해
parallel_tool_calls파라미터 유지 - 크로스 프로바이더 도구 호출을 위해 모델 폴백 시스템과 통합
- 단일 모델 조회 엔드포인트 - 가용성 상태가 포함된 단일 모델 조회를 위한 GET /v1/models/{model} 엔드포인트 추가 (#236)
- 실시간 가용성을 나타내는 추가
available필드와 함께 모델 정보 반환 available: true: 최소 하나의 건강한 백엔드가 모델을 제공할 때available: false: 모델은 존재하지만 이를 제공하는 모든 백엔드가 비정상일 때- 모델이 어느 백엔드에도 존재하지 않으면 404 반환
- 특정 모델 조회를 타겟팅하여 전체 모델 집계를 피함으로써 성능 최적화
- 실시간 가용성을 나타내는 추가
v0.25.0 - 2025-12-26¶
추가됨¶
- CORS (Cross-Origin Resource Sharing) 지원 - 웹 애플리케이션에 라우터를 임베드하기 위한 설정 가능한 CORS 미들웨어 (#234)
- Tauri 데스크톱 앱, Electron 앱, 웹 프론트엔드 지원
- 와일드카드 오리진 및 포트 패턴 (예:
http://localhost:*) - 커스텀 스킴 지원 (예:
tauri://localhost) - 설정 가능한 메서드, 헤더, credentials
- 설정 가능한 max-age의 프리플라이트 캐시
- Unix 도메인 소켓 백엔드 지원 - Unix 소켓을 통한 안전한 로컬 LLM 통신 (#232)
- 로컬 백엔드를 위한
unix:///path/to/socketURL 스킴 사용 - 파일 시스템 권한을 통한 더 나은 보안 (TCP 포트 노출 없음)
- localhost TCP 대비 낮은 지연 시간 (~30% 개선)
- 여러 LLM 서버 실행 시 포트 충돌 없음
- 플랫폼 지원: Linux 및 macOS (Windows는 향후 릴리스 예정)
- 로컬 백엔드를 위한
v0.24.0 - 2025-12-26¶
추가됨¶
변경됨¶
- 설정의 백엔드 추가/제거에 핫 리로드 활성화 (#229)
v0.23.1 - 2025-12-25¶
CI/CD¶
- Windows 빌드 지원 - 릴리스 워크플로우에 Windows x86_64 빌드 타겟 추가 (#224)
- 릴리스 파이프라인에서 네이티브 Windows 빌드 활성화
- mingw-w64를 사용한 Linux에서의 크로스 컴파일
v0.23.0 - 2025-12-23¶
추가됨¶
- GLM 4.7 모델 지원 - 사고 기능을 포함한 Zhipu AI의 GLM 4.7 모델 지원 추가 (#222)
- 전체 사양이 포함된 model-metadata.yaml의 모델 메타데이터 (355B MoE, 32B 활성 파라미터)
- 사고(thinking) 파라미터 지원:
enable_thinking(불린) 및thinking_budget(1-204,800 토큰) - 최대 131K 토큰 출력이 가능한 200K 컨텍스트 윈도우
- config.yaml.example에 Z.AI 백엔드 설정 예제 추가
- SiliconFlow 대체 백엔드 설정
- 모델 메타데이터에 대한 종합적인 통합 테스트
- 가격: 입력 토큰 $0.60/1M, 출력 토큰 $2.20/1M
- 사고 패턴 메타데이터 - 암시적 시작 태그를 사용하는 모델에 대한 사고 패턴 메타데이터 추가 (#218)
- 암시적 사고 시작 태그를 사용하는 모델 지원
- 사고 콘텐츠 추출을 위한 패턴 기반 감지
- GCP 서비스 계정 인증 - Gemini 백엔드에 GCP 서비스 계정 인증 지원 추가 (#208)
- JSON 키 파일 인증 지원
- 환경 변수 기반 인증
- 자동 토큰 갱신 및 관리
- 분산 추적 - 상관관계 ID 전파를 통한 분산 추적 추가 (#207)
- traceparent 헤더를 사용한 W3C Trace Context 지원
- 설정 가능한 trace ID, request ID, correlation ID 헤더
- 모든 재시도 시도에 걸친 Trace ID 전파
- 헤더에서 가져온 trace ID에 대한 보안 검증
- 새 모델 메타데이터 - NVIDIA Nemotron 3 Nano, Qwen Image Layered, Kakao Kanana-2 모델 메타데이터 추가 (#202)
- ASCII 다이어그램 교체 - MkDocs용 ASCII 다이어그램을 이미지로 교체하는 시스템 추가 (#200)
- MkDocs 빌드 시 ASCII 다이어그램을 SVG 이미지로 자동 교체
- 원시 Markdown에서 ASCII 아트 가시성 유지
변경됨¶
- CI 최적화 - 비코드 파일만 변경된 경우 Rust 테스트 건너뛰기 (#204)
- 문서 전용 변경에 대한 더 빠른 CI
- 테스트 실행을 위한 경로 기반 필터링
수정됨¶
- 캐시 스탬피드 방지 - singleflight, stale-while-revalidate, 백그라운드 갱신으로 캐시 스탬피드 방지 (#220)
- Singleflight 패턴으로 모델 캐시 만료 시 썬더링 허드 방지
- Stale-while-revalidate로 백그라운드에서 갱신하면서 즉시 캐시된 데이터 반환
- 백그라운드 갱신으로 만료 전 캐시를 사전에 업데이트
- 전역 프롬프트 핫 리로드 - 핫 리로드를 통한 global_prompts 변경 적용 (#219)
- 전역 프롬프트 설정 변경이 이제 재시작 없이 적용
- 모델 캐시 무효화 - 백엔드 설정 변경 시 모델 캐시 무효화 (#206)
- 백엔드 설정 변경이 이제 모델 캐시 갱신을 적절히 트리거
- 문서 개선 - 인라인 SVG 및 반응형 크기 조정으로 다이어그램 렌더링 개선
- 번역 오타 - 문서의 번역 오타 수정
- Docker CI 수정 - Docker 매니페스트 생성에서 멀티라인 태그 처리
- 프라이빗 저장소 접근 - CI에서 프라이빗 저장소 에셋 다운로드에 gh CLI 사용
- GitHub 토큰 인증 - 프라이빗 저장소 접근을 위한 GitHub 토큰 인증 추가
- 문서 워크플로우 - 환경 보호 오류를 방지하기 위해 문서 워크플로우에서 릴리스 트리거 제거
CI/CD¶
- actions/github-script를 7에서 8로 업데이트 (#210)
- apple-actions/import-codesign-certs를 3에서 6으로 업데이트 (#212)
- actions/cache를 4에서 5로 업데이트 (#211)
- actions/checkout을 4에서 6으로 업데이트 (#209)
v0.22.0 - 2025-12-19¶
추가됨¶
- 사전 빌드 바이너리 Docker 지원 - GitHub Releases에서 사전 빌드된 바이너리를 다운로드하는 Dockerfile 및 Dockerfile.alpine 추가 (#189)
- 일반 사용을 위한 Debian Bookworm 기반 이미지 (~50MB)
- 최소 배포를 위한 Alpine 3.20 기반 이미지 (~10MB)
- TARGETARCH를 사용한 멀티 아키텍처 지원 (linux/amd64, linux/arm64)
- 릴리스 버전 선택을 위한 VERSION 빌드 인자
- 보안을 위한 비루트 사용자 실행
- 이미지 메타데이터를 위한 OCI 라벨
- 컨테이너 헬스 체크 CLI - 컨테이너 오케스트레이션을 위한
--health-checkCLI 인자 구현 (#189)- 서버가 정상이면 종료 코드 0, 비정상이면 1 반환
- 사용자 정의 헬스 엔드포인트를 위한 선택적
--health-check-url - 적절한 IPv6 주소 처리
- 기본 5초 타임아웃
- Docker Compose 빠른 시작 - 간편한 배포를 위한 docker-compose.yml 추가 (#189)
- 설정을 위한 볼륨 마운트
- 환경 변수 지원 (RUST_LOG)
- 리소스 제한 및 헬스 체크
- 자동화된 Docker 이미지 퍼블리싱 - 릴리스 워크플로우에 Docker 빌드 및 ghcr.io 푸시 추가 (#189)
- 바이너리 릴리스 후 Debian 및 Alpine 이미지 빌드
- 멀티 플랫폼 지원 (linux/amd64, linux/arm64)
- semver로 자동 태깅 (VERSION, MAJOR.MINOR, latest)
- Alpine 이미지는 -alpine 접미사로 태그
- 더 빠른 빌드를 위한 GitHub Actions 캐시
- MkDocs 문서 웹사이트 - Material 테마로 종합적인 문서 사이트 구축 (#183)
- 시작하기, 기능, 운영, 개발 섹션이 있는 전체 네비게이션 구조
- GitHub Pages 자동 배포를 위한 GitHub Actions 워크플로우
- 사용자 정의 스타일시트 및 테마 구성
- 한국어 문서 번역 - 모든 문서의 완전한 한국어 현지화 (#190)
- 20개 문서 파일 모두 한국어로 번역
- 네비게이션에 언어 전환기 (영어/한국어)
- GitHub Actions 워크플로우에서 다국어 빌드
- 의존성 보안 감사 - 취약점 스캔을 위한 cargo-deny 추가 (#192)
- CI 워크플로우에서 보안 권고 확인
- 라이선스 준수 검증
- 의존성 소스 검증
- Dependabot 통합 - Cargo 및 GitHub Actions 자동 의존성 업데이트 (#192)
- 보안 정책 - 취약점 보고 프로세스를 포함한 종합적인 SECURITY.md 추가 (#191)
변경됨¶
- 고아 아키텍처 문서를 MkDocs 사이트에 통합 (#186)
- URL 친화적 파일명을 위해 문서 파일명을 소문자 케밥케이스로 변경 (#186)
- 다양한 GitHub Actions를 최신 버전으로 업데이트 (checkout@v6, setup-python@v6, upload-artifact@v6 등)
수정됨¶
- 헬스 체크 응답 검증 로직 버그 (연산자 우선순위 문제)
- 설정 오류를 조용히 숨기던 주소 파싱 폴백
- 헬스 체크에서 IPv6 주소 형식 지정 (이제 브라켓 표기법을 올바르게 사용)
보안¶
- reqwest 0.11→0.12, prometheus 0.13→0.14, validator 0.18→0.20 업데이트
- 더 나은 유지보수를 위해 dotenv를 dotenvy로 교체
- 빌드 컨텍스트에서 민감한 파일 제외를 위한 .dockerignore 추가
v0.21.0 - 2025-12-19¶
추가됨¶
- Gemini 3 Flash Preview 모델 - gemini-3-flash-preview 모델 지원 추가 (#168)
- 백엔드 오류 전달 - 4xx 응답에 대해 백엔드의 상세 오류 메시지 전달 (#177)
- OpenAI, Anthropic, Gemini 백엔드의 원본 오류 메시지 파싱 및 전달
- 가능한 경우
param필드 보존 (잘못된 파라미터 오류에 유용) - 백엔드 응답 파싱 불가 시 일반 오류 메시지로 폴백
- OpenAI 호환 오류 형식 유지
- 모든 백엔드 형식에 대한 오류 파싱 단위 테스트 포함
- API 엔드포인트 기본 인증 모드 - API 엔드포인트에 대한 설정 가능한 인증 적용 (#173)
api_keys설정에 새mode필드:permissive(기본값) 또는blockingpermissive모드: API 키 없는 요청 허용 (하위 호환성 유지)blocking모드: 인증된 요청만 처리, 미인증 요청은 401 수신- 보호 엔드포인트:
/v1/chat/completions,/v1/completions,/v1/responses,/v1/images/*,/v1/models - 헬스 엔드포인트 (
/health,/healthz)는 항상 인증 없이 접근 가능 - 인증 모드 변경에 대한 핫 리로드 지원
- 두 모드에 대한 포괄적인 통합 테스트
- API.md, configuration.md, manpage 문서 업데이트
수정됨¶
- UTF-8 멀티바이트 문자 손상 - 스트리밍 응답에서 UTF-8 멀티바이트 문자 손상 처리 (#179)
- GPT Image response_format - GPT Image 모델에서 response_format 파라미터 제거 (#176)
- 자동 검색 검증 - Anthropic을 제외한 모든 백엔드에 대해 자동 검색 허용 (#172)
변경됨¶
v0.20.0 - 2025-12-18¶
추가됨¶
- Gemini 이미지 변형 지원 - Gemini (nano-banana) 모델에 대한 이미지 변형 지원 추가 (#165)
- Gemini 이미지 편집 지원 - Gemini (nano-banana) 모델에 대한 제한적 이미지 편집 지원 구현 (#164)
- 향상된 이미지 생성 - 스트리밍 및 GPT Image 기능으로 /v1/images/generations 향상 (#161)
- GPT Image 1.5 모델 - gpt-image-1.5 모델 지원 추가 (#159)
- 이미지 변형 엔드포인트 - 이미지 변형을 위한 /v1/images/variations 엔드포인트 구현 (#155)
- 이미지 편집 엔드포인트 - 이미지 편집(인페인팅)을 위한 /v1/images/edits 엔드포인트 구현 (#156)
- 완전한 OpenAI Images Edit API 호환성
- GPT Image 모델 지원:
gpt-image-1,gpt-image-1-mini,gpt-image-1.5(권장) dall-e-2모델 레거시 지원- 공유 유틸리티를 사용한 multipart form-data 파싱
- PNG 이미지 검증 (형식, 크기, 정사각형 치수)
- 선택적 마스크 검증 (원본 이미지와 치수 일치)
- 공유 이미지 유틸리티 - 이미지 편집/변형 엔드포인트를 위한 공유 유틸리티 구현 (#154)
- 외부 프롬프트 파일 - 외부 Markdown 파일에서 시스템 프롬프트 로딩 지원 (#146)
BackendPromptConfig및ModelPromptConfig에 새prompt_file필드GlobalPromptConfig에 새default_file및prompts_dir필드- 경로 순회 공격 방지를 위한 안전한 경로 검증
- 프롬프트 파일 관리를 위한 REST API 엔드포인트
- 크기 제한이 있는 파일 캐싱 (최대 100개 항목, 총 50MB)
- 프롬프트 파일에 대한 핫 리로드 지원
- Solar Open 100B 모델 - Solar Open 100B 모델 메타데이터 추가
- 자동 모델 검색 - 모델이 명시적으로 설정되지 않은 경우 백엔드가
/v1/modelsAPI에서 자동으로 사용 가능한 모델 검색 (#142)- OpenAI, Gemini, vLLM 백엔드가 자동 검색 지원
- Ollama 백엔드는 vLLM의 검색 메커니즘 사용 (OpenAI 호환 API)
- 10초 타임아웃으로 시작 차단 방지
- 검색 실패 시 하드코딩된 기본값으로 폴백
변경됨¶
BackendFactory::create_backend_from_typed_config()가 비동기 모델 검색을 지원하기 위해 async로 변경- OpenAI, Gemini, vLLM의 백엔드
from_config()메서드가 async로 변경
보안¶
- API 키 수정 - 자격 증명 노출 방지를 위한 API 키 수정 구현 (#150)
성능¶
- 바이너리 크기 최적화 - 릴리스 바이너리 크기를 20MB에서 6MB로 최적화 (70% 감소) (#144)
리팩토링¶
- 이슈 #147의 우선순위 2를 위한 대용량 파일 분할
- 각 파일을 500줄 미만으로 유지하기 위한 대용량 파일 분할 (#148)
v0.19.0 - 2025-12-13¶
추가됨¶
- 런타임 설정 관리 API - 런타임에 설정을 보고 수정하기 위한 포괄적인 REST API (#139)
- 설정 쿼리 API:
GET /admin/config/full- 민감 정보가 마스킹된 전체 설정 조회GET /admin/config/sections- 15개 설정 섹션 전체 목록GET /admin/config/{section}- 특정 섹션 설정 조회GET /admin/config/schema- 클라이언트 측 검증을 위한 JSON Schema
- 설정 수정 API:
PUT /admin/config/{section}- 섹션 설정 교체PATCH /admin/config/{section}- 부분 업데이트 (JSON merge patch)POST /admin/config/validate- 적용 전 설정 검증POST /admin/config/apply- 핫 리로드로 설정 적용
- 설정 저장/복원 API:
POST /admin/config/export- 설정 내보내기 (YAML/JSON/TOML)POST /admin/config/import- 설정 가져오기 및 적용GET /admin/config/history- 설정 변경 이력 조회POST /admin/config/rollback/{version}- 이전 버전으로 롤백
- 백엔드 관리 API:
POST /admin/backends- 새 백엔드 추가GET /admin/backends/{name}- 백엔드 설정 조회PUT /admin/backends/{name}- 백엔드 설정 업데이트DELETE /admin/backends/{name}- 백엔드 제거PUT /admin/backends/{name}/weight- 백엔드 가중치 업데이트PUT /admin/backends/{name}/models- 백엔드 모델 목록 업데이트
- API 키, 비밀번호, 토큰에 대한 민감 정보 마스킹
- 모든 설정 섹션에 대한 JSON Schema 생성
- 설정 이력 추적 (최대 100개 항목, 설정 가능)
- 크기 기반 퇴거를 통한 메모리 효율적 이력 저장 (10MB 제한)
- 스레드 안전성을 위한 AtomicU64 사용 원자적 버전 카운터
- 오류 코드가 포함된 구조화된 오류 응답
- 설정 쿼리 API:
- Admin REST API 문서 - 포괄적인 개발자 가이드 (docs/admin-api.md)
- 요청/응답 예제가 포함된 완전한 API 레퍼런스
- Python, JavaScript/TypeScript, Go용 클라이언트 SDK 예제
- 모범 사례 및 보안 고려사항
- 통합 테스트 - 설정 관리 API 엔드포인트에 대한 33개 통합 테스트
수정됨¶
- 심각: 설정 변경이 이제 실행 중인 시스템에 실제로 적용됨
- 심각: JSON 문자열 저장 및 크기 기반 퇴거로 메모리 증가 제어
- 높음: 입력 검증 추가 (1MB 콘텐츠 제한, 32레벨 중첩 깊이)
- 높음: 민감 내보내기에 권한 상승 및 감사 로깅 필요
- 높음: 포괄적인 민감 필드 감지 (30개 이상 패턴)
- 중간: 검증 함수가 이제 실제 검증 수행
- 중간: 버전 카운터에 AtomicU64로 경쟁 조건 수정
- 중간: 허용된 백엔드 이름 문자에서 콜론 제거
- 중간: 오류 코드가 포함된 구조화된 오류 응답
- 중간: 초기화 플래그로 중복 이력 항목 방지
- 낮음: 더 나은 성능을 위해 불필요한 클론 제거
- 낮음: AdminConfig를 통해 제한 설정 가능
- 낮음: 중복 검증 로직 리팩토링
- 낮음: 엣지 케이스에 대한 테스트 커버리지 개선
변경됨¶
- 모든 가이드에서 설정 관리 API 문서 향상
- 새 관리 엔드포인트로 manpage 업데이트
- 포괄적인 설정 관리 API 섹션으로 API.md 업데이트
v0.18.0 - 2025-12-13¶
추가됨¶
- API 키별 속도 제한 - API 키별 속도 제한 구현 (#137)
- 각 API 키에 대한 개별 속도 제한
- 키당 분당 요청 수 설정 가능
- API 키 관리 시스템 - 포괄적인 API 키 관리 및 설정 시스템
- 다중 키 소스: 설정 파일, 외부 파일, 환경 변수
- 키 속성: 스코프, 속도 제한, 만료, 활성화 상태
- 키 설정 변경에 대한 핫 리로드 지원
- Files API 인증 - Files API에 대한 인증 및 권한 부여 구현 (#131)
- 파일 작업에 대한 API 키 인증
- 파일 소유권 적용
- 모든 파일에 대한 관리자 접근 제어
- 런타임 설정 핫 리로드 - 런타임 설정 업데이트를 위한 완전한 핫 리로드 기능 (#130)
- 자동 설정 파일 감시
- 분류된 업데이트: 즉시, 점진적, 재시작 필요
변경됨¶
- 모듈식 구조로 대규모 리팩토링
- 리팩토링된 모듈 구조를 반영하도록 architecture.md 업데이트
수정됨¶
- admin/metrics/files 엔드포인트에 ConnectInfo 확장 추가
- API 키 관리의 보안 취약점 해결
- API 키 관리의 코드 품질 문제 해결
문서¶
- API 키 관리 문서 추가
- 포괄적인 API 키 관리 테스트 추가
v0.17.0 - 2025-12-12¶
추가됨¶
- Anthropic 백엔드 파일 콘텐츠 변환 - 라우터에 업로드된 파일을 이제 Anthropic 백엔드와 함께 사용 가능 (#126)
- 파일 콘텐츠를 Anthropic 메시지 형식으로 자동 변환
- base64 인코딩을 사용한 텍스트 및 문서 파일 지원
- 파일 해결 미들웨어와 원활한 통합
- Gemini 백엔드 파일 콘텐츠 변환 - 라우터에 업로드된 파일을 이제 Gemini 백엔드와 함께 사용 가능 (#127)
- 파일 콘텐츠를 Gemini API 형식으로 자동 변환
- 적절한 MIME 타입 처리를 통한 인라인 데이터 지원
- 크로스 프로바이더 파일 지원으로 한 번 업로드한 파일을 모든 백엔드에서 사용 가능
수정됨¶
- 스트리밍 파일 업로드 - 메모리 소진 방지를 위한 스트리밍 파일 업로드 구현 (#128)
- 대용량 파일 업로드가 더 이상 전체 파일을 메모리에 로드하지 않음
- 효율적인 메모리 사용을 위한 스트리밍 처리
- 대용량 파일 업로드 시 OOM 오류 방지
변경됨¶
- 없음
v0.16.0 - 2025-12-12¶
추가됨¶
- OpenAI 호환 Files API - OpenAI Files API 엔드포인트 전체 구현 (#111)
- multipart/form-data 지원 파일 업로드
- 파일 목록, 조회, 삭제
- 파일 콘텐츠 다운로드
- 용도 지원: fine-tune, batch, assistants, user_data
- 파일 해결 미들웨어 - 채팅 완료를 위한 자동 파일 콘텐츠 주입 (#120)
- 파일 ID로 채팅 메시지에서 업로드된 파일 참조
- 채팅 컨텍스트에 자동 콘텐츠 주입
- 영속적 메타데이터 스토리지 - 서버 재시작 후에도 파일 메타데이터 유지 (#125)
- 데이터 파일과 함께 저장되는 사이드카 JSON 파일 (.meta.json)
- 파일에서 메타데이터 재빌드를 통한 시작 시 자동 복구
- 고아 파일 감지 및 선택적 정리
- OpenAI 백엔드 파일 처리 - 로컬에 업로드된 파일이 필요시 OpenAI로 전달됨 (#121, #122)
- GPT-5.2 모델 지원 - OpenAI 백엔드에 GPT-5.2 모델 메타데이터 추가 (#124)
- 서킷 브레이커 패턴 - 서킷 브레이커를 통한 자동 백엔드 장애 조치 (#93)
- 상태: Closed → Open → Half-Open → Closed 사이클
- 설정 가능한 실패 임계값 및 복구 타임아웃
- 백엔드별 서킷 브레이커 인스턴스
- 서킷 브레이커 상태 및 제어를 위한 관리자 엔드포인트
- 관리자 엔드포인트 인증 - 인증 및 감사 로깅으로 관리자 엔드포인트 보안
- 설정 가능한 폴백 모델 - 모델 사용 불가 시나리오에 대한 자동 모델 폴백 (#50)
- 기본 모델에 대한 폴백 체인 정의 (예: gpt-4o → gpt-4-turbo → gpt-3.5-turbo)
- 크로스 프로바이더 폴백 지원 (예: OpenAI → Anthropic)
- 프로바이더 간 자동 파라미터 변환
- 계층화된 장애 조치 보호를 위한 서킷 브레이커와 통합
- 설정 가능한 트리거 조건 (오류 코드, 타임아웃, 연결 오류, 서킷 브레이커 오픈)
- 폴백 사용 시 응답 헤더 표시 (X-Fallback-Used, X-Original-Model, X-Fallback-Model)
- 폴백 모니터링을 위한 Prometheus 메트릭
- Pre-commit Hook - 커밋 전 자동 코드 포맷팅 및 린팅
수정됨¶
- 폴백 체인 검증 - 체인 검증을 Validate derive에 통합
- 폴백 성능 - 폴백 체인 순회를 위한 인덱스 기반 조회 사용
- 락 경합 - 스냅샷 패턴으로 FallbackService의 락 경합 감소
- 보안 - 폴백 오류 헤더 및 메트릭 레이블 살균
- 서킷 브레이커 보안 - 관리자 엔드포인트에 백엔드 이름 검증 추가
- 스레드 안전성 - half-open 요청 제한을 위한 스레드 안전 CAS 루프 사용
변경됨¶
- 문서 업데이트 - 폴백 설정, 서킷 브레이커, Files API에 대한 포괄적인 문서
- 코드 품질 - clippy 경고 수정 및 코드 포맷팅
- Pre-commit Hook 위치 - pre-commit hook을 .githooks 디렉토리로 이동
v0.15.0 - 2025-12-05¶
추가됨¶
- Nano Banana API 지원 - OpenAI 호환 인터페이스로 Gemini 이미지 생성 API 지원 추가 (#102)
- nano-banana 및 nano-banana-pro 모델 지원
- OpenAI Images API와 Gemini Imagen API 간 자동 형식 변환
- /v1/models 엔드포인트 분리 - 표준 경량 응답 vs 확장 메타데이터 응답 (#101)
/v1/models는 더 나은 성능을 위해 경량 응답 반환/v1/models?extended=true는 상세 모델 정보를 위한 전체 메타데이터 반환
변경됨¶
- StreamService 추출 - 모듈식 아키텍처를 위해 스트리밍 핸들러 로직을 전용 StreamService로 추출 (#106)
- 재시도 로직 중복 제거 - proxy.rs에서 재시도 로직 코드 통합 (#103)
수정됨¶
- 적절한 오류 전파 - HttpClientFactory에서
.expect()패닉을 적절한 오류 전파로 교체 (#104)
성능¶
- LRU 캐시 최적화 - 캐시 조회에 쓰기 락 대신 읽기 락 사용 (#105)
v0.14.2 - 2025-12-05¶
추가됨¶
- 토큰 사용량 로깅 - 요청 완료 시 입력/출력 토큰 수 로깅 (#92)
- 리포트 제외 목록 - 리포트에 대한 제외 목록 설정 추가
변경됨¶
- 없음
수정됨¶
- 없음
v0.14.1 - 2025-12-05¶
추가됨¶
- TTFB 벤치마크 타겟 - Makefile에 TTFB 벤치마크 타겟 추가
- 연결 사전 준비 - Anthropic, Gemini, OpenAI 백엔드에 대한 연결 사전 준비 추가
수정됨¶
- Anthropic 백엔드 TTFT - 연결 풀링 및 HTTP/2로 Anthropic 백엔드 TTFT 최적화 (#90)
- Gemini 백엔드 TTFT - 연결 풀링 및 HTTP/2로 Gemini 백엔드 TTFT 최적화 (#88)
- 모델 메타데이터 별칭 매칭 - 모델 메타데이터 조회에서 별칭에 기본 이름 폴백 매칭 적용 (#84)
변경됨¶
- 공유 HTTP 클라이언트 - HealthChecker와 요청 핸들러 간 HTTP 클라이언트 공유
- 아키텍처 및 성능 문서 업데이트
v0.14.0 - 2025-12-04¶
추가됨¶
- 전역 시스템 프롬프트 주입 - 라우터 전역 시스템 프롬프트 주입 추가 (#82)
수정됨¶
- GitHub Actions - 더 이상 사용되지 않는 actions-rs/toolchain을 dtolnay/rust-toolchain으로 교체
- macOS ARM64 빌드 - macOS ARM64 ring 빌드를 위한 RUSTFLAGS 추가
- musl 빌드 - musl 크로스 컴파일 지원을 위해 rustls-tls로 전환
변경됨¶
- GitHub Action runner 업데이트
v0.13.0 - 2025-12-04¶
추가됨¶
- OpenAI Responses API (
/v1/responses) - OpenAI Responses API 전체 구현 (#49)- 자동 만료가 있는 세션 기반 응답 관리
- 만료된 세션에 대한 백그라운드 정리 작업
- Responses API와 Chat Completions 간 요청/응답 형식 변환기
- API 키용 SecretString - 모든 백엔드에서 SecretString을 사용한 안전한 API 키 저장 (#76)
- 모델 메타데이터 오버라이드 - model-metadata.yaml을 통한 /v1/models 응답 필드 오버라이드 허용 (#75)
수정됨¶
- 진정한 SSE 스트리밍 - /v1/responses API에 대한 적절한 Server-Sent Events 스트리밍 구현
변경됨¶
- SseParser 즉시 모드 - 즉시 파싱 모드로 첫 응답 지연 감소
- 문자열 할당 최적화 - 할당 감소로 성능 개선
- 오류 처리 표준화 - 코드베이스 전체에 일관된 오류 처리 패턴
보안¶
- 세션 접근 제어 - 세션 관리에 적절한 접근 제어 추가
- 입력 검증 - Responses API에 대한 포괄적인 입력 검증
v0.12.0 - 2025-12-04¶
추가됨¶
- SSRF 방지 모듈 - 포괄적인 SSRF 방지를 위한 새 UrlValidator 모듈 (#66)
- 중앙화된 HTTP 클라이언트 팩토리 - 백엔드 간 일관된 HTTP 클라이언트 생성을 위한 HttpClientFactory (#67)
수정됨¶
- 일관된 해시 알고리즘 - 적절한 라우팅을 위해 이진 검색에서 정확한 해시 일치 처리 (#72)
- 패닉을 Option 반환으로 교체 - 패닉을 Option 반환으로 교체하여 신뢰성 향상 (#71)
- 하드코딩된 인증 요구사항 제거 - /v1/models 엔드포인트에 더 이상 하드코딩된 인증 필요 없음
- GitHub Actions - Projects V2 API 접근을 위해 GitHub App 토큰 사용
변경됨¶
- OpenAI 모델 메타데이터 재구성 - 더 나은 유지보수성을 위해 모델 메타데이터를 패밀리별로 구성 (#74)
- AnthropicStreamTransformer 추출 - Anthropic 스트림 변환을 위한 전용 모듈 (#73)
- Backends 모듈 분할 - 더 깔끔한 아키텍처를 위해 backends mod.rs를 별도 모듈로 분할 (#69)
- 임베디드 테스트 추출 - 더 나은 구성을 위해 테스트를 별도 파일로 이동 (#68)
- RequestExecutor 추출 - 요청 실행을 위한 공유 공통 모듈 (#65)
- HeaderBuilder 추출 - 인증 전략을 전용 모듈로 이동 (#64)
- AtomicStatistics 추출 - 원자적 통계를 위한 공유 공통 모듈
기술 개선¶
- 모듈식 아키텍처로 코드 구성 개선
- 더 나은 관측성을 위한 통계 집계 구현
- SSRF 방지 기능으로 보안 강화
v0.11.0 - 2025-12-03¶
추가됨¶
- 네이티브 Anthropic Claude API 백엔드 (
type: anthropic) - OpenAI 호환 엔드포인트 (#33)CONTINUUM_ANTHROPIC_API_KEY환경 변수에서 자동 API 키 로드- Claude 사고 모델을 위한 확장 사고 블록 지원
- OpenAI에서 Claude로 reasoning 파라미터 변환 (
reasoning_effort) - 플랫
reasoning_effort파라미터 지원
- Claude 4, 4.1, 4.5 모델 메타데이터 문서
수정됨¶
- Anthropic/Gemini 백엔드에 대한 헬스 체크 및 모델 가져오기 개선
- 압축 문제 방지를 위해 스트리밍 요청에
Accept-Encoding: identity헤더 추가 - 적절한 Accept-Encoding 처리를 위해 proxy.rs의
make_backend_request수정
변경됨¶
- 리팩토링: 코드 포맷팅 적용 및 clippy 경고 수정
- 리팩토링: Accept-Encoding 헤더 대신 reqwest
no_gzip/no_brotli/no_deflate사용
v0.10.0 - 2025-12-03¶
추가됨¶
- 네이티브 Google Gemini API 백엔드 (
type: gemini) - OpenAI 호환 엔드포인트 (#32)CONTINUUM_GEMINI_API_KEY환경 변수에서 자동 API 키 로드- 사고 모델 (gemini-2.5-pro, gemini-3-pro)에 대한 300초 확장 스트리밍 타임아웃
- 응답 잘림 방지를 위한 사고 모델의 자동
max_tokens조정 reasoning_effort파라미터 지원
- 네이티브 OpenAI API 백엔드 (
type: openai) - 내장 설정CONTINUUM_OPENAI_API_KEY환경 변수에서 자동 API 키 로드- /v1/models 응답에 내장 OpenAI 모델 메타데이터
- OpenAI Images API 지원 (
/v1/images/generations) - DALL-E 및 gpt-image-1 모델 (#35)- 설정 가능한 이미지 생성 타임아웃 (
timeouts.request.image_generation) - 이미지 생성 파라미터에 대한 포괄적인 입력 검증
- 이미지 생성 API에 대한 응답 형식 검증
- 설정 가능한 이미지 생성 타임아웃 (
- OpenAI 및 API 키 백엔드에 대한 인증된 헬스 체크
- 스트리밍 요청에 API 키 인증
- 설정된 모델만 표시하도록 /v1/models 필터링
- -c/--config로 명시적으로 지정된 경우 모든 설정 파일 경로 허용
.env.example및 타입이 지정된 백엔드 설정 예제- GLM 4.6, Kimi K2, DeepSeek, GPT, Qwen3 시리즈에 대한 포괄적인 모델 메타데이터
수정됨¶
- 사고 모델 (gemini-2.5-pro, gemini-3-pro)의 스트리밍 응답 잘림
- Gemini 백엔드의 모델 ID 정규화 및 스트리밍 호환성
- 최신 OpenAI 모델에 대해
max_tokens를max_completion_tokens로 변환 - 모든 API 엔드포인트에 대한 올바른 URL 구성
- 보안: 디버그 로그에서 민감 데이터 제거
- 보안: DoS 공격 방지를 위한 요청 본문 크기 제한 추가
변경됨¶
- 리팩토링: RequestType enum으로 요청 재시도 로직 통합
- 리팩토링: 락 프리 통계 및 슬라이스 반환으로 Gemini 백엔드 성능 개선
- Gemini 백엔드 문서 및 max_tokens 동작 문서 추가
- 이미지 생성 API 문서 추가
- model-metadata.yaml의 기능 명명 표준화
v0.9.0 - 2025-12-02¶
추가됨¶
- 네이티브 Google Gemini API 백엔드 (
type: gemini) - OpenAI 호환 엔드포인트 (#32)CONTINUUM_GEMINI_API_KEY환경 변수에서 자동 API 키 로드- 사고 모델 (gemini-2.5-pro, gemini-3-pro)에 대한 300초 확장 스트리밍 타임아웃
- 응답 잘림 방지를 위한 사고 모델의 자동
max_tokens조정 reasoning_effort파라미터 지원
- DALL-E 및 gpt-image-1 모델을 위한 OpenAI Images API 지원 (
/v1/images/generations) (#35) - 설정 가능한 이미지 생성 타임아웃 (
timeouts.request.image_generation) - GPT-5 패밀리, o-시리즈, 오디오/음성, 비디오 (Sora), 임베딩 모델을 포함한 OpenAI 모델의 포괄적인 메타데이터
- 토큰 버킷 알고리즘을 사용한 향상된 속도 제한 (#11)
- 포괄적인 Prometheus 메트릭 및 모니터링 (#10)
- 설정 파일 마이그레이션 및 자동 수정 CLI 유틸리티 (#29)
- 메트릭 엔드포인트에 대한 포괄적인 인증
수정됨¶
- 심각: 토큰 리필에서 경쟁 조건 제거
- 심각: SHA-256 해싱으로 API 키 보호
- 심각: 무제한 버킷 증가를 통한 메모리 소진 방지
- 심각: 헤더 인젝션 취약점 방지
- 높음: X-Forwarded-For 조작을 통한 IP 스푸핑 방지
- 높음: 메모리 누수 방지를 위한 메트릭 싱글톤 패턴 구현
- 높음: 불필요한 문자열 할당 제거
- 높음: 속도 제한을 위한 모델 추출 구현
- 메트릭 폭발 DoS 공격 방지를 위한 포괄적인 카디널리티 제한 및 레이블 살균 추가
- 패닉 조건 방지를 위한 오류 처리 개선
- 설정 테스트에서 환경 변수 경쟁 조건 해결
- 메트릭 RequestTimer의 통합 테스트 실패 수정
- 메트릭 보안 모듈의 단위 테스트 실패 수정
변경됨¶
- 리팩토링: 속도 제한에서 과도한 Arc 래핑 제거
- 더 나은 유지보수성을 위한 문서 구조 재구성
- 포괄적인 메트릭 문서 추가
- 속도 제한 기능 문서 업데이트
- 개발용 mock 서버 및 샘플 설정 파일 제거
- 임시 테스트 파일 제거 및 gitignore 개선
- 중복 man 페이지 제거 및 gitignore 업데이트
- 올바른 레포지토리를 언급하도록 README.md 업데이트
- 릴리스 워크플로우 업데이트
v0.8.0 - 2025-09-09¶
추가됨¶
- 메타데이터 공유를 위한 모델 ID 별칭 지원 (#27)
- 포괄적인 속도 제한 문서
- 캐시 포이즈닝을 통한 DoS 방지를 위한 models 엔드포인트에 강력한 속도 제한
수정됨¶
- 모든 백엔드가 비정상일 때 503 대신 빈 목록 반환 (#28)
- 오류 처리 및 분류 개선
- await 지점에서 보유된 MutexGuard에 대한 clippy 경고 해결
변경됨¶
- 더 실용적인 /v1/models 엔드포인트 속도 제한 증가
- configuration.md에 별칭 기능 문서 추가
v0.7.1 - 2025-09-08¶
수정됨¶
- 홈 디렉토리 및 실행 파일 경로에 대한 설정 경로 검증 개선 (#26)
v0.7.0 - 2025-09-07¶
추가됨¶
- 풍부한 메타데이터 지원으로 /v1/models 엔드포인트 확장 (#23) (#25)
- 향상된 설정 관리 (#9) (#22)
- 향상된 오류 처리를 포함한 고급 로드 밸런싱 전략 (#21)
수정됨¶
- 하드코딩된 25초 제한 대신 config.yaml의 스트리밍 타임아웃 설정 사용
변경됨¶
- 제외 목록에 yaml 추가
v0.6.0 - 2025-09-03¶
추가됨¶
- GitHub Project 자동화 워크플로우
- 포괄적인 타임아웃 설정 및 모델 문서 업데이트
수정됨¶
- 하드코딩된 값 대신 config.yaml의 타임아웃 설정 사용 (#19)
- clippy 경고 및 벤치마크 컴파일 문제 수정
변경됨¶
- cargo fmt 적용
v0.5.0 - 2025-09-02¶
추가됨¶
- 계층화된 설계를 갖춘 확장 가능한 아키텍처 (#16)
- 포괄적인 통합 테스트 및 성능 최적화
- 완전한 서비스 레이어 구현
- 미들웨어 아키텍처 및 향상된 백엔드 추상화
- CLI 및 설정 파일 지원을 포함한 설정 가능한 연결 풀 크기
- YAML 지원을 포함한 포괄적인 설정 관리 (#7)
- continuum-router용 Debian 패키징 및 man 페이지
수정됨¶
- 테스트에서 Option
올바르게 처리 - 모델 필드 없는 스트리밍 요청을 우아하게 처리하도록 테스트 업데이트
- 테스트에서 부동 소수점 정밀도 및 타이밍 문제 해결
- 객체 풀 및 SSE 파서에서 테스트 실패 및 교착 상태 해결
- CI 테스트 실패 해결 및 테스트 성능 개선
- CI 환경에서 설정 감시자 테스트 실패 해결
- 초기 헬스 체크 경쟁 조건 해결
- 오류 처리 및 재시도 로직의 심각한 보안 취약점
- 타이밍 변동에 대한 타임아웃 테스트 허용 오차 조정
변경됨¶
- 더 나은 가독성을 위해 복잡한 타입을 타입 별칭으로 추출
- 모든 cargo fmt 및 clippy 경고 해결
- 합리적인 기본값으로 재시도 설정을 선택적으로 변경
- 설정 접근 최적화 및 포괄적인 타임아웃 관리 추가
- 타임아웃 설정의 모델 이름을 최신 버전으로 업데이트
- 완전한 문서 업데이트
- 과대 모듈을 계층화된 아키텍처로 분할
성능¶
- 설정 접근 최적화 및 포괄적인 타임아웃 관리 추가
v0.4.0 - 2025-08-25¶
추가됨¶
- 헬스 모니터링을 포함한 모델 기반 라우팅 (#6)
수정됨¶
- 더 나은 호환성을 위한 헬스 체크 통합 및 SSE 파싱 개선
변경됨¶
- README.md 업데이트
v0.3.0 - 2025-08-25¶
추가됨¶
- 실시간 채팅 완료를 위한 SSE 스트리밍 지원 (#5)
수정됨¶
- 스트리밍 응답에서 비성공 상태 코드 처리
- 백엔드가 404 또는 기타 오류 상태 코드를 반환해도 스트리밍 계속 허용
- 클라이언트에 백엔드 오류 상태를 알리기 위해 SSE 오류 이벤트를 먼저 전송
v0.2.0 - 2025-08-25¶
추가됨¶
- 여러 엔드포인트에서 모델 집계 (#4)
v0.1.0 - 2025-08-24¶
추가됨¶
- OpenAI 호환 엔드포인트 및 프록시 기능
- 사용 가능한 모델 목록을 위한
/v1/models엔드포인트 - 레거시 OpenAI completions API를 위한
/v1/completions엔드포인트 - 채팅 API를 위한
/v1/chat/completions엔드포인트 - 라운드 로빈 로드 밸런싱을 포함한 다중 백엔드 지원 (#1)
- 적절한 오류 메시지를 포함한 정의되지 않은 라우트용 폴백 핸들러
수정됨¶
- 모든 엔드포인트에서 오류 처리 일관성 개선
변경됨¶
- 변경 내역 및 버전 정보로 README 업데이트