모델 폴백¶
모델 폴백은 요청 모델을 순서가 있는 대체 모델 목록에 매핑합니다. 설정한 HTTP 오류, 타임아웃, 연결 실패, 모델 없음, 또는 비정상 백엔드 선택 실패가 발생하면 이 목록을 차례로 시도합니다.
설정¶
fallback:
enabled: true
mid_stream_enabled: true
fallback_chains:
gpt-5.4:
- gpt-5.4-mini
- claude-sonnet-4-6
# 위에 명시적 체인이 없는 모델에 적용되는 기본 체인입니다.
default_chain:
- claude-haiku-4-5
- gpt-5.4-mini
fallback_policy:
trigger_conditions:
error_codes: [429, 500, 502, 503, 504]
timeout: true
connection_error: true
model_not_found: true
backend_unhealthy: true
context_window_exceeded: false
max_fallback_attempts: 3
fallback_timeout_multiplier: 1.5
preserve_parameters: true
max_concurrent_dials_per_backend: 50
model_settings:
gpt-5.4:
fallback_enabled: true
notify_on_fallback: true
fallback_chains의 키는 기본 모델이고 값은 시도 순서입니다. 체인은 비어 있거나 자기 자신을 참조하거나 순환할 수 없습니다.
- 허용 문자: 모델 이름은 1~128바이트의 유니코드 문자와 숫자,
-,_,.,/,:로 구성됩니다. 따라서 HuggingFace 경로(meta-llama/Llama-3.1-8B-Instruct)나 Ollama 태그(llama3:8b,hf.co/org/repo:Q4_K_M)도 기본 모델이나 홉으로 지정할 수 있습니다. - 검증 및 정규화: 공백, 제어 문자, 그 밖의 문장 부호는 거부되며, 오류 메시지에 해당 문자와 바이트 오프셋이 표시됩니다. 이름은 앞뒤 공백 제거, 대소문자 변환, 디코딩 없이 적힌 그대로 사용됩니다.
기본 체인(Default chain)¶
default_chain은 fallback_chains 항목도, model_settings[].custom_fallback_chain도 없는 모델에 적용되는 포괄 체인입니다. 해석 순서는 사용자 지정 체인, 모델의 fallback_chains 항목, default_chain 순서이므로, 명시적 항목이 항상 우선합니다. 목록을 비워 두면 이전 동작(체인이 없는 모델은 페일오버하지 않음)이 그대로 유지됩니다. 이 목록은 즉시 리로드됩니다.
- 평면 공유 목록: 요청 모델 자신의 이름은 해석된 목록에서 제외되므로 모델이 자기 자신으로 홉하지 않습니다. 또한 실행기는 홉 대상의 체인을 다시 따라가지 않으므로 목록이 순환할 수 없습니다.
- 검증: 이름은
fallback_chains와 같은 규칙을 따르며, 중복된 이름은 거부됩니다. - 키 범위 적용: 요청 키의
allowed_models가 허용하지 않는 기본 체인 대상은 디스패치하지 않고 건너뜁니다. 이때 경고를 기록하고fallback_default_chain_hop_skipped_total{original_model,fallback_model}로 집계합니다. 명시적 체인은 모델별로 운영자가 작성한 것이므로 기존 동작을 유지하며, 홉 단위로 다시 게이트하지 않습니다. - 모든 대상이 제외된 경우: 사용 가능한 대상이 하나도 없으면 해당 모델에 체인이 없는 것과 동일하게 동작합니다.
- 연동되는 기존 의미: 목록에 없는 모든 모델이 체인을 가진 것으로 취급됩니다. 따라서
models목록이 비어 있는quality_check는 해당 모델의 응답도 판정하며, 응답 캐시 라우트도 명시적 체인이 있는 모델과 동일하게 비활성화됩니다.
페일오버를 허용하지 않아야 하는 모델에는 model_settings 항목을 함께 지정하세요. 명시적 항목이 없으면 기본 체인 때문에 목록에 없는 모든 모델에서 폴백이 활성화됩니다.
fallback_policy는 시도 횟수, 홉별 타임아웃 배수, 동시 연결 상한을 제어합니다.
max_fallback_attempts(1~10): 최대 폴백 시도 횟수입니다.fallback_timeout_multiplier(1.0~5.0): 각 홉의 자체 요청 타임아웃 배수를 결정합니다. 기본 시도(attempt 1)는 설정된 타임아웃을 그대로 쓰고, 홉n은base * multiplier^(n-1)을 적용하되 해당 타임아웃 종류의timeouts.limits상한으로 제한됩니다.- 기준 타임아웃(
base): 체인 없이 실행했을 때의 타임아웃입니다. OpenAI 호환 비스트리밍 경로는 모델별timeouts.request.standard.total, 스트림 시작 전 연결 단계는 시도별 스트리밍 창,/anthropic/v1/messages와/v1/responses는 모델별 프로필(total및 스트리밍의 첫 SSE 바이트 기한 포함)을 사용합니다. - 배수 미적용 대상:
timeouts.connection, 폴백 다이얼 퍼밋 대기 시간, 홉 내부 재시도, 클라이언트가 이미 스트림을 수신 중인 상태에서 발생하는 스트림 중간 홉에는 배수가 적용되지 않습니다. - 스트리밍 예산과의 합성: 스트리밍 요청의 모든 시도가 공유하는 전체 시간 예산을 정하는
timeouts.streaming_fallback_budget_multiplier와는 별개입니다. 연결 단계에서 홉의 타임아웃 창에 배수를 먼저 적용한 뒤, 공유 예산의 남은 시간으로 다시 상한을 적용합니다.
- 기준 타임아웃(
max_concurrent_dials_per_backend(0~10000,0= 무제한, 기본값50): 모든 폴백 경로가 공유하는 백엔드별 동시 폴백 홉 연결(dial) 상한이며, 공급자 핸드셰이크가 완료될 때까지만 유지됩니다. 유지 범위, 포화 시 동작, 리로드 동작은 엣지 케이스 처리의 동시 요청 폭주 항목을 참고하세요.
스키마는 trigger_conditions.circuit_breaker_open도 허용합니다. 프록시는 백엔드 선택 시 서킷 브레이커 상태를 조회하지만, 열린 서킷에 의해 거부된 요청은 별도의 circuit_breaker_open 사유가 아니라 backend_unhealthy 트리거로 처리됩니다. 백엔드 서킷이 열렸거나 비정상 상태일 때 폴백 체인을 진행하려면 backend_unhealthy(및 능동 헬스 체크)를 활성화하세요.
컨텍스트 윈도우 폴백¶
trigger_conditions.context_window_exceeded(기본값 false)가 켜져 있으면, 입력이 기본 모델의 선언된 컨텍스트 윈도우에 들어가지 않을 때 디스패치 전에 체인 대상으로 홉합니다. 인그레스 진입 검사는 이미 일반적인 입력 추정치와 기본 모델의 유효 윈도우를 비교합니다. 트리거가 켜져 있으면 라우터는 설정된 체인을 순서대로 확인하고, 같은 추정치를 담을 수 있는 첫 번째 대상을 기본 시도로 디스패치합니다.
- 옵트인: 트리거가 꺼져 있으면 길이 초과 입력은 기존
400context_length_exceeded거부 응답을 그대로 받습니다. - 입력만 판정: 대상 윈도우는 입력 추정치만으로 판정하므로,
max_tokens나max_completion_tokens가 없는 요청도 홉합니다. - 적합한 대상 없음: 허용된 모든 대상의 윈도우가 너무 작으면, 클라이언트는 기본 모델의 윈도우와 추정 입력 토큰 수를 명시한 거부 응답을 받고, 백엔드는 호출되지 않습니다.
- fail-open: 한도를 알 수 없거나 윈도우가
0으로 선언된 모델은 그대로 전달되며, 홉이 시작되지 않습니다. - 시도 예산: 기본 시도가 1번이므로 최대
max_fallback_attempts - 1개의 대상만 검토합니다. - 귀속: 디스패치된 홉은 기존
X-Fallback-*헤더를 설정하며,X-Fallback-Reason은context_window_exceeded입니다. - 가드레일 범위: 홉은 입력 가드레일 게이트보다 먼저 실행되므로, 요청은 실제로 수신하는 모델의
guardrails.routes정책으로 검사됩니다. 일반 폴백 홉은 먼저 기본 모델의 라우트로 검사되므로, 체인으로 묶는 모델들에는 필수 라우트 규칙을 동일하게 적용하세요.
스트림 시작 전 실행¶
비스트리밍 요청 또는 스트림이 시작되기 전에는 라우터가 다음 순서로 동작합니다.
- 기본 모델과 설정된 체인을 결정합니다.
- 기본 시도를 실행합니다.
- 폴백 대상 실패인지 분류합니다.
- 다음 모델을 선택하고 페이로드의 모델 이름을 바꾼 뒤, chat 출력 토큰 필드를 알려진 대상 한도에 맞춥니다.
- 처음 성공하거나
max_fallback_attempts에 도달하면 중단합니다.
폴백은 모델 단위입니다. 각 대체 모델은 일반 백엔드 풀, 상태 필터, 접근 정책, 선택 전략을 거쳐 해석됩니다.
Chat 출력 토큰 예산¶
모든 /v1/chat/completions 폴백 홉은 공급자 연결을 열기 전에 요청된 출력 토큰 예산을 대상 모델 한도에 맞춥니다. 이 동작은 버퍼링 실행, 스트림 시작 전 연결 폴백, 스트림 중간 복구, 응답 품질 폴백에 모두 적용됩니다.
- 한도 출처와 우선순위: 라우터는 요청 시작 시점의 설정 스냅샷에 있는 명시적
metadata.limits와 모델 API용 집계 모델 카탈로그를 결합합니다. 요청 스냅샷의 명시적 한도는 이전 설정 세대의 캐시 카탈로그보다 우선하며, 백엔드별 메타데이터는 전역 메타데이터보다 우선합니다. - 스냅샷 전용 조회: 폴백 홉은 해당 스냅샷과 라우터가 이미 보유한 카탈로그(최신 여부 무관)만 읽으며, 새로 집계하거나 재검증하지 않습니다. 폴백 홉은 백엔드 실패 직후 공유 시간 예산 안에서 실행되므로, 그 시점에 모델 검색을 수행하면 실패한 백엔드에 다시 연결하게 되기 때문입니다.
- 시작 시 카탈로그 워밍: 라우터를 빌드하면 진입점(
serve,serve_embedded, 임베디드용into_router())이나model_aggregation.background_refresh설정 여부와 관계없이 백그라운드 집계 1회로 카탈로그를 미리 채웁니다. 워밍은 시작을 지연시키지 않고 백그라운드 갱신기의 첫 실행과 팬아웃을 공유하며,model_aggregation.request_timeout,max_retries,retry_delay제한을 따릅니다. 첫 워밍 완료 전이나 모든 워밍 실패 후에 실행되는 홉에서 대상 모델에 명시적 메타데이터도 없다면 페이로드를 변경하지 않습니다.
요청에 max_completion_tokens, max_tokens 또는 둘 다 있으면 제공된 각 필드를 대상의 알려진 max_output과 대상 컨텍스트 창에서 추정 입력 크기를 뺀 값으로 제한합니다. 두 필드가 모두 제공되었으면 둘 다 유지되며 기존 max_completion_tokens 우선순위도 바뀌지 않습니다. 카탈로그 한도가 0이거나 없으면 알 수 없는 값으로 취급합니다. 두 한도를 모두 알 수 없거나 요청에 두 필드가 모두 없으면 페이로드를 바꾸지 않습니다.
입력 크기는 정확한 토크나이저 결과가 아니라 추정치입니다. name, tool_calls, function_call, tool_call_id를 포함한 모든 프롬프트 관련 메시지 값과 tools, functions, response_format을 순회하며, 메시지별 및 전체 채팅 템플릿 고정 여유분을 더합니다.
- 일반 추정치 (수용 가능 여부 판단): ASCII 텍스트는 3글자당 1토큰으로, 그 밖의 문자는 1글자당 1토큰으로 계산하여 다국어 텍스트가 과소 추정되지 않도록 합니다. 과대 추정으로 요청을 거절하면 정상 처리 가능한 요청이
503이 되므로, 대상 모델이 프롬프트를 수용할 수 있는지는 일반 추정치로 판단합니다. - 상한 추정치 (제한 크기 계산): 과소 추정으로 인해 공급자
400오류가 발생하는 것을 막기 위해 UTF-8 1바이트를 1토큰으로 계산합니다. 상한 추정치가 이미 컨텍스트 창을 채운 경우에는 일반 추정치가 남긴 여유의 절반을 제한값으로 사용합니다. - 대상 건너뛰기 및 예산 하향 규칙: 일반 추정치만으로 이미 컨텍스트 창이 소진되었거나, 요청이 더 많은 토큰을 요구하는데 남는 출력 토큰이 16개 미만인 대상은 디스패치 전에 건너뛰고 다음 호환 모델로 진행합니다. 예산은 클라이언트가 요청한 값보다 낮추기만 하며(
8192.0같은 정수형 실수는 정수로 처리), 출력 예산을 임의로 1토큰으로 줄이지 않습니다. - 체인 소진 시 동작: 성공한 응답 없이 체인이 소진되면 결정적인
503비호환 오류를 반환합니다. 응답 품질 폴백은 기존 fail-open 규칙을 유지하여 남은 대상이 모두 부적합하면 마지막으로 성공한 응답을 반환합니다.
이 보호 동작은 공급자의 HTTP 400 응답을 전역 재시도 대상으로 만들지 않습니다. 공급자마다 토큰화 방식이나 비공개 한도가 다를 수 있고, 원격 이미지 URL만으로는 멀티모달 토큰 비용을 미리 알 수 없으므로 HTTP 400 응답은 기존 상태 코드 정책을 따릅니다.
클램프 로그에는 대상 모델과 숫자 예산 메타데이터만 기록되며 프롬프트 내용은 포함되지 않습니다. 체인에 호출자의 키로 직접 요청할 수 없는 모델이 포함될 수 있으므로 503 본문에는 대상 모델 이름을 노출하지 않습니다. 두 토큰 필드를 모두 생략한 요청은 제한하지 않으므로 공급자 측 기본값(예: Anthropic 변환의 max_tokens 기본값)이 그대로 적용됩니다.
교차 공급자 변환¶
폴백 홉의 페이로드는 처음부터 끝까지 표준 OpenAI chat-completions 형태를 유지하며 와이어 형식 변환은 디스패치 시점에 선택된 백엔드의 설정값 backend_type을 기준으로 정확히 한 번만 일어납니다. 따라서 messages, tools, tool_choice, stop을 비롯한 모든 샘플링 매개변수는 클라이언트가 보낸 그대로 백엔드 변환기에 도달하고 해당 홉을 실제로 처리하는 백엔드가 무엇인지 아는 유일한 계층인 변환기가 그 표준 입력으로부터 공급자 고유 요청을 만듭니다.
위의 대상 한도 조정 외에 홉은 페이로드의 모델 이름을 대상으로 바꿉니다. 공급자 형식으로 미리 변환하거나 매개변수를 미리 걸러내지 않습니다. 홉이 알 수 있는 것은 모델 이름뿐이고 그 추정은 Bedrock으로 서비스되는 claude-* 모델, OpenAI 호환 엔드포인트 뒤의 Gemini, vLLM에 올린 gpt- 접두 오픈 모델에서 틀리며 운영자가 붙인 별칭에서는 아예 아무 공급자도 추정하지 못하기 때문입니다.
표준 본문에 함께 실려 오는 Anthropic 고유 필드는 speed(fast mode)와 확장 사고 쌍인 thinking, output_config 세 가지입니다. 세 필드 모두 OpenAI chat-completions 스키마에 속하지 않으며, 홉 시점이 아니라 디스패치 시점에 선택된 백엔드의 backend_type 설정에 따라 제거됩니다.
- OpenAI 와이어 백엔드 (
generic,openai,azure,gemini,vllm,ollama,llamacpp,mlxcel,lmstudio,sglang): 모든 전송 지점의/v1/chat/completions본문에서 제거됩니다. OpenAI 와이어 대상은 이 필드 이름을 읽지 않으며, 엄격한 클라우드 엔드포인트는 알 수 없는 최상위 키에 HTTP 400을 반환하기 때문입니다. anthropic및bedrock: 필드를 유지합니다. 해당 변환기가thinking과output_config를 직접 읽으며,speed는 백엔드의anthropic_fast_mode옵트인 설정이 제어합니다.continuumrouter: 필드를 유지합니다. 하위 Continuum Router는 동일한 표준 확장 필드를 인식하고 자체 백엔드에 같은 규칙을 적용하므로, Anthropic 앞단의 하위 라우터로 홉할 때 클라이언트의 추론 의도가 보존됩니다.- 스냅샷에 없는 백엔드: 요청 진행 중 핫 리로드로 백엔드가 삭제되거나 이름이 바뀌어 설정 스냅샷에서 백엔드 이름을 찾을 수 없는 경우에만 필드를 건드리지 않습니다.
type:을 생략한 백엔드는 기본값인generic으로 해석되어 다른 OpenAI 와이어 유형과 동일하게 제거됩니다.
reasoning_effort는 제거되지 않으며 클라이언트가 보낸 값을 덮어쓰지도 않습니다. 모든 대상이 존중하거나 무해하게 무시하는 공급자 중립 표기이며 홉을 건너 클라이언트의 추론 의도를 실어 나르는 것이 바로 이 키입니다. 아래에서 설명하는 변환이 클라이언트가 값을 주지 않았을 때 이 키에 값을 쓰는 이유도 같습니다. extra_body도 건드리지 않습니다. 제거는 최상위 키에만 적용됩니다.
thinking 설정은 reasoning_effort가 됩니다¶
제거만 하는 것은 안전하지만 정보를 잃습니다. 확장 사고를 요청한 요청이 대상의 기본 추론 동작으로 실행되는 모델로 넘어가 버리기 때문입니다. 같은 디스패치 게이트가 제거하기 전에 먼저 변환합니다. 제거가 일어나는 바로 그 요청들에 한해 Anthropic thinking 설정을 추론 수준으로 매핑해 reasoning_effort로 기록하고 그다음에야 세 필드를 제거합니다. 이 매핑은 OpenAI에서 Anthropic으로 가는 방향의 역방향이며 같은 표를 재사용하므로 두 방향은 서로의 역함수로 유지됩니다.
| 표준 본문의 입력 | 방출되는 수준 |
|---|---|
output_config: {"effort": "max"} | 대상 어휘에 있으면 xhigh, 없으면 high |
output_config: {"effort": "high"} / "medium" / "low" | 같은 수준 |
thinking: {"type": "enabled", "budget_tokens": N}, N <= 4096 | low |
thinking: {"type": "enabled", "budget_tokens": N}, 4097 <= N <= 10240 | medium |
thinking: {"type": "enabled", "budget_tokens": N}, N > 10240 | high |
budget이 없는 thinking: {"type": "enabled"} | medium |
effort가 없는 thinking: {"type": "adaptive"} | 없음 |
thinking: {"type": "disabled"} | 없음. output_config에 effort가 있어도 마찬가지 |
| 형식이 잘못된 모든 경우 | 없음 |
이 budget 행들은 홉에만 적용되는 규칙이 아닙니다. /anthropic/v1/messages 인그레스도 같은 함수인 ReasoningEffort::from_budget_tokens로 budget_tokens를 해석하며 Chat Completions 변환과 responses_only 모델용 Responses API 변환 양쪽 모두에 적용됩니다. 따라서 budget 하나는 어느 문으로 들어오든 같은 추론 수준으로 해석됩니다.
output_config.effort가 thinking보다 우선합니다. 다만 disabled인 경우에는 표현할 effort가 없으므로 아무것도 방출하지 않으며, 알 수 없는 effort 문자열은 budget 기반 매핑으로 넘어갑니다. none, minimal, auto, max는 일부 대상이 거부하고 기본값 사용은 필드 생략으로 표현하므로 방출하지 않습니다.
기존 reasoning_effort 또는 중첩 표기인 reasoning.effort가 있으면 클라이언트가 명시한 공급자 중립 의도이므로 항상 우선하며 덮어쓰지 않습니다. 형식이 잘못된 thinking 설정은 홉 실패가 아니라 단순 제거로 처리됩니다.
대상 어휘에 맞추기¶
이 변환은 클라이언트가 보낸 reasoning_effort를 검사하는 추론 검증 이후 전송 지점에서 실행되므로, 선택된 대상이 지원하는 어휘에 맞춰 값을 조정합니다.
openai및azure: OpenAI 모델 어휘 표를 참조합니다.- 추론 미지원 모델 (
gpt-4o, chat/instant 계열, 표에 없는 Azure 배포 이름):reasoning_effort를 넣지 않고 단순 제거로 진행합니다. - 모델별 허용 집합:
gpt-5-pro는high만, 구형 pro 계열은medium·high·xhigh를, o 시리즈·codex 계열 및 표에 아직 없는 추론 가능 GPT-5 변형은low·medium·high를 허용합니다. - 인접 수준 맞춤: 허용되는 수준이면 그대로 쓰고,
xhigh미지원 대상에서는high로 낮춥니다. 그 외에는low < medium < high < xhigh단계에서 가장 가까운 허용 수준을 고르되 거리가 같으면 더 높은 쪽을 택합니다(예:gpt-5-pro행low는high,gpt-5.2-pro행low는medium). - Azure 배포 이름: 클라이언트가 보낸
reasoning_effort검증과 마찬가지로model의 배포 이름이 실제 모델군을 가리킨다고 신뢰하므로, Azure 배포 이름은 실제 서빙하는 모델군에 맞춰 지정해야 합니다.
- 추론 미지원 모델 (
- 그 밖의 모든 OpenAI 와이어 대상 (
generic,gemini, 자체 호스팅 엔진vllm,ollama,llamacpp,mlxcel,lmstudio,sglang):low,medium,high만 전송하며xhigh는 전송하지 않습니다.
로컬 엔진 대상도 변환된 effort를 받습니다¶
자체 호스팅 엔진도 변환 대상에 포함됩니다. reasoning_effort는 모든 대상이 소비하거나 무해하게 무시하는 표준 chat-completions 표기이며, 인바운드 /v1/messages 핸들러도 같은 엔진을 향해 이 값을 생성합니다.
vLLM, SGLang, llama.cpp, Ollama, LM Studio는 OpenAI 호환 표면에서 이 필드를 처리하거나 알 수 없는 샘플링 옵션으로 무시하며 요청을 실패시키지 않습니다. 클라우드 Gemini 변환기는 이를 thinking_level로 매핑하므로 직접 활용하고, 자체 호스팅 엔진으로 향하는 복구 홉에서도 추론 의도가 유지됩니다.
스트리밍 폴백¶
fallback.mid_stream_enabled는 활성 SSE 응답을 버퍼링해 업스트림 스트림 실패 뒤 복구할지 결정합니다.
true(기본값): 스트림 중간 폴백과 스트림별 버퍼를 허용합니다.false: 스트림 시작 전 폴백은 유지하지만 SSE 출력이 시작된 뒤의 실패는 스트림 오류로 반환합니다.
streaming.mid_stream_fallback.enabled는 버퍼링 스위치가 아니라 모드 선택자입니다.
true: 충분한 내용이 누적되면 누적 출력으로 연속 요청을 만듭니다.false: 폴백 모델에서 원 요청을 처음부터 다시 실행합니다.
streaming.mid_stream_fallback.max_fallback_attempts, min_accumulated_tokens, fallback_delay_ms, continuation_prompt로 이 경로를 조정할 수 있습니다. 스트림 전체 및 청크 간격 제한 시간은 계속 적용됩니다.
같은 모델 형제 백엔드 연속¶
streaming.mid_stream_fallback.same_model_first(기본값 false)를 켜면 스트림 중간 실패 후 체인보다 먼저 같은 모델을 제공하는 다른 백엔드를 찾습니다.
- 형제 자격: 사용자 라우팅 대상이고, API 키에 허용되며, 확정된 OpenAI 와이어 릴레이가 처리할 수 있고, 서킷 브레이커가 현재 요청을 허용하며, 이 요청에서 해당 모델로 아직 호출되지 않은 백엔드입니다.
- 홉 동작: 형제 백엔드는 체인 대상에 보낼 때와 같은 연속 또는 재시작 요청을 받고, 응답의
model은 클라이언트가 요청한 모델로 유지되며, 이 홉은max_fallback_attempts에 포함됩니다. - 체인 폴백: 형제 집합이 비어 있거나 형제 시도도 실패하면, 플래그를 끈 경우와 동일하게 체인이 진행됩니다.
시도 트리에는 대체된 시도가 기존 백엔드의 fallback_source로, 형제 시도가 같은 request_id 아래의 정상 후속 시도로 기록됩니다. 홉은 streaming_fallback_total{reason="same_model_sibling"}로 집계됩니다. 스트림 중간 복구는 SSE 응답 헤더가 이미 전송되었으므로 X-Fallback-* 헤더를 실을 수 없어, 헤더만으로는 형제 홉과 모델 변경을 구분할 수 없습니다. 이 플래그는 OpenAI 와이어 스트림 중간 릴레이에만 적용됩니다.
Cross-provider 스트리밍 홉¶
스트리밍 폴백 홉은 직접 라우팅이 쓰는 것과 같은 백엔드별 타입 디스패치로 다시 진입하며 선택된 백엔드의 backend_type 설정을 기준으로 분기합니다. native Anthropic, Bedrock, Gemini 백엔드로 향하는 홉은 해당 provider의 native 스트리밍 파이프라인으로 처리되고(Anthropic이면 x-api-key와 anthropic-version을 붙여 /v1/messages의 Messages API 호출) 클라이언트는 계속 OpenAI 형식의 SSE를 받습니다. 기본 백엔드가 native인 모델에 체인을 설정해도 정상 상태의 직접 스트리밍에는 영향이 없습니다.
기본 시도도 같은 디스패치를 거치므로 체인 탐색은 기본 백엔드의 와이어 형식에 좌우되지 않습니다.
- 네이티브 기본 백엔드의 핸드셰이크 이전 실패: 네이티브 타입 기본 백엔드(
anthropic,gemini,bedrockRuntime·Converse 엔드포인트)가 공급자 응답 전에 실패하면(연결 거부, 전송 오류, 핸드셰이크 타임아웃, 서킷 거부) 두mid_stream_enabled모드 모두에서 스트림 시작 전 체인 탐색으로 진입하며, 복구 응답에 동일한X-Fallback-*헤더를 붙입니다. - 네이티브 체인 항목의 다음 홉 진행: 네이티브 백엔드로 해석되는 체인 항목이 실패하더라도 체인이 중단되지 않고 OpenAI 와이어 항목과 동일하게 다음 항목으로 진행합니다.
- 공급자 상태 코드 정책: Gemini 또는 Bedrock이 반환한 상태 코드는 OpenAI 와이어 정책을 따릅니다.
429가 아닌4xx응답은 최종 응답으로 반환되고,429와5xx응답은fallback.fallback_policy.trigger_conditions.error_codes에 포함된 경우에만 다음 홉으로 진행합니다. 연결 거부는trigger_conditions.connection_error를 따릅니다. - Anthropic 및 Unix 소켓 예외: Anthropic 네이티브 응답은 기존 동작을 유지하며, Unix 소켓으로 연결하는 네이티브 기본 백엔드는 단일 단위로 처리되므로 실패 시 클라이언트에게 그대로 반환됩니다.
복구 시점에 따라 홉이 도달할 수 있는 범위가 달라집니다.
- SSE 응답 확정 전 (초기 연결 및 모든 스트림 시작 전 홉): 두
mid_stream_enabled모드 모두에서 어떤 백엔드 타입으로도 홉할 수 있습니다. 첫 공급자 핸드셰이크가 성공할 때 응답이 확정되므로, 스트림 시작 전 단계에서 체인이 소진되면 오류 이벤트를 담은200스트림이 아니라 정상 HTTP 오류로 반환되며X-Fallback-*헤더도 첨부됩니다. - SSE 출력 시작 후 (스트림 중간 복구): 응답 헤더가 이미 전송된 뒤에는 OpenAI 호환 와이어만 사용하므로, 네이티브 프로토콜이나 Unix 소켓 백엔드로 해석되는 체인 항목은 건너뛰고 다음 항목을 시도합니다. 네이티브 파이프라인으로 처리 중인 스트림은 출력 시작 후 실패 시 일반 스트림 오류로 종료됩니다.
튜닝 옵션과 시간 예산도 같은 기준으로 나뉩니다.
- 시도 횟수 상한: 스트림 시작 전 홉은 비스트리밍 폴백과 동일하게
fallback.fallback_policy.trigger_conditions와fallback.fallback_policy.max_fallback_attempts를 따르며,streaming.mid_stream_fallback.max_fallback_attempts는 확정된 릴레이가 수행하는 스트림 중간 홉만 제한합니다. - 연결 단계 예산 및 keep-alive: 첫 핸드셰이크 성공 시점에 응답이 확정되므로 연결 중에는 SSE keep-alive 주석이 전송되지 않습니다. 연결 단계는 릴레이와 단일 예산(
timeouts.request.streaming.total×timeouts.streaming_fallback_budget_multiplier)을 공유하며, 각 핸드셰이크 시도를 남은 예산으로 제한하고 예산 소진 시 보존된 업스트림 실패를 반환합니다. 핸드셰이크가 성립하면 릴레이가 스트림을 인계받은 시점부터 읽기 시간을 측정합니다.
인그레스 적용 범위¶
폴백 체인은 요청이 디스패치되는 모델 이름을 키로 사용하며, 해당 이름으로 백엔드를 선택하는 모든 인그레스에서 동작합니다.
- OpenAI 형태 인그레스 (
/v1/chat/completions,/v1/completions,/v1/embeddings, rerank, sparse embeddings,/v1/images/generations): 공용 디스패치 퍼널을 통해 체인을 실행합니다. - 공급자 형태 인그레스 (
/anthropic/v1/messages,/v1/responses): 네이티브로 디스패치하며 시도별 선택과 디스패치를 통해 동일한 체인을 실행합니다. 트리거 조건,max_fallback_attempts, 백엔드별 홉 다이얼 상한이 모두 chat과 동일하게 적용됩니다. - 체인 미적용 엔드포인트:
/v1/responses/compact와count_tokens는 폴백 체인을 실행하지 않습니다.
공급자 형태의 두 인그레스에서 각 시도는 고유 와이어 형식을 유지합니다.
- 와이어 충실도 유지: 기본 시도는 클라이언트가 보낸 네이티브 Anthropic Messages 또는 Responses 요청을 그대로 사용하고, 폴백 홉은 모델 이름만 바꾼 뒤 디스패치 시점에 선택된 백엔드의
backend_type에 맞춰 변환합니다(/v1/responses의 passthrough 전략과 모든 convert 전략 포함). - 결과 판정 및 트리거:
2xx응답은 시도를 확정합니다. 연결 거부, 타임아웃, 알 수 없는 모델, 가용 백엔드 없음은 각각connection_error,timeout,model_not_found,backend_unhealthy조건에 따라 홉하며, 공급자 상태 코드는trigger_conditions.error_codes에 포함된 경우에만 홉합니다. - 상태 코드 기반 분류: 라우터 측 전체 타임아웃(
504)과 convert 전략의 연결 실패(502)는 상태 코드로 분류되며, 둘 다 기본error_codes에 포함됩니다. - 최종 오류 형식: 체인이 소진되었거나 첫 실패가 트리거 대상이 아닐 때(예:
400) 클라이언트는 마지막 시도의 오류를 해당 인그레스의 고유 형식으로 받습니다.
두 인그레스의 스트리밍 경로는 첫 바이트 이전에만 홉합니다. 공급자 핸드셰이크 전에 실패하면 다음 체인 항목에서 네이티브 스트림을 다시 시작하고, 첫 핸드셰이크 성공 시 응답을 확정하며 X-Fallback-* 헤더를 첨부합니다. 핸드셰이크 이후의 실패는 스트림 중간 복구 없이 클라이언트에 그대로 전달됩니다.
응답 품질 폴백¶
위의 모든 트리거는 전송 계층이나 공급자가 보고하는 실패입니다. 백엔드가 200으로 거절, 비어 있거나 잘린 응답, 또는 다른 질문에 대한 답을 돌려주면 그대로 제공됩니다.
fallback.quality_check는 비스트리밍 /v1/chat/completions 응답에 대해 이 공백을 메웁니다. System One 결정 모델(TypeSafe의 호스팅 Jev, 또는 자체 호스팅 laya-serve, Kev, Nimble 서버, 결정 모델 참고)이 완성된 응답을 판정하고 응답을 쓸 수 없을 가능성이 높으면 라우터는 실패 시 전진했을 바로 그 체인을 전진합니다.
fallback:
fallback_chains:
qwen3.8-8b: [qwen3.8-27b]
quality_check:
target:
endpoint: "http://laya.internal:8000/v1/systemone"
threshold: 0.8 # 1 - answered, refusal, truncated 중 하나가 이 값에 도달하면 홉
max_extra_hops: 1 # 요청당 품질 홉 수
models: [qwen3.8-8b] # 판정할 체인 기본 모델, 비우면 체인이 있는 모든 모델
mode: shadow # 기본값: 판정하고 기록만 하며 홉하지 않음. enforce는 홉함
블록이 없으면 꺼져 있으며 target은 설정 도우미 문서에 설명된 공유 System One 대상 블록입니다(endpoint와 선택적 api_key_env, 또는 자체 호스팅 OpenAI 와이어 백엔드를 가리키는 backend: 참조).
shadow로 시작하기¶
mode의 기본값은 다른 모든 결정 모델 소비자와 마찬가지로 shadow입니다. 0.8 임계값은 레이블된 응답으로 보정된 값이 아니라 초기 시작값이며, 오탐이 발생하면 클라이언트가 받는 응답이 달라집니다. shadow 모드에서 라우터는 대상 응답을 판정하여 세 확률을 debug 로그로 남기고, enforce였다면 홉했을 응답을 fallback_quality_checks_total{outcome="shadow"}로 집계하되 항상 첫 응답을 그대로 반환합니다.
mode: enforce로 전환하기 전에 shadow 수치를 passed와 비교해 예상 트리거 비율을 확인하고, 표본 응답을 검토하여 교체하고 싶은 응답에서만 트리거되도록 threshold를 조정하세요. 보정 절차는 결정 모델을 참고하세요.
판정 대상¶
결정 호출 한 번이 정확히 두 텍스트로 이루어진 상태에 대해 noul 질문 세 개를 묻습니다. usage.quality_judgment가 호출을 공유하면(같은 target과 상한, 결정 모델 참고) 원래 응답에 대한 호출에 그 판정의 language_match를 최선 노력 질문으로 함께 싣습니다. 이 질문의 답이 없거나 형식이 잘못되면 빠질 뿐, 아래 판정을 바꾸지 않습니다.
| 질문 | 확률의 의미 | 트리거 조건 |
|---|---|---|
answered | 응답이 요청을 요청대로 다룸 | 1 - answered >= threshold (reason="not_answered") |
refusal | 응답이 요청을 거절하거나 회피함 | refusal >= threshold |
truncated | 응답이 끝나기 전에 멈춤 | truncated >= threshold, 단 응답의 finish_reason이 length이면 제외 |
둘 이상이 임계값에 도달하면 가장 확률이 높은 것을 보고합니다. finish_reason이 length인 응답은 클라이언트 자신의 max_tokens에서 멈춘 것이며 같은 한도로 홉해도 똑같이 잘리므로 truncated 답은 로그에만 남고 트리거하지 않습니다.
상태는 마지막 user 메시지 텍스트와 응답의 assistant 텍스트(또는 refusal 필드)로 구성된 {user_message, ai_response}입니다. 긴 응답의 끝부분이 잘림 판정에 계속 보이도록 각각 앞부분과 뒷부분을 남기는 방식으로 max_request_bytes와 max_reply_bytes에 맞춰 자릅니다.
이후 직렬화된 상태를 target.max_state_bytes 이하로 맞춥니다. JSON 이스케이프로 인해 코드나 JSON 비중이 높은 응답이 커지더라도 거부하지 않고 더 짧은 발췌본으로 판정합니다. system·developer 프롬프트, 이전 턴, 도구 호출 및 결과, 이미지, 추론 텍스트는 결정 모델로 전송하지 않으며, 도구를 호출하는 응답은 판정 대상에서 제외합니다.
상한은 체크포인트별로 정합니다.
| 체크포인트 | 컨텍스트 | max_request_bytes / max_reply_bytes |
|---|---|---|
Laya 다국어 (비라틴 텍스트에 대한 laya-serve 기본) | 1,024 토큰 | 512 / 1536 (기본값) |
| Laya 영어 | 512 토큰 | 256 / 768 |
| 호스팅 Jev | 질문당 32k 토큰 | 최대 4096 / 12288, target.max_state_bytes를 그에 맞게 올림 |
품질 홉의 실행 방식¶
- 가드레일 우선 실행: 검사는 모든 가드레일 단계를 통과한 응답에만 실행됩니다. 입력
Block은 디스패치 전에, 출력Block은 품질 검사 전에 반환되며 차단된 요청이나 응답은 결정 모델로 전송되지 않습니다. 홉의 응답도 동일한 출력 게이트를 거치며,Block이면 직전 응답을 유지하고 monitor 모드의Flag는 기록 후 제공됩니다. - 동일한 체인과 한도 공유: 품질 홉은 체인의 다음 모델을 선택해 일반 체인 실행기와 같은 경로로 디스패치합니다.
fallback_policy.max_fallback_attempts횟수에 포함되고 자체fallback_timeout_multiplier시간 창과 폴백 다이얼 퍼밋(max_concurrent_dials_per_backend)을 사용하며, 키의allowed_backends와model_settings.<model>.fallback_enabled를 따릅니다. - 키의 모델 허용 목록 재적용: 전송 실패 홉은 운영자 체인이 대상을 지정하므로 키의
allowed_models검사를 건너뛰지만, 품질 홉은 클라이언트가 거절이나 부적절한 응답을 유도해 트리거할 수 있습니다. 따라서 판정 전에 다음 체인 모델을 키의allowed_models와 대조하며, 허용되지 않은 모델이면 검사를 건너뛰고(fallback_quality_checks_total{outcome="skipped"}) 원래 응답을 반환합니다. refusal트리거의 거절 요청 재전송: 첫 모델이 거절했을 때 품질 홉은 동일한 요청을 다음 모델로 다시 보냅니다. 한 모델이 거절한 요청을 다른 모델에서 재시도할지는 안전성에 영향을 미치는 운영 결정이므로, 원하지 않는 경우 해당 모델을models에서 제외하거나 체인 다음 순서에 더 보수적인 모델을 배치하세요. 가드레일은 모든 홉의 입출력에 동일하게 적용됩니다.- 단일 체인 시간 예산: 전체 기한은 기본 모델의
timeouts.request.standard.total×timeouts.streaming_fallback_budget_multiplier(기본값2.0)이며 디스패치 시작 시점부터 측정됩니다. 전송 시도는 각자의 시도별 시간 창으로 실행되고 품질 검사와 품질 홉은 남은 예산만 사용합니다. 남은 시간이 없으면 판정 없이 응답을 반환합니다(outcome="budget_spent"). max_extra_hops상한 준수: 추가 홉이 가능할 때만 응답을 판정하므로 실행할 수 없는 결정 호출에 비용을 쓰지 않습니다. 마지막 허용 홉의 응답은 추가 판정 없이 반환됩니다.- Fail-open: 결정 호출의 실패·타임아웃, 홉의 오류·예산 초과, 출력 가드레일에 의한 홉 응답 차단이 발생하면 이미 게이트를 통과한 가장 최근 응답(원래 응답 또는
max_extra_hops >= 2일 때의 이전 홉 응답)을 반환합니다.
클라이언트와 운영자가 보는 것¶
품질 홉이 디스패치되면 응답에 x-continuum-quality-fallback: 1이 붙습니다. 홉의 응답이 제공되면 아래 X-Fallback-* 헤더가 X-Fallback-Reason: quality_check로 이를 보고합니다(notify_on_fallback을 따름). 홉이 실패해 원래 응답이 제공되면 x-continuum-quality-fallback만 붙습니다.
결정 호출 자체의 사용량은 라우터 내부 사용량으로 로그에만 기록되고 호출자 예산에는 합산되지 않으며, target.count_toward_budgets: true는 무시됩니다(config validate가 경고 출력).
판정 대상 응답과 품질 홉 응답은 둘 다 실제 업스트림 비용이므로 각각 과금됩니다. 트리거된 요청은 요청 통계 행 2개와(control-plane 빌드에서는 고유 멱등성 키를 가진 사용량 레코드 2개)로 기록됩니다. 따라서 판정 대상 모델의 요청 수는 클라이언트 요청 수의 최대 2배가 될 수 있으며, 홉 행의 지연 시간은 해당 홉 자체의 소요 시간입니다.
적용 범위 밖: 스트리밍 응답, 체인 홉을 하지 않는 웹 검색 도구 루프, 응답 캐시에서 제공된 응답, /anthropic/v1/messages와 /v1/responses 인그레스. 홉의 응답이 제공되면 응답 캐시에 저장되는 것도 그 응답입니다.
빈 응답 재시도¶
백엔드가 정상 응답을 반환했지만 완성 텍스트가 전혀 없는 경우도 있습니다. fallback.empty_reply_retry는 결정 모델 없이 이 경우를 처리합니다. 비스트리밍 /v1/chat/completions 응답에서 첫 choice에 message가 없거나, message.content(없으면 message.refusal)가 없거나 공백뿐이면 같은 체인을 전진시킵니다.
fallback:
fallback_chains:
qwen3.8-8b: [qwen3.8-27b]
empty_reply_retry:
enabled: false # 기본값: 선택과 서빙이 기존과 동일합니다
max_extra_hops: 1 # 요청당 이 트리거가 취하는 홉 수, 1-5
품질 검사가 적용되는 요청은 이 트리거를 건너뜁니다. 품질 검사는 빈 응답을 이미 거절로 처리하므로, 둘을 함께 실행하면 같은 응답을 판정하고 홉하게 됩니다. 다음 두 응답 형태는 빈 응답으로 보지 않습니다.
- 비어 있지 않은
tool_calls배열(또는 레거시function_call): 도구 호출은 텍스트가 없는 완성된 응답입니다. finish_reason이length인 응답: 클라이언트의max_tokens에서 잘린 응답이며, 같은 한도의 홉도 같은 방식으로 잘립니다.
빈 응답 홉의 실행 방식¶
- 출력 게이트를 먼저 거칩니다. 이 트리거는 가드레일 출력 게이트가 통과시킨 본문만 검사하며, 게이트가 대체한 본문은 검사하지 않습니다. 홉의 응답도 제공되기 전에 같은 게이트를 거칩니다.
- 동일한 체인과 한도 공유: 홉은 품질 홉 경로를 사용합니다. 체인의 시도 번호를 이어받고,
fallback_policy.max_fallback_attempts횟수에 포함되며, 자체fallback_timeout_multiplier시간 창과 폴백 다이얼 퍼밋(max_concurrent_dials_per_backend)을 사용하고, 키의allowed_backends를 따릅니다. 키의allowed_models는 모든 대상에 다시 적용합니다. - 단일 체인 시간 예산: 전체 기한은 기본 모델의
timeouts.request.standard.total×timeouts.streaming_fallback_budget_multiplier이며, 디스패치 시작 시점부터 측정됩니다. - 빈 응답이면 반복합니다. 홉의 응답도 비어 있으면
max_extra_hops와 시도 한도가 허용하는 동안 체인을 다시 전진시킵니다. 홉 대상은 전달할 요청에 대해 미리 검사하므로, 컨텍스트가 출력 여지를 남기지 않는 대상은 다이얼 없이 건너뜁니다. - 실패 시 원래 응답 제공: 홉이 실패하거나 예산을 초과하거나, 홉의 응답을 출력 가드레일이 차단하면 게이트를 통과한 가장 최근 응답을 그대로 제공합니다. 그 응답이 빈 응답이어도 마찬가지입니다.
- 적용 범위: 선택지가 하나이고, 로그 확률이 없고, 응답 형식이 plain 또는
json_object인 버퍼링 요청이며, HTTP(S)의 OpenAI 와이어 백엔드가 서빙한 경우에만 적용됩니다.unix:백엔드, 네이티브azure,anthropic,gemini,bedrock백엔드, 그리고replay: disabled백엔드는 기존 동작을 유지합니다.
홉이 응답을 제공하면 X-Fallback-* 헤더에 X-Fallback-Reason: empty_reply_retry를 붙입니다(notify_on_fallback을 따름). 트리거는 fallback_empty_reply_retry_total{result}로 집계하며, result는 hopped(홉 응답 제공), triggered(홉을 디스패치했지만 실패 또는 차단), skipped_no_hop(허용된 다음 대상 없음), budget_spent(예산 소진), not_enforced(서빙 백엔드가 위 적용 범위 밖: 네이티브 프로바이더, unix:, replay: disabled) 중 하나입니다. 디스패치마다 원래 모델, 서빙 백엔드, 홉 모델을 담은 debug 로그를 한 줄 남깁니다.
콘텐츠 정책 폴백¶
입력 가드레일 차단은 기본적으로 최종 결정입니다. 입력 게이트는 디스패치 전에 실행되며, 그 판정이 요청을 종료합니다. fallback.fallback_policy.content_policy_fallback을 켜면 이 판정이 요청을 끝내는 대신 fallback_chains를 전진시킬 수 있습니다. 따라서 기본 모델에 엄격한 정책이 적용되어도, 더 느슨한 홉 모델이 허용할 요청을 거부하지 않습니다.
fallback:
fallback_chains:
qwen3.8-8b: [qwen3.8-27b]
fallback_policy:
content_policy_fallback: true # 기본값 false: 차단은 최종 결정
트리거는 기본적으로 꺼져 있으며, 필드가 없는 설정은 위 계약과 정확히 동일하게 동작합니다. 이 트리거는 비스트리밍 POST /v1/chat/completions에만 적용됩니다. 스트리밍 요청, 라우팅된 모델이 정해지기 전에 실행되는 분류기 이그레스 게이트, responses 브리지, 웹 검색 루프 요청, 그리고 그 밖의 모든 인그레스는 기존의 최종 차단 계약을 유지합니다. 다른 모델로의 홉을 끈 요청(disable_fallbacks: true, 이슈 #2347, 또는 fallback: within_variant 실험)도 차단을 최종 결정으로 유지합니다.
콘텐츠 정책 홉의 실행 방식¶
- 홉 모델의 입력 게이트를 먼저 실행합니다. 클라이언트 문서는 디스패치 전에 홉 모델의
guardrails.routes항목(없으면 전역 정책)으로 다시 게이트됩니다. 한 정책이 거부한 입력이 다른 모델 이름으로 백엔드에 도달하지 않도록 하기 위해서입니다.Transform판정은 그 복사본을 제자리에서 마스킹합니다. - 동일한 체인과 한도 공유: 홉은 디스패치된 모델 체인의 다음 모델이며, 품질 홉과 같은 홉 경로로 전송됩니다.
fallback_policy.max_fallback_attempts횟수에 포함되고, 자체fallback_timeout_multiplier시간 창과 폴백 다이얼 퍼밋(max_concurrent_dials_per_backend)을 사용하며, 키의allowed_backends와model_settings.<model>.fallback_enabled를 따릅니다. 클라이언트의models목록(이슈 #2403)이 해당 요청의 체인을 대체하며, 웹 검색 도구 루프를 실행하는 백엔드로 향하는 홉 대상은 건너뜁니다. - 키의 모델 허용 목록 재적용: 전송 실패 홉과 달리 차단으로 트리거된 홉은 클라이언트가 유도할 수 있습니다. 따라서 다음 체인 모델은 게이트를 실행하기 전에
allowed_models와 대조합니다. 허용되지 않은 대상이면 기본 모델의 차단이 그대로 제공됩니다. - 요청당 한 번만 트리거합니다. 홉 모델의 정책도 차단하면, 홉 자체의 차단 응답을 제공하고 체인을 멈춥니다.
- 단일 체인 시간 예산: 전체 기한은 기본 모델의
timeouts.request.standard.total×timeouts.streaming_fallback_budget_multiplier이며, 트리거가 설정된 시점부터 측정됩니다. 남은 예산 없이 시작하는 홉은 디스패치하지 않고 차단 응답을 제공합니다. - 입력 결정 하나: 기본 패스와 모든 홉의 재실행 패스는 요청 수준 결정 하나로 합쳐지므로,
guardrail_verdicts_total과 감사 로그는 요청을 한 번만 집계합니다. 실제 적용된 결정은 홉 패스가 기록합니다. - 출력 게이트: 홉의 응답은 제공되기 전에 홉 모델의 출력 게이트를 거칩니다. 이 게이트가
Block을 반환하면 생성된 응답 대신 그 차단 응답을 제공합니다. - 제공된 응답은 품질 검사의 판정을 받습니다. 홉이 제공한 응답은 다른 비스트리밍 응답과 마찬가지로
fallback.quality_check를 거치며, 설정된 경우usage.quality_judgment도 수행합니다. 품질 검사는 체인을 한 번 더 전진시킬 수 있습니다. - 실패한 홉은 종료 실패가 아니면 다음 모델로 진행합니다. 전송 실패는 같은 시도 예산 안에서 다음 체인 모델로 이동합니다.
replay: disabled백엔드에 도달했을 수 있는 홉이나 이미 출력을 생성한 내부 스트리밍 디스패치는 최종 실패이므로 다른 모델로 디스패치하지 않습니다(이슈 #1993, #2043). 어떤 모델도 응답할 수 없으면 기본 모델의 차단 응답을 제공합니다.
클라이언트와 운영자가 보는 것¶
응답을 제공한 홉은 X-Fallback-* 헤더에 X-Fallback-Reason: content_policy를 붙입니다(notify_on_fallback을 따름). 트리거는 fallback_content_policy_hops_total{result}로 집계되며, 홉은 일반 폴백 계열에 트리거 사유 content_policy로 기록됩니다.
사용량 귀속¶
사용량은 클라이언트가 요청한 모델이 아니라 응답을 실제로 생성한 모델 이름으로 귀속됩니다.
- 귀속 대상 카운터 및 레코드: 기본 모델이 실패하고 체인 대상이 응답하면
/admin/stats의 모델 행, 모델별·키별 토큰 카운터, 모델 실험 토큰 귀속, 그리고control-plane빌드의 사용량 레코드model필드(실제 응답한backend및backend_id와 일치)에 모두 체인 대상 모델이 기록됩니다. - 적용 인그레스 및 시점:
notify_on_fallback설정과 무관하게 버퍼링되는/v1/chat/completions,/v1/completions,/v1/embeddings, rerank, sparse embeddings 인그레스와 스트리밍 채팅의 모든 홉(백엔드 선택 시점, 첫 바이트 이전, 스트림 도중)에 적용됩니다. 이미지 엔드포인트는 토큰 사용량을 기록하지 않습니다. - 정규 모델 ID: 공급자가 응답 본문에 적은
model문자열이 아니라 라우터에 설정된 체인 대상 ID로 기록되므로, 표기 차이로 인해 한 모델의 통계 행이 나뉘지 않습니다.
시도별 레코드¶
백엔드에 도달한 모든 디스패치는 클라이언트 요청의 개별 시도(attempt)로 정확히 한 번씩 기록됩니다(버퍼링 재시도, 폴백 체인 홉, 스트리밍 첫 바이트 이전 홉, 품질 검사 홉, web_search 도구 루프의 각 턴 포함).
- 레코드 연결 (
control-plane): 각 시도는 별도의 사용량 레코드로 생성되며, 같은 요청 내의 다른 시도와 공유request_id, 1부터 시작하는attempt_seq, 그리고 직전 시도를 가리키는parent_attempt로 연결됩니다. 최종 응답 시도는 일반 요청 단위 기록기가 기록합니다. - 실패한 시도: 다음 재시도나 홉을 시작하기 전에 즉시 기록됩니다.
outcome: upstream_error(타임아웃 전에 바이트가 전혀 오지 않았으면timeout_first_byte), 해당 시도의 업스트림http_status(연결 오류나 타임아웃이면 생략), 백엔드가 오류 본문에 보고한 토큰 수가 담깁니다. - 백엔드 사용량 미보고 시: 출력 토큰 0,
token_source: none, 그리고estimated: true가 붙은 라우터 추정 입력 토큰(base64 이미지가 텍스트로 계산되는 것을 제한하기 위해 최대 32,768 토큰 상한 적용)을 기록합니다. 추정치는 사용량 레코드에만 기록되며/admin/stats와 Prometheus 토큰 카운터는 엔진 보고 값만 유지합니다. - 디스패치 이전 거부: 라우터가 자체 거부한 디스패치(잘못된 백엔드 URL, 요청 변환 실패)는 시도로 집계되지 않습니다. 백엔드에 전혀 도달하지 못한 요청(키 거부, 모든 백엔드 비정상)은 사용량을 기록하지 않으며
/admin/stats의 실패 행에 백엔드가not_dispatched로 표시됩니다.
예를 들어 첫 시도가 백엔드 A에서 503을, 재시도가 백엔드 B에서 429를 받고 백엔드 C가 응답한 버퍼링 요청은 세 개의 레코드를 남깁니다.
attempt_seq | parent_attempt | backend | outcome | http_status | 출력 토큰 |
|---|---|---|---|---|---|
| 1 | 없음 | A | upstream_error | 503 | 0 (token_source: none) |
| 2 | 1 | B | upstream_error | 429 | 0 (token_source: none) |
| 3 | 2 | C | completed | 200 | 보고된 값 (token_source: engine_final) |
/admin/stats는 이를 요청 하나(total_requests: 1, successful_requests: 1)와 시도 셋(attempts_total: 3, failed_attempts: 2, successful_attempts: 1)으로 집계합니다. 품질 검사 홉도 마찬가지로 두 번째 요청이 아니라 같은 요청의 시도 하나로 집계됩니다. 스트림 종료 레코드의 시도 식별자는 스트리밍 릴레이가 직접 기록하며 첫 바이트 이전 홉은 다이얼한 시도를 요청 원장의 현재 시도로 남겨 둡니다.
스트림 도중 홉¶
스트림 도중(mid-stream) 폴백 홉은 하나의 클라이언트 스트림을 여러 시도로 나누지만, 백엔드에 도달한 각 시도는 정확히 한 번씩 기록됩니다.
- 부분 출력 귀속 (
fallback_source): 시도 A가 출력을 스트리밍한 뒤 끊기고 릴레이가 시도 B로 이어 가면, A의 레코드는 끊기기 전까지 A가 생성한 출력(백엔드가 사용량을 보내지 않았다면 라우터가 집계한 값)을outcome: fallback_source로 기록하고 B의 레코드는 B가 생성한 출력만 기록합니다. - 실패한 시도: 출력 없이 후속 시도로 대체된 실패 시도는 출력 0과
outcome: upstream_error를 기록합니다. 후속 시도가 없는 최종 실패 시도(체인·예산 소진 또는 클라이언트 연결 종료)는timeout_first_byte,timeout_mid_stream,upstream_error중 해당 종료 원인을 기록합니다. - 시도 번호 부여 (
attempt_seq및parent_attempt): 연결된 각 홉은 동일한request_id아래에서 다음attempt_seq를 부여받고 이어받은 시도를parent_attempt로 기록합니다(첫 바이트 이전 홉이 있었다면 연결 단계가 넘겨준 번호에서 이어짐). 요청 전송 전에 거부된 홉(다이얼 한도 포화, 열린 서킷)은 시도로 기록되지 않습니다. - 메트릭, 품질 판정 및 내보내기:
streaming_fallback_total은 각 홉에 실패한backend레이블을 기록합니다.usage.quality_judgment는 최종 응답을 제공한 시도의 레코드에만 첨부되며, 사용량 내보내기(GET /admin/usage/export)도 시도마다 한 행씩 동일한 레코드를 출력합니다.
일반 체인 홉은 Hub 정책과 짝지어 정의되는 requested_model, requested_provider, substitution_rule_id를 기록하지 않습니다. Hub 대체(substitution), 차익 거래(arbitrage), 예산 폴백이 먼저 요청을 재작성한 뒤 체인 홉이 이어지면 requested_model은 Hub 정책의 값을 유지하고 model에는 최종 응답 모델이 기록됩니다.
모델 예산 승인은 클라이언트가 요청한 오퍼링의 풀을 검사하고 정산은 실제 소비된 풀에 부과되며, 다음 Hub 예산 스냅샷이 로컬 원장을 재조정합니다. /anthropic/v1/messages와 /v1/responses 체인은 아직 시도별 레코드 대상에 포함되지 않습니다.
응답 헤더¶
모델의 notify_on_fallback이 true(기본값)이고 폴백이 성공하면 응답에 다음 헤더가 포함될 수 있습니다.
X-Fallback-Used: true
X-Original-Model: gpt-5.4
X-Fallback-Model: gpt-5.4-mini
X-Fallback-Reason: error_code_429
X-Fallback-Attempts: 2
라우터가 내보내는 폴백 헤더는 X-Fallback-Used, X-Original-Model, X-Fallback-Model, X-Fallback-Reason, X-Fallback-Attempts 다섯 개입니다. 모델에 notify_on_fallback: false를 지정하면 폴백 응답은 그대로 제공하면서 이 헤더들만 억제합니다.
X-Original-Model은 클라이언트가 요청한 모델을 가리킵니다. 요청이 메타데이터 별칭을 지정했고 라우터가 그것을 정규 id로 디스패치했다면 헤더에는 체인 조회에 쓰인 정규 id가 아니라 클라이언트가 보낸 별칭이 실립니다. X-Fallback-Model은 여전히 실제로 응답한 모델을 가리킵니다.
X-Fallback-Attempts는 요청을 처리하기 위해 시도한 모델 수를 기본 시도를 포함해 보고합니다. 이 헤더는 폴백 백엔드가 응답을 만든 경우에만 나타나므로 값은 항상 2 이상입니다. 값이 2이면 기본 모델이 한 번 실패하고 첫 폴백이 성공했다는 뜻이고 값이 더 크면 성공 전에 실패한 홉이 더 있었다는 뜻입니다. 이를 통해 클라이언트는 한 번에 복구된 경우와 여러 모델을 거친 경우를 구분할 수 있습니다.
이 헤더들은 비스트리밍 응답과 SSE 출력 시작 전에 폴백이 일어난 스트리밍 응답(선택 시점 폴백 및 두 mid_stream_enabled 모드의 스트림 시작 전 체인 전진)에 첨부되며, /anthropic/v1/messages와 /v1/responses를 포함해 체인을 실행하는 모든 인그레스에서 반환됩니다.
스트리밍(SSE) 응답은 본문보다 먼저 헤더를 전송하므로 스트림 중간 복구에서는 시도 횟수를 헤더로 보고할 수 없으며, 대신 streaming_fallback_* 메트릭으로 관찰할 수 있습니다.
메트릭¶
metrics 기능이 활성화되면 폴백 수집기가 다음 항목을 등록합니다.
fallback_attempts_total{original_model,fallback_model,backend}fallback_success_total{original_model,fallback_model,backend}fallback_exhausted_total{original_model}models_using_fallback{original_model}fallback_duration_seconds{original_model,success}fallback_triggers_by_reason{original_model,reason}fallback_default_chain_hop_skipped_total{original_model,fallback_model}: 요청 키의allowed_models가 허용하지 않아 요청의 해석된 체인에서 건너뛴fallback.default_chain대상 수이며, 여러 단계에서 체인을 구체화하더라도 요청당 한 번만 집계합니다
스트림 중간 복구는 streaming_fallback_* 메트릭도 제공합니다.
응답 품질 검사는 닫힌 레이블 집합을 가진 세 가지 계열을 추가하고 품질 홉을 위 계열에 트리거 사유 quality_check로 기록합니다.
fallback_quality_triggered_total{reason}: 디스패치된 품질 홉,reason은not_answered,refusal,truncated중 하나fallback_quality_checks_total{outcome}: 검사한 응답,outcome은passed,triggered,shadow,error,skipped,budget_spent중 하나fallback_quality_hops_total{result}: 디스패치된 홉의 결과,result는served,failed,blocked중 하나
결정 호출 자체는 공유 클라이언트가 systemone_requests_total{consumer="fallback_quality"}로 집계합니다.
빈 응답 트리거는 닫힌 레이블 집합을 가진 계열 하나를 추가합니다.
fallback_empty_reply_retry_total{result}: 검사한 빈 응답,result는hopped,triggered,skipped_no_hop,budget_spent,not_enforced중 하나
콘텐츠 정책 트리거는 닫힌 레이블 집합을 가진 계열 하나를 추가하고, 홉을 위 계열에 트리거 사유 content_policy로 기록합니다.
fallback_content_policy_hops_total{result}: 트리거 결과,result는triggered(트리거 발동),hopped(홉이 자체 게이트를 통과해 디스패치됨),no_chain(허용된 체인 대상 없음),blocked_again(홉 모델 게이트가 차단하여 그 차단 응답 제공),budget_spent(체인 예산 소진) 중 하나
부수 효과가 있는 백엔드¶
replay: disabled가 설정된 백엔드에는 동일한 호출을 두 번 전송하지 않습니다. 요청이 백엔드에 도달했을 가능성이 있는 시점 이후에 실패하면 모든 경로에서 해당 실패가 최종 응답이 됩니다.
- 비스트리밍 퍼널: 시도를
TriggerReason::ReplayDisabled로 분류하며, 재시도나 폴백 체인 홉을 트리거하지 않습니다. - 스트리밍 경로: 스트림 시작 전 연결 단계와 스트림 도중 릴레이 모두 해당 백엔드에서 체인 순회를 멈춥니다. 스트리밍 초기 선택이
replay: disabled백엔드에 도달하면 체인 순회 없이 한 번만 디스패치합니다. - 공급자 형태 인그레스 및 품질 홉:
/v1/responses와/anthropic/v1/messages체인은 해당 백엔드로 디스패치를 확정한 뒤 발생한 실패에서 즉시 멈추며, 해당 백엔드가 제공한 응답에서는 품질 홉을 수행하지 않습니다.
앞선 백엔드에서 replay: disabled 백엔드로 홉해 들어오는 것은 허용됩니다. 자세한 내용은 재전송 정책을 참고하세요.
백엔드가 출력을 일부 생성한 뒤 실패한 내부 스트리밍 버퍼링 디스패치(proxy.internal_streaming)도 같은 방식으로 최종 실패 처리됩니다. 비스트리밍 퍼널은 해당 시도를 TriggerReason::PartialOutput(partial_output)으로 분류하므로 재시도 루프, 체인 홉, 분리형 서빙의 통합 폴백 모두 요청을 다시 보내지 않습니다. 출력이 생성되기 전의 실패는 일반 실패와 동일하게 재시도하고 홉합니다. 버퍼링 요청의 내부 스트리밍을 참고하세요.
재적용 동작¶
fallback_chains, fallback_policy, quality_check, empty_reply_retry 변경은 실행 중인 폴백 서비스가 사용합니다. quality_check와 empty_reply_retry는 각 요청이 실행되는 설정에서 읽으며, quality_check.target이 바뀌면 다음 요청에서 캐시된 결정 클라이언트를 다시 만듭니다. 폴백 서비스의 존재 여부나 마스터 enabled 상태를 바꾸려면 재시작해야 합니다. 스트리밍 서비스 설정 변경은 /admin/config/hot-reload-status와 생성 설정의 주석을 따르세요.