고급 설정¶
전역 프롬프트¶
전역 프롬프트를 사용하면 모든 요청에 시스템 프롬프트를 주입하여 보안, 규정 준수 및 동작 가이드라인에 대한 중앙 집중식 정책 관리를 제공할 수 있습니다. 프롬프트는 인라인으로 정의하거나 외부 Markdown 파일에서 로드할 수 있습니다.
기본 설정¶
global_prompts:
# 인라인 기본 프롬프트
default: |
회사 보안 정책을 따라야 합니다.
내부 시스템 세부 정보를 공개하지 마십시오.
도움이 되고 전문적이어야 합니다.
# 병합 전략: prepend (기본), append, 또는 replace
merge_strategy: prepend
# 전역 프롬프트와 사용자 프롬프트 사이의 사용자 정의 구분자
separator: "\n\n---\n\n"
외부 프롬프트 파일¶
복잡한 프롬프트의 경우 외부 Markdown 파일에서 콘텐츠를 로드할 수 있습니다. 그러면 구문 강조가 있는 편집 환경, 설정 파일 노이즈 없는 버전 관리, 프롬프트 업데이트에 대한 핫 리로드 지원이 가능해집니다.
global_prompts:
# 프롬프트 파일이 있는 디렉토리 (설정 디렉토리 기준 상대 경로)
prompts_dir: "./prompts"
# 파일에서 기본 프롬프트 로드
default_file: "system.md"
# 파일에서 백엔드별 프롬프트
backends:
anthropic:
prompt_file: "anthropic-system.md"
openai:
prompt_file: "openai-system.md"
# 파일에서 모델별 프롬프트
models:
gpt-5.6-sol:
prompt_file: "gpt5-6-sol-system.md"
claude-opus-5:
prompt_file: "claude-opus-system.md"
merge_strategy: prepend
프롬프트 해석 우선순위¶
요청에 사용할 프롬프트 결정 시:
- 모델별 프롬프트 (최고 우선순위) -
global_prompts.models.<model-id> - 백엔드별 프롬프트 -
global_prompts.backends.<backend-name> - 기본 프롬프트 -
global_prompts.default또는global_prompts.default_file
각 레벨에서 prompt (인라인)와 prompt_file이 모두 지정되면 prompt_file이 우선합니다.
병합 전략¶
| 전략 | 동작 |
|---|---|
prepend | 전역 프롬프트가 사용자 시스템 프롬프트 앞에 추가 (기본) |
append | 전역 프롬프트가 사용자 시스템 프롬프트 뒤에 추가 |
replace | 전역 프롬프트가 사용자 시스템 프롬프트를 완전히 대체 |
REST API 관리¶
프롬프트 파일은 Admin API를 통해 런타임에 관리할 수 있습니다:
# 모든 프롬프트 목록
curl http://localhost:8080/admin/config/prompts
# 특정 프롬프트 파일 가져오기
curl http://localhost:8080/admin/config/prompts/prompts/system.md
# 프롬프트 파일 업데이트
curl -X PUT http://localhost:8080/admin/config/prompts/prompts/system.md \
-H "Content-Type: application/json" \
-d '{"content": "# 업데이트된 시스템 프롬프트\n\n새 콘텐츠."}'
# 디스크에서 모든 프롬프트 파일 리로드
curl -X POST http://localhost:8080/admin/config/prompts/reload
전체 API 문서는 Admin REST API 참조를 참조하세요.
보안 고려 사항¶
- 경로 탐색 보호: 디렉토리 탐색 공격 방지를 위한 모든 파일 경로 검증
- 파일 크기 제한: 개별 파일 1MB, 전체 캐시 50MB 제한
- 상대 경로만: 프롬프트 파일은 설정된
prompts_dir또는 설정 디렉토리 내에 있어야 함 - 샌드박스 접근: 허용된 디렉토리 외부 파일은 거부
핫 리로드¶
전역 프롬프트는 즉시 핫 리로드를 지원합니다. 프롬프트 설정 또는 파일 변경 사항은 서버 재시작 없이 다음 요청에 적용됩니다.
모델 메타데이터¶
Continuum Router는 모델 기능, 가격, 한도에 대한 상세 정보를 제공하는 풍부한 모델 메타데이터를 지원합니다. 이 메타데이터는 /v1/models API 응답에 반환되며 클라이언트가 정보에 입각한 모델 선택 결정을 내리는 데 사용할 수 있습니다.
메타데이터 소스¶
모델 메타데이터는 세 가지 방법으로 설정할 수 있습니다 (우선순위 순):
- 백엔드별 model_configs (최고 우선순위)
- 외부 메타데이터 파일 (model-metadata.yaml)
- 메타데이터 없음 (모델은 메타데이터 없이도 작동)
외부 메타데이터 파일¶
model-metadata.yaml 파일을 만드세요:
models:
- id: "gpt-5.6-sol"
aliases: # 이 메타데이터를 공유하는 대체 ID
- "gpt-5.6"
metadata:
display_name: "GPT-5.6 Sol"
summary: "복잡한 전문 작업을 위한 프런티어 GPT-5.6 모델"
capabilities: ["chat", "vision", "code", "reasoning", "tool"]
knowledge_cutoff: "2026-02"
pricing:
input_tokens: 5.0 # 1M 토큰당 USD
output_tokens: 30.0 # 1M 토큰당 USD
limits:
context_window: 1050000
max_output: 128000
- id: "llama-3-70b"
aliases: # 동일 모델의 다른 양자화
- "llama-3-70b-instruct"
- "llama-3-70b-chat"
- "llama-3-70b-q4"
- "llama-3-70b-q8"
metadata:
display_name: "Llama 3 70B"
summary: "강력한 성능의 오픈 소스 모델"
capabilities: ["text", "code"]
knowledge_cutoff: "2023-12"
pricing:
input_tokens: 1.0 # 1M 토큰당 USD
output_tokens: 2.0 # 1M 토큰당 USD
limits:
context_window: 8192
max_output: 2048
이미지 모델 가격¶
이미지 모델은 토큰 단가로 표현할 수 없는 축으로 과금하므로, pricing은 선택 키 세 개를 추가로 받습니다.
# 생성 이미지 단위 과금: 크기별 단일 가격, 티어 이름별 단일 가격,
# 크기 다음 품질의 중첩 맵. 세 형태를 모두 지원합니다.
- id: "dall-e-2"
metadata:
pricing:
input_tokens: 0 # 실제로 적힌 값이며 누락이 아님
output_tokens: 0
per_image: # 생성 이미지 1장당 USD
1024x1024: 0.02
512x512: 0.018
- id: "dall-e-3"
metadata:
pricing:
input_tokens: 0
output_tokens: 0
per_image:
1024x1024: { standard: 0.04, hd: 0.08 }
limits:
context_window: 0 # 0 = 해당 없음 (토큰 컨텍스트가 없음)
max_output: 0
max_prompt_length: 4000 # 토큰이 아니라 문자 수
supported_sizes: ["1024x1024", "1792x1024", "1024x1792"]
max_n: 1 # 요청당 최대 이미지 수
supported_qualities: ["standard", "hd"]
supported_styles: ["vivid", "natural"]
# 토큰 단위 과금이면서 이미지 입력에 별도 요율을 적용하는 경우
- id: "gpt-image-2"
metadata:
pricing:
input_tokens: 5.0
output_tokens: 30.0
image_input_tokens: 8.0 # 1M 이미지 입력 토큰당 USD
cached_image_input_tokens: 2.0 # 1M 캐시 이미지 입력 토큰당 USD
image_input_tokens및cached_image_input_tokens:input_tokens와 동일하게 1M 토큰당 USD 단위를 쓰며, 텍스트 요율을 대체하지 않고 추가로 합산됩니다.cached_image_input_tokens는 비율(0.0~1.0)로 지정하는cached_input_discount와 달리 명시적인 단가(USD)입니다.per_image: 생성된 이미지 1장당 USD 단위를 씁니다. 이미지당 과금 모델은 토큰 단위로 과금하지 않음을 나타내기 위해 의도적으로input_tokens: 0,output_tokens: 0을 보고합니다.
세 키 모두 선택 항목이며 기존 설정에 추가로 적용됩니다. /v1/models/extended와 /v1/models/{model}에서 제공되며, 자세한 내용은 가격 객체 필드를 참고하세요.
긴 컨텍스트 가격¶
프롬프트 임계값 이후 더 높은 요율을 부과하는 모델은 long_context를 선언할 수 있습니다. 프롬프트 토큰 수가 threshold_tokens보다 클 때만 티어 요율을 요청 전체에 적용하며, 임계값과 같으면 기본 요율을 사용합니다.
- id: "claude-haiku-5-5"
metadata:
pricing:
input_tokens: 0.1
output_tokens: 0.5
long_context:
threshold_tokens: 100000
input_tokens: 0.5
output_tokens: 2.5
임계값은 1~10,000,000 토큰이어야 합니다. 두 티어 요율은 1M 토큰당 USD 단위의 유한한 0 이상 값이어야 합니다. 이 필드는 모델 API로 제공되며 스마트 라우팅 티어 추론은 계속 기본 입력 요율을 사용합니다.
이미지 모델은 위 dall-e-3 예시처럼 선택적 limits 키(max_prompt_length(토큰이 아닌 문자 수), supported_sizes, max_n, supported_qualities, supported_output_formats, supported_styles, supports_streaming)로 요청 제약을 기술할 수 있습니다. 이 값들은 API 소비자를 위한 참고 정보이며, 라우터는 메타데이터를 읽지 않고 모델 계열별 이미지 요청 제한을 코드에서 직접 강제합니다. 토큰 과금 대상이 아닌 모델의 context_window: 0과 max_output: 0은 '해당 없음'을 뜻합니다. 메타데이터 파일에 알 수 없는 키가 있으면 점(.) 표기 경로를 포함한 로드 경고를 남기고 무시합니다.
pricing과 limits 값은 모두 허용 범위를 검사합니다(예: context_window와 max_output은 0~10,000,000 허용):
- 런타임 로드: 범위를 벗어난 값이 있으면 모델과 필드 이름을 포함한 경고를 남기고, 설정된 값을 그대로 계속 서빙합니다.
metadata download: 잘못된 다운로드 파일이 정상 동작 중인 파일을 덮어쓰지 않도록, 범위 위반을 치명적 오류로 처리하여 디스크의 파일을 변경하기 전에 거부합니다.
설정에서 참조하세요:
model_metadata_file은 기본 계층일 뿐입니다. 라우터는 model-metadata.d/ 드롭인 디렉터리와 model_metadata_dirs 설정 키에서도 메타데이터를 조립하므로, 운영자가 바꾼 내용을 metadata download가 덮어쓰는 파일에 그대로 둘 필요가 없습니다. 탐색 순서, 병합 규칙, metadata show는 계층형 모델 메타데이터에서 다룹니다.
Thinking 패턴 설정¶
일부 모델은 추론/사고 콘텐츠를 비표준 방식으로 출력합니다. 라우터는 스트리밍 응답을 적절히 변환하기 위해 모델별 thinking 패턴 설정을 지원합니다.
패턴 유형:
| 패턴 | 설명 | 예시 모델 |
|---|---|---|
none | thinking 패턴 없음 (기본값) | 대부분의 모델 |
standard | 명시적 시작/종료 태그 (<think>...</think>) | 커스텀 추론 모델 |
unterminated_start | 시작 태그 없이 종료 태그만 있음 | nemotron-3-nano |
설정 예시:
models:
- id: nemotron-3-nano
metadata:
display_name: "Nemotron 3 Nano"
capabilities: ["chat", "reasoning"]
# Thinking 패턴 설정
thinking:
pattern: unterminated_start
end_marker: "</think>"
assume_reasoning_first: true
Thinking 패턴 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
pattern | string | 패턴 유형: none, standard, 또는 unterminated_start |
start_marker | string | standard 패턴용 시작 마커 (예: <think>) |
end_marker | string | 종료 마커 (예: </think>) |
assume_reasoning_first | boolean | true인 경우, 종료 마커까지 첫 토큰들을 추론으로 처리 |
buffered | boolean | true인 경우 첫 토큰들을 분류가 끝날 때까지 내보내지 않고 보류 (기본값 false) |
max_buffer_size | integer | buffered 모드의 결정 버퍼 상한(바이트, 기본값 51200, 최대 8388608) |
reasoning_timeout | string | buffered 모드의 결정 대기 시간, 예: "10s" (기본값 "10s") |
작동 방식:
모델에 thinking 패턴이 설정되면:
- 스트리밍 응답이 가로채져 변환됨
end_marker이전 콘텐츠는reasoning_content필드로 전송end_marker이후 콘텐츠는content필드로 전송- 출력은 호환성을 위해 OpenAI의
reasoning_content형식을 따름
출력 예시:
// 추론 콘텐츠 (종료 마커 이전)
{"choices": [{"delta": {"reasoning_content": "분석해 보겠습니다..."}}]}
// 일반 콘텐츠 (종료 마커 이후)
{"choices": [{"delta": {"content": "답은 42입니다."}}]}
버퍼링 분류¶
assume_reasoning_first는 근거가 모이기 전에 먼저 추측합니다. 첫 토큰들을 reasoning_content로 내보낸 뒤 </think>가 나오면 채널을 바꿉니다. 사고 단계를 건너뛰고 바로 답하는 하이브리드 모델은 이 마커를 아예 내보내지 않으므로, 답변 전체가 추론 채널로 나가고 content만 렌더링하는 클라이언트에는 빈 응답이 보입니다.
buffered: true로 두면 추측 대신 판단을 합니다. 라우터가 첫 토큰들을 보류한 뒤 실제로 도착한 내용으로 분류합니다.
| 이벤트 | 결과 |
|---|---|
end_marker 도착 | 보류한 앞부분을 reasoning_content로, 마커 이후를 content로 전송 |
reasoning_timeout 만료 | 보류한 앞부분을 content로 전송 |
max_buffer_size 도달 | 보류한 앞부분을 content로 전송 |
| 스트림이 먼저 종료 | 보류한 앞부분을 content로 전송 |
models:
- id: nemotron-3-nano
metadata:
thinking:
pattern: unterminated_start
end_marker: "</think>"
assume_reasoning_first: true
buffered: true
max_buffer_size: 51200
reasoning_timeout: "10s"
결정 대기 시간은 요청 시점이 아니라 첫 토큰이 도착한 시점부터 셉니다. 대기 중 스트림이 조용해져도 라우터가 다음 청크를 기다리지 않고 마감 시각에 깨어나므로 판단은 제때 끝납니다.
buffered는 자신이 대체하는 무조건 방출이 적용되는 경우, 곧 pattern: unterminated_start와 assume_reasoning_first: true가 함께 설정된 경우에만 동작합니다. 다른 패턴에서는 무시됩니다. 기본값이 꺼짐이므로 기존 배포는 직접 켜기 전까지 동작이 바뀌지 않습니다.
첫 토큰까지 걸리는 시간(TTFT):
버퍼링은 정확한 분류를 얻는 대신 TTFT를 내줍니다. 판단이 끝나기 전에는 아무것도 클라이언트로 나가지 않으므로, 첫 토큰 지연은 모델의 사고 단계 길이가 되고 그 상한은 reasoning_timeout입니다. 사고 단계를 400ms로 둔 모의 백엔드 측정값(tests/streaming/thinking_buffered_streaming_test.rs)은 다음과 같습니다.
| 모드 | 첫 델타까지 |
|---|---|
buffered: false | 약 0.14ms (첫 추론 토큰을 즉시 중계) |
buffered: true | 약 401ms (</think> 마커가 판단을 끝냄) |
결정 구간에 들어가지 않는 스트림은 비용을 전혀 내지 않습니다. buffered: false이거나 다른 패턴이면 타이머도 걸지 않고 추가 버퍼도 쓰지 않습니다. reasoning_timeout은 클라이언트가 빈 화면을 견딜 수 있는 지연 이하로 두고 바로 답하는 경우가 잦은 하이브리드 모델에 버퍼링 모드를 쓰세요.
Responses-API 전용 모델¶
OpenAI는 일부 모델을 Responses API(/v1/responses)로만 노출합니다. 이런 모델은 /v1/chat/completions에서 닿을 수 없어, Chat Completions 엔드포인트로 요청하면 업스트림에서 404 not_found가 돌아옵니다.
responses_only capability 플래그가 이런 모델을 표시하면, 라우터가 자동으로 Responses API 쪽으로 dispatch합니다. 기본값은 false이므로 기존 모델 항목은 그대로 두면 됩니다.
설정 예시:
models:
- id: gpt-5.4-pro
metadata:
display_name: "GPT-5.4 Pro"
capabilities: ["chat", "vision", "code", "reasoning", "tool"]
# /v1/responses에서만 제공되며, /v1/chat/completions로는 접근 불가.
responses_only: true
limits:
context_window: 1050000
max_output: 128000
기본 제공되는 Responses-API 전용 모델¶
아래 목록은 model-metadata.yaml과 내장 OpenAI 레지스트리(src/infrastructure/backends/openai/models/gpt5_family.rs)와 동기화되어 있습니다. 새로운 Responses-API 전용 모델이 업스트림에 추가되면 두 파일을 함께 갱신해야 합니다.
| 모델 ID | 출처 | 비고 |
|---|---|---|
gpt-5-pro | 내장 OpenAI 메타데이터 + model-metadata.yaml | 원래 GPT-5 Pro, high reasoning effort만 지원 |
gpt-5.2-pro | 내장 OpenAI 메타데이터 + model-metadata.yaml | 어려운 질문용 최상위 모델, xhigh reasoning effort |
gpt-5.4-pro | model-metadata.yaml | Frontier 급 심층 추론, medium/high/xhigh 지원 |
gpt-5.5-pro | model-metadata.yaml | 고난도 작업용 GPT-5.5 고성능 변형 |
플래그는 메타데이터 전체와 같은 우선순위 체인(백엔드 model_configs > model-metadata.yaml > 내장 OpenAI 메타데이터)을 따르므로, 운영자가 정의한 항목으로 어떤 모델의 기본값도 덮어쓸 수 있습니다.
새 모델을 Responses-API 전용으로 표시하기¶
추가 모델을 Responses-API 전용으로 표시하려면, 지원되는 소스 중 한 곳의 모델 항목 metadata 블록에 responses_only: true를 추가합니다. 배포 범위에 맞는 우선순위를 골라 사용합니다:
model-metadata.yaml: 모든 백엔드에 적용되는 라우터 전체 기본값. 기존 capability 메타데이터 옆에 플래그를 추가하면 되고 다른 필드는 건드릴 필요가 없습니다. 공급자 전반에서 일관되게 Responses-API 전용으로 출시되는 새 Pro 모델에는 이 위치를 권장합니다.config.yaml의 백엔드model_configs: 백엔드별 오버라이드 (예: 자체 호스팅한 Pro 모델 클론을 Chat Completions 엔드포인트에 노출했고/v1/responses로 보내면 안 되는 경우). 백엔드 단의responses_only: false는 해당 백엔드에서만 메타데이터 파일 기본값을 덮어씁니다.src/infrastructure/backends/openai/models/gpt5_family.rs의 내장 OpenAI 레지스트리: 바이너리에 같이 들어가는 모델용. 여기에 새 항목을 추가하면 외부에서 로드되는 메타데이터와 일관성을 유지하기 위해model-metadata.yaml에도 같이 반영합니다.
이런 소스를 갱신한 뒤에는 라우터를 재시작하거나 핫 리로드를 트리거해야 새 플래그가 후속 요청에 적용됩니다.
Dispatch 동작¶
responses_only=true가 설정된 모델은 /v1/chat/completions로 도달했을 모든 공개 surface에서 라우터가 Responses API로 보냅니다:
/v1/chat/completions: 요청이 업스트림/v1/responses엔드포인트로 투명하게 전달되고 응답은 strict 모드의chat.completion(스트리밍은chat.completion.chunk) 봉투로 다시 변환됩니다./anthropic/v1/messages: Anthropic 형식 요청이 Responses API 형태로 변환되어/v1/responses로 dispatch되고 업스트림 응답이 다시 Anthropic Messages JSON(스트리밍은 Anthropic SSE 이벤트 시퀀스)으로 변환됩니다. 도구 호출 왕복, 웹 검색 에뮬레이션, Unix 소켓 전송 모두 이 플래그에서 분기합니다.
두 경우 모두 dispatch는 클라이언트에 투명합니다. 요청과 응답 모양이 클라이언트가 호출한 surface와 일치하므로, responses_only 모델을 쓰기 위해 클라이언트 쪽을 바꿀 필요가 없습니다.
Chat Completions 브리지는 reasoning_effort, 평면 verbosity, metadata, safety_identifier, prompt_cache_key, prompt_cache_options, prompt_cache_retention 등 호환되는 GPT-5 제어를 보존합니다. 잘못된 값은 변환 과정에서 조용히 누락하지 않고 명시적으로 실패합니다. 응답 측면에서도 /v1/chat/completions에서 /v1/responses로의 브리지는 비스트리밍과 스트리밍 호출 모두에서 reasoning_content(추론 항목과 요약 델타에서 추출), refusal(finish_reason: "content_filter"), incomplete_details 잘림(finish_reason: "length" 또는 "content_filter"), 그리고 업스트림 failed 또는 cancelled 오류 봉투(envelope)를 보존합니다.
백엔드 유형 제약¶
/v1/responses는 OpenAI와 Azure OpenAI 백엔드에서만 제공됩니다. responses_only 모델이 OpenAI/Azure OpenAI가 아닌 백엔드와 짝지어지면, 라우터가 업스트림 dispatch 전에 400 invalid_request_error로 거절합니다(/anthropic/v1/messages에서는 Anthropic 모양, /v1/chat/completions에서는 OpenAI 모양). 메시지에 모델 이름과 설정된 백엔드 유형이 함께 들어가므로, 잘못된 설정이 클라이언트 로그에서 바로 보입니다.
(backend, model) 쌍별 첫 dispatch는 info 레벨로 로깅되어, 디버그 로그를 켜지 않고도 운영자가 Responses-API 라우팅 여부를 확인할 수 있습니다. 로그 라인과 responses_bridge_total 메트릭 모두 reason 필드를 가집니다. 이 플래그는 responses_only_model, 아래의 더 좁은 조건부 우회는 tools_with_reasoning입니다.
함수 도구 + reasoning 조합을 위한 더 좁은 플래그¶
responses_only는 모델에 동작하는 Chat Completions 경로가 아예 없을 때만 맞는 플래그입니다. 어떤 모델은 /v1/chat/completions로 정상 제공되면서 딱 한 가지 요청 형태, 즉 함수 tools와 none이 아닌 reasoning effort의 조합만 거절합니다. 이런 모델에 responses_only를 걸면 동작은 하지만 tools 없는 평범한 대화까지 포함해 그 모델의 모든 요청이 Responses API를 거치는 비용을 치릅니다.
대신 chat_completions_tools_require_none_reasoning을 사용하세요. 거절되는 요청만 우회하고 나머지는 Chat Completions에 남기며 영향을 받는 OpenAI 모델에는 이미 설정되어 출하됩니다. 백엔드 model_configs 항목이 그런 모델에 responses_only: true를 걸면 continuum-router config validate가 경고합니다. 모델 매트릭스와 플래그를 끄는 드롭인은 Reasoning Effort 문서의 함수 도구와 reasoning effort 조합 절을 참고하세요.
네임스페이스 인식 매칭¶
라우터는 네임스페이스 접두사가 있는 모델 ID를 지능적으로 처리합니다. 예:
- 백엔드 반환:
"custom/gpt-5.6-sol","openai/gpt-5.6-sol","optimized/gpt-5.6-sol" - 메타데이터 정의:
"gpt-5.6-sol" - 결과: 모든 변형이 일치하고 동일한 메타데이터 수신
따라서 다른 백엔드가 공통 메타데이터 정의를 공유하면서 자체 명명 규칙을 사용할 수 있습니다.
메타데이터 우선순위 및 별칭 해석¶
모델의 메타데이터를 조회할 때, 라우터는 다음 우선순위 체인을 사용합니다:
- 정확한 모델 ID 매칭
- 정확한 별칭 매칭
- 날짜 접미사 정규화 (자동, 설정 불필요)
- 양자화 / 형식 접미사 정규화 (자동, 설정 불필요, 아래 참고)
- 날짜 + 형식 접미사 결합 정규화
- 와일드카드 패턴 별칭 매칭
- 기본 모델 이름 폴백 (네임스페이스 제거)
각 소스(백엔드 설정, 메타데이터 파일, 내장) 내에서도 동일한 우선순위가 적용됩니다:
-
백엔드별
model_configs(최고 우선순위) -
외부 메타데이터 파일 (두 번째 우선순위)
-
내장 메타데이터 (OpenAI 및 Gemini 백엔드용)
자동 날짜 접미사 처리¶
LLM 프로바이더는 날짜 접미사가 있는 모델 버전을 자주 릴리스합니다. 라우터는 설정 없이 자동으로 날짜 접미사를 감지하고 정규화합니다:
지원되는 날짜 패턴:
-YYYYMMDD(예:claude-opus-4-5-20251130)-YYYY-MM-DD(예:gpt-4o-2024-08-06)-YYMM(예:o1-mini-2409)@YYYYMMDD(예:model@20251130)
작동 방식:
요청: claude-opus-4-5-20251215
↓ (날짜 접미사 감지됨)
조회: claude-opus-4-5-20251101 (기존 메타데이터 항목)
↓ (기본 이름 일치)
결과: claude-opus-4-5-20251101 메타데이터 사용
이는 모델 패밀리당 메타데이터를 한 번만 설정하면 되고 새로운 날짜 버전이 자동으로 메타데이터를 상속한다는 것을 의미합니다.
자동 양자화 및 형식 접미사 처리¶
실제 환경에서 /v1/models, 라우팅 로직, 백엔드 메타데이터 보강에 도착하는 모델 ID는 정규 기본 ID 뒤에 하나 이상의 양자화, 형식, 플레이버 토큰이 붙은 형태인 경우가 많습니다. 라우터는 허용 목록에 등록된 이런 토큰을 반복적으로 제거하고 각 peel 이후 정확한 ID, 정확한 별칭, 날짜 접미사 매칭을 다시 시도하므로, 메타데이터는 정규 기본 ID에 대해서만 설정하면 됩니다.
토큰 카테고리¶
다음 후행 토큰이 감지되어 제거됩니다 (대소문자 무시):
| 카테고리 | 예시 |
|---|---|
| 비트 폭 | -2bit, -3bit, -4bit, -5bit, -6bit, -8bit, -16bit |
| GGUF / llama.cpp 양자화 | -Q4_K_M, -Q4_K_S, -Q5_K_M, -Q6_K, -Q8_0, -Q2_K, -IQ2_XS, -IQ3_XXS, -IQ4_XS, -F16, -F32, -BF16 |
| FP 형식 | -FP4, -FP8, -FP16, -FP32, -NVFP4, -MXFP4, -MXFP8 |
| INT 형식 | -INT2, -INT4, -INT8 |
| 가중치/활성값 (compressed-tensors) | -W4A16, -W8A8, -W4A8, 그리고 quantized. 접두사가 붙은 -quantized.w4a16 |
| 스케일 단위 (compressed-tensors) | -block, -dynamic, -static. FP / INT / 가중치-활성값 토큰 뒤에 올 때만 제거됩니다 (-FP8-Block, -FP8-dynamic) |
| 라이브러리 태그 | -AWQ, -GPTQ, -BNB, -HQQ, -EXL2, -EXL3, -MLX |
| Imatrix / 축약형 | -i1부터 -i8, -q2부터 -q8 |
| Unsloth 동적 | -UD-Q*, -UD-IQ*, 그리고 재배포자 표식 -unsloth |
| 컨테이너 형식 | -GGUF, -GGML, -SAFETENSORS, -ONNX |
| 플레이버 | -it, -instruct, -chat, -base, -thinking, -qat |
| 하드웨어 / 가속기 | -rngd, -warboy (FuriosaAI), -atom, -atommax, -rebel (Rebellions) |
파라미터 수 접미사는 보존됩니다¶
파라미터 수처럼 보이는 토큰은 끝이 같은 b로 끝나더라도 절대 제거되지 않습니다:
- 유지:
-32b,-70b,-8b,-4b,-a3b,-a22b,-0.6b,-1.7b,-e4b - 제거:
-4bit,-8bit,-16bit(리터럴bit접미사가 양자화를 나타냄)
이 구분 덕분에 qwen3-32b 같은 파라미터 수 변형은 명시적인 qwen3-32b 메타데이터로만 해석되며 실수로 제거되어 일반 qwen3 항목으로 연결되는 일이 없습니다.
계층적 peel¶
토큰은 한 번에 하나씩 제거됩니다. 각 peel 이후 라우터는 다음 peel을 시도하기 전에 정확한 ID, 정확한 별칭, 날짜 접미사 매칭을 다시 실행합니다. 그래서 요청이 gemma-3-4b-it-qat-4bit이더라도 gemma-3-4b-it-qat 같은 별칭 설정이 여전히 승리할 수 있습니다:
요청: gemma-3-4b-it-qat-4bit
↓ (-4bit peel)
시도: gemma-3-4b-it-qat
↓ (gemma-3-4b-qat의 별칭과 매칭)
결과: gemma-3-4b-qat 메타데이터 사용
날짜 접미사로 끝나는 peel 결과는 날짜를 제거한 뒤 다시 peel 루프에 투입됩니다. 따라서 끝에 붙은 날짜 때문에 체인이 중단되지 않습니다:
요청: Ministral-3-14B-Instruct-2512
↓ (-2512 날짜 제거)
시도: ministral-3-14b-instruct
↓ (-instruct peel)
결과: ministral-3-14b 메타데이터 사용
우선순위 참고¶
제거는 정확한 ID 및 정확한 별칭 매칭 이후에 실행됩니다. 정규 기본 ID가 우연히 허용 목록 토큰으로 끝나더라도 (예: gemma-3-12b-qat) peel 단계가 실행되기 전에 승리하므로, 기존 설정은 안정적으로 유지됩니다.
접미사 순서 모호성¶
실제 모델 ID에는 -qat-4bit 순서와 -4bit-qat 순서가 모두 나타나며, peel은 오른쪽에서 토큰을 하나씩 제거합니다:
- Peel 매칭 순서:
gemma-3-12b-qat-4bit은gemma-3-12b-qat-4bit->gemma-3-12b-qat->gemma-3-12b순서로,gemma-3-12b-4bit-qat은gemma-3-12b-4bit-qat->gemma-3-12b-4bit->gemma-3-12b순서로 진행됩니다. - 공통 QAT 메타데이터 연결: 두 접미사 순서가 모두 동일한 QAT 메타데이터로 해석되어야 한다면 정규 QAT 기본 ID(
gemma-3-12b-qat)에 해당 메타데이터를 설정하고 비QAT 형태(gemma-3-12b)에는 별도 항목을 두세요(각 peel 깊이에서 가장 먼저 성공한 매칭이 우선합니다). - 변형별 개별 메타데이터 분리: QAT 변형과 비QAT 변형에 서로 다른 티어 또는 기능 메타데이터가 필요하다면 peel 순서에만 의존하지 말고 두 순서 조합을 명시적 별칭으로 등록하는 것이 좋습니다.
길이 제한¶
레이어드 peel 단계는 비정상적인 입력에 대한 심층 방어로 입력 길이를 256자, 반복 횟수를 8회 peel로 제한합니다. 매칭 자체는 계속 실행되지만 (정확한 ID 및 정확한 별칭 단계는 그대로 유효), peel 단계는 긴 허용 목록 토큰 체인을 따라가는 대신 조기에 중단됩니다. 요청 핸들러도 모든 채팅 / 완료 / 임베딩 엔드포인트의 model 필드에 동일한 256자 제한을 적용하므로, 정상적인 트래픽이 내부 제한에 걸리는 일은 없습니다.
대소문자 무시¶
제거는 대소문자를 구분하지 않으므로 Qwen3.5-4B-4bit, QWEN3.5-4B-4BIT, qwen3.5-4b-4bit가 모두 동일한 qwen3.5-4b 메타데이터 항목으로 해석됩니다. 정확한 ID 및 정확한 별칭 매칭 단계 (1단계와 2단계)는 여전히 대소문자를 구분하므로, BAAI/bge-m3 같은 HuggingFace 스타일 별칭은 기존 동작을 유지합니다.
와일드카드 패턴 매칭¶
별칭은 * 문자를 사용한 glob 스타일 와일드카드 패턴을 지원합니다:
- 접두사 매칭:
claude-*가claude-opus,claude-sonnet등과 매칭 - 접미사 매칭:
*-preview가gemini-3.1-pro-preview,o1-preview등과 매칭 - 중위 매칭:
gpt-*-turbo가gpt-4-turbo,gpt-3.5-turbo등과 매칭
와일드카드 패턴이 있는 설정 예:
models:
- id: "claude-opus-4-5-20251101"
aliases:
- "claude-opus-4-5" # 기본 이름의 정확한 매칭
- "claude-opus-*" # 모든 claude-opus 변형에 대한 와일드카드
metadata:
display_name: "Claude Opus 4.5"
# 자동 매칭: claude-opus-4-5-20251130, claude-opus-test 등
- id: "gpt-5.6-sol"
aliases:
- "gpt-5.6" # 카탈로그 별칭의 정확한 매칭
- "gpt-5.6-*" # 모든 GPT-5.6 변형에 대한 와일드카드
metadata:
display_name: "GPT-5.6 Sol"
우선순위 참고: 정확한 별칭은 항상 와일드카드 패턴보다 먼저 매칭되어 둘 다 매칭될 수 있는 경우에도 예측 가능한 동작을 보장합니다.
별칭 디스패치¶
메타데이터 해석은 어떤 항목이 가격, 기능, /v1/models 목록을 제공할지 결정하는 반면, 백엔드 선택은 요청된 모델 이름을 각 백엔드의 models: 목록 및 집계된 라이브 카탈로그와 문자 그대로 대조합니다. 클라이언트가 메타데이터 별칭으로 요청했으나 해당 이름을 문자 그대로 서빙하는 백엔드가 없을 때, 다음 네 조건이 모두 충족되면 라우터는 백엔드 선택 전에 요청의 model 필드를 정규(canonical) ID로 재작성합니다:
- 정확한 별칭 매칭: 요청된 이름(또는
models/접두사가 붙은 형태)이 활성 백엔드의model_configs나model-metadata.yaml에 다른 정규 ID의 정확한aliases항목으로 선언되어 있습니다. 퍼지 매칭 단계(날짜 접미사, 양자화 접미사 peel, HuggingFace 접두사, 와일드카드 별칭)는 디스패치 재작성에 사용되지 않습니다. - 모델 검색 완료:
models:목록이 요청된 ID를 정확한 항목 또는 해당 ID와 일치하는 glob 항목(*처럼 모든 이름과 일치하는 항목은 제외)으로 지정한 모든 활성 백엔드가 라이브 카탈로그에 최소 한 개 이상의 항목을 등록한 상태여야 합니다(초기 모델 검색이 아직 끝나지 않은 백엔드는 부재 근거로 삼지 않습니다). - 별칭 이름 직접 서빙 없음: 라이브 카탈로그(
GET /v1/models로 조회한 ID 목록, 또는 검색을 쓰지 않는 백엔드의 설정된 모델 이름)에서 요청된 이름을 문자 그대로 서빙하는 백엔드가 없습니다. - 정규 ID 서빙 가능: 사용자 라우팅이 가능한 백엔드 중 하나 이상이 라이브 카탈로그에서 정규 ID를 제공합니다.
네 조건이 모두 충족되면 라우터는 정규 ID로 요청을 디스패치합니다:
- 정규 ID 재작성:
model필드를 정규 ID로 바꾸고 요청 이름과 정규 이름을 함께 담아Dispatching a model alias as the canonical id its backend serves로그를info레벨로 기록합니다(예: Google이-previewID만 제공할 때gemini-3.1-pro요청을gemini-3.1-pro-preview로 변환). 업스트림 백엔드는 실제로 서빙한 ID로 응답하므로 응답의model필드에는 정규 ID가 담깁니다. - 리터럴 서빙 우선: 어느 백엔드든 요청된 이름을 문자 그대로 제공하면(예: 로컬 엔진이
unsloth/Qwen3.6-35B-A3B-GGUF를 직접 서빙하는 경우) 이름을 바꾸지 않으며, 어느 백엔드도 두 이름을 모두 제공할 수 없으면 원래 이름을 유지한 채model-not-found로 실패합니다.
참고: 카탈로그 항목이 하나도 없는 백엔드의
models:에openai/*같은 glob 항목이 있으면, 해당 항목과 일치하는 모든 이름이 유지되므로 그 이름에 대한 별칭 디스패치는 동작하지 않습니다. 정적models:목록을 쓰는 백엔드(모델 검색 비활성화)는 카탈로그에 glob 항목을 게시하지 않으므로, 목록이 이러한 항목만으로 이루어진 백엔드는 해당 이름을 계속 유지합니다. 모든 이름과 일치하는 항목(*또는models/*)은 여기에 해당하지 않습니다. 이 항목은 백엔드의 모델 검색이 아직 진행 중일 때도 이름을 유지시키지 않습니다. 더 좁은 glob이 일치하는 이름에 대해 별칭 디스패치를 유지하려면 정확한 모델 ID를 나열하거나 모델 검색으로 카탈로그를 채우세요.경고: 모든 이름과 일치하는 항목(
*또는models/*)은 요청을 해당 백엔드에 유지하지 않습니다. 메타데이터 별칭 요청은 정식 ID로 바뀌어 그 ID를 제공하는 백엔드로 전달되며, 이 백엔드는 외부 제공자일 수 있습니다. 별칭이 아닌 일반 모델 이름은 바뀌지 않습니다. 요청을 해당 백엔드에 유지해야 하면 이름을 정확히 나열하거나 더 좁은 glob을 사용하세요. 다른 백엔드가 활성화되어 있으면continuum-router config validate가 이 항목에 대해 경고합니다.
다른 기능과의 상호작용:
- 접근 제어 및 파라미터 정책: 키별
allowed_models목록과request_params.models범위는 클라이언트가 원래 요청한 모델 이름을 기준으로 판정하므로, 별칭을 허용한 API 키는 재작성 후에도 그대로 동작합니다. - 모델 목록: 활성화된 백엔드의
model_configs에 선언된aliases항목은 위 네 조건이 성립하는 동안GET /v1/models,/v1/models/extended,/v1/models/{model},/anthropic/v1/models에 별도 항목으로 게시되며, 정규 항목의 백엔드와 메타데이터를 그대로 가집니다. 자세한 내용은 모델 목록의 백엔드 별칭을 참고하세요.model-metadata.yaml의 별칭과model_aliases섹션은 목록에 나오지 않습니다. - 폴백 체인:
fallback.fallback_chains는 재작성된 정규 ID로 조회됩니다. 폴백 체인의 키는 별칭이 아니라 정규 ID로 설정하세요. 재작성 대상 별칭에 폴백 체인이 걸려 있으면 라우터가 요청 시점에 경고를 남깁니다. - 모델 고정 요청 제외: Hub 귀속 요청, Hub 정확 모델 예산 가드가 적용된 요청, AppProxy 인그레스 고정 요청은 이미 모델 결정이 완료된 상태이므로 재작성하지 않습니다.
- 적용 엔드포인트:
/v1/chat/completions,/anthropic/v1/messages,/anthropic/v1/messages/count_tokens,/v1/responses,/v1/responses/compact,/v1/embeddings(네이티브 Gemini 멀티모달 하위 경로 포함),/v1/images/generations,/v1/images/edits,/v1/images/variations, Realtime 핸드셰이크에 적용됩니다. ACP만 예외로 문자 그대로 매칭을 유지합니다(ACP 사용법 및 ACP 아키텍처 참고). - 오류 메시지 및 이미지 엔드포인트: 재작성 이후 발생하는
404나403오류는 라우터가 변환한 ID가 아니라 클라이언트가 요청한 원래 모델 이름을 반환합니다. 또한/v1/images/edits와/v1/images/variations는 허용된 모델 이름의 고정 목록을 재작성 전에 먼저 검사하므로, 별칭 자체가 그 목록에 포함되어 있을 때만 해당 엔드포인트에 도달합니다.
백엔드별 업스트림 모델 ID¶
백엔드가 클라이언트가 쓰는 ID와 다른 ID로 모델을 서빙할 수 있습니다. 해당 백엔드의 model_configs 항목에 upstream_id를 선언하세요. 공개 id는 라우팅 식별자로 남고, 그 백엔드로 보내는 요청에만 업스트림 표기가 실립니다. 이를 통해 하나의 공개 이름으로 모델 이름이 서로 다른 백엔드를 묶을 수 있습니다. 예를 들어 양자화된 체크포인트 ID를 쓰는 사내 vLLM 배포와 외부 OpenAI 호환 서비스를 함께 둘 수 있습니다.
backends:
- name: "in-house-vllm"
type: vllm
url: "http://localhost:8000"
model_configs:
- id: "gemma4"
upstream_id: "gemma-4-26b-a4b-it-fp8"
- name: "external"
type: openai
url: "https://api.example.com"
api_key: "${EXTERNAL_API_KEY}"
models: ["gemma4"]
- 라우팅 식별자: 백엔드 선택, 키별
allowed_models와request_params범위, 모델 목록, 폴백 체인 조회, 사용량 원장은 모두 공개id를 사용합니다.upstream_id를 설정하지 않은 백엔드는 계속 공개 ID를 받습니다. - 요청 재작성: 각 시도가 직렬화되기 직전에, 라우터는 요청을 받을 백엔드에 맞춰 본문의
model을upstream_id로 바꿉니다. 네이티브 Gemini 호출에서는models/{id}경로 조각도 함께 바꿉니다. 재시도와 폴백 홉은 요청이 도착하는 백엔드에 맞춰 다시 재작성합니다. - 응답 복원: 최상위
model이 업스트림 ID인 응답은 공개 ID로 되돌립니다. 버퍼링된 JSON 본문은 디스패치 지점에서, OpenAI 와이어 SSE 청크는 스트리밍 릴레이에서 복원하므로 Anthropic과 Responses 인그레스도 공개 ID를 봅니다. - 카탈로그: 백엔드의 라이브 모델 목록은 공개 ID로 기여되므로, 풀은 해당 이름에 대해 항목 하나를 게시합니다. 업스트림 ID는 목록에 나오지 않으며 라우팅 대상도 아닙니다. 어떤 백엔드도 그 이름을 문자 그대로 나열하지 않으면, 해당 이름으로 보낸 요청은 모델 없음(model-not-found)으로 실패합니다.
- 검증:
config validate는 비어 있는upstream_id와, 같은 백엔드의 다른model_configs[].id와 충돌하는 값을 거부합니다. 서로 다른 백엔드가 같은 공개 ID를 서로 다른 업스트림 ID로 매핑하는 것은 허용되며, 이것이 풀링 사례입니다.
참고:
upstream_id는 별칭이 아닙니다. 별칭은 클라이언트가 요청할 수 있는 대체 이름이고, 업스트림 ID는 해당 백엔드에만 전송되며 백엔드가models나model_configs로 공개하지 않는 한 요청 이름으로 허용되지 않습니다.
모델 변형에 별칭 사용¶
별칭은 특히 다음에 유용합니다:
- 다른 양자화:
qwen3-32b-i1,qwen3-23b-i4→ 모두qwen3메타데이터 사용 - 버전 변형:
gpt-4-0125-preview,gpt-4-turbo→gpt-4메타데이터 공유 - 배포 변형:
llama-3-70b-instruct,llama-3-70b-chat→ 동일 기본 모델 - 날짜 버전:
claude-3-5-sonnet-20241022,claude-3-5-sonnet-20241201→ 메타데이터 공유 (날짜 접미사 처리로 자동)
별칭이 있는 설정 예:
model_configs:
- id: "qwen3"
aliases:
- "qwen3-32b-i1" # 1비트 양자화된 32B
- "qwen3-23b-i4" # 4비트 양자화된 23B
- "qwen3-16b-q8" # 8비트 양자화된 16B
- "qwen3-*" # 다른 모든 qwen3 변형에 대한 와일드카드
metadata:
display_name: "Qwen 3"
summary: "Alibaba의 Qwen 모델 계열"
# ... 나머지 메타데이터
별칭과 접미사 정규화: 선택 기준¶
비정규 모델 id를 해당 메타데이터 항목에 연결하는 두 가지 레이어가 있습니다. 명시적 YAML 별칭과 src/models/pattern_matching.rs의 레이어드 접미사 peel 허용 목록이 그것입니다. 이 둘은 상호 보완적이며 중복이 아닙니다. 이 섹션에서는 새 항목을 추가할 때 어느 쪽을 선택해야 하는지 설명합니다.
매칭 단계 순서¶
파이프라인은 다음 순서로 실행되며 앞 단계에서 매칭이 성공하면 이후 단계는 건너뜁니다.
- 정확한 모델 id (대소문자 구분).
- 정확한 별칭 (대소문자 구분).
- 날짜 접미사 정규화 (
-YYYYMMDD,-YYYY-MM-DD,-YYMM,@YYYYMMDD). - 레이어드 양자화 / 형식 / 플레이버 peel (대소문자 무시; 각 peel 이후 정확한 id + 정확한 별칭 + 날짜 접미사 단계를 재실행; 날짜 + 형식 결합도 같은 루프에서 처리).
- HuggingFace 리포지토리 접두사 제거 (
vendor/repo->repo). 제거된 잔류 문자열로 1-4단계에 한 번만 재진입하며 재귀는 구조적으로 1회로 제한. - 와일드카드 별칭 (glob 스타일
*패턴).
명시적 별칭은 2단계에서 실행되어 peel(4단계)과 접두사 제거(5단계)보다 엄격하게 앞섭니다. 이 순서가 중요한 이유는 보존된 별칭과 peel-or-strip 경로가 서로 다른 메타데이터를 가리킬 때 별칭이 결정론적으로 우선하기 때문입니다. 별칭은 peel-or-strip 커버리지보다 약한 신호가 아니라 더 강한 의도 신호입니다.
세 가지 별칭 클래스¶
model-metadata.yaml의 모든 별칭은 세 클래스 중 하나에 속합니다.
peel-coverable¶
별칭 없이도 정규화가 동일한 소유자 id에 도달하고 대상 메타데이터가 올바른 경우입니다. 삭제 후보입니다. 예시: qwen3.6-35b-a3b의 별칭인 qwen3.6-35b-a3b-instruct. 4단계에서 FLAVOR 토큰 -instruct를 peel하면 바로 기본 id에 도달하므로, 명시적 별칭이 커버리지를 추가하지 않습니다. 이 클래스의 별칭을 삭제할 때는 tests/format_suffix_normalization_test.rs::real_metadata_removed_aliases_still_resolve에 회귀 어서션을 추가해서, 별칭을 대신하는 peel 커버리지를 고정해 두세요.
vendor-prefix¶
별칭이 접미사 peel로는 제거할 수 없는 벤더 또는 리포지토리 접두사를 포함하는 경우입니다. 5단계(HuggingFace 접두사 제거 + 1-4단계 재진입, 그중 4단계는 대소문자를 구분하지 않음)가 있어서 대소문자가 혼합된 HF 형식도 별칭 없이 해결됩니다. 예시: qwen3.6-35b-a3b의 별칭인 Qwen/Qwen3.6-35B-A3B. 명시적 별칭이 2단계에서 먼저 승리하지만 5단계도 Qwen/ -> 잔류 Qwen3.6-35B-A3B -> 4단계 대소문자 무시 매칭으로 기본 id에 도달합니다. 이들 별칭은 peel-coverable-adjacent 클래스입니다. 해석에는 5단계만으로 충분해 중복이지만 결정론적인 의도 표시 역할은 남아 있으니 커버하는 단계를 YAML 주석에 기록한 채 유지하세요.
intentional-override¶
별칭이 운영자 결정으로 다른 가중치를 가진 모델을 다른 항목의 메타데이터 아래 의도적으로 라우팅하는 경우입니다. 유지해야 합니다. 예시: smoothie-qwen3의 별칭인 smoothie-qwen3-32b-i1. smoothie-qwen3-32b-i1 파인튜닝 모델은 자체 가중치를 가지고 있으며 운영자가 전용 항목을 만드는 대신 smoothie-qwen3 메타데이터 아래 노출하기로 선택한 것입니다. peel이 이 동등성을 자체적으로 추론해서는 안 됩니다. 이 클래스에 속하는 별칭은 YAML 주석에 소유자 id와 가중치가 다름을 명시해야 합니다.
새 별칭 추가 지침¶
model-metadata.yaml에 줄을 추가하기 전에, peel이 이미 커버하는지 확인하세요.
- 새 id가 허용 목록에 있는 후행 양자화, 형식, 플레이버 토큰이 붙은 정규 기본 형태이고 가중치가 기본 메타데이터와 동일하면, 별칭을 추가하지 마세요. peel이 처리하며 별칭은 데드 코드가 됩니다.
- 새 id가 기본과 동일한 가중치를 공유하지만 peel이 처리하지 않는 토큰 클래스로 끝나는 경우 (예:
-abliterated같은 새로운 파인튜닝 레이블이나-nf4같은 새 양자화 형식),src/models/pattern_matching.rs의 peel 허용 목록을 확장하는 것을 우선 고려하세요. 이는 테스트 커버리지가 있는 코드 변경이며 한 번의 작업으로 해당 토큰 클래스 전체에 적용됩니다. - 새 id에 벤더 접두사, 대소문자가 맞지 않는 리포지토리 네임스페이스, peel 체인을 막는 파라미터 수 토큰 (
-Nb,-aNb,-eNb), 또는 의도적으로 다른 가중치가 있는 경우, 이유를 명시한 YAML 주석과 함께 별칭을 추가하세요. 가중치가 소유자 id와 다르다면 주석에 그 사실을 명시하세요.
표면 구분: 코드 게이트 vs. YAML 게이트¶
| 변경 위치 | 게이트 | 릴리스 주기 | 사용 목적 |
|---|---|---|---|
src/models/pattern_matching.rs의 peel 허용 목록 | 코드 리뷰 + Rust 릴리스 | 다음 라우터 릴리스에 포함 | 모든 모델에 걸쳐 전체 토큰 클래스를 커버하는 전략적 정규화. |
model-metadata.yaml의 별칭 | YAML 리뷰 + 핫 리로드 | 관리자 API를 통해 당일 리로드 | 개별 오버라이드, 벤더 접두사 수정, 가중치 차이 오버라이드, 새 토큰이 peel 허용 목록 항목을 얻기 전의 임시 커버리지. |
peel 허용 목록은 전략적 레이어이고 별칭은 전술적 오버라이드 및 긴급 채널입니다.
peel 허용 목록에 있는 토큰 카테고리¶
src/models/pattern_matching.rs의 허용 목록은 현재 다음을 커버합니다.
BIT_WIDTH:-2bit,-3bit,-4bit,-5bit,-6bit,-8bit,-16bitGGUF_QUANT:-Q4_K_M,-Q4_K_S,-Q5_K_M,-Q6_K,-Q8_0,-Q2_K,-IQ2_XS,-IQ3_XXS,-IQ4_XS,-F16,-F32,-BF16FP_FORMAT:-FP4,-FP8,-FP16,-FP32,-NVFP4,-MXFP4,-MXFP8INT_FORMAT:-INT2,-INT4,-INT8WEIGHT_ACTIVATION:-W<bits>A<bits>(-W4A16,-W8A8,-W4A8).quantized.접두사 형태(-quantized.w4a16)도 인식합니다SCALING_GRANULARITY:-block,-dynamic,-static. FP / INT / 가중치-활성값 토큰 뒤에서만 제거됩니다LIBRARY:-AWQ,-GPTQ,-BNB,-HQQ,-EXL2,-EXL3,-MLXIMATRIX:-i1부터-i8,-q2부터-q8UNSLOTH:-UD-Q<digit>_<KIND>,-UD-IQ<digit>_<KIND>,-unslothCONTAINER:-GGUF,-GGML,-SAFETENSORS,-ONNXFLAVOR:-it,-instruct,-chat,-base,-thinking,-qatHARDWARE:-rngd,-warboy,-atom,-atommax,-rebel
동작이 달라지는 파생 모델을 나타내는 토큰은 의도적으로 제외했습니다. peel한 기본 id가 다른 논리적 모델이 되기 때문입니다: -abliterated, -heretic, -uncensored, -distilled, -REAP-<params>, -DFlash / -DSpark, -speculator.eagle3, -MTP, -assistant.
파라미터 수 접미사(-Nb, -aNb, -eNb, -0.6b, -1.7b)는 peel되지 않습니다. 이는 정규 모델 식별자의 일부이며 peel 체인을 종료시킵니다. 이 때문에 qwen3-32b-i1은 qwen3의 명시적 별칭으로 유지되어야 합니다. 4단계에서 -i1을 peel한 뒤 -32b에서 멈추기 때문에, 별칭이 없으면 체인이 기본 id에 도달하지 못하고 소진됩니다.
HuggingFace 리포지토리 접두사 제거 (5단계)¶
5단계는 모델 id의 왼쪽에서 HuggingFace 스타일 vendor/repo 접두사를 정규화해서 오른쪽 접미사 peel을 보완합니다. 사용자가 unsloth/Qwen3.6-35B-A3B-GGUF 같은 id를 제출할 때 매번 별칭을 등록하지 않고도 정규 qwen3.6-35b-a3b 메타데이터로 라우팅되게 하는 단계입니다. 즉, vendor x base x quant 조합마다 별칭을 수동으로 추가할 필요가 없습니다.
5단계 동작 방식¶
- 입력에
/구분자가 있는지 확인합니다. 없으면 no-op입니다. - 전체 세그먼트 수(
/개수 + 1)가MAX_PREFIX_SEGMENTS(3) 이하여야 합니다.org/team/repo는 허용되고a/b/c/d/model은 즉시 거부됩니다. - 모든 세그먼트는 비어있지 않아야 하며 ASCII 공백 문자를 포함하지 않아야 합니다.
/repo,vendor/,vendor//repo,vendor /repo같은 잘못된 입력은 거부됩니다. - 성공 시 마지막
/이후의 부분 문자열이 잔류 문자열입니다. 이 잔류 문자열이 재진입 게이트가 닫힌 상태로 1-4단계에 다시 투입됩니다. 5단계는 재귀하지 않고 내부 호출은 다시 5단계를 트리거할 수 없으므로 재귀 깊이는 구조적으로 정확히 1입니다.
접미사 peel과의 조합¶
재진입은 4단계를 거치므로 접두사 제거는 단일 조회 내에서 접미사 peel과 조합됩니다. unsloth/Qwen3.6-35B-A3B-GGUF는 Qwen3.6-35B-A3B-GGUF로 제거되고 4단계가 -GGUF를 peel한 뒤 대소문자를 무시하고 qwen3.6-35b-a3b에 매칭됩니다. 이 단계의 핵심 사용 사례이며 수동으로 등록된 별칭 없이 HuggingFace GGUF 포크를 커버합니다.
등록된 별칭 우선 순위¶
운영자가 vendor/repo 형식을 명시적으로 YAML 별칭으로 등록하면 결정론적인 제어권이 유지됩니다. 2단계가 5단계보다 먼저 실행되므로, 정확한 별칭은 접두사 제거 레이어가 입력을 고려하기도 전에 승리합니다. 접두사 형식이 정규 기본 id와는 다른 메타데이터 항목으로 라우팅되어야 할 때 이 기법을 사용하세요.
범위 밖¶
- 하이픈으로 구분된 벤더 접두사 (예:
smoothie-qwen/smoothie-qwen3-32b-i1). 다른 의미 클래스이고 탐지 난이도도 다르며 종종 다른 가중치를 의미하기 때문에 기본 메타데이터로의 조용한 라우팅이 잘못된 결정일 수 있습니다. - HuggingFace API를 통한 자동 벤더 발견. 이 레이어는 순수하게 구문적입니다.
- 접미사 peel 허용 목록 확장. 직교적인 변경입니다. 새 토큰 클래스에 대해서는 peel 확장 경로를 따르세요.
보안 경계¶
접미사 peel과 동일한 구조입니다.
| 경계 | 값 | 효과 |
|---|---|---|
MAX_PREFIX_SEGMENTS | 3 | 세그먼트가 더 많은 입력은 스캔 전에 거부됩니다. |
MAX_MODEL_ID_LEN | 256 | 과도하게 긴 입력은 4단계와 마찬가지로 5단계를 건너뜁니다. |
| 재진입 깊이 | 1 | 카운터가 아니라 재귀 게이트로 구조적으로 강제됩니다. |
5단계는 적대적 입력에 대해 상수 시간입니다. 세그먼트 수, 빈 세그먼트, 공백, 길이 가드를 통과한 뒤에는 작업이 단일 슬라이스 조회 + 1-4단계 한 번의 추가 순회로 축소됩니다.
감사 절차¶
YAML을 재감사하려면 다음을 실행하세요.
헬퍼는 각 별칭을 분류(REDUNDANT, LOAD-BEARING-DRIFT, LOAD-BEARING-LOSS, 또는 WILDCARD)와 제거 후 결정 대상과 함께 출력합니다.
API 응답¶
/v1/models 엔드포인트는 풍부한 모델 정보를 반환합니다:
{
"object": "list",
"data": [
{
"id": "gpt-5.6-sol",
"object": "model",
"created": 1234567890,
"owned_by": "openai",
"backends": ["openai-proxy"],
"metadata": {
"display_name": "GPT-5.6 Sol",
"summary": "복잡한 전문 작업을 위한 프런티어 GPT-5.6 모델",
"capabilities": ["chat", "vision", "code", "reasoning", "tool"],
"knowledge_cutoff": "2026-02",
"pricing": {
"input_tokens": 5.0,
"output_tokens": 30.0
},
"limits": {
"context_window": 1050000,
"max_output": 128000
}
}
}
]
}
요청 파라미터 정책¶
선택 사항인 request_params 섹션은 요청 파라미터 정책의 typed 기본값, 운영자 override, 제한 계약을 정의합니다. 라우터는 이 설정 스냅샷을 검증하고 핫 리로드한 뒤 Chat Completions, Completions, Responses, Anthropic Messages 진입점에서 캐시 identity나 프로바이더 라우팅을 정하기 전에 한 번 적용합니다.
request_params:
defaults:
max_tokens: 1024
overrides:
temperature: 0.0
limits:
max_tokens:
min: 1
max: 4096
models:
gpt-5:
defaults:
top_p: 0.9
limits:
max_tokens:
max: 2048
관리되는 예제는 블록 스타일 YAML을 사용합니다. { max_tokens: 1024 } 같은 동등한 플로 매핑도 허용되며 TOML 설정도 같은 의미를 가집니다.
적용 순서¶
프로토콜 통합은 세 모드를 정해진 순서로 적용합니다.
defaults는 누락되었거나 JSON/YAML null인 필드를 채우고 클라이언트가 제공한 값은 보존합니다.overrides는 클라이언트 값이나 기본값에서 만들어진 결과를 항상 대체합니다.limits는 존재하는 값을 포괄적min및max경계 안으로 제한합니다. 제한만으로 누락된 필드를 만들지는 않습니다.
최상위 defaults, overrides, limits 블록은 전역 정책을 정의하며, 각 models.<name> 항목은 특정 모델에 대해 동일한 세 블록을 정의할 수 있습니다:
- 모델별 필드는 각 모드 안에서 전역 필드를 개별적으로 덮어쓰며, 생략된 필드는 전역 값이나 범위를 상속합니다.
- 모델 이름은 로컬 별칭 해석 후의 모델 ID와 정확히 일치해야 합니다(백엔드 범위, 정규식, 글로브 패턴은 지원하지 않습니다).
- 선택된 범위는 컨트롤 플레인 모델 치환, 차익 라우팅(arbitrage), 백엔드 선택, 재시도, 폴백보다 먼저 확정되며, 이후 재시도와 폴백 홉에서도 그대로 유지됩니다.
control_plane.policy를 통해 전달된 Hub 티어 제한은limits전용이며 각 경계에서 로컬 제한과 교차 적용됩니다(어느 쪽도 상대방의 제한을 넓힐 수 없으며, 교집합이 비어 있으면 요청을 거부합니다).
지원 파라미터 및 유효성 검사¶
다음 canonical 파라미터 이름만 허용됩니다.
| 파라미터 | 타입 | 허용 정책 범위 |
|---|---|---|
temperature | 실수 | 0 이상 2 이하 |
top_p | 실수 | 0 이상 1 이하 |
max_tokens | 부호 없는 정수 | 1 이상 1,000,000 이하 |
presence_penalty | 실수 | -2 이상 2 이하 |
frequency_penalty | 실수 | -2 이상 2 이하 |
top_k | 부호 없는 정수 | 1 이상 1,000,000 이하 |
min_p | 실수 | 0 이상 1 이하 |
알 수 없는 모드나 파라미터 이름, 잘못된 스칼라 타입, 유한하지 않은 실수, 범위를 벗어난 값, min > max는 경로가 포함된 오류로 거부됩니다. models에는 정확히 일치하는 범위를 최대 64개까지 둘 수 있으며, 각 모델 키는 앞뒤 공백 제거 후 비어 있지 않고 256바이트 이하여야 하며 제어 문자를 포함할 수 없습니다. 적용 순서가 결정적이므로 같은 파라미터를 여러 모드에 지정해도 됩니다. 정책의 null 값은 생략으로 취급하며 JSON null을 강제로 주입하지 않습니다.
런타임 프로바이더 호환성¶
호환성 검사는 정책에 설정되어 실제로 변경되거나 제한된 필드에만 적용됩니다. 정책이 관리하지 않는 클라이언트 필드는 기존 프로바이더별 passthrough 동작을 유지합니다. 적격 기본 백엔드나 설정된 폴백 경로가 정책 적용 필드를 보존할 수 없으면, 라우터는 값을 조용히 버리는 대신 캐시 조회나 업스트림 I/O 전에 unsupported_request_parameter 오류로 요청을 거부합니다.
| 진입점과 선택 경로 | 보존 가능한 정책 적용 필드 | 거부되는 정책 적용 필드 또는 조건 |
|---|---|---|
| Chat Completions → 로컬 OpenAI 호환 백엔드 | canonical 필드 7개 전부 | 이 정책 표면에서는 없음 |
| Chat Completions → cloud OpenAI 또는 Gemini | temperature, top_p, max_tokens, presence_penalty, frequency_penalty | top_k, min_p |
| Chat Completions → Anthropic, Mantle 또는 Anthropic 호환 runtime 변환 | temperature, top_p, max_tokens, top_k | penalty 두 개와 min_p; 모델이나 reasoning 모드가 sampling 필드를 추가로 제한할 수 있음 |
| Chat Completions → Bedrock Converse | temperature, top_p, max_tokens | top_k, min_p, penalty 두 개 |
| Legacy Completions | OpenAI 호환 raw 경로는 canonical 필드를 보존 | 이 진입점에는 손실 없는 native 변환이 없으므로 native Anthropic, Gemini, Bedrock 경로는 정책 적용 필드를 거부 |
| Responses passthrough 또는 Chat bridge | temperature, top_p, max_tokens, penalty 두 개 | top_k, min_p; 프로바이더별 모델 또는 reasoning 제한은 계속 적용 |
| Responses → Anthropic 또는 Gemini 변환 | temperature, top_p, max_tokens | top_k, min_p, penalty 두 개 |
| Native Anthropic Messages | 선택 전에 모든 적격 백엔드의 capability를 해석 | 그 경로 중 하나라도 지원하지 않는 필드는 선택 전에 거부 |
네이티브 Anthropic Messages 인그레스에서 네이티브 Anthropic 백엔드는 클라이언트의 anthropic-version을 받으며, 클라이언트가 보내지 않으면 백엔드의 anthropic_version(기본값 2023-06-01)을 받습니다. Chat Completions와 Responses 변환은 항상 백엔드의 anthropic_version을 보냅니다(백엔드 가이드의 "Anthropic API 버전" 참고). 2026-10-21 이상에서는 Anthropic이 모든 모델에서 temperature, top_p, top_k를 거부하므로, 이런 백엔드에서는 세 필드를 지원하지 않는 필드로 취급합니다:
- 정책 쓰기: 이 필드의 기본값이나 덮어쓰기 값을 지정하는 정책은 업스트림 I/O 전에 거부됩니다. 오류 메시지에
anthropic-version원인이 표시됩니다. - 값이 없는 필드의 제한: 요청에 없는 필드에 대한 제한은 아무것도 바꾸지 않습니다. 네이티브 Messages의 모든 후보 백엔드가 클라이언트 샘플링 값을 제거하면, 제한 정책도 해당 필드가 없는 것으로 처리합니다. 서로 다른 제공자가 후보에 섞여 있으면 기존 호환성 검사를 유지합니다.
- Fallback 정책: 정책이 기록한 샘플링 값을 버리거나, 기본 홉에서 제한 정책의 평가 대상에서 제외된 클라이언트 값을 전달하는 홉은 거부합니다.
- 클라이언트 값: 네이티브 Messages, Chat Completions, Responses는 샘플링을 지원하지 않는 Anthropic 홉에서 클라이언트 샘플링 필드를 제거합니다. 네이티브 Messages는 원래 값을 보존하므로, 이후 해당 값을 지원하는 제공자 홉에는 그대로 전달합니다.
- 설정 점검:
2026-10-21백엔드가2023-06-01에서 샘플링을 받는 모델(Claude Opus 및 Sonnet 4.7 미만, Haiku 4.5)을 서빙하고 이 섹션이 그 모델에 샘플링 필드를 지정하면,continuum-router config validate가backends[N].anthropic_version경로로 경고합니다.
스트리밍 폴백 및 캐시 격리:
- 프리스트림 폴백: 백엔드별 타입 디스패치로 다시 진입하므로, 네이티브 Anthropic, Bedrock, Gemini 백엔드로 향하는 홉은 디스패치 시점에 와이어 형식을 한 번만 변환하여 해당 프로바이더의 네이티브 파이프라인으로 처리됩니다.
- 미드스트림 폴백: SSE 출력이 시작된 뒤의 복구는 OpenAI 호환 와이어 형식만 사용합니다. 네이티브 프로토콜이나 Unix 소켓 백엔드로 해석되는 체인 항목은 불완전하게 변환된 요청을 보내지 않고 건너뛰어 다음 항목을 시도합니다. 각 홉은 정규 유효 요청(canonical effective request)에서 다시 생성되므로, 다단계 폴백을 거치더라도 변환이 누적되거나 복원된 필드가 유실되지 않습니다.
- 응답 캐시 격리: 응답 캐시 조회와 저장은 라우팅 결과가 정책 호환 백엔드 하나로 확정되고 폴백 체인이 목적지를 바꿀 수 없을 때만 활성화되며, 캐시 네임스페이스에 백엔드 식별자가 포함됩니다. 적격 백엔드가 여러 개이거나 폴백 체인이 활성화된 요청은 exact, prefix, streaming 캐시를 모두 우회합니다.
관측 및 강제 적용¶
정책이 요청 필드를 하나 이상 변경하고 선택된 프로바이더 경로가 유효 요청을 보존할 수 있으면, 라우터는 정보 제공용 x-continuum-request-params 응답 헤더를 추가합니다. 쉼표로 구분된 토큰에는 default:max_tokens, limit:temperature처럼 정해진 동작과 canonical 파라미터 이름만 들어가며, 요청 값, 모델 이름, API 키, 티어 식별자는 절대 포함되지 않습니다.
이 정책은 운영자가 강제하는 설정이므로, 클라이언트가 요청 헤더, 쿼리 파라미터, 요청 본문 옵션으로 정책을 끄거나 우회할 수 없습니다. 응답 헤더 역시 옵트아웃 수단이나 인증 자격 증명이 아닙니다.
유효한 변경이 적용된 뒤 생성된 성공 응답, 호환 캐시 적중, 업스트림 오류 응답에는 이 헤더가 포함됩니다. 정책이 아무 필드도 바꾸지 않았거나 호환성 검사에서 거부된 요청에는 헤더가 생략되며, 거부된 트랜잭션은 롤백됩니다. 로그와 Prometheus 메트릭도 동일한 applied/rejected 결과를 기록합니다.
핫 리로드, 설정 동기화 우선순위 및 개인정보 보호¶
request_params는 서버 재시작 없이 즉시 핫 리로드됩니다. control-plane 빌드에서 control_plane.config_sync.enabled: true를 켜면, Continuum Hub가 request_params와 함께 라우팅 및 폴백 설정도 동기화할 수 있습니다:
- Hub 관리 대상 필드:
request_params의 기본값, 오버라이드, 제한, 모델별 범위.- 공개
kv_routing하위 필드:selection_strategy,prefix_routing.{enabled,max_prefix_length,load_factor_epsilon,virtual_nodes,anthropic_cache_control_injection}, 그리고transport: sse소스의kv_cache_index.{enabled,max_entries,entry_ttl_seconds,scoring.*,storage_offloading.*,event_sources.<backend>.{endpoint,reconnect_interval_ms}}. - 모델
fallback의 스위치,fallback_chains,fallback_policy,model_settings,empty_reply_retry.
- 배포 로컬 전용 필드 (Hub가 관측하거나 덮어쓰지 않음):
prefix_routing.salt_echo,kv_cache_index.backend, 그리고transport: event_bus인kv_cache_index.event_sources항목.config.yaml이나 Admin API로 설정한 로컬 값은 관리 대상 필드를 고정하지 않아도 Hub 설정 동기화, 리베이스, 재시작 이후 그대로 유지됩니다.fallback.quality_check는 항상 로컬 전용으로 남으며, Fleet 관측 및 다이제스트에서 제외됩니다.
- 관측되는 필드:
fallback.fallback_policy.trigger_conditions.context_window_exceeded는 관측되며 다이제스트에 반영됩니다. 이 필드는 배포 로컬 전용이 아니므로, 관측 경로를 등록한 Hub가 게시할 수 있습니다. 이 라우터 릴리스는 그런 Hub를 요구합니다. 알 수 없는 경로를 받은 이전 Hub는 하트비트 전체를 거부하며, 보고를 멈추면 Hub의 desired 다이제스트와 observed 다이제스트가 어긋난 채 영구 드리프트로 남기 때문입니다. - 아직 관측되지 않는 필드:
fallback.empty_reply_retry는 적용되지만 관측되지 않으며 다이제스트에도 반영되지 않습니다. 이 필드는 배포 로컬 전용이 아니므로, Hub가 나중에 게시할 수 있습니다. - 로컬 고정(Pinning) 우선순위:
- 섹션 전체를 로컬에 고정하려면
control_plane.config_sync아래에request_params_immutable,kv_routing_immutable,fallback_immutable스위치를 설정하세요. - 로컬
fallback블록이 존재하면 폴백 스위치와fallback_policy전체가 한 단위로 고정되며, 로컬 체인이나model_settings항목이 있으면 해당 컬렉션 전체가 고정됩니다.
- 섹션 전체를 로컬에 고정하려면
- 원자적 리로드 및 진행 중 요청 격리:
- 파싱과 유효성 검사를 통과한 새 리비전은 활성 설정 스냅샷을 원자적으로 교체하고, 다음 요청 전에 폴백 서비스의 활성화, 비활성화, 초기화, 다이얼 상한(dial bound) 변경을 반영합니다.
- 거부된 리비전은 직전의 정상 스냅샷을 그대로 유지합니다.
- 각 요청은 필드를 변경하기 전에 불변 설정 스냅샷 하나와 인증된 Hub 티어 정책 하나를 캡처하므로, 진행 중인 요청·재시도·폴백 도중에 정책이 바뀌지 않습니다.
- 개인정보 보호: 정책 텔레메트리는 정해진 파라미터 이름, 동작, 조합 출처, 결과만 노출하며 요청 값은 기록하지 않습니다. Admin 설정 조회 및 MCP 화면에는 운영자가 작성한 정책이 표시될 수 있으나, 프롬프트 본문이나 임의의 요청 데이터는 직렬화하지 않으며 비밀(secret) 필드는 항상 마스킹합니다.
Responses 도구 정책¶
호스티드 도구는 백엔드에서 실행되고 비용은 운영자의 업스트림 키로 청구됩니다. 자체 호스팅 백엔드에서는 내부 네트워크에 접근할 수도 있습니다. 이런 도구는 클라이언트가 요청마다 고르므로, POST /v1/responses가 통과시킬 유형은 responses_tools로 정합니다. 이 정책은 모든 백엔드 유형에 적용되며 백엔드 선택과 캐시 조회보다 먼저 실행됩니다.
responses_tools:
unmodeled_types: deny # allow | deny
modeled_hosted_types: allow # allow | deny
allow: [image_generation] # exact wire types, overrides the two defaults
deny: [web_search] # wire types in any ASCII case, overrides the two defaults
deny_resource_fields: [vector_store_ids, container_ids] # default
각 도구는 type 문자열로 다음 순서에 따라 판단합니다.
function,custom,local_shell,namespace,execution: "client"인tool_search는 클라이언트에서 실행되므로 항상 허용합니다.namespace는 컨테이너일 뿐이므로 멤버마다 깊이와 상관없이 같은 규칙으로 판단합니다.deny에 있는 유형은 거부합니다.allow에 있는 유형은 전달합니다.web_search,file_search,code_interpreter,computer_use,programmatic_tool_calling은modeled_hosted_types를 따릅니다.mcp,image_generation, 다른execution값의tool_search같은 나머지 유형은unmodeled_types를 따릅니다.
deny에 tool_search를 넣어도 백엔드에서 실행되는 형태만 거부합니다. execution: "client"인 tool_search는 1번 규칙에 따라 계속 허용됩니다. deny는 ASCII 대소문자를 구분하지 않으므로 deny: [mcp]는 MCP도 거부합니다. allow와 4번 규칙의 목록은 정확히 일치해야 하므로, 대소문자 변형은 거부될 수는 있어도 허용되지는 않습니다. 같은 순회가 전달되는 도구가 선언한 모든 중첩 목록을 판단합니다. tool_search의 tools 목록, programmatic_tool_calling의 tools 목록, 원시 tool_choice가 담은 tools(allowed_tools 등)가 여기에 해당합니다. tools 값이 배열이 아니면 tools[unknown]으로 거부합니다.
거부된 도구는 tools[mcp]나 tools[mcp] member of namespace 'ns'처럼 도구를 명시한 400 unsupported_request_parameter를 받습니다. 라우터는 업스트림 요청을 보내지 않으며, 해당 도구만 빼고 나머지를 전달하지도 않습니다. 읽을 수 없는 중첩 네임스페이스는 tools[unknown]으로 거부합니다.
거부 시 프롬프트 로깅이 해당 요청을 수집하면 refused 레코드를 기록하며, 로그 줄에는 키 자체가 아니라 호출자의 키 ID만 남습니다.
항목은 출력 가능한 ASCII여야 합니다. 공백, 제어 문자, 너비 없는 공백이나 소프트 하이픈처럼 보이지 않는 유사 문자를 포함한 항목은 로드 오류입니다. 절대 일치할 수 없는 이름을 조용히 무시하지 않고 거부한다는 뜻입니다. 빈 항목, 두 목록에 모두 있는 항목, 항상 허용되는 유형을 지정한 항목도 로드 오류입니다. 마지막 두 가지는 ASCII 대소문자를 구분하지 않고 비교합니다. 변경 사항은 재시작 없이 다음 요청부터 적용됩니다.
클라이언트가 보낸 리소스 ID¶
여러 테넌트가 업스트림 키 하나를 공유하는 라우터에서는 리소스 ID를 가진 클라이언트가 운영자의 키로 해당 리소스에 접근할 수 있습니다. deny_resource_fields는 이런 참조를 tools[file_search].vector_store_ids처럼 필드를 명시한 400 unsupported_request_parameter로 거부하며, 업스트림 요청은 보내지 않습니다.
| 항목 | 거부 대상 | 기본값 | 적용 범위 |
|---|---|---|---|
vector_store_ids | 비어 있지 않은 vector_store_ids가 있는 file_search | 포함 | 모든 백엔드 |
container_ids | container가 file_ids 없는 {"type": "auto"}가 아닌 code_interpreter | 포함 | 모든 백엔드 |
previous_response_id | previous_response_id 필드 | 미포함 | 패스스루만 |
conversation | conversation 필드 | 미포함 | 패스스루만 |
prompt | prompt 필드 | 미포함 | 패스스루만 |
item_reference | item_reference 입력 항목. POST /v1/responses/compact에도 적용 | 미포함 | 패스스루만 |
input_item_resource_ids | container_id, file_id, vector_store_ids, previous_response_id, conversation 중 하나를 모든 깊이에서 담은 입력 항목. POST /v1/responses/compact에도 적용 | 미포함 | 패스스루만 |
기본 항목 두 개는 네임스페이스 멤버와 tool_search, programmatic_tool_calling, 원시 tool_choice의 tools 목록에도 적용합니다. 네이티브 file_search나 이름을 지정한 컨테이너를 쓰는 단일 테넌트 라우터는 deny_resource_fields: []를 설정해야 합니다. 알 수 없는 항목은 로드 오류입니다.
input_item_resource_ids는 콘텐츠 파트와 타입이 있는 항목이 담은 추가 필드를 포함해 모든 입력 항목을 순회하고, 리소스를 가리키는 첫 값을 input[code_interpreter_call].container_id나 input[message].file_id처럼 위치를 명시해 거부합니다. null, 빈 문자열, 빈 배열인 키는 아무것도 가리키지 않으므로 통과합니다. 라우터가 로컬 파일로 해석한 file_id는 검사 전에 지워지므로, 로컬 파일을 가리키는 요청은 어느 쪽이든 허용됩니다.
키를 공유하는 라우터라면 나머지 다섯 항목도 추가할지 검토하세요. 이 항목들은 오래전부터 전달해 왔고, 모델 자신의 출력을 다시 보내는 클라이언트는 그 ID를 그대로 재전송하므로 previous_response_id나 재전송된 입력 항목을 거부하면 일반적인 다중 턴 클라이언트가 동작하지 않습니다. 변환 전략은 이 필드들을 자체적으로 처리하거나 무시하며, 이 항목들로 동작이 바뀌지 않습니다.
패스스루 전략이 거부할 요청은 응답 캐시에서 조회하지도 저장하지도 않습니다. 따라서 새 항목을 추가한 재로드는 이전 정책으로 캐시된 응답을 재생하지 않고 즉시 적용됩니다. 변경 전에 캐시된 항목은 캐시 TTL과 함께 만료됩니다.
라우터는 호출자가 업스트림 리소스를 소유하는지 확인하지 않습니다. 이 목록은 request_extensions.body와 GET /v1/responses/{id} 및 cancel 경로에는 적용되지 않습니다.
정책이 다루지 않는 범위¶
이 정책은 Chat Completions와 Anthropic Messages, 저장된 prompt가 붙이는 도구, request_extensions.body에 직접 넣은 도구, mcp.server_url 값은 다루지 않습니다. 앞으로 클라이언트가 새로 쓰는 클라이언트 실행 유형은 allow에 추가하거나 라우터 릴리스가 항상 허용 목록에 넣기 전까지 400을 받습니다.
요청 프리셋¶
요청 프리셋은 클라이언트가 슬러그로 선택하는 모델 기본값과 샘플링 값의 묶음입니다. 현재는 POST /v1/chat/completions만 이 필드를 읽으며, 다른 엔드포인트는 아직 지원하지 않습니다.
설정¶
request_presets:
support-bot:
model: gpt-5-mini # 선택: 요청에 model이 없을 때만 사용
params: # 선택: request_params와 같은 7개 정규 필드
temperature: 0.2
max_tokens: 1024
검증 규칙은 다음과 같습니다.
- 슬러그는
[A-Za-z0-9._-]로 이루어진 1자 이상 64자 이하이며, 설정 하나에 프리셋을 최대 256개까지 선언할 수 있습니다. model은 trim 후 비어 있지 않아야 하고, 256바이트 이하이며, 제어 문자를 포함할 수 없습니다.- 모든 프리셋은
model,params, 또는 둘 다를 설정해야 합니다. params는temperature,top_p,max_tokens,presence_penalty,frequency_penalty,top_k,min_p만 허용하며, 값 범위는request_params와 같습니다.- 알 수 없는 프리셋 키나 파라미터 키는 로드와 핫 리로드에서 거부됩니다.
요청 처리¶
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"preset": "support-bot", "messages": [{"role": "user", "content": "Hello"}]}'
- 우선순위: 요청에 명시한 필드가 프리셋보다 우선하고, 프리셋은
request_params.defaults보다 우선합니다. 프리셋 적용 뒤 기존 정책 패스가 실행되므로request_params.overrides와limits는 프리셋 값에도 그대로 적용됩니다. - 모델: 요청에 사용할 수 있는
model이 없을 때만 프리셋 모델을 사용하며, 유효한 요청 모델이 있으면 항상 요청 값을 유지합니다. 요청별models목록도 요청 모델로 취급합니다.model이 없으면 목록의 첫 항목이 기본 모델이 되므로 프리셋 모델은 쓰지 않습니다. - 승인 단계: 요청에
model이 없으면per_model속도 제한과 Continuum Hub 모델 게이트는 요청이 실제로 실행될 모델(models첫 항목, 없으면 프리셋 모델)을 기준으로 판단합니다. 따라서 프리셋을 지정해 모델별 제한을 건너뛸 수 없습니다. - 검증 순서: 클라이언트가 보낸
max_tokens는 프리셋의 샘플링 값을 채우기 전에 타입과 범위를 검사합니다. 잘못된 타입의 예산은 해당 필드를 지목하는 일반400 invalid_type응답을 받습니다. - 디스패치: 라우터는 디스패치 전에
preset필드를 제거하므로, 백엔드는 이 필드를 받지 않습니다.
오류¶
| 상황 | 응답 |
|---|---|
| 형식이 유효한 알 수 없는 슬러그 | 400 unknown_request_preset, 슬러그 이름 포함 |
| 형식이 잘못된 슬러그 | 400 unknown_request_preset, 메시지에 슬러그 형식 규칙만 안내하고 슬러그는 노출하지 않음 |
| 문자열이 아닌 값 | 400 invalid_type, param: "preset" |
null | 무시하고 제거 |
핫 리로드와 Hub 동기화¶
request_presets는 즉시 핫 리로드되므로 재시작 없이 다음 요청부터 새 프리셋이 적용됩니다. 이 섹션은 Hub 설정 동기화가 통째로 교체하는 request_params와 별개의 최상위 섹션이므로, Hub 적용으로 로컬 프리셋이 사라지지 않습니다.
이 기능은 OpenRouter Presets에 대응하지만 model: "@preset/<slug>" 표기는 지원하지 않습니다. 대신 preset 필드에 프리셋 이름을 지정하세요.
핫 리로드¶
hot-reload 기능과 파일 감시자가 활성화되면 유효한 파일 개정이 새 불변 설정 스냅샷으로 게시됩니다. 유효하지 않은 개정은 이전 스냅샷을 유지합니다. 파싱되는 모든 필드에 실행 중인 런타임 소비자가 있는 것은 아니므로 아래의 보수적인 표를 따르십시오.
재시작 없이 적용¶
- 백엔드 추가/삭제/수정(풀이 조정되고 새 백엔드는 즉시 상태 확인)
- 상태 확인 주기와 임계값
- 해당 서비스가 활성화된 경우 속도 제한 및 서킷 브레이커 정책
- 새 요청이 읽는 전역 프롬프트, 요청 매개변수, 요청 프리셋, 스트리밍 정책
- 새 요청이 읽는
responses_tools호스티드 도구 정책 - process-lifetime 서비스의 dormant/live 전환을 포함한 모든 폴백 스위치, 체인, 정책, 모델별 설정, dial bound 변경
- 스마트 라우팅 정책 스냅샷
selection_strategy(실행 중인 풀에 원자적으로 교체되며 멤버십, 통계, 처리 중 요청 집계가 보존됨)- 재시도 정책(새 요청은 갱신된 전체 정책을 캡처하고 진행 중 루프는 진입 시 스냅샷을 유지함)
- 요청/스트리밍/이미지/모델 오버라이드/상태 확인 타임아웃 예산
- 모든
prefix_routing.*필드:enabled와max_prefix_length는 요청 시 접두사 추출에 사용되고load_factor_epsilon과virtual_nodes는 실행 중인 풀에 교체되며(virtual_nodes변경 시 해시 링도 다시 만듦),anthropic_cache_control_injection과salt_echo는 요청마다 읽힘 routing.engine_load.max_staleness_intervals와routing.engine_load.admission.*(엔진 통계 설정 감시자가 리로드 발행마다 적용)
재시작 필요 또는 현재 실시간 미적용¶
server.bind_address,server.socket_mode,server.workers,server.shutdown_drain_timeoutserver.connection_pool_size와 CORS 라우터 계층- 로깅 subscriber 형식/필터와 추적 미들웨어 구성
- 시작 시 존재하지 않았던 선택 서비스 토글(예:
fallback.enabled) - 응답 캐시 백엔드/객체 구성
timeouts.connection과timeouts.request.streaming.total(공유 HTTP 클라이언트의 연결/전체 상한이며 요청별 예산 자체는 실시간 반영)routing.engine_load스코어링 필드(enabled,engine_load_weight,balance_abs_threshold,balance_rel_threshold): KV 오버랩 스코어러가 생성 시점에 캡처
재시작 필드를 수정한 뒤 검증하고 재시작합니다.
인증된 Admin API에서 감시자 가용성과 라우터 기능 요약을 확인합니다.
재시작 필요 값이 바뀐 경우 새로 파싱된 파일만이 아니라 활성 서브시스템 상태를 기준으로 판단하십시오.
분산 추적¶
Continuum Router는 백엔드 서비스 간 요청 상관관계를 위한 분산 추적을 지원합니다. 이 기능은 여러 서비스를 통과하는 요청의 디버깅 및 모니터링에 도움을 줍니다.
이 절의 내용은 모두 헤더 전파, 즉 라우터가 요청에서 어떤 식별자를 읽고 백엔드로 무엇을 전달하는지에 대한 것입니다. 항상 동작하며 별도의 빌드가 필요 없습니다. 여기에 더해 라우터 자신의 스팬을 OpenTelemetry 컬렉터로 보내려면 분산 트레이스 내보내기에 설명된 tracing.otlp 하위 섹션을 설정하세요. 이 하위 섹션은 기본적으로 꺼져 있고 otel Cargo 피처로 빌드한 바이너리가 필요하며 설정하지 않아도 이 절에 설명된 동작은 달라지지 않습니다.
설정¶
tracing:
enabled: true # 분산 추적 활성화/비활성화 (기본값: true)
w3c_trace_context: true # W3C Trace Context 헤더 지원 (기본값: true)
headers:
trace_id: "X-Trace-ID" # 32자리 16진수 추적 ID 헤더 이름 (기본값)
request_id: "X-Request-ID" # 클라이언트 요청 ID 헤더 이름 (기본값)
correlation_id: "X-Correlation-ID" # 상관 ID 헤더 이름 (기본값)
client_ref: "" # 선택적 클라이언트 참조 ID 헤더 이름, 예: "X-Client-Ref" (기본값: "")
동작 방식¶
-
추적 ID 결정 (
trace_id): 요청이 도착하면 라우터는 다음 우선순위로 W3C 규격의 0이 아닌 32자리 소문자 16진수trace_id([0-9a-f]{32})를 결정합니다:- 유효한 인바운드 W3C
traceparent헤더 (w3c_trace_context활성화 시) - 설정된
trace_id헤더(X-Trace-ID)의 유효한 32자리 소문자 16진수(0이 아닌 값) - 새로 생성된 128비트 난수 32자리 16진수 추적 ID (
Uuid::new_v4().simple())
- 유효한 인바운드 W3C
-
클라이언트 요청 ID (
X-Request-ID) 및 Continuum 요청 ID (x-continuum-request-id):^[A-Za-z0-9._:/-]{1,128}$를 만족하는 인바운드X-Request-ID값은 32자리 16진수trace_id를 덮어쓰지 않고x_request_id로 수락되어 로그에 기록됩니다. 유효하지 않은 값은 거부됩니다.- 요청 원장(request ledger) 미들웨어 범위 안에서 처리된 모든 응답(스트리밍
200, 비스트리밍200,4xx/5xx)에는 원장 레코드 및router.request_id스팬 속성과 일치하는x-continuum-request-id: req_<32 hex>헤더가 포함됩니다. 원장 범위 이전에 API 키 인증, Hub 정책 집행, ID 단위 속도 제한이 거부한 응답(401,403,429)에도 같은 헤더가 붙으므로 실패한 호출도 ID로 지목할 수 있습니다. 이때는 사용량 레코드가 없으므로 ID는event=request_rejected로그 줄의request_id와 일치합니다. API 경로보다 앞에서 동작하는 계층이 거부한 응답(전역 및 클라이언트 단위 속도 제한기의429, 동시성 상한의503, 라우터 홉 가드의400또는508)에도 같은 헤더가 붙습니다. - 응답
x-request-id헤더는 유효한 클라이언트X-Request-ID가 있으면 그대로 에코하고, 없으면x-continuum-request-id로 대체됩니다. - Anthropic Messages API 경로(
/anthropic/...)의 응답에는 Anthropic API가 반환하는request-id헤더도x-continuum-request-id와 같은 값으로 포함됩니다. 이 값은 에코된 클라이언트X-Request-ID가 아니라 라우터 ID이므로,request-id를 기록하는 Anthropic 클라이언트는 그 기록을 요청 원장 레코드와 대조할 수 있습니다. 네이티브 Anthropic 백엔드가 자체request-id를 반환한 경우에는 라우터가 그 값을 그대로 전달하며, 이때도x-continuum-request-id에는 라우터 ID가 담깁니다.
-
선택적 클라이언트 참조 헤더 (
client_ref):tracing.headers.client_ref가 설정된 경우(예:"X-Client-Ref"),^[A-Za-z0-9._:-]{1,128}$를 만족하는 인바운드 값은client_ref로 로그에 남고router.request스팬의router.client_ref속성으로 내보내지며, 설정된 헤더 이름으로 백엔드에 전달됩니다.- 유효하지 않은 값은 요청을 실패시키지 않고 제거되며,
client_ref_rejected_total{reason}(empty,too_long,control_char,invalid_char) 메트릭을 증가시킵니다.
-
헤더 전파: 백엔드 서비스로 나가는 요청에는 다음 헤더가 전파됩니다:
X-Request-ID,X-Trace-ID,X-Correlation-ID: 32자리 16진수trace_id값으로 설정traceparent:w3c_trace_context활성화 시 W3C Trace Context(00-{trace_id}-{16자리 16진수 span_id}-{flags})tracestate: 유효한 인바운드traceparent와 함께 전달된 유효한tracestate- 설정된
client_ref헤더:tracing.headers.client_ref가 비어 있지 않고 인바운드 값이 검증을 통과한 경우 전달
-
재시도 시 보존: 동일한
trace_id,x_request_id,client_ref가 모든 재시도에서 보존되며, 각 아웃바운드 시도는traceparent에 새로운 16자리 16진수 span ID를 생성합니다.
구조화된 로깅¶
추적이 활성화되면 로그 메시지에 trace_id와 선택적 x_request_id, client_ref 필드가 포함됩니다:
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "info",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"x_request_id": "REF.001:A/7",
"client_ref": "billing-dept-01",
"message": "Processing chat completions request",
"backend": "openai",
"model": "gpt-5.6-sol"
}
W3C Trace Context¶
w3c_trace_context가 활성화되면 라우터는 W3C Trace Context 표준을 지원합니다:
- 수신:
traceparent헤더 파싱 (형식:00-{trace_id}-{span_id}-{flags}) - 송신: 32자리 16진수
trace_id와 새 16자리 16진수 span ID로traceparent헤더 생성 - 상태: 유효한
traceparent와 함께 전달된 검증된tracestate헤더 전달
traceparent 예시: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
추적 비활성화¶
분산 추적을 비활성화하려면:
로드 밸런싱 전략¶
최상위 selection_strategy 필드를 사용합니다. 구현된 여섯 값은 RoundRobin, WeightedRoundRobin, LeastLatency, Random, ConsistentHash, PrefixAwareHash입니다. 백엔드 상태 필터링은 선택 전에 수행됩니다.
selection_strategy: WeightedRoundRobin
backends:
- name: large
url: http://large.example.com
weight: 3
- name: small
url: http://small.example.com
weight: 1
정확한 의미와 현재 시작/핫 리로드 제한은 로드 밸런싱을 참고하십시오.
엔진 부하 인식 선택¶
routing.engine_load 섹션은 engine_stats 스냅샷을 선택 신호로 바꿉니다. KV 오버랩 스코어러를 확장하므로 스코어러가 존재하려면 prefix_routing.enabled: true와 활성화된 kv_cache_index가, 데이터를 위해서는 engine_stats.enabled: true가 필요합니다. 둘 중 하나라도 빠지면 continuum-router config validate가 경고합니다.
routing:
engine_load:
enabled: false # 스코어링 항 스위치 (재시작 필요)
engine_load_weight: 0.3 # 가산 스코어러 가중치, 0.0-1.0 (재시작)
max_staleness_intervals: 3 # 폴링 주기 단위 신선도 한계, 1-100
# (핫 리로드, gradual)
balance_abs_threshold: 64 # 순위를 매기기 위한 waiting 요청 편차
# (max-min, 재시작)
balance_rel_threshold: 1.5 # 함께 넘어야 하는 waiting 요청 비율
# (max/min, >= 1.0, 재시작)
admission:
enabled: false # 포화 어드미션 힌트 (핫 리로드)
kv_usage_threshold: 0.98 # KV 사용률 거부 임계값, 0.0-1.0
스코어링 및 어드미션 동작 방식:
- 스코어링 (
enabled: true): 접두사를 보유한 후보 백엔드 가운데 엔진이 보고한waiting_requests가 적고(신선한 후보 전부가total_slots를 보고하면 그 값으로 정규화하고, 그렇지 않으면 후보 집합의 최대waiting_requests로 정규화),kv_cache_usage가 낮은 쪽을 우선합니다. - 신선도 판정:
engine_stats.interval * max_staleness_intervals보다 오래된 스냅샷은 없는 것으로 취급하여 해당 후보는 기본 점수를 유지하며, 모든 후보의 스냅샷이 오래되면 기본 스코어링과 동일하게 동작합니다. - 히스테리시스 데드 밴드: SGLang Model Gateway의 밸런스 게이트와 마찬가지로
balance_abs_threshold(max - min)와balance_rel_threshold(max / min)를 둘 다 넘어야 순위에 반영되므로, 대기열 진동으로 인해 선택이 출렁이지 않습니다. 게이트 판정, 정규화, 스코어러 캐시는 라이브 후보 집합별로 격리되며, 결정 내역은routing_engine_load_decisions_total{backend,reason}메트릭에 집계됩니다(메트릭 참고). - 어드미션 힌트 (
admission.enabled: true): 스코어러와 독립적으로 공용 선택 지점에서 동작합니다. 모든 정상 후보가kv_usage_threshold를 넘는 신선한kv_cache_usage스냅샷을 보고하면 대기열에 넣는 대신 선택을 거부합니다. 채팅 경로에서 이 거부는 재시도 가능한503오류를 반환하며,BackendUnhealthy트리거를 통해fallback.fallback_chains로 넘어가므로 클라이언트에 오류를 노출하지 않고 폴백 체인이 이어받습니다. 신선한 스냅샷이 없는 후보는 항상 요청을 수락합니다.
사용량 가중 선택¶
routing.usage_weight 섹션은 제공자가 선언한 토큰·요청 쿼터 중 라우터가 아직 쓰지 않은 몫으로 후보 순위를 매깁니다. 제공자 한도는 백엔드에 선언합니다.
backends:
- name: provider-a
url: https://api.provider-a.example/v1
provider_tpm: 200000 # 제공자 분당 토큰 (1 이상)
provider_rpm: 500 # 제공자 분당 요청 (1 이상), 선택 사항
routing:
usage_weight:
enabled: false # 스코어링 항 스위치 (핫 리로드)
weight: 0.2 # 가산 상한, 0.0-1.0
staleness_interval: 60s # 소비 윈도 길이, 1s-1h
- 선언이며 강제가 아님:
provider_tpm과provider_rpm은 제공자가 허용하는 양을 뜻합니다. 라우터는 이 값을 넘겨도 계속 디스패치하며, 강제는rate_limiting이나 키별 토큰 예산이 담당합니다. - 순위: 선언한 후보는
weight x 남은 비율을 받습니다. 남은 비율은(설정값 - 소비량) / 설정값을0.0..=1.0으로 자른 값이며, 선언한 축 중 가장 작은 비율이 기준이 됩니다. 아무것도 선언하지 않았거나 예산을 모두 쓴 후보는 정확히0.0을 받아 기존 점수를 유지합니다. - 결정: 다른 모든 후보보다 엄격히 우위일 때만 항이 백엔드를 정합니다. 동점은
selection_strategy로 되돌립니다.weight상한이 스코어러 문턱보다 낮으므로, KV나 엔진 부하 항이 확정하지 못한 문턱 아래 패스를 이 항이 결정합니다. - 소비 데이터: 백엔드별 라우터 로컬 토큰 계량을
staleness_interval길이의 tumbling window로 집계합니다. 윈도는 재시작 뒤에도 0에서 시작합니다. RPM 축은 사용량을 보고한 정상 종료 attempt 수로 요청량을 근사합니다. - 메트릭:
routing_usage_weight_decisions_total{backend,reason}이며,reason은applied,no_declaration,no_differentiation중 하나입니다(메트릭 참고). - 검증과 경고:
weight는 유한한0.0..=1.0,staleness_interval은 1초 이상 1시간 이하의 duration, 선언한 한도는 1 이상이어야 합니다. 항이 켜져 있는데 선언한 백엔드가 하나도 없으면, 그리고 항이 꺼져 있는데 한도를 선언한 백엔드가 있으면config validate가 경고합니다. - 기본값: 블록이 없거나 꺼져 있으면 선택 동작은 이 섹션이 없는 빌드와 동일합니다.
전체 규칙은 로드 밸런싱을 참고하십시오.
업스트림 관측 쿼터¶
routing.quota_aware는 routing.usage_weight 위에 쿼터 인식 선택의 업스트림 관측 절반을 더합니다. 라우터는 제공자가 응답마다 남기는 쿼터 잔량 헤더를 파싱해 백엔드별 최신 스냅샷을 보관합니다. 스냅샷이 신선한 동안 관측된 축이 해당 축의 라우터 로컬 추정을 대체하므로, 관측 데이터가 있는 백엔드는 provider_tpm이나 provider_rpm 선언 없이도 순위에 참여합니다.
routing:
usage_weight:
enabled: true # 필수: quota_aware는 이 항을 수정합니다
quota_aware:
enabled: true # 업스트림 헤더를 캡처하고 순위에 사용
ttl: 45s # 스냅샷 신선도, 1s-1h
- 읽는 헤더: Anthropic
anthropic-ratelimit-requests-{limit,remaining},anthropic-ratelimit-tokens-{limit,remaining},anthropic-ratelimit-input-tokens-{limit,remaining},anthropic-ratelimit-output-tokens-{limit,remaining}, Anthropic 인그레스가 전달하는anthropic-ratelimit-unified-{requests,tokens}-*별칭, 그리고 OpenAIx-ratelimit-limit-requests/x-ratelimit-remaining-requests와x-ratelimit-limit-tokens/x-ratelimit-remaining-tokens입니다. - 정규화: 잔량 값과 대응하는
-limit헤더가 모두 있고 한도가 0보다 클 때만 그 축을 순위에 씁니다. 비율은remaining / limit을0.0..=1.0으로 자른 값입니다. 분모가 없는 잔량 값은 Admin API에 노출하지만 순위에는 쓰지 않으므로, 지어낸 비율이 라우터 로컬 계량을 앞지르지 않습니다. 요청 축은 제공자의 요청 비율을, 토큰 축은tokens,input-tokens,output-tokens가운데 쓸 수 있는 가장 작은 비율을 씁니다. - 캡처 시점: 업스트림 응답이 실제로 도착했을 때만 관측을 기록하며, 라우터가 관측하는 TTFT와 같은 계약입니다. 전송 오류, 연결 실패, 타임아웃은 아무것도 기록하지 않습니다.
- 신선도: 스냅샷이 없거나 만료되면 선언한 추정을 그대로 둡니다. 기본 TTL은 기본
engine_stats오래됨 창(interval15s 곱하기max_staleness_intervals3)과 같습니다. - 소유권: 이 헤더는 라우터 백엔드가 아니라 제공자 키를 설명합니다. 백엔드마다 스냅샷 하나를 두고 응답마다 교체하므로 서로 다른 자격 증명의 관측이 섞이지 않으며, 단일 자격 증명 백엔드에서 의미가 있습니다.
- 가시성:
GET /admin/stats/backends가 백엔드마다engine_stats옆에quota를 붙이고,GET /admin/backends/{name}/engine-stats도 함께 돌려줍니다. 뷰에는 관측 시각, 현재 TTL 대비 경과, 캡처한 축과 비율, 인식한 원본 헤더가 들어갑니다. - 메트릭:
routing_quota_aware_decisions_total{backend,reason}이며,reason은observed_used,stale,absent,unusable중 하나입니다. - 의존성과 기본값:
routing.usage_weight.enabled가 참이 아니면 이 블록은 아무 효과가 없고,config validate가 그 조합을 경고합니다. 블록이 없거나 꺼져 있으면 선택 동작은 이 섹션이 없는 빌드와 같습니다.
전체 규칙은 로드 밸런싱을 참고하십시오.
백엔드별 재시도 설정¶
backends:
- name: "slow-backend"
url: "http://slow.example.com"
retry_override: # 완전한 백엔드별 재시도 정책
max_attempts: 5
initial_delay: "500ms"
max_delay: "60s"
backoff_multiplier: 2.0
jitter: true
retryable_status_codes: [429, 502, 503, 504]
retryable_errors: [ConnectionError, TimeoutError]
timeout: "120s"
라우터는 retry_override를 파싱, 검증, 저장, 내보내기까지 하지만 디스패치에서는 적용하지 않습니다. 모든 백엔드는 전역 retry 섹션에 따라 재시도합니다. 백엔드에 같은 요청이 두 번 전송되지 않게 하려면 대신 replay: disabled를 설정하세요. 이 설정은 폴백 체인, 스트리밍 릴레이, 품질 홉까지 다룹니다. 재전송 정책을 참고하세요.
모델 폴백¶
최상위 fallback 섹션에서 순서가 있는 대체 모델을 설정합니다.
fallback:
enabled: true
mid_stream_enabled: true
fallback_chains:
gpt-5.4:
- gpt-5.4-mini
- claude-sonnet-4-6
fallback_policy:
trigger_conditions:
error_codes: [429, 500, 502, 503, 504]
timeout: true
connection_error: true
model_not_found: true
backend_unhealthy: true
max_fallback_attempts: 3
fallback_timeout_multiplier: 1.5
preserve_parameters: true
max_concurrent_dials_per_backend: 50
content_policy_fallback: false
empty_reply_retry:
enabled: false # 텍스트가 전혀 없는 응답을 재시도합니다
max_extra_hops: 1 # 1-5
model_settings:
gpt-5.4:
fallback_enabled: true
notify_on_fallback: true
체인의 모델 이름은 키든 항목이든 서빙되는 모델 ID이며 1-128바이트의 유니코드 문자와 숫자, -, _, ., /, :로 이루어집니다. 따라서 HuggingFace 경로와 Ollama 태그를 그대로 체인에 넣을 수 있고 뒤에 공백이 오지 않는 :는 YAML에서 따옴표가 필요 없습니다(TOML에서는 /, :, .가 들어간 키를 따옴표로 감싸야 합니다):
fallback:
fallback_chains:
ggml-org/gpt-oss-20b-MXFP4:
- meta-llama/Llama-3.1-8B-Instruct
- hf.co/org/repo:Q4_K_M
공백, 제어 문자, 그 밖의 문장 부호는 거부되며 오류 메시지는 해당 문자와 바이트 오프셋을 알려 주고 값은 이스케이프한 뒤 128자로 잘라 다시 보여 줍니다. fallback.quality_check.models의 항목은 체인의 기본 모델이므로 같은 규칙을 따릅니다. 공급자가 아니라 운영자가 정하는 이름, 예를 들어 decisions.targets[].name과 모델 실험의 이름 및 변형 ID는 더 좁은 [A-Za-z0-9._-]를 유지하며 /를 포함할 수 없습니다.
일반 프록시 경로는 circuit_breaker_open을 만들지 않습니다. 추론 트래픽에는 능동 상태 확인과 backend_unhealthy를 사용하세요. 검증, 헤더, 메트릭, 공급자 간 변환, 스트림 시작 전/중간 동작의 차이는 모델 폴백을 참고하세요.
fallback.empty_reply_retry는 첫 choice에 텍스트도 도구 호출도 없는 비스트리밍 채팅 응답을 체인의 다음 모델에서 재시도합니다. 기본값은 꺼짐이고, 클라이언트의 max_tokens에서 잘린 응답으로는 트리거하지 않으며, fallback.quality_check가 적용되는 요청에서는 건너뜁니다. 빈 응답 재시도를 참고하세요.
미드스트림 폴백¶
미드스트림 폴백은 기본 백엔드가 응답 도중 실패할 때 라우터가 진행 중인 SSE 스트림을 폴백 백엔드에서 투명하게 이어가도록 합니다. 클라이언트 연결은 열린 채 유지되고 전환 중 잠깐의 멈춤만 있을 뿐 끊김 없는 응답을 받습니다.
미드스트림 폴백은 fallback.enabled: true이고 요청된 모델에 폴백 체인이 설정되어 있으면 자동으로 활성화됩니다. streaming.mid_stream_fallback 섹션은 폴백이 일어날지 여부가 아니라 폴백 백엔드를 어떻게 호출할지 (연속 vs 재시작 모드)를 제어합니다.
mid_stream_fallback.enabled는 버퍼링이나 메모리 킬 스위치가 아닙니다
streaming.mid_stream_fallback.enabled는 폴백이 트리거된 뒤의 복구 모드 (연속 vs. 재시작)만 선택합니다. 스트림당 버퍼링을 비활성화하거나 메모리 사용량을 줄이지는 않습니다. enabled: false로 설정해도:
- 스트림 누적기는 여전히 생성되고 스트리밍 응답을 계속 버퍼링합니다 (스트림당 최대 약 100 KB).
- 백엔드 실패 시 미드스트림 폴백은 여전히 활성화됩니다.
- 유일한 차이는 폴백 요청이 이어서 계속되는 대신 처음부터 재시작된다는 점입니다.
프리스트림 폴백은 유지하면서 스트림당 버퍼링과 그 메모리 비용을 없애려면 fallback.mid_stream_enabled: false로 설정하세요 (미드스트림 버퍼링 비활성화 참고). 폴백을 완전히 비활성화하려면 fallback.enabled: false로 설정하거나 fallback.fallback_chains에서 해당 모델을 제거하세요.
미드스트림 버퍼링 비활성화¶
fallback.mid_stream_enabled(기본값 true)는 프리스트림 폴백과 미드스트림 버퍼링을 분리합니다. fallback.enabled: true이고 체인이 설정된 상태에서 false로 두면 다음과 같이 동작합니다.
- 초기 연결은 연결 오류, 타임아웃, 트리거 오류 코드 발생 시 여전히 폴백 체인을 따라 다시 라우팅됩니다(프리스트림 폴백은 영향받지 않음).
- SSE 스트림이 시작된 뒤에는 스트림 누적기를 할당하지 않으므로 스트림당 버퍼링과 그에 따른 메모리 비용이 없습니다.
- 스트림이 시작된 다음에 발생한 실패는 백엔드를 조용히 전환하는 대신 일반 스트림 오류로 클라이언트에 전달됩니다.
메모리가 빠듯한 호스트나, 스트림당 약 100~200 KB 버퍼가 누적되는 긴 컨텍스트 스트리밍 세션이 동시에 많은 환경에서 mid_stream_enabled: false를 사용하면 됩니다.
이 플래그는 streaming.mid_stream_fallback.enabled와 다릅니다:
| 플래그 | 섹션 | 제어 대상 |
|---|---|---|
fallback.enabled | fallback | 모든 폴백의 마스터 스위치 |
fallback.mid_stream_enabled | fallback | 미드스트림 버퍼링 자체의 동작 여부(false면 프리스트림만 유지) |
streaming.mid_stream_fallback.enabled | streaming | 미드스트림 폴백 발생 후 복구 모드(연속 vs 재시작), 버퍼링은 제어하지 않음 |
fallback:
enabled: true # 프리스트림 폴백은 그대로 유지
mid_stream_enabled: false # 스트림당 버퍼링 없음, 미드스트림 오류는 클라이언트로 전달
fallback_chains:
"gpt-5.6-sol":
- "gpt-5.6-terra"
- "gpt-5.6-luna"
설정¶
fallback:
enabled: true # 필수: 미드스트림 폴백 경로를 활성화
fallback_chains:
"gpt-5.6-sol":
- "gpt-5.6-terra"
- "gpt-5.6-luna"
streaming:
mid_stream_fallback:
# 연속 모드 활성화 (기본값: true).
# true이면 누적된 부분 응답으로 연속 프롬프트를 구성하여
# 클라이언트에 끊김 없는 출력을 제공합니다.
# false이면 폴백 백엔드가 요청을 처음부터 다시 실행하며, 부분 출력이
# 이미 전송된 경우 중복되거나 일관성 없는 내용이 생길 수 있습니다.
enabled: true
# 연속 모드를 사용하기 전에 누적되어야 하는 최소 추정 토큰 수 (기본값: 50)
# 이 임계값 미만이면 연속 프롬프트를 덧붙이는 대신
# 폴백 백엔드에서 요청을 처음부터 다시 실행합니다.
min_accumulated_tokens: 50
# SSE 출력이 시작된 뒤의 최대 홉 수 (기본값: 2, 최대: 10). 첫 청크 이전의
# 홉은 fallback.fallback_policy.max_fallback_attempts를 따릅니다.
max_fallback_attempts: 2
# 부분 어시스턴트 응답 뒤에 사용자 메시지로 덧붙이는 프롬프트
continuation_prompt: "Continue from where you left off exactly. Do not repeat any previously generated content."
# 체인을 진행하기 전에 같은 모델의 다른 백엔드를 먼저 시도합니다
# (기본값: false). 아래 "같은 모델 형제 백엔드 연속"을 참고하십시오.
same_model_first: false
같은 모델 형제 백엔드 연속¶
하나의 모델을 여러 백엔드에 분산한 운영 환경에서는 모델을 바꾸지 않고 멈춘 스트림을 이어갈 수 있습니다. streaming.mid_stream_fallback.same_model_first: true이면 스트림 중간 실패 시 같은 공개 모델을 제공하는 다른 백엔드를 먼저 시도하고, 그런 백엔드가 없을 때만 fallback.fallback_chains를 진행합니다.
- 형제 자격: 사용자 라우팅 대상이고, API 키에 허용되며, OpenAI 와이어 릴레이가 처리할 수 있고, 서킷 브레이커가 현재 요청을 허용하며, 이 요청에서 해당 모델로 아직 호출되지 않은 백엔드입니다.
- 홉 동작: 형제 백엔드는 체인 대상에 보낼 때와 같은 연속 또는 재시작 요청을 받고, 응답의
model은 클라이언트가 요청한 모델로 유지되며, 이 홉은max_fallback_attempts에 포함됩니다. - 체인 폴백: 형제 집합이 비어 있거나, 형제 시도도 실패하거나, 서킷이 열려 있으면 플래그를 끈 경우와 동일하게 체인이 진행됩니다.
이 플래그는 OpenAI 와이어 스트림 중간 릴레이에만 적용됩니다. 스트림 중간 복구는 응답 헤더가 이미 전송되었으므로 X-Fallback-* 헤더를 붙이지 않으며, 홉은 streaming_fallback_total{reason="same_model_sibling"}과 시도 기록에 나타납니다.
동작 방식¶
- 클라이언트가 스트리밍 채팅 완료 요청을 보냅니다.
- 라우터가 기본 백엔드에서 스트리밍을 시작하면서 응답 내용을 누적합니다.
-
백엔드가 스트림 도중 실패하면 (연결 끊김, 타임아웃, 오류 이벤트):
- 오류는 클라이언트로 전달되지 않습니다.
- 누적된 부분 응답이 캡처됩니다.
same_model_first가 켜져 있으면 같은 모델을 제공하는 다른 적격 백엔드를 먼저 시도합니다.- 그렇지 않으면 폴백 체인에서 다음 정상 백엔드가 선택됩니다 (비정상 백엔드는 건너뜀).
- 연속 또는 재시작 요청이 폴백 백엔드로 전송됩니다.
- 클라이언트 연결을 닫지 않은 채 폴백 백엔드에서 스트리밍이 재개됩니다.
-
클라이언트는 전환 중 잠깐의 멈춤만 있을 뿐 끊김 없는 응답을 받습니다.
연속 vs. 재시작 모드¶
min_accumulated_tokens 임계값이 어떤 복구 모드를 사용할지 결정합니다:
| 조건 | 모드 | 동작 |
|---|---|---|
enabled: true (기본값)이고 토큰 수가 min_accumulated_tokens 이상이며 잘리지 않음 | 연속 | 원본 메시지 + 부분 어시스턴트 응답 + 연속 프롬프트 |
enabled: true (기본값)이고 토큰 수가 min_accumulated_tokens 미만 | 재시작 | 원본 요청 재실행 (이어가기에는 컨텍스트 부족) |
enabled: true (기본값)이고 내용이 잘림 (> 100 KB) | 재시작 | 일관성 없는 컨텍스트를 피하기 위해 강제 재시작 |
enabled: true (기본값)이고 부분 응답에 추론만 있음 (콘텐츠도 도구 호출도 없음) | 재시작 | 수가 얼마든 이어갈 내용이 없음 |
mid_stream_fallback.enabled: false | 재시작 | 원본 요청을 폴백 백엔드에서 처음부터 재실행 |
min_accumulated_tokens와 비교하는 수는 라우터가 센 스트리밍 델타 이벤트 수입니다. 청크마다 콘텐츠, 추론, 도구 인자 델타당 하나로, vLLM 계열 엔진에서는 청크당 토큰 하나에 해당합니다. 백엔드가 사용량을 보고하지 않은 스트림이 usage_missing으로 과금되는 수도 같은 값입니다.
연속 모드 (기본값)는 클라이언트에 끊김 없는 출력을 제공합니다. 재시작 모드는 의미 있게 이어가기에는 컨텍스트가 너무 적거나, 누적된 응답이 안전하게 포함하기에는 너무 길 때 자동으로 사용됩니다. enabled: false를 명시적으로 설정하면 무조건 재시작 모드가 강제되며 클라이언트에 중복되거나 일관성 없는 내용이 보일 수 있습니다.
엣지 케이스 처리¶
미드스트림 폴백 경로는 여러 엣지 케이스를 자동으로 처리합니다:
- 전역 타임아웃 예산: 모든 폴백 시도는 원래 요청의 시작 시각을 공유합니다. 각 시도는 전송 전에 남은 예산을 확인하므로, 체인 전체에 걸쳐 타임아웃이 무한정 누적되지 않습니다.
- 표준 형태 크로스 프로바이더 페이로드: 폴백 모델이 다른 프로바이더에 있어도 (예: OpenAI → Anthropic) 요청은 표준 OpenAI chat-completions 형태를 유지합니다. 홉은 모델 이름만 바꿉니다. 와이어 형식 변환도, 어떤 필드가 와이어까지 나갈 수 있는지에 대한 결정도, 선택된 백엔드의 설정 타입을 기준으로 디스패치에서 한 번만 일어납니다.
-
동시 요청 폭주: 기본 백엔드에 장애가 발생하면, 해당 백엔드로 향하던 모든 진행 중 요청이 동시에 실패하면서 동일한 대체(폴백) 백엔드로 한꺼번에 몰립니다.
fallback.fallback_policy.max_concurrent_dials_per_backend(기본값50,0= 무제한, 범위0..=10000)는 백엔드별 동시 폴백 연결(dial) 수를 제한합니다:- 다이얼(연결) 구간만 제한: 퍼밋은 폴백 홉의 아웃바운드 요청을 보내기 직전에 획득하고, 프로바이더 핸드셰이크가 돌아오는 즉시(HTTP 상태 코드와 헤더 수신, 또는 전송 오류 발생 시) 반납합니다. 응답 본문을 수신하는 구간은 이미
server.max_concurrent_requests, 타임아웃, 서킷 브레이커로 보호되므로 퍼밋을 유지하지 않으며, 스트림 전체 동안 퍼밋을 붙잡아 두면 장애 시 가용성이 급격히 떨어지는 현상을 막습니다. - 전송 계층 예외: Unix 소켓을 통한 비스트리밍 홉과 Bedrock 런타임 백엔드로 향하는 비스트리밍 홉은 전송 계층에 별도의 핸드셰이크 경계가 없으므로 호출 전체 동안 퍼밋을 유지합니다.
- 적용되는 폴백 경로: 다음 네 가지 폴백 경로 모두에 적용됩니다:
- 비스트리밍 퍼널 (홉 내부의 재시도마다 다시 다이얼하며 퍼밋을 새로 획득).
- 두
mid_stream_enabled모드의 프리스트림 연결 단계. - 네이티브 프로토콜 백엔드로 전환하는 홉.
- SSE 응답이 커밋된 뒤 스트리밍 릴레이가 수행하는 미드스트림 홉.
- 기본 요청 제외: 최초(primary) 시도는 폴백 다이얼 퍼밋을 쓰지 않으며, 첫 다이얼 전에 백엔드 선택 단계에서 이미 폴백 모델로 넘어간 요청도 퍼밋을 쓰지 않습니다(기본 백엔드가 이미 비정상으로 판정된 상태에서는 대체 백엔드가 사실상 기본 백엔드 역할을 하므로, 이를 제한하면 평상시 처리량까지 제한되기 때문입니다).
- 포화 시 대기열 및 체인 진행: 백엔드의 다이얼 퍼밋이 포화되면 요청은 FIFO 대기열에서 유효
timeouts.connection동안(스트리밍 경로에서는 시도 간 체인 전체 예산의 잔여 시간 한도 내에서) 대기합니다. 대기 시간이 만료되면 해당 백엔드에 대한 홉은backend_unhealthy트리거(기본 활성)로 실패 처리되어 체인의 다음 항목으로 넘어가거나, 더 이상 후보가 없으면 보존된 업스트림 실패로 종료됩니다. 네이티브 프로토콜 백엔드로 향하는 홉에서 발생한 포화 타임아웃도 동일하게backend_unhealthy로 체인을 진행시킵니다(모델 폴백 참고). - 서킷 브레이커 격리: 퍼밋은 서킷 브레이커 승인 이전에 획득하므로, 포화 타임아웃이 발생하더라도 half-open 프로브 슬롯을 소모하거나 서킷 브레이커에 실패를 기록하지 않습니다.
- 핫 리로드 및 메트릭: 제한값을 변경하면 백엔드별 세마포어가 즉시 재설정됩니다. 새 다이얼은 변경된 한도를 사용하고, 기존 세마포어를 보유 중이거나 대기 중인 다이얼은 기존 세마포어에서 정상 완료된 뒤 반납되므로 전환 도중에는 한 백엔드의 동시 퍼밋 수가 일시적으로
이전 한도 + 새 한도까지 늘어날 수 있습니다. 포화 타임아웃 횟수는fallback_dial_bound_saturated_total{backend}메트릭에 집계됩니다. - 누적기 잘림: 누적된 응답 내용이 100 KB를 초과하면, 일관성 없는 컨텍스트가 폴백 백엔드로 전송되지 않도록 연속 모드가 재시작으로 강제됩니다.
- 헬스 재확인: 체인의 각 폴백 시도 전에 백엔드 헬스를 다시 검증합니다. 비정상 백엔드는 건너뛰고 다음 항목으로 넘어갑니다.
[DONE]마커 누락:[DONE]없이 끝났지만finish_reason: "stop"이 있는 스트림은 정상 완료로 처리되어 불필요한 폴백을 방지합니다.
- 다이얼(연결) 구간만 제한: 퍼밋은 폴백 홉의 아웃바운드 요청을 보내기 직전에 획득하고, 프로바이더 핸드셰이크가 돌아오는 즉시(HTTP 상태 코드와 헤더 수신, 또는 전송 오류 발생 시) 반납합니다. 응답 본문을 수신하는 구간은 이미
메트릭¶
세 가지 Prometheus 메트릭이 미드스트림 폴백 활동을 추적합니다. 자세한 내용은 미드스트림 폴백 메트릭을 참고하세요.
장애 조치 지연 최소화¶
스트리밍 도중 백엔드가 다운되면, 폴백 백엔드가 이어받기까지 걸리는 시간은 여러 하위 시스템에 흩어진 설정 매개변수에 따라 달라집니다. 아래는 이 전환 지연을 최소화하기 위한 튜닝 가이드입니다.
장애 조치 지연의 구성¶
미드스트림 장애 조치 중 클라이언트가 기다리는 총 시간은 대략 다음과 같습니다:
각 구성 요소는 특정 설정에 대응합니다:
| 구성 요소 | 결정 요인 | 기본값 | 튜닝 대상 |
|---|---|---|---|
| 장애 감지 | 첫 청크 전에는 first_byte, 그 뒤에는 chunk_interval, 또는 TCP 읽기 오류 (즉시) | first_byte 120초, chunk_interval 30초 | chunk_interval 낮추기, 추론 모델이 아니라면 first_byte도 낮추기 |
| 헬스 재확인 | 폴백 시도 전 헬스 체크 | timeout: 5s | 낮게 유지 |
| 폴백 연결 | 폴백 백엔드로의 TCP 연결 + TLS 핸드셰이크 | connection: 10s | connection 낮추기 |
빠른 장애 조치를 위한 권장 설정¶
# 1. 타임아웃: 장애 조치 속도에 가장 큰 영향을 주는 설정
timeouts:
connection: 5s # 더 빠른 TCP 연결 타임아웃 (기본값: 10s)
request:
streaming:
first_byte: 30s # 첫 청크를 기다리는 시간 (기본값: 120s)
chunk_interval: 10s # 두 번째 청크부터의 최대 침묵 시간, 초과 시 실패로 간주 (기본값: 30s)
total: 600s # 전체 스트리밍 예산 (넉넉하게 유지)
# 2. 헬스 체크: 백엔드 장애를 선제적으로 감지
health_checks:
interval: 10s # 30초 대신 10초마다 확인 (기본값: 30s)
timeout: 3s # 헬스 체크를 더 빨리 실패 처리 (기본값: 5s)
unhealthy_threshold: 2 # 2회 실패 후 비정상으로 표시 (기본값: 3)
healthy_threshold: 1 # 1회 성공 후 복구 (기본값: 2)
warmup_check_interval: 1s # 백엔드 시작 중에는 빠르게 확인
# 3. 폴백 체인: 미드스트림 폴백이 활성화되려면 반드시 설정해야 함
fallback:
enabled: true
fallback_chains:
"gpt-5.6-sol":
- "gpt-5.6-terra"
- "gpt-5.6-luna"
fallback_policy:
trigger_conditions:
error_codes: [429, 500, 502, 503, 504]
timeout: true
connection_error: true
backend_unhealthy: true
# 4. 미드스트림 폴백: 연속 모드 (기본값: 활성화)
streaming:
mid_stream_fallback:
enabled: true # 연속 모드 사용 (기본값)
max_fallback_attempts: 3 # 복원력을 위해 더 많은 재시도 허용 (기본값: 2)
min_accumulated_tokens: 30 # 연속 vs 재시작 임계값 낮추기 (기본값: 50)
매개변수 영향 요약¶
| 매개변수 | 장애 조치 속도에 미치는 영향 | 트레이드오프 |
|---|---|---|
timeouts.request.streaming.chunk_interval | 높음: 첫 청크 이후 멈춘 스트림을 얼마나 빨리 감지할지 직접 제어 | 너무 낮으면 느린 모델 (예: thinking 단계가 긴 추론 모델)에서 오탐이 발생할 수 있음. 상한은 timeouts.limits.max_chunk_interval(기본값 60초, 절대 상한 300초)이며 모델별 오버라이드에도 적용됨 |
timeouts.request.streaming.first_byte | 높음: 첫 청크가 오기 전까지의 대기를 제한하며 요청은 받아 놓고 침묵하는 백엔드가 여기서 드러남 | 너무 낮으면 추론 모델을 thinking 도중에 끊음. 기본값 120초는 그런 모델에 맞춘 값이고 상한은 timeouts.limits.max_first_byte_timeout(480초) |
timeouts.connection | 중간: 폴백 백엔드로의 TCP 연결 지연을 제한 | 너무 낮으면 지연이 큰 네트워크에서 실패할 수 있음 |
health_checks.interval | 중간: 더 빨리 감지할수록 죽은 백엔드를 정상 후보 집합에서 더 빨리 제외 | 확인이 잦을수록 백엔드 부하 증가 |
health_checks.unhealthy_threshold | 중간: 더 적은 실패 횟수로 백엔드를 비정상으로 표시 | 값이 낮을수록 일시적 오류에 민감해짐 |
mid_stream_fallback.max_fallback_attempts | 낮음: 시도가 많을수록 복원력은 높아지지만 개별 전환 속도는 빨라지지 않음 | 시도가 많을수록 전역 타임아웃 예산을 더 많이 소비. SSE 출력이 시작된 뒤의 홉만 제한하며 첫 청크 이전의 홉은 fallback.fallback_policy.max_fallback_attempts를 따름 |
장애 감지 시나리오¶
장애 유형마다 감지 속도가 다릅니다:
| 장애 유형 | 감지 시간 | 메커니즘 |
|---|---|---|
| TCP 연결 재설정 / 백엔드 크래시 | 즉시 (< 1초) | 스트림 읽기 오류가 즉각 폴백을 트리거 |
| 백엔드가 5xx 오류 반환 | 즉시 (< 1초) | 스트리밍 시작 전 HTTP 상태 확인 |
| 백엔드가 요청을 받고도 첫 청크를 내보내지 않음 | first_byte (기본값 120초) | 스트림 첫 청크 기한 |
| 스트리밍 시작 후 백엔드 무응답 (멈춤) | chunk_interval (기본값 30초) | 스트림 비활성 타임아웃 |
| 백엔드가 오류 SSE 이벤트 전송 | 5회 오류 후 | 스트림 처리의 오류 횟수 임계값 |
| 응답 도중 백엔드 프로세스 종료 | 즉시 (< 1초) | TCP FIN/RST가 스트림 읽기 오류로 감지됨 |
프로덕션에서 가장 흔한 시나리오인 스트리밍 시작 후 백엔드 무응답은 chunk_interval이 좌우합니다. 요청을 받아 놓고 침묵하는 백엔드는 first_byte가 맡습니다. 두 제한은 모델별 오버라이드를 포함해 아래의 모든 스트리밍 경로에 적용됩니다. first_byte는 첫 청크까지의 대기를, chunk_interval은 그 뒤 청크 사이의 간격을 제한합니다:
- 일반 챗 릴레이와 thinking 패턴 릴레이
- HTTP Anthropic 백엔드의 응답을 OpenAI 호환 클라이언트에 전달하는 릴레이
- 미드스트림 폴백 릴레이
- Bedrock 트레이트 스트림 파이프라인과 Gemini 스트리밍 파이프라인
- Unix 소켓 챗, thinking 패턴, Anthropic 릴레이
지연에 민감한 애플리케이션에서는 chunk_interval을 10~15초로 낮추는 것이 좋고, 느린 모델에는 모델별 오버라이드를 두면 됩니다:
timeouts:
request:
streaming:
chunk_interval: 10s # 대부분의 모델에 빠른 감지
model_overrides:
gemini-2.5-pro: # 추론 모델에는 더 긴 간격 필요
streaming:
chunk_interval: 30s
first_byte: 120s
chunk_interval을 60초보다 크게 설정하면, streaming에 두든 model_overrides 항목에 두든, timeouts.limits.max_chunk_interval을 함께 올리기 전까지 로드 시점에 거부되며 오류에는 값이 is too large, maximum is 60s라고 나옵니다. 이 상한의 기본값은 60초이고 절대 상한은 300초입니다. 전체 타임아웃 상한은 검증 상한에 정리되어 있습니다.
발동한 타임아웃 확인¶
스트리밍 기한이 스트림을 끝내면, 클라이언트가 받는 오류에 발동한 제한과 적용된 값이 담깁니다. 오류의 type과 code는 그대로입니다. OpenAI 호환 경로는 timeout, Anthropic Messages 경로는 api_error이므로 기존 클라이언트는 변경 없이 동작합니다.
| 제한 | 메시지 (Backend stream timed out: 뒤) | error.details.timeout |
|---|---|---|
first_byte | the first_byte limit (120s) passed before the backend sent its first byte | first_byte |
chunk_interval | the chunk_interval limit (30s) passed with no data from the backend | chunk_interval |
total | the stream reached the total limit (600s) | total |
| 시도 간 폴백 예산 | the streaming fallback budget (1200s) ran out across attempts | fallback_budget |
error.details.timeout_ms에는 적용된 값이 밀리초 단위로 들어갑니다. 이 값은 해당 시도에 실제로 적용된 값이므로, 모델별 오버라이드와 폴백 홉의 배율이 반영됩니다. 미드스트림 폴백 릴레이에서는 메시지에 백엔드 이름도 들어갑니다 (Backend '<name>' stream timed out: ...). 폴백 홉이 시도 간 예산(timeouts.streaming_fallback_budget_multiplier 곱하기 total)에 걸려 먼저 끝나면 fallback_budget으로 보고합니다.
{"error": {"message": "Backend stream timed out: the chunk_interval limit (30s) passed with no data from the backend", "type": "timeout", "param": null, "code": "timeout", "details": {"timeout": "chunk_interval", "timeout_ms": 30000}}}
속도 제한¶
설정 가능한 다차원 속도 제한은 속도 제한 문서에서 다룹니다. 그와 별개로 Continuum Router는 모델 목록 엔드포인트에 대해 항상 켜져 있는 내장 보호를 제공하며 이 제한값은 고정되어 있습니다.
내장 /v1/models 보호¶
GET /v1/models는 클라이언트별로 속도가 제한됩니다:
| 제한 | 값 |
|---|---|
| 지속 | 분당 100 요청 |
| 버스트 | 5초 윈도우당 20 요청 |
이 리미터에서 클라이언트는 Authorization: Bearer API 키의 처음 16자로 식별되고 키가 없으면 클라이언트 IP 주소로 폴백합니다. 클라이언트마다 독립적인 할당량을 가집니다.
POST /v1/models/refresh (강제 새로고침)는 훨씬 빡빡한 예산을 갖습니다. 새로고침 한 번마다 캐시를 비우고 설정된 모든 백엔드로 fan-out하기 때문인데, 분당 12 요청에 5초 윈도우당 버스트 3회까지만 허용됩니다. 검증된 API 키만 키별 버킷을 받고 익명이거나 토큰이 유효하지 않은 호출자는 전역 버킷 하나를 공유하므로 헤더를 위조해도 새 할당량을 만들어낼 수 없습니다.
제한을 초과하면 엔드포인트가 버스트와 지속 중 어느 제한에 걸렸는지를 알려주는 메시지와 함께 429 Too Many Requests를 반환하고 거부 내역은 클라이언트 식별자와 함께 로그에 남습니다.
캐시 TTL 최적화¶
백엔드 장애 중 캐시 오염을 막기 위해, 빈 모델 목록은 처음에 5초만 캐시되고 (빈 응답이 연속되면 최대 60초까지 백오프), 일반 응답은 표준 모델 캐시 TTL을 사용합니다.
스마트 라우팅¶
스마트 라우팅은 요청의 복잡도와 도메인을 분류한 뒤, 설정 가능한 정책에 따라 가장 적합한 모델 티어로 요청을 보내는 기능입니다. 모델 티어 레지스트리(모델을 티어와 도메인에 매핑)와 규칙 기반 요청 분류기, 그리고 분류 결과를 라우팅 결정으로 변환하는 정책 엔진이 결합되어 동작합니다.
요청에서 model: "auto"를 사용하면 분류 → 정책 평가 → 티어 내 모델 선택 순으로 파이프라인이 실행됩니다. intercept_all: true로 설정하면 모든 요청에 동일한 파이프라인이 적용됩니다. LLM 또는 System One 분류기가 디스패치하는 모든 분류 호출은 파싱 실패 후 재시도까지 포함해 분류 대상 요청의 키와 request_id 아래 purpose: classifier 사용량 레코드로 계량되며 캐시된 분류는 디스패치하지 않으므로 기록도 하지 않습니다.
설정¶
smart_routing:
enabled: true
# 프로필 매칭 실패 및 자동 추론 불가 시 사용할 기본 티어.
# 1 = Flagship, 2 = Standard, 3 = Lightweight. 기본값 2.
default_tier: 2
# 스마트 라우팅을 트리거하는 가상 모델 이름. 기본값 "auto".
virtual_model: "auto"
# true로 설정하면 모델 이름과 관계없이 모든 요청에 스마트 라우팅이 적용됩니다.
intercept_all: false
# model_profiles 항목과 매칭되지 않는 모델: default_tier(기본값)는 추론된 티어나
# 기본 티어로 후보에 남기고, exclude는 후보에서 제외합니다.
unprofiled_models: default_tier
model_profiles:
# 정확한 모델 이름 매칭
- model: "gpt-5.6-sol"
tier: 1
domains: [general, code, reasoning, creative]
cost_per_1k_input_tokens: 0.005
cost_per_1k_output_tokens: 0.030
# 또 다른 정확한 매칭
- model: "gpt-5.6-terra"
tier: 2
domains: [general, code]
cost_per_1k_input_tokens: 0.0025
cost_per_1k_output_tokens: 0.015
# 글로브 패턴: Q4_K_M 양자화 모델 전체에 매칭
- model_pattern: "*-q4_K_M"
tier: 3
domains: [general]
# 라우팅 정책: 위에서 아래로 평가하며 첫 번째 매칭이 적용됩니다.
routing_policies:
- name: "trivial_to_lightweight"
when:
complexity: [trivial, simple]
domain: [general]
route_to:
tier: 3
- name: "code_to_flagship"
when:
domain: [code]
complexity: [moderate, complex, expert]
route_to:
tier: 1
prefer_domains: [code]
- name: "vision_required"
when:
requires: [vision]
route_to:
tier: 1
require_capabilities: [vision]
- name: "complex_to_flagship"
when:
complexity: [complex, expert]
route_to:
tier: 1
- name: "default_to_standard"
when: {} # 항상 매칭되는 캐치올
route_to:
tier: 2
모델 프로파일 비용 필드¶
cost_per_1k_input_tokens와 cost_per_1k_output_tokens는 둘 다 토큰 1,000개당 USD 값입니다. 두 필드 모두 선택 사항이지만 값을 적을 때는 유한하고 0 이상이어야 합니다. 0은 유효한 값이고 무료 모델을 뜻하는 반면 음수, .nan, .inf, -.inf는 설정 로드 시점과 PUT /admin/smart-routing/model-profiles 양쪽에서 거부됩니다.
거부는 경고가 아니라 하드 에러입니다. 이런 값이 들어간 config.yaml은 문제가 된 프로파일을 지목하는 메시지와 함께 기동 단계에서 실패하고 핫 리로드에서는 이전 설정이 그대로 유지됩니다.
같은 검증 호출은 길이 제한도 함께 검사하여 model과 model_pattern은 config.yaml과 어드민 API 모두에서 각각 200자 이하여야 합니다.
모델 선택에 반영되는 값은 입력 비용뿐입니다. 스코어러는 cost_per_1k_input_tokens에 최대 0.3점을 배정하므로 같은 티어에서 다른 조건이 같다면 입력 비용이 낮은 쪽이 선택됩니다. 필드를 생략한 프로파일은 중립값인 0.15점을 받습니다. cost_per_1k_output_tokens는 참고용입니다. 어드민 API와 WebUI가 값을 그대로 돌려주고 검증도 동일하게 받지만 라우팅 판단에는 쓰이지 않습니다.
티어 분류¶
| 티어 | 값 | 의미 | 대표 예시 |
|---|---|---|---|
| Flagship | 1 | 최고 성능, 최고 비용 | gpt-5.6-sol, claude-opus-5, gemini-3.1-pro |
| Standard | 2 | 성능과 속도의 균형 | gpt-5.6-terra, claude-haiku-4-5 |
| Lightweight | 3 | 속도와 저비용에 최적화 | llama-3-8b, phi-3-mini, 양자화 모델 |
도메인 전문화 태그¶
| 태그 | 설명 |
|---|---|
general | 특정 전문 분야 없음 |
code | 코드 생성, 디버깅, 리뷰 |
reasoning | 복잡한 다단계 추론, 수학 |
creative | 창작 글쓰기, 스토리텔링 |
multilingual | 번역 등 언어를 넘나드는 작업. 의도 신호가 없을 때 규칙 분류기는 primary_language가 아닌 문자 체계의 텍스트에도 이 태그를 대신 붙입니다 |
vision | 이미지 이해 |
자동 추론¶
명시적 프로필이나 글로브 패턴에 매칭되는 모델이 없으면 라우터가 다음 순서로 티어를 자동 추론합니다.
-
가격 (
model-metadata.yaml기준,pricing은 1M 토큰당 USD): 입력 토큰 100만 개당 $3 이상이면 Flagship, $0.50 이상이면 Standard, 그 미만이면 Lightweight. 무료 모델은 가격 기반 추론을 건너뜁니다. -
기능 (
model-metadata.yaml기준):vision,reasoning,audio,video,function_calling,tool중 3개 이상이면 Flagship, 1개 이상이거나 전체 기능이 3개 이상이면 Standard. -
이름 휴리스틱:
pro,ultra,opus,sonnet,turbo키워드가 포함되면 Flagship;mini,small,tiny,nano,lite,flash,haiku및 양자화 마커(q4_,q5_,q8_,gguf,gptq,awq)가 포함되면 Lightweight.
자동 추론 결과는 모델 ID별로 최대 10,000개까지 캐시됩니다. 핫 리로드 시 또는 /admin/smart-routing/model-profiles PUT 엔드포인트 호출 시 캐시가 초기화됩니다.
후보 풀과 프로필 없는 모델¶
스마트 라우팅은 라우터가 집계한 정상 모델 가운데서 대상을 고르며 사용자용 모델 목록과 같은 가시성 규칙으로 걸러냅니다. 모든 백엔드가 internal: true이거나 enabled: false인 모델은 후보가 되지 않으며 공개 백엔드도 있는 모델은 공개 백엔드만으로 상태를 확인합니다. 클라이언트 요청에서는 키별 모델·백엔드 허용 목록과 회로 허용 여부를 정책 선택 전에 교차하고 디스패치에서 다시 검사합니다. decisions는 광고하지만 text나 chat은 광고하지 않는 모델은 생성 후보가 되지 않으며 이중 capability 모델은 일반 적격성 게이트를 따릅니다. internal: true 백엔드는 classifier.llm.backend로 지정하면 여전히 LLM 분류기용으로 쓸 수 있습니다.
unprofiled_models는 어떤 model_profiles 항목(정확한 model 또는 model_pattern, 로컬 설정이나 Continuum Hub 정책, PUT /admin/smart-routing/model-profiles로 추가한 항목)과도 매칭되지 않는 모델을 어떻게 다룰지 정합니다.
| 값 | 동작 |
|---|---|
default_tier(기본값) | 자동 추론된 티어로, 추론이 결론을 내지 못하면 default_tier로 후보에 남습니다. |
exclude | 티어 선택에서 제외합니다. 프로필이 있는 모델로만 라우팅하며 자동 추론만으로는 후보가 되지 않습니다. |
exclude에서 정책이 고른 티어에 프로필이 있는 모델이 없으면 기존 티어 폴백대로 다른 티어를 찾습니다. 이 옵션은 smart_routing의 다른 필드와 함께 즉시 다시 로드됩니다. 스마트 라우팅이 활성이고 exclude인데 model_profiles가 비어 있으면 Hub 정책이 프로필을 지정하지 않는 한 모든 모델이 제외되므로, continuum-router config validate가 smart_routing.unprofiled_models에서 경고합니다. GET /admin/smart-routing/status는 후보 풀을 candidate_models(각 항목의 model, tier, source: explicit_exact, explicit_pattern, auto_inferred, default)로, 프로필 없는 제외를 excluded_unprofiled_models로, 결정 전용 출력 계약 제외를 excluded_incompatible_models로 보여 줍니다.
글로브 패턴 문법¶
*만 와일드카드로 사용하며 여러 개를 중첩할 수 있습니다.
| 패턴 | 매칭 | 미매칭 |
|---|---|---|
gpt-* | gpt-5.6-sol, gpt-5.6-terra | claude-3 |
*-q4_K_M | llama-3-8b-q4_K_M | llama-3-8b-q5_K_M |
gpt-*-turbo | gpt-4-turbo, gpt-3.5-turbo | gpt-5.6-sol |
* | 모든 문자열 | 해당 없음 |
요청 분류기¶
규칙 기반 분류기는 11가지 신호를 분석해 복잡도 수준, 도메인 태그, 필요 기능, 신뢰도 점수로 구성된 ClassificationResult를 생성합니다.
복잡도 수준¶
| 수준 | 설명 | 예시 |
|---|---|---|
trivial | 인사말, 예/아니오, 단순 사실 조회 | 2+2는? |
simple | 짧은 설명, 기본 요약 | 이 문단을 요약해줘 |
moderate | 다단계 추론, 중간 수준의 코드 작업 | 이 함수를 리팩토링해줘 |
complex | 고급 알고리즘, 시스템 설계 | 분산 캐시를 설계해줘 |
expert | 연구 수준 문제, 형식적 증명 | $\sqrt{2}$가 무리수임을 증명해줘 |
규칙 분류기의 expert 상한¶
규칙 기반 분류기는 대부분의 expert 요청을 길이가 길 때만 알아봅니다. 복잡도 점수는 신호별 비율의 가중 평균이고 0.15, 0.35, 0.55, 0.75를 경계로 나뉘며 expert가 되려면 0.75 이상이어야 합니다. 평균은 가장 강한 항을 넘을 수 없는데 대부분의 항은 0.75 아래에서 멈춥니다. 각 항의 가중치와 최대 비율은 다음과 같습니다.
| 항 | 가중치 | 최대 비율 |
|---|---|---|
message_length | 추정 200토큰까지 0.15, 그 이상 0.3 | 50토큰까지 0.25, 200까지 0.4, 500까지 0.6, 그 이상 0.8 |
complexity_keywords (complex) | 0.25 | 서로 다른 키워드 네 개에서 0.8 |
| 코드 근거 | 0.2 | 펜스 블록 0.75, 인라인 코드나 코드 의도 0.5 |
math_notation | 0.2 | 복잡도 키워드가 함께 있으면 1.0, 없으면 0.5 |
system_prompt_complexity | 0.15 | 0.6 |
tool_definitions | 0.15 | 0.667 |
분류기는 현재 턴만 채점하므로(아래 '규칙 분류기가 채점하는 범위' 참고), 길이 구간별 최선의 경우는 어떤 요청에서나 다음과 같습니다.
- 추정 200토큰 미만(시스템·개발자 메시지와 마지막 사용자 메시지 합산)이고 수식 표기가 없으면 점수는
(0.4*0.15 + 0.8*0.25 + 0.75*0.2) / 0.6 = 0.683을 넘지 못합니다. - 추정 50토큰 미만이면 수식 표기를 포함해 모든 항이 최대여도
(0.25*0.15 + 0.8*0.25 + 0.75*0.2 + 1.0*0.2) / 0.8 = 0.734를 넘지 못합니다. -
시스템 프롬프트와 도구 정의는 최대값이 0.75보다 낮은 항을 더하므로, 에이전트형 요청의 점수를 올리지 않고 끌어내립니다. 대화의 이전 턴은 아무 항도 더하지 않습니다.
-
수식 표기가 없을 때: 추정 길이가 500토큰을 넘는 요청에서만
expert에 도달합니다. - 수식 표기가 있을 때: 아래의 형식적 증명 하한 외에, 추정 51~200토큰이면서 수식 표기와 서로 다른 complex 키워드 네 개 이상을 포함한 요청이
(0.4*0.15 + 0.8*0.25 + 1.0*0.2) / 0.6 = 0.767로expert에 도달할 수 있습니다(펜스 코드 블록이 있으면0.7625, 키워드가 세 개면 최대0.747). 단, 같은 요청에 인라인 코드나 코드 의도가 함께 잡히면 비율 0.5인 항이 추가되어 점수가 다시 0.75 아래로 내려갑니다.
따라서 새로운 아키텍처나 연구 수준의 설계 질문처럼 짧고 어려운 요청은 높아야 complex, 대개 moderate로 분류됩니다. 이는 가중치 조정의 문제가 아니라 사용 가능한 신호의 한계입니다(예: expert로 라벨링된 비잔틴 합의 프로토콜 설계 요청과 complex로 라벨링된 레이트 리미터 설계 요청은 신호 벡터가 동일하여 어떤 가중치나 경계로도 구분할 수 없습니다).
형식적 증명 예외 (expert 하한)¶
형식적 증명 요청은 텍스트 형태만으로 식별할 수 있으므로 예외적으로 길이와 관계없이 expert 하한이 적용됩니다. 마지막 사용자 메시지에 수식 표기(LaTeX 매크로 또는 수식 구분자 두 종류)와 증명 표현이 함께 있고, 증명에 대해 묻는 질문이 아니라 증명을 요청하는 경우 다음 규칙에 따라 expert로 분류됩니다.
| 구분 | 판정 기준 및 예외 |
|---|---|
| 인정되는 증명 표현 | 온전한 단어로서의 prove, proof, disprove, disproof(-s, -ing 형태 포함), 문장을 시작하는 show that(please나 now 뒤 허용), 증명, 그리고 명사형 서술어(임, 함, 됨, 짐, 음, 움, 큼, 예: 무리수임을 보여라, 수렴함을 보이시오, 같음을 보여줘) 또는 -는 것 절(무리수라는 것을 보이시오) 뒤에 을 보이/을 보여가 오는 한국어 구문. (demonstrate는 demonstrate how 등 일반 용법이 흔하므로 제외.) |
| 증명 요청 vs 증명에 대한 질문 | simple 키워드가 complex 키워드보다 많으면 증명에 대해 묻는 요청으로 보아 하한을 적용하지 않습니다. 이때 증명 표현은 두 언어 모두 요청 동사(prove, disprove, 문두의 show that, 증명해줘/증명하시오, 을 보이)일 때만 complex 키워드 하나로 세고, 명사(proof, 증명을)는 세지 않습니다. 따라서 Explain this proof of ...나 이 증명을 설명해줘 ...는 원래 점수를 유지하고, Prove ... and explain each step이나 증명하고 각 단계를 설명해줘에는 expert 하한이 적용됩니다. |
| 한국어 복합어 및 경계 예외 | 증명 뒤에 다른 명사를 만드는 음절이 오거나(증명서, 증명사진), 앞 음절이 증으로 끝나는 다른 한자어를 닫으면서(검, 인, 보, 입, 반, 논, 확, 실, 위, 고, 공, 사, 방, 예 및 의학 용어 병, 유, 염, 통, 울, 면, 포, 박, 혈, 핍, 예: 검증명령, 인증명세, 합병증명단) 뒤에 동사 어미가 오지 않으면 제외합니다(동사 어미가 붙은 보이고증명하시오나 증명 복합어 존재증명, 귀납증명, 직접증명은 인정). 받침만 ㅁ인 일반 명사(이 그림을 보여줘, 교점을 보여줘, 덧셈을 보여줘, 관심을 보이는)와 대명사(이것을 보여줘, 모든 것을 보여줘)도 제외합니다. |
| 알려진 과잉 인식 | 지시 한정사로 쓰인 문두의 show that(Show that step again with $\frac{a}{b}$.)과 마지막 음절이 우연히 명사형 어미와 같은 명사(게임, 얼음)가 수식과 함께 쓰이면 증명 표현으로 인식됩니다. |
| 평가 범위 및 부하 관리 | 수식과 키워드 모두 마지막 사용자 메시지에서만 해당 메시지의 언어 표로 평가합니다. 시스템 프롬프트나 이전 턴의 수식·키워드(what is, implement 등)는 영향을 주지 않으며, 수식 없이 문장으로만 쓴 짧은 증명 요청(이 정리를 증명해줘)에는 하한이 적용되지 않습니다. 부하 관리에서 reject_expert를 켠 경우(smart_routing.load_management.degradation, 기본값 꺼짐) 짧은 증명 요청도 expert로 분류되어 해당 부하 상태에서 거부됩니다. |
분류 방식별 영향 및 검증 경고¶
이 상한은 라우터가 규칙 분류 결과를 사용하는 모든 경로에 영향을 줍니다.
분류기 method | 짧고 어려운 요청이 expert에 도달할 수 있는가 |
|---|---|
rule | 형식적 증명, 또는 추정 51~200토큰에 수식 표기와 complex 키워드 네 개 이상이 함께 있는 경우에만 도달합니다. |
hybrid, hybrid_system_one | 규칙 신뢰도가 rule.confidence_threshold 미만이어서 에스컬레이션될 때만 도달합니다. 펜스 코드나 LaTeX가 포함된 어려운 요청은 대개 기본 임계값(0.7)을 넘어 규칙 결과를 그대로 사용합니다. |
llm, system_one | 도달할 수 있습니다. 규칙 결과는 모델 호출 실패, 부하에 따른 분류기 비활성화, 또는 System One 응답의 min_confidence 미달 시 폴백으로만 쓰입니다(단, llm이나 hybrid에서 LLM 분류기 백엔드를 해석할 수 없으면 모든 요청이 규칙 결과로 라우팅됩니다). |
custom_complexity_levels는 LLM 분류기와 System One 분류기만 반환할 수 있으므로 규칙 분류기의 상한을 바꾸지 않습니다.
continuum-router config validate는 expert 전용 정책이 실제로는 도달하기 어려운 설정일 때 smart_routing.routing_policies[N].when.complexity 위치에 경고를 출력합니다.
- 경고가 발생하는 경우: 스마트 라우팅이 켜져 있고
method가rule,hybrid,hybrid_system_one중 하나(또는 분류기 백엔드가 없는llm/hybrid)일 때complex없이expert만 매칭하는 정책. - 경고하지 않는 경우:
- 하이브리드 모드에서
rule.confidence_threshold가 규칙 분류기의 최대 신뢰도(0.95)보다 높아 모든 요청이 에스컬레이션되는 경우. - 정책에
[complex, expert]를 함께 지정하여 상한에 걸린 요청이complex를 통해 동일한 정책으로 라우팅되는 경우.
- 하이브리드 모드에서
어려운 요청을 길이가 아닌 난이도 기준으로 라우팅하려면 정책에 [complex, expert]를 함께 지정하거나 분류기 method를 llm 또는 system_one으로 설정하십시오.
규칙 분류기가 채점하는 범위¶
규칙 분류기는 대화 전체가 아니라 현재 턴(system, developer 메시지와 마지막 사용자 메시지)만 채점합니다. 이전 사용자·어시스턴트 턴과 마지막 사용자 메시지 뒤에 오는 내용(어시스턴트 prefill, 도구 실행 결과)은 복잡도, 도메인, 신뢰도 계산에서 제외됩니다. 이는 LLM 분류기 및 System One 분류기가 읽는 범위와 동일하므로, hybrid의 에스컬레이션 판정과 모든 분류 방식의 규칙 기반 폴백이 일관된 입력을 기준으로 동작합니다. 따라서 맥락에 의존하는 짧은 후속 요청(예: 이제 파일 전체에 똑같이 해줘)은 그 문장 자체를 기준으로 분류됩니다.
단, 다음 두 항목은 예외입니다.
required_capabilities: 이전 턴에 첨부된 이미지도 비전 백엔드가 필요하고tools는 요청 전체 필드이므로 대화 전체를 기준으로 판단합니다.language:when.language조건 매칭과 키워드 표 선택에 쓰이는 언어는 마지막 사용자 메시지에서 감지합니다(영어 대화 뒤에 이어진 한국어 질문은ko로 분류). 마지막 사용자 메시지에 글자가 전혀 없을 때(??등)만 이전 사용자 턴 전체에서 언어를 감지합니다.
분류 신호¶
| 신호 | 감지 내용 |
|---|---|
message_length | 시스템·개발자 메시지와 마지막 사용자 메시지의 추정 토큰 수 |
code_blocks | 펜스 코드 블록 또는 인라인 코드 |
math_notation | LaTeX, 수식, 수학 기호 |
system_prompt_complexity | 시스템 프롬프트의 길이와 복잡도 |
image_attachments | 대화의 모든 메시지에 있는 멀티모달 이미지 콘텐츠 |
tool_definitions | 요청의 도구/함수 정의 |
complexity_keywords | 최적화, 설계, 증명 같은 키워드 |
multilingual | 비라틴 문자. primary_language 언어에서는 억제됩니다. 의도가 아니라 대체 신호입니다 |
creative_markers | 소설, 시, 상상해줘 같은 표현 |
analysis_markers | 분석해줘, 비교해줘, 평가해줘 같은 표현 |
code_intent | 코드 마크업 없이 코드 마커가 둘 이상 잡힌 경우 |
translation_intent | 번역 요청: 단어로 쓰인 translate(translation, translateX, untranslated 제외), 번역(번역가, 번역자, 번역기, 번역본, 번역서 제외), 翻訳, 翻译, 翻譯 |
감지된 신호가 많을수록 복잡도와 도메인 점수가 올라가며, 상충하는 신호(예: 창작 마커와 코드 마커가 동시에 존재)가 있으면 신뢰도 점수가 낮아집니다.
도메인은 가장 강한 의도 신호(code_blocks, inline_code, math_notation, creative_markers, analysis_markers, code_intent, translation_intent)로 결정됩니다. multilingual은 의도가 아니라 문자 체계 관찰 결과이므로 의도 신호가 하나도 없을 때만 폴백 도메인으로 선택됩니다.
translation_intent및multilingual도메인:multilingual태그는 번역 등 교차 언어 작업을 나타내며, LLM 분류기와 System One 분류기도 번역 요청에 이 태그를 반환합니다.translation_intent는primary_language설정에 의해 억제되지 않으므로when.domain: [multilingual]은 어떤 배포 환경에서든 영어, 한국어, 일본어, 중국어 번역 요청과 매칭됩니다.- 신호 강도(
0.72)는 창작 마커 1개나 분석 마커 2개보다 높고code_intent(0.75),math_notation(0.8), 펜스 코드 블록(0.9)보다는 낮습니다. 따라서 시 번역은multilingual이 되지만 펜스 코드, 코드 마커 2개 이상, 또는 수식이 포함된 번역 요청은code나reasoning으로 유지됩니다. - 다른 의도 신호와 마찬가지로 규칙 신뢰도를 높이므로 짧은 번역 요청도 기본 하이브리드 임계값(
0.7)을 넘습니다. - 문맥 의미를 해석해야만 구별할 수 있는 영어 비유 표현(
this translates to a 2x speedup), 한국어 주제 용법(기계 번역 품질을 분석해줘), 코드 마커가 1개뿐인 비펜스 코드 번역 요청(Translate this Python function to Rust.)은multilingual로 분류됩니다.
code_intent:- 코드 블록이나 인라인 백틱 없이 코드 작성을 요청하는 프롬프트(예:
이 REST API의 rate limiting 로직을 구현해줘.)를 감지합니다. 요청에 이미 펜스 코드 블록이나 인라인 백틱이 있으면 이 신호는 건너뜁니다. code키워드 목록에서 서로 다른 두 개 이상의 마커가 매칭되어야 발화합니다. 동작 마커만 보면Write a poem about autumn leaves가, 대상 마커만 보면What is a REST API?가 코드 도메인으로 오분류되기 때문입니다.- 신호 강도(
0.75)는math_notation(0.8)보다 낮아 수식 위주 요청을reasoning으로 유지하고, 문자 체계 기반multilingual(0.7)보다 높아 마크업 없는 한국어 코딩 요청이multilingual로 빠지지 않게 합니다.
- 코드 블록이나 인라인 백틱 없이 코드 작성을 요청하는 프롬프트(예:
언어 인식¶
분류기는 사용자 메시지 본문의 언어를 감지해 분류 결과에 실어 보냅니다. 감지는 문자 체계를 기준으로 합니다. 한글은 ko, 가나는 ja, 가나 없는 한자는 zh, 라틴 문자는 en, 나머지는 und(미확정)입니다. 한글과 가나는 글자의 10분의 1만 있어도 확정하므로, 영어 기술 용어를 그대로 인용한 한국어 요청도 한국어로 감지됩니다.
감지한 언어는 복잡도·창작·분석 신호가 대조할 키워드 표를 고릅니다. 영어와 한국어 표가 기본 탑재되어 있고 모든 표는 영어 키워드를 포함합니다. 따라서 여러 언어가 섞인 요청도 영어 목록만 있었을 때 잡히던 신호를 그대로 잡습니다. 자체 표가 없는 언어는 영어 표로 돌아갑니다.
요청 길이는 UTF-8 바이트가 아니라 문자 수를 문자 체계별로 가중해 추정합니다(한글 한 음절과 한자 한 글자는 라틴 문자 두 개 반으로 셉니다). 이 가중치는 한국어 요청과 그 영어 번역의 추정 길이가 평균적으로 같아지도록 정한 값입니다. 저장소의 영어·한국어 짝 픽스처에서 한국어 쪽은 라틴 문자 두 개 기준일 때 12.9% 짧게, 두 개 반 기준일 때 1.6% 길게 추정되었습니다. 다만 짧은 짝 하나는 한쪽 표현에 단어가 하나 더 있으면 message_length 단계 경계를 사이에 두고 갈릴 수 있습니다.
주 언어¶
classifier.rule.primary_language는 이 배포판의 트래픽이 보통 어떤 언어로 오는지를 짧은 BCP-47 태그로 지정합니다(ko, ko-KR, ja, fr. 대소문자를 가리지 않고 첫 서브태그만 읽습니다).
이 값을 지정하면 해당 언어 요청에서 multilingual 신호를 억제합니다. 이 신호는 비ASCII 비중이 높은 텍스트면 무조건 발생하므로 한국어 배포판에서는 평범한 한국어 요청마다 켜졌고 의도 신호가 전혀 없는 한국어 인사말은 multilingual로, 같은 뜻의 영어 인사말은 general로 갈렸습니다. 주 언어를 지정한다는 것은 그 언어로 쓰인 텍스트 자체가 번역 작업은 아니라는 뜻이고 그런 요청은 의도만으로 분류됩니다. 다른 언어 요청에서는 신호가 그대로 유지됩니다.
라틴 문자 텍스트가 쓸 키워드 표도 이 값이 고릅니다. 감지는 라틴 문자를 전부 en으로 보고하기 때문입니다. primary_language: fr로 지정해야 keywords의 fr 항목이 실제로 쓰입니다. 감지가 스스로 구분하는 태그(ko, ja, zh)는 감지 결과를 덮어쓰지 않습니다. 한국어 배포판에 들어온 라틴 문자 텍스트는 그대로 영어 표를 씁니다.
선택 항목입니다. 지정하지 않으면 모든 언어가 multilingual 신호를 유지합니다.
사용자 정의 키워드¶
classifier.rule.keywords로 언어별 키워드를 추가합니다. 기본 표를 대체하지 않고 합산하며, 모든 언어는 영어 기본 키워드를 함께 물려받습니다. 값은 앞뒤 공백을 제거하고 소문자로 정규화하며 중복은 제거됩니다. 기본 표가 없는 언어 태그(fr, de)를 지정하면 영어 표 위에 새 표가 만들어지며, primary_language를 같은 태그로 지정해야 해당 표가 선택됩니다.
기본 키워드와 사용자 정의 키워드는 모두 동일한 규칙으로 대조됩니다.
- 단일 단어 키워드: 단순 부분 문자열 매칭으로 동작합니다(예: 한국어 어간
요약은요약해줘에 매칭되고,poem은poems에 매칭됨). - 다중 단어 키워드(간격 매칭): 각 단어가 한 문장 안에 순서대로 나타나고, 인접한 단어 사이에 토큰이 최대 두 개까지만 끼어 있을 때 매칭됩니다(예:
write a story는write a short story에,시를 써는시를 하나 써줘에 매칭됨). 문장 종결 부호(.,!,?, 줄바꿈)는 범위를 닫으므로 문장 경계를 넘어서는 매칭되지 않습니다. - 토큰 경계 및 어미 예외: 다중 단어 키워드의 각 단어는 토큰 양끝의 문장부호를 제외한 전체 토큰과 일치해야 합니다. 단, 마지막 단어가 비라틴 문자로 끝나면 같은 토큰 안에서 뒤에 글자가 더 붙는 것을 허용합니다(예:
써가써줘에 매칭됨). 앞 단어에는 이 예외를 적용하지 않아시 한 편의시가시스템에 잘못 매칭되는 일을 방지하며, 라틴 문자 단어에도 적용하지 않습니다. - 관용구 인접 제한: 첫 단어와 마지막 단어가 모두 라틴 문자 기능어(
a,the,is,what,once,upon등)인 마커는 간격 규칙을 적용하지 않고 인접해 있을 때만 매칭합니다(once upon이Once agreed upon, the schema is frozen에,what is가What throughput is achievable here에 오매칭되는 것을 방지). 바깥쪽 두 단어만 확인하므로write a story는 간격 규칙이 그대로 적용됩니다. - 수식어와 복합명사 한계: 간격 규칙은 수식어와 복합명사를 구분하지 않으므로
Write a short story와Write a user story가 모두creative로 분류됩니다. 이런 표현이 트래픽에 자주 등장해 라우팅이 어긋난다면 더 구체적인 구문으로 대조하십시오.
기본 탑재된 한국어 키워드는 단순 의문사가 아니라 영어 표의 요청 표현에 대응하도록 형태를 구분해 집계합니다.
| 한국어 키워드 | 매칭 조건 (집계함) | 제외 조건 (집계하지 않음) |
|---|---|---|
설명 (simple) | 요청 형태 (설명해, 설명하세요, 설명하시오, 설명 부탁) | 명사 또는 평서형 서술어 (설명:, 설명한다) |
무엇 (simple) | 영어 what is에 대응하는 문미 서술어(무엇인가(요), 무엇입니까, 무엇이야, 무엇일까) 및 (이)란 뒤의 무엇이며 (이것은 무엇입니까 포함) | 주어가 의존명사 것인 선택 질문(…것은 무엇입니까, 영어 which), 과거형(무엇이었는가), 목적어·주어 격조사 결합(무엇을, 무엇이), 부정대명사(무엇인가를) |
어떻게 (simple) | 영어 how do I에 대응하여 같은 문장 내 최대 두 단어 뒤에 현재형 의문·요청 동사(설치하나요, 해야, 하면, 해줘)가 오는 경우 | 과거형 동사(어떻게 바꾸었는가, 어떻게 되었나요), 인사말(어떻게 지내세요), 관용구(어떻게 보면) |
정의 (simple) | 독립 명사 및 동사 파생형(정의하시오) | 속격 조사 경계(과정의, 영어 of the process)에 걸친 경우 |
증명 (complex) | 동사 형태(증명해줘, 영어 prove에 대응) | 명사 용법(이 증명을 설명해줘) |
smart_routing:
classifier:
rule:
# 이 배포판 트래픽의 언어. 해당 언어의 `multilingual` 도메인 신호를
# 억제합니다. 선택 항목입니다.
primary_language: ko
# 기본 표에 더할 키워드. 선택 항목입니다. 여기에 `fr` 항목을 두려면
# `primary_language: fr`로 지정해야 그 표가 선택됩니다.
keywords:
ko:
complex: ["커널 스케줄러"]
creative: ["판소리 사설"]
# `code` fires only when two distinct entries match, so add
# entries in pairs: an action and an object.
code: ["배포", "헬름 차트"]
두 항목 모두 핫 리로드됩니다. 값이 바뀌면 규칙 분류기를 즉시 재구성합니다.
LLM 기반 분류기¶
규칙 기반 분류기는 빠르지만 일부 요청은 분류가 모호합니다. 정확도를 높여야 할 때, LLM 기반 분류기는 소형 저비용 모델에 분류 요청을 보내고 System One 분류기(아래)는 결정 모델에 타입이 지정된 질문 묶음을 대신 묻습니다. classifier.method로 다섯 가지 동작 방식을 선택할 수 있습니다.
| 방식 | 동작 |
|---|---|
rule | 규칙 기반만 사용 (기본값). LLM 호출 없음. |
llm | 항상 LLM 분류기를 호출하고 실패 시 규칙 기반으로 폴백. |
hybrid | 규칙 기반을 먼저 실행하고 신뢰도가 confidence_threshold 미만일 때만 LLM으로 에스컬레이션. |
system_one | 항상 System One 결정 모델을 호출하고 실패하거나 신뢰도가 system_one.min_confidence 미만이면 규칙 기반으로 폴백. System One 분류기 참고. |
hybrid_system_one | 규칙 기반을 먼저 실행하고 규칙 신뢰도가 confidence_threshold 미만이면 System One을, System One 신뢰도가 min_confidence 미만이면 (설정된 경우) LLM 분류기를 사용하며 그 외에는 규칙 결과를 사용. |
하이브리드 모드(hybrid) 동작 및 주의사항:
- 임계값 게이트: 규칙 분류 신뢰도가
rule.confidence_threshold(기본값0.7) 이상이면 규칙 결과로 즉시 라우팅하고, 미만이면 LLM 분류기로 에스컬레이션합니다. - 신뢰도 점수의 특성: 규칙 신뢰도는 라벨이 맞을 확률이 아니라 감지된 신호 개수를 집계합니다(정확도 데이터셋 기준 복잡도 AUROC
0.46). 따라서 에스컬레이션 비율은 요청의 실제 모호함이 아니라 신호 밀도에 좌우됩니다.- 라벨링된 정확도 데이터셋에서는 요청의
53.7%가 에스컬레이션됩니다. - 영어 및 한국어 공개 벤치마크 프롬프트에서는
91%~99%가 기본 임계값에 미달하여 매번 LLM 분류기 왕복 지연을 치릅니다.
- 라벨링된 정확도 데이터셋에서는 요청의
- 운영 권장사항: 프로덕션에서 하이브리드 모드를 신뢰하기 전에
smart_routing_hybrid_gate_total(메트릭) 지표로 실제 에스컬레이션 비율을 확인하십시오.- 에스컬레이션 비율이 지나치게 높다면
method: rule과shadow: true로 LLM 왕복 없이 System One을 규칙 결과와 비교 측정할 수 있습니다(단, 각 요청은 여전히 결정 모델 호출을 기다립니다). - 또는
hybrid_system_one을 사용하여 보정된min_confidence를 가진 결정 모델을 게이트 뒤에 배치하면 전체 LLM 왕복 대신 결정 호출 1회의 지연(측정된 p5047 ms~291 ms)만 부담합니다(min_confidence미달 시에는 LLM 분류기가 설정되어 있으면 LLM으로 넘어가며, 한국어 위주 트래픽에서는 System One이 규칙 분류기보다 높은 정확도를 보인 적이 없습니다. 결정 모델 참고).
- 에스컬레이션 비율이 지나치게 높다면
smart_routing:
enabled: true
classifier:
# 분류 방식: "rule" (기본값), "llm", "hybrid"
method: hybrid
rule:
# 하이브리드 모드에서 LLM 에스컬레이션을 트리거하는 신뢰도 임계값.
# 범위: 0.0 ~ 1.0. 기본값: 0.7.
confidence_threshold: 0.7
# 이 배포판 트래픽의 언어를 짧은 BCP-47 태그로 지정합니다. 해당
# 언어의 `multilingual` 도메인 신호를 억제합니다. 선택 항목이며
# 위 "언어 인식" 절을 참고하세요.
primary_language: ko
# 기본 표에 더할 언어별 분류 키워드. 선택 항목입니다.
keywords:
ko:
creative: ["판소리 사설"]
# `code` fires only when two distinct entries match, so add
# entries in pairs: an action and an object.
code: ["배포", "헬름 차트"]
llm:
# 분류에 사용할 모델. 빠르고 저렴한 모델이면 충분합니다.
model: "gpt-5.6-terra"
# 분류 요청을 보낼 백엔드 이름. 설정된 백엔드여야 합니다.
# 생략하면 일반 사용자 트래픽이 닿을 수 있는 첫 번째 백엔드를 쓰며,
# `internal: true`나 `enabled: false` 항목은 건너뜁니다.
# 아래 "분류기 백엔드 선택" 참고.
backend: "openai-fast"
# 분류 요청의 최대 허용 시간 (밀리초).
timeout_ms: 2000
# 분류기에 전송하는 최대 입력 토큰 수 (초과 콘텐츠는 잘림).
max_input_tokens: 500
# 파싱 실패 후 재시도 횟수 (0 또는 1). 기본값: 1.
max_retries: 1
# 분류 온도. 0.0이면 결정적 출력.
# 범위: 0.0 ~ 2.0. 기본값: 0.0.
temperature: 0.0
# 분류 응답의 최대 출력 토큰 수.
max_output_tokens: 150
# 구조화 출력 전략. "auto"는 백엔드 유형에 따라 최적 방식을 선택합니다:
# json_schema (OpenAI/vLLM), tool_use (Anthropic), json_object (Ollama/Gemini/LM Studio),
# prompt_only (기타).
structured_output: auto # auto | json_schema | tool_use | json_object | prompt_only
# 시스템 프롬프트에 기본 퓨샷 예제 포함 여부.
few_shot_examples: true
# 기본 예제 다음에 추가되는 커스텀 퓨샷 예제.
custom_examples:
- user: "이부프로펜의 권장 복용량은 얼마인가요?"
classification:
complexity: simple
domain: medical
# 분류 결과의 캐시 유효 시간 (초). 기본값: 300.
cache_ttl_seconds: 300
# 최대 캐시 항목 수. 기본값: 10000.
max_cache_entries: 10000
# 이 부하 상태에 도달하면 LLM 분류기를 비활성화합니다.
# "critical" (기본값) 또는 "warning". 비워두면 항상 활성.
disable_under_load_state: critical
# 기본 복잡도 분류 체계를 커스텀 수준으로 확장합니다.
custom_complexity_levels:
- name: specialized
description: "도메인 특화 파인튜닝 모델이 필요한 요청"
rank: 6 # 선택적 순서 힌트 (높을수록 어려움)
# 기본 도메인 분류 체계를 커스텀 카테고리로 확장합니다.
custom_domains:
- name: medical
description: "의료 및 임상 관련 질문"
rule.confidence_threshold와 llm.temperature는 설정 로드 시점에 각각 검증됩니다. confidence_threshold는 [0.0, 1.0] 범위로, temperature는 이 문서에서 이미 정의한 canonical temperature 요청 파라미터와 같은 [0.0, 2.0] 범위로 제한됩니다. 두 경계값 모두 유효한 설정이라 confidence_threshold: 0.0이면 하이브리드 모드가 항상 LLM 분류기로 넘어가고 confidence_threshold: 1.0이면 절대 넘어가지 않습니다. 범위를 벗어나거나 유한하지 않은 값(.nan, .inf, -.inf)은 위의 모델 프로파일 비용 필드와 같은 방식으로 거부됩니다. 이런 값이 든 config.yaml은 필드 이름을 지목하는 메시지와 함께 기동 단계에서 실패하고 핫 리로드는 이전 설정을 그대로 유지합니다.
classifier.backend(또는 분류기가 실제로 연결하는 백엔드 URL)가 네이티브 Anthropic 백엔드를 가리키고 classifier.llm.model이 Claude Opus 또는 Sonnet 4.7 이상이면 설정된 temperature는 요청에 실려 거부되는 대신 분류 요청에서 조용히 빠지며 메인 요청 경로와 같은 샘플링 파라미터 제한은 Anthropic 확장 사고 모델에서 다룹니다.
분류기는 일반 트래픽을 처리하는 것과 동일한 백엔드 구현을 통해 분류 요청을 보내므로 설정된 백엔드 유형은 모두 지원되며 각 백엔드 고유의 인증과 요청 변환을 그대로 사용합니다. classifier.llm.backend가 가리키는 백엔드가 실제 백엔드 풀에 없으면 기동 시점이 아니라 해당 호출에서만 실패하는데 이때 백엔드 이름을 명시한 오류와 함께 규칙 기반 분류기로 넘어갑니다.
분류기 백엔드 선택¶
분류는 사용자가 입력한 프롬프트 본문을 분류기 백엔드로 보냅니다. 따라서 어떤 백엔드가 선택되는지가 그 내용이 어디로 가는지를 결정합니다. 선택은 요청마다가 아니라 라우터가 상태를 구성할 때 한 번, 그리고 핫 리로드마다 한 번 이루어집니다. 즉 설정이 바뀌기 전까지는 같은 백엔드로 계속 분류 프롬프트가 갑니다.
가드레일 입력 단계는 LLM 또는 System One 분류기(그리고 System One 섀도)보다 먼저 실행됩니다. enforce 모드의 차단은 분류기를 호출하기 전에 요청을 멈추고 PII 마스킹 같은 transform 판정은 분류기가 읽기 전에 system 프롬프트를 포함한 모든 메시지를 제자리에서 마스킹하며 그렇게 적용할 수 없는 마스크는 요청을 거부합니다. monitor 모드에서는 요청을 바꾸지 않으므로 분류기는 보낸 그대로의 프롬프트를 받습니다. 패스 순서와 요청이 두 번 평가되는 경우는 가드레일: 라이프사이클 훅을 참고하세요.
classifier.llm.backend를 지정한 경우와 생략한 경우는 의도적으로 다르게 처리됩니다.
classifier.llm.backend | 해석 |
|---|---|
| 일반 백엔드 지정 | 그 백엔드를 사용합니다. |
internal: true 백엔드 지정 | 그 백엔드를 사용합니다. 내부 가드 모델을 분류기로 지목하는 것은 운영자의 의도적 선택으로 보며 어떤 내부 백엔드를 선택했는지 info 수준으로 기록합니다. |
enabled: false 백엔드 지정 | 거부합니다. 로드 오류입니다. 비활성 백엔드는 라우터 내부 트래픽을 포함해 어떤 트래픽도 받지 않습니다. |
| 설정에 없는 이름 지정 | 거부합니다. 로드 오류이며 아래의 암묵적 선택으로 넘어가지 않습니다. |
| 생략 또는 빈 값 | backends 순서에서 일반 사용자 트래픽이 닿을 수 있는 첫 번째 백엔드, 즉 internal: true도 enabled: false도 아닌 첫 항목을 사용합니다. 어떤 백엔드를 선택했는지 info 수준으로 기록합니다. 해당하는 백엔드가 없으면 LLM 분류기를 구성하지 않고 규칙 기반 분류를 유지합니다. 이때 method: llm에서는 모든 요청이, hybrid에서는 에스컬레이션된 모든 요청이 규칙 결과로 라우팅됩니다. config validate는 이 경우 smart_routing.classifier.llm.backend 위치에 경고를 냅니다. |
이름은 앞뒤 공백을 제거해 읽고 빈 값은 생략한 것으로 봅니다. 로드할 때와 분류기를 만들 때 모두 같습니다. config validate와 라우터는 같은 함수로 백엔드를 해석하므로 라우터가 규칙 기반으로 동작할 때에만 정확히 경고가 나옵니다. 분류기 백엔드를 지정하는 Continuum Hub 선언은 로컬 섹션보다 우선합니다. hybrid_system_one은 LLM 단계가 System One 다음의 선택적 마지막 에스컬레이션이므로 경고하지 않습니다.
System One 분류기¶
System One 결정 모델(TypeSafe의 호스팅 Jev, Foundry의 Microsoft-Decision-1, 또는 네이티브 llama.cpp 결정 모델)은 state에 대한 타입이 지정된 질문들을 한 번의 순전파로 답하고 프로바이더가 보고한 확률과, 제공되는 경우 신뢰도를 돌려줍니다. System One 분류기는 요청마다 두 질문, 즉 정렬된 복잡도 수준에 대한 score와 도메인에 대한 choice를 묻고 다른 분류기와 같은 ClassificationResult를 채우므로 routing_policies[].when.complexity와 .domain은 바꿀 필요가 없습니다. 달라지는 것은 캐시되지 않은 분류 한 번의 비용과 confidence의 의미입니다.
| 분류기 | 캐시되지 않은 지연 시간 | confidence |
|---|---|---|
rule | 마이크로초 | 신호 개수와 강도에서 나온 휴리스틱 점수로, 0.1~0.95로 고정. multilingual 문자 체계 신호는 세지 않으므로 primary_language로 이 신호를 억제해도 신뢰도는 바뀌지 않음. 확신이 아니라 근거의 양을 세는 값이라, 정확도 데이터셋에서 맞은 복잡도 라벨을 틀린 라벨보다 높게 매기는 능력이 우연 수준을 넘지 못함(AUROC 0.46). cargo test --test smart_routing_accuracy_test -- --nocapture가 방식별로 보고함. |
llm | 수백 밀리초 (챗 컴플리션 왕복 한 번, 형식이 어긋난 답이면 재시도 추가) | 항상 0.95: 모델이 신뢰도를 보고하지 않음. |
system_one | 2026-09-24 측정 p50: 자체 호스팅 Laya 47ms(Apple M4 Pro GPU), compose 파일의 laya-serve CPU 291ms, Kev 0.8B(MLX) 84ms, 호스팅 Jev·Foundry·새 llama.cpp 프로필은 측정하지 않음(결정 모델) | 프로바이더가 보고한 두 답 신뢰도의 최솟값. 보정은 정확한 모델·버전·질문 묶음마다 다르며 VLLM Decision 2/D3의 정규화 엔트로피 신뢰도는 TypeSafe 신뢰도와 서로 바꿔 쓸 수 없음. |
공유 System One 대상 블록(protocol, auth, endpoint + api_key_env 또는 적격한 backend, 그리고 model, 프로바이더 상한과 기존 시간 제한·예산·동시성 필드)은 classifier.system_one 아래에 평탄화되어 들어갑니다. 전체 대상 계약은 결정 모델: 프로바이더 프로필과 실험적 미디어를 참고하세요. 분류기는 다음 필드를 추가합니다.
smart_routing:
classifier:
method: hybrid_system_one # 또는 system_one; rule | llm | hybrid에는 shadow: true를 더할 수 있음
rule:
confidence_threshold: 0.7 # hybrid_system_one: 이 값 미만이면 System One에 묻습니다
system_one:
# 호스팅 Jev:
protocol: typesafe
endpoint: "https://api.typesafe.ai/v1/systemone"
api_key_env: TYPESAFE_API_KEY
model: jev-1.14.0 # 보정에 사용한 버전을 고정합니다.
# 또는 이미 백엔드로 설정된 자체 호스팅 서버 (endpoint와 상호 배타적,
# internal: true 백엔드 허용):
# backend: laya
timeout_ms: 1500
max_concurrent: 32
# 이 신뢰도 미만의 System One 답은 라우팅에 쓰이지 않습니다.
# 범위 0.0~1.0, 기본값 0.15(측정값, 결정 모델 문서 참고).
min_confidence: 0.15
# 라우팅 결정을 바꾸지 않고 라우팅 분류기 옆에서 질문 묶음을 실행합니다.
# 아래 "섀도 모드" 참고. 기본값 false. system_one / hybrid_system_one
# 방식과 함께 쓰면 거부됩니다.
shadow: false
# 선택적 noul 질문. 분류 신호로만 기록됩니다.
signals:
needs_tools: false # system_one_needs_tools (capability 신호)
is_sensitive: false # system_one_is_sensitive (domain 신호)
max_input_tokens: 500 # 마지막 사용자 요청의 절단 길이.
cache_ttl_seconds: 300
max_cache_entries: 10000
disable_under_load_state: critical # critical | warning | ""
# hybrid_system_one의 선택적 마지막 에스컬레이션 단계.
llm:
model: "gpt-5.6-terra"
backend: openai-fast
프로바이더별 설정 안내:
- Microsoft Foundry:
protocol: foundry, Foundry 리소스 루트 또는 Microsoft 프로바이더 엔드포인트,auth: api_key(외부에서 갱신하는 토큰이면entra), 그리고 배포 이름을model에 설정하십시오. - 네이티브 llama.cpp 결정 서버:
protocol: llama_cpp,/v1/systemone엔드포인트 또는 적격한 자체 호스팅backend, 설정한 별칭을model에 설정하십시오. 이 경로는 네이티브 결정 호출만 수행하며, 공개 Decisions API의 챗 모델 에뮬레이션 폴백은 분류기에 사용하지 않습니다. - 초기 보정 절차: 새 대상은
method: rule과shadow: true로 시작해 대표적인 영어·한국어 트래픽에서 측정하고, 실제 사용할 모델·버전·질문 묶음으로make calibrate-systemone을 실행한 뒤 도출된 임계값으로system_one또는hybrid_system_one을 선택하십시오. 기본값0.15는 측정된 Laya 기준값일 뿐 Jev, Microsoft-Decision-1, Kev, Decision 2/D3 또는 다른 체크포인트의 보정값이 아닙니다.
질문 묶음 (Question Battery)¶
질문 묶음은 요청 자체의 난이도와 도메인만 평가하며 후보 모델의 성공 가능성은 묻지 않습니다(결정 모델은 타 모델의 실패 확률에 대해 유의미한 신호를 제공하지 않기 때문입니다).
| 질문 | 유형 | 구성 및 제약 |
|---|---|---|
complexity | score | 5개 내장 복잡도 설명에 custom_complexity_levels를 rank 순서로 합산(내장 수준과 같은 rank는 그 뒤에, rank 미지정 항목은 맨 뒤에 배치). 응답 점수는 가장 가까운 수준으로 반올림됩니다(예: 3.4 → complex). score 질문은 최대 10개 수준만 담을 수 있으므로 system_one 블록이 있을 때 custom_complexity_levels는 최대 5개까지 허용됩니다. |
domain | choice | 6개 내장 도메인에 custom_domains를 합산(내장 ID와 중복되는 이름은 무시). Laya는 선택지가 약 20개를 넘으면 성능이 저하되므로 16개를 초과하면 로드 시 경고를 출력합니다. |
needs_tools, is_sensitive | noul (선택) | 라우팅 결정 자체는 바꾸지 않으며, 각각 ClassificationSignal(strength = 확률)로 기록되어 관리자 classify 진단 및 정책 조건에 활용됩니다. |
신뢰도 의미 및 폴백 동작¶
- 신뢰도 계산: 최종
confidence는 복잡도 신뢰도와 도메인 신뢰도 중 최솟값이므로, 두 답변이 모두 집중되어 있을 때만 결과를 신뢰합니다. - 임계값 미달 시 동작:
method: system_one:min_confidence미만인 응답은 버리고 규칙 분류 결과를 사용합니다.method: hybrid_system_one:classifier.llm이 백엔드를 해석할 수 있으면(분류기 백엔드 선택 참고) LLM 분류기로 에스컬레이션하고, 그렇지 않으면 규칙 결과를 사용합니다.
- 오류 폴백: 호출 실패, 형식 오류, 대상 미해석 등 모든 오류는 규칙 분류 결과를 반환하며
smart_routing_system_one_classifier_fallbacks_total{reason}(error,low_confidence,load)에 집계됩니다. - 기능 및 언어 감지: 두 벤더 모두 CJK 언어 감지 정확도가 낮고 규칙 기반 감지기는 비용이 들지 않으므로,
required_capabilities와 감지된language는 항상 규칙 분류기에서 가져옵니다.
외부 전송 데이터 및 캐시 격리¶
- 전송 페이로드(
state): LLM 분류기 사용자 메시지와 동일한 요약 정보만 담은 JSON 객체를 전송합니다:request: 마지막 사용자 메시지 (토큰당 4자 기준max_input_tokens까지 절단)system_excerpt:system또는developer메시지의 앞 200자 (존재할 때만)message_count,has_images,has_tools,model(요청된 모델)
- 가드레일 및 개인정보 보호: 대화의 이전 메시지나 공개 Decisions API 미디어는 전송되지 않습니다.
state는 가드레일 입력 단계 이후에 생성되므로 차단된 프롬프트는 전송되지 않고, PII 마스킹 등이 적용된 프롬프트는system_excerpt를 포함해 마스킹된 상태로 전송됩니다(monitor모드에서는 원본 전송;shadow: true도 동일). 호스팅 Jev와 Foundry는 외부 제3자 서비스이므로 민감한 워크로드에서는 자체 호스팅 서버를endpoint나backend로 지정하십시오. - 캐시 키 격리: 캐시 키는 제한된
state, 대상 프로필 전체, 결정 모델 식별자, 캐시 형식 버전, 생성된 질문 묶음, 그리고(backend:대상인 경우) 라이브 설정 스냅샷의 토폴로지 세대를 함께 해시한 SHA-256입니다. 따라서 대상, 프로필, 자격 증명·헤더, 비활성 상태, 모델, 질문 묶음이 리로드되면 이전 계약에서 만든 캐시 응답을 재사용하지 않습니다. 자격 증명 값과 프롬프트 본문은 캐시나 메트릭 라벨에 저장되지 않습니다.
사용량과 통계¶
- 별도 목적 집계: 실패나 재시도를 포함해 실제 네트워크로 전송된 모든 네이티브 시도는
purpose: classifier, 해석된 응답 모델, 대상·백엔드 귀속, 입출력 토큰 사용량, 지연 시간, 결과를 담은 별도 사용량 레코드로 기록됩니다.GET /admin/stats는 분류기 호출과 토큰을by_purpose.classifier아래에 노출하며, 클라이언트 생성 요청(by_purpose.client)과 합산하거나 이중 집계하지 않습니다. - 비정상 응답 및 비용 처리: 형식이 어긋난 HTTP 2xx 응답은 HTTP 상태
200, 출력 0인upstream_error로 기록됩니다. 유효한 2xx 응답에서 신뢰도가 누락된 경우 과금된 토큰 사용량을 보존한 뒤 라우팅 폴백을 수행합니다. 카탈로그 출력 단가가 0인 모델도 0이 아닌 출력 토큰 수를 그대로 보존하며, 비용 계산은 보존된 토큰 수에 버전별 카탈로그 단가를 별도로 적용합니다. - Prometheus 지표:
smart_routing_system_one_classifier_input_tokens_total{counted},systemone_requests_total{consumer="classifier", result},systemone_request_duration_seconds{consumer="classifier"},smart_routing_system_one_classifier_cache_hits_total을 노출합니다(캐시 적중 시에는 업스트림 호출이나 사용량 레코드가 생성되지 않음).
섀도 모드¶
method: rule, llm, hybrid에서 shadow: true를 설정하면 실제 라우팅을 결정 모델에 맡기기 전에 정확도와 신뢰도 분포를 측정할 수 있습니다.
- 비교 지표 및 디버그 헤더: 질문 묶음은 매 요청마다 라우팅 분류기와 나란히 실행되며, 실제 라우팅은 설정된 분류기 결과를 따릅니다. 두 결과의 일치 여부는
smart_routing_shadow_agreement_total{field, agree}(field:complexity또는domain,agree:true또는false)에 기록되며,debug_headers: true이면 응답에X-Smart-Route-Shadow: complexity=<level>;domain=<tag>;confidence=<0.00>;agree=<both|complexity|domain|none>헤더가 추가됩니다. - 도입 절차:
- 현재 분류기에서
shadow: true를 켜고 대표 트래픽에서 실행합니다. - 필드별 일치율과 헤더·로그의 신뢰도 분포를 확인하여 적절한
min_confidence를 결정합니다. method를system_one또는hybrid_system_one으로 전환하고shadow설정을 제거합니다.
- 현재 분류기에서
- 지연 시간 및 부하 영향: 요청은 두 분류기 응답을 모두 기다리므로
method: rule옆에서 섀도 모드를 켜면 모든 요청이 결정 호출 시간(최대timeout_ms, 기본 1.5초)을 추가로 부담합니다(llm이나hybrid옆에서는 두 호출이 병렬로 실행됨). 섀도 실행 시에는timeout_ms를 짧게 설정하십시오. 섀도 호출도disable_under_load_state를 따르므로 과부하 시에는 측정을 먼저 중단합니다. 실패한 섀도 호출은debug로그로 기록되고systemone_requests_total{consumer="classifier"}및purpose: classifier사용량 통계에 집계됩니다.
측정된 Laya 기준값¶
기본 min_confidence: 0.15는 2026-09-24에 compose 파일의 자체 호스팅 laya-serve(CPU)를 대상으로 라우팅 코퍼스에서 도출한 값입니다.
- 전체 코퍼스에서
min_confidence: 0.15일 때method: system_one체인의 도메인·복잡도 정답률은0.510으로, 임계값0.5(0.460) 및 규칙 분류기 단독(0.475)보다 높았습니다. - 그러나 한국어 슬라이스에서는 Laya 실행(
0.405)이 규칙 분류기(0.487)를 앞지르지 못했으므로, 한국어 비중이 높은 환경에서는 실제 대상 모델의 우위가 확인될 때까지method: rule과shadow: true를 유지하십시오. - 슬라이스별 상세 수치와 자체 엔드포인트 보정 절차(
make calibrate-systemone)는 결정 모델을 참고하십시오. Jev, Microsoft-Decision-1, 신규 llama.cpp 프로필은 해당 코퍼스에서 측정되지 않았으므로 이 임계값이나 Laya의 정확도 수치를 그대로 적용하지 마십시오.
검증과 핫 리로드¶
- 시작 및 리로드 검증: 공유 대상 규칙(
endpoint와backend중 정확히 하나 지정,/v1/systemone경로, 백엔드 적격성 및 가시성),[0, 1]범위의 유한한min_confidence,disable_under_load_state(critical,warning,""), System One 방식 선택 시 블록 필수 여부, System One 방식과shadow: true동시 사용 금지, 최대 5개의 커스텀 복잡도 수준을 로드 시 검증합니다.classifier.llm.backend를 사용하는 방식에서 존재하지 않거나enabled: false인 백엔드를 지정하면 조용히 폴백하지 않고 로드 오류로 거부합니다. - 무중단 핫 리로드:
smart_routing의 나머지 설정과 함께 핫 리로드됩니다.method: llm에서method: system_one으로(또는 반대로) 전환하면 새 설정에 필요한 분류기만 재구성하며,backend:참조는 호출 시점마다 라이브 설정을 읽으므로 Admin API 편집이나 설정 동기화 변경이 재구성 없이 다음 호출부터 반영됩니다.
재귀 방지¶
System One 결정 호출은 대상의 /v1/systemone 엔드포인트로 직접 전송되며 라우터의 챗 컴플리션 경로를 거치지 않으므로 자기 자신의 요청을 다시 분류하는 순환 루프가 발생하지 않습니다. hybrid_system_one의 LLM 에스컬레이션 단계를 포함한 모든 LLM 분류기 챗 호출에는 우회 헤더의 X-Smart-Route-Bypass: true 가드가 동일하게 적용됩니다.
구조화 출력 전략¶
LLM 분류기는 분류 모델에서 구조화된 JSON을 받아야 합니다. auto 전략이 백엔드 유형에 따라 적절한 방식을 자동 선택하지만 명시적으로 지정할 수도 있습니다.
| 전략 | 방식 | 지원 백엔드 |
|---|---|---|
json_schema | response_format: { type: "json_schema" } | OpenAI, Azure, vLLM |
tool_use | 도구/함수 호출 | Anthropic, Gemini |
json_object | response_format: { type: "json_object" } | OpenAI, Ollama, Gemini, LM Studio, llama.cpp |
prompt_only | 자유형 텍스트에서 정규식으로 JSON 추출 | 모든 백엔드 |
분류기 응답을 파싱할 수 없으면 수정 프롬프트로 한 번 재시도합니다(max_retries로 제어). 재시도도 실패하면 규칙 기반 분류기 결과를 사용합니다.
분류 캐시¶
동일한 요청에 대한 반복 업스트림 호출을 줄이기 위해 분류 결과를 TTL(cache_ttl_seconds) 및 항목 수 상한(max_cache_entries)과 함께 인메모리에 캐시합니다. LLM 분류기에서 캐시 키는 잘린 사용자 메시지의 SHA-256 해시입니다. System One 분류기에서 캐시 키는 캐시 형식 버전, 대상 프로필, 모델, 생성된 질문 묶음, 라이브 cache_target_identity(backend: 대상의 라이브 설정 스냅샷 세대 포함), 가드레일 통과 후 제한된 state(request, system_excerpt, message_count, has_images, has_tools, model)를 함께 해시하므로 대상, 프로필, 자격 증명이나 헤더, 비활성 상태, 모델, 질문 묶음 중 하나라도 리로드되면 캐시된 결과가 무효화됩니다. 캐시는 프로세스 단위이며 여러 라우터 인스턴스 간에 공유되지 않습니다.
커스텀 분류 체계¶
복잡도 수준과 도메인 태그 모두 확장 가능합니다. custom_complexity_levels와 custom_domains로 추가한 커스텀 값은 분류기의 시스템 프롬프트와 구조화 출력 스키마에 반영됩니다. 라우팅 정책에서도 기본 값처럼 커스텀 값을 참조할 수 있습니다.
우회 헤더¶
LLM 분류기는 분류 요청에 X-Smart-Route-Bypass: true 헤더를 추가합니다. 이 헤더가 있는 요청은 스마트 라우팅을 건너뛰어, 분류기 백엔드가 동일한 라우터 인스턴스 뒤에 있을 때 발생할 수 있는 순환 분류 루프를 방지합니다.
라우팅 정책¶
정책은 위에서 아래로 평가하며 첫 번째로 매칭된 정책이 적용됩니다. 매칭되는 정책이 없고 캐치올도 없으면 default_tier로 폴백됩니다.
정책 조건 논리¶
when블록 내 필드들은 AND로 평가됩니다. 명시된 모든 필드가 일치해야 합니다.- 하나의 필드 안에서 값들은 OR로 평가됩니다.
complexity: [trivial, simple]은 둘 중 하나와 일치하면 됩니다. when: {}는 항상 매칭되는 캐치올입니다. 신원 필드(key_tier,org,language,header)나input_tokens만 지정한 조건은 캐치올이 아닙니다.
정책 필드¶
when 조건:
| 필드 | 타입 | 설명 |
|---|---|---|
complexity | [string] | 매칭할 복잡도 수준 목록 (OR 논리) |
domain | [string] | 매칭할 도메인 태그 목록 (OR 논리) |
requires | [string] | 모두 존재해야 하는 기능 목록 (AND 논리) |
key_tier | [string] | 매칭할 허브 티어 ID 목록 (OR 논리) |
org | [string] | 매칭할 조직 ID 목록 (OR 논리) |
language | [string] | 매칭할 요청 언어 목록. 짧은 BCP-47 태그 (OR 논리) |
header | map[string][string] | 요청 헤더 힌트. 나열한 모든 헤더 이름이 각각의 값 중 하나와 일치해야 합니다 (이름 사이는 AND, 한 이름의 값들 사이는 OR) |
input_tokens | {gt, gte, lt, lte} | 요청 전체의 추정 입력 토큰 범위. 지정한 경계를 모두 만족해야 합니다 |
신원 조건¶
앞의 세 필드는 요청이 무엇인지를 나타내고 뒤의 네 필드는 누가 어떤 방식으로 요청했는지를 나타냅니다. 덕분에 하나의 auto 별칭으로 키별, 조직별, 요청 언어별, 또는 클라이언트가 명시한 힌트별로 다르게 라우팅할 수 있습니다.
routing_policies:
# 유료 티어는 플래그십 모델로 보냅니다.
- name: "build_tier_flagship"
when:
key_tier: [tier_build]
route_to:
tier: 1
# 특정 조직은 복잡도와 무관하게 가장 저렴한 티어에 고정합니다.
- name: "sandbox_org_lightweight"
when:
org: [org_sandbox]
route_to:
tier: 3
# 한국어 요청은 한국어를 잘 처리하는 모델로 보냅니다.
- name: "korean_to_flagship"
when:
language: [ko]
route_to:
tier: 1
# 클라이언트가 더 충실한 답변을 요청할 수 있습니다.
- name: "thorough_hint"
when:
header:
x-route-hint: [thorough]
route_to:
tier: 1
- name: "default_to_standard"
when: {}
route_to:
tier: 2
key_tier는 요청의 API 키에 대해 해석된 Continuum Hub 티어 ID(tier_id)와 비교합니다. control-plane 기능 없이 빌드한 바이너리는 티어를 전혀 해석하지 않으며 허브에 연결되지 않은 키의 요청도 마찬가지입니다. 따라서 그런 환경에서 key_tier를 지정한 정책은 아무것도 매칭하지 않습니다. 모든 요청을 매칭하지는 않는다는 점이 중요합니다. 티어 기반 정책은 티어가 존재하지 않는 환경에서 열리는 대신 닫히는 쪽으로 동작합니다. continuum-router config validate는 티어를 해석할 수 없는 빌드에서 key_tier를 사용하는 정책에 경고를 냅니다.
org는 인증된 API 키의 organization_id와 비교합니다. 인증되지 않은 요청에는 조직이 없으므로 매칭되지 않습니다.
language는 분류기가 사용자 메시지 본문에서 감지한 언어를 읽습니다. 언어별 키워드 테이블을 고르는 것과 동일한 감지 결과입니다. 기본 서브태그만 의미가 있고 대소문자를 구분하지 않으므로 ko, KO, ko-KR은 모두 한국어를 가리킵니다. 두 가지 부재 상황을 의도적으로 구분합니다. 글자가 하나도 없는 요청(빈 메시지, 또는 숫자와 문장부호만 있는 경우)은 언어를 갖지 않으며 어떤 language 조건과도 매칭되지 않습니다. 반면 검사는 했지만 언어를 특정할 수 없는 텍스트는 und를 갖고 und를 나열한 조건과 매칭됩니다.
header는 요청 헤더를 읽습니다. 이름과 값 모두 대소문자를 구분하지 않으며 값 목록이 비어 있으면 존재 여부만 확인합니다(x-route-hint: []는 값과 무관하게 헤더가 있으면 매칭). 값은 256바이트로 제한되며 그보다 긴 값은 잘라내지 않고 부재로 처리합니다. 설정한 값으로 시작하기만 하는 긴 값을 보내서 정책을 매칭시키는 일을 막기 위해서입니다. 이 헤더들은 클라이언트가 직접 설정하므로, header는 호출자의 선호를 표현하는 용도로 쓰고 권한과 관련된 부분은 클라이언트가 설정할 수 없는 key_tier나 org로 통제하십시오. 헤더 값은 메트릭 레이블이나 로그에 남지 않습니다.
smart_routing.debug_headers를 켜면 x-smart-route-policy가 매칭된 정책 이름을 알려주므로 신원 조건이 실제로 동작하는지 가장 빠르게 확인할 수 있습니다. POST /admin/smart-routing/simulate도 payload와 함께 key_tier, org, headers 필드를 받으므로, 해당 티어의 키를 갖고 있지 않아도 티어 기반 정책을 확인할 수 있습니다.
요청 크기 조건¶
input_tokens는 요청의 추정 크기로 라우팅합니다. 긴 프롬프트가 작은 컨텍스트 모델에서 400 context_length_exceeded로 실패한 뒤 폴백에 기대는 대신, 첫 시도부터 긴 컨텍스트 티어로 가게 할 수 있습니다.
routing_policies:
# 긴 대화는 긴 컨텍스트 티어로 보냅니다.
- name: "long_context"
when:
input_tokens: { gt: 120000 }
route_to:
tier: 1
- name: "default_to_standard"
when: {}
route_to:
tier: 2
- 범위 경계 및 검증: 범위에는
gt,gte,lt,lte(음이 아닌 정수)를 사용하며 하한과 상한은 각각 하나까지만 지정할 수 있습니다. 빈 범위(input_tokens: {}),gt와gte동시 지정,lt와lte동시 지정, 또는 허용 가능한 추정값이 1개 이하인 범위(gte: 10, lte: 10,gt: 10, lt: 11)는 거부됩니다. 이 규칙을 어긴 핫 리로드, 허브 합성,PUT /admin/smart-routing/policies편집은 거부되고 기존 정책이 유지됩니다(시작 시에는 다른 라우팅 정책 오류와 마찬가지로Smart routing policy error경고를 기록한 뒤 작성된 그대로 로드하며, 알 수 없는 키는 파싱 오류로 거부합니다). - 추정 방식: 토크나이저 실행 결과가 아니라 폴백 예산 계획이 컨텍스트 창 적합성을 판단할 때 쓰는 것과 동일한 휴리스틱 추정치입니다. ASCII 텍스트는 약 3자당 1토큰, 비ASCII 문자(CJK, 이모지)는 문자당 1토큰, JSON 숫자와 불리언은 수 토큰으로 계산하며 메시지 프레이밍에는 고정 여유분을 더합니다. 대화의 모든 메시지(최신 턴만이 아님),
role을 제외한 모든 메시지 필드,tools,functions,response_format까지 요청 전체를 합산합니다. - 권한 통제 주의사항: 요청 본문은 클라이언트가 작성하므로 추정치 역시 클라이언트가 부풀릴 수 있습니다(
role외의 미과금 필드까지 모두 합산되며 배열 안의 2바이트짜리 JSON 숫자 하나도 약 5토큰으로 계산됨). 따라서input_tokens는 긴 프롬프트를 수용 가능한 모델로 보내는 용도로만 사용하고, 고비용 티어 접근 권한은 클라이언트가 조작할 수 없는key_tier나org와 AND 조건으로 묶어 통제하십시오. - 인라인 바이너리(base64) 콘텐츠:
image_url파트에 포함된 base64data:URL도 일반 문자열과 동일하게 길이를 합산하므로, 이미지 한 장이 첨부된 짧은 질문도 수십만 토큰으로 추정되어gt경계를 넘을 수 있습니다. 긴 컨텍스트 티어가 멀티모달을 지원하지 않는다면requires: [vision]정책을 크기 조건 정책보다 위에 배치하십시오. - 평가 비용 및 관찰 가능성: 추정치는 로드된 정책 중
input_tokens를 사용하는 정책이 있을 때만 요청당 한 번 계산됩니다(사용하는 정책이 없으면 비용 0이며 매칭되지 않음).debug_headers를 켜면X-Smart-Route-Input-Tokens헤더와Routing policy matched디버그 로그(input_tokens필드)에 프롬프트 본문 없이 추정치만 기록되며,POST /admin/smart-routing/simulate도input_tokens를 반환합니다. - 지원 엔드포인트 및 허브 거버넌스: 스마트 라우팅과 마찬가지로
/v1/chat/completions(스트리밍 및 비스트리밍)에서만 동작하며/anthropic/v1/messages와/v1/responses에서는 실행되지 않습니다. Continuum Hub 정책 계약에는input_tokens필드가 없으므로 크기 기반 규칙은 로컬에 정의해야 하며, 허브가 자체 규칙 목록을 내려보내면 다른 로컬 규칙과 함께 대체됩니다.
route_to 액션:
| 필드 | 타입 | 설명 |
|---|---|---|
tier | int | 대상 티어 (1=Flagship, 2=Standard, 3=Lightweight) |
prefer_domains | [string] | 도메인 특화 모델에 대한 소프트 선호도 |
require_capabilities | [string] | 하드 필터: 해당 기능이 있는 모델만 선택 |
티어 내 모델 선택¶
매칭된 티어에 여러 모델이 있을 경우 다음 기준으로 점수를 매겨 선택합니다.
- 도메인 선호도 매칭 (소프트 보너스)
- 기능 매칭 (
require_capabilities가 설정된 경우 하드 필터) - 비용 점수 (같은 티어 안에서 낮은 비용이 높은 점수)
- 동점일 경우 무작위 선택
매칭된 티어에 사용 가능한 모델이 없으면 인접 티어를 순서대로 시도합니다(예: Lightweight가 비어 있으면 Standard, 그다음 Flagship 순).
부하 기반 동적 티어 조정¶
평상시에는 각 요청에 가장 적합한 티어를 선택하지만 트래픽이 급증하거나 지연 시간이 높아지면 부하 모니터가 실시간 지표를 추적해 자동으로 더 가벼운 모델 티어로 라우팅을 전환하고 부하가 회복되면 원래대로 돌아옵니다.
부하 관리는 기본적으로 비활성화되어 있습니다. smart_routing.load_management 아래에서 활성화합니다:
smart_routing:
enabled: true
load_management:
enabled: true
# 부하 지표 평가 주기(밀리초). 기본값: 1000.
assessment_interval_ms: 1000
# Warning/Critical 상태로 전환하는 임계값.
# 하나의 지표라도 초과되면 해당 상태로 전환됩니다.
thresholds:
warning:
requests_per_second: 100
avg_latency_ms: 3000
error_rate: 0.05 # 범위: 0.0 ~ 1.0
in_flight_requests: 50
kv_cache_usage: 0.7 # 엔진 측 지표. 범위: 0.0 ~ 1.0
waiting_requests: 4 # 엔진 측 대기 큐 길이
critical:
requests_per_second: 200
avg_latency_ms: 5000
error_rate: 0.15
in_flight_requests: 100
kv_cache_usage: 0.9
waiting_requests: 16
# 각 부하 상태에서 적용할 라우팅 제한.
degradation:
warning:
max_tier: 2 # Standard 티어까지만 허용
prefer_quantized: false
reject_expert: false
critical:
max_tier: 3 # Lightweight 티어까지만 허용
prefer_quantized: true
reject_expert: false
# 복구 동작.
recovery:
cooldown_seconds: 30 # 낮은 부하 상태로 전환하기 전 최소 대기 시간(초)
hysteresis_factor: 0.8 # 임계값의 80% 이하로 떨어져야 복구됨. 범위: 0.0 ~ 1.0
엔진 측 입력¶
requests_per_second, avg_latency_ms, error_rate, in_flight_requests는 라우터 자체 카운터에서 계산하는 라우터 측 지표입니다. kv_cache_usage와 waiting_requests는 라우터가 이미 수집하고 있는 엔진 통계(engine_stats)에서 읽어오는 엔진 측 지표입니다. 이 두 필드가 필요한 이유는, 라우터 측 숫자가 모두 정상으로 보이는 동안에도 GPU는 이미 포화 상태일 수 있기 때문입니다. KV 캐시가 거의 찬 백엔드도 연결은 계속 받고 헬스 체크도 통과하며 라우터 지연 시간은 큐가 이미 쌓인 뒤에야 움직이기 시작합니다. 그때는 가벼운 티어로 트래픽을 옮기기에 늦습니다.
두 필드 모두 스냅샷이 신선한 백엔드들 중 가장 높은 값을 읽습니다. 평균을 쓰면 유휴 백엔드 세 대가 포화된 한 대를 가려버리는데 그 상황이 바로 이 임계값이 존재하는 이유입니다.
신선도는 별도 설정 항목이 아닙니다. 모니터는 엔진 부하 라우팅 항목이 쓰는 것과 동일한 TTL 기준 뷰를 읽으므로, 스냅샷은 engine_stats.interval * routing.engine_load.max_staleness_intervals보다 젊은 동안에만 유효합니다. 여기서 세 가지가 따라옵니다.
engine_stats를 활성화하지 않은 환경에는 스냅샷 자체가 없으므로, 임계값을 어떻게 설정하든 엔진 측 판정은 발동하지 않습니다.- 폴러가 멈추거나 실패하면 엔진 측 판정은 마지막으로 본 값에 라우터를 묶어두는 대신 조용히 침묵합니다.
- 두 필드 중 하나를 보고하지 않는 엔진은 그 필드에 대해 아무 진술도 하지 않습니다. 없는 필드를 0으로 읽지 않습니다.
각 필드는 개별적으로 선택합니다. kv_cache_usage나 waiting_requests를 설정하지 않으면 라우터 측 임계값과 마찬가지로 아예 평가하지 않습니다. kv_cache_usage는 [0.0, 1.0] 범위로 제한되며 error_rate, hysteresis_factor와 함께 설정 로드 시점에 검증됩니다.
tier_thresholds 재정의 안에 두 필드를 넣는 것도 허용되지만 동작은 전역 임계값과 같습니다. 엔진 포화는 라우팅 티어가 아니라 백엔드 풀의 속성이기 때문입니다. 이는 requests_per_second, avg_latency_ms, error_rate가 티어 재정의에서 이미 보이는 동작과 같으며 실제로 티어 단위로 평가되는 필드는 in_flight_requests 하나뿐입니다.
부하 상태¶
| 상태 | 의미 | 기본 라우팅 제한 |
|---|---|---|
normal | 모든 지표가 정상 범위 내 | 제한 없음 |
warning | Warning 임계값을 초과한 지표 발생 | Standard 티어(tier 2) 이하로 제한 |
critical | Critical 임계값을 초과한 지표 발생 | Lightweight 티어(tier 3) 이하로 제한, 양자화 모델 우선 |
본래 Flagship(tier 1)으로 라우팅될 요청이 Warning 상태에서는 Standard로 자동 하향됩니다. 라우팅 결정 로그에는 조정된 정책 이름이 <원래_정책>__load_warning 또는 <원래_정책>__load_critical 형식으로 기록됩니다.
히스테리시스와 쿨다운¶
부하 상태가 빠르게 오락가락하면 그 자체로 시스템 불안정을 유발할 수 있어, 두 가지 메커니즘으로 이를 방지합니다:
- 히스테리시스: Warning 상태를 벗어나려면 지표가
임계값 * hysteresis_factor(기본 0.8) 이하로 내려가야 합니다. 100 RPS에서 Warning에 진입했다면 80 RPS 이하로 떨어져야 Normal로 회복됩니다. - 쿨다운: 상태 전환 후
cooldown_seconds(기본 30초) 동안은 더 낮은 상태로의 복구가 차단됩니다. 반면 상태 상향(Normal에서 Warning, Warning에서 Critical)은 쿨다운과 무관하게 즉시 적용됩니다.
recovery.hysteresis_factor와 thresholds.warning, thresholds.critical, tier_thresholds 재정의에 들어가는 모든 error_rate 필드는 [0.0, 1.0] 범위로 제한되며 설정 로드 시점에 검증됩니다. 범위를 벗어나거나 유한하지 않은 값(.nan, .inf, -.inf)은 필드 이름을 지목하는 메시지와 함께 기동 단계에서 실패하고 tier_thresholds 항목이면 티어 키까지 함께 지목합니다. 핫 리로드가 같은 값을 담고 있으면 적용하지 않고 이전 설정을 그대로 유지합니다. hysteresis_factor는 0.0도 유효 범위 안에 있으며 따로 짚어둘 만합니다. 임계값 * 0.0은 항상 0.0이므로 트래픽이 조금이라도 있으면 Warning/Critical 유지 판정이 계속 참이 됩니다. 라우터는 트래픽이 흐르는 한 계속 격상 상태에 머물다가 스냅샷이 완전히 유휴 상태일 때만 Normal로 돌아옵니다.
티어별 임계값 재정의¶
티어마다 처리 용량이 다를 때는 티어별로 임계값을 별도 지정할 수 있습니다:
smart_routing:
load_management:
enabled: true
thresholds:
warning:
requests_per_second: 100
tier_thresholds:
"1": # Flagship은 RPS 내성이 낮음
warning:
requests_per_second: 50
"3": # Lightweight는 더 많은 요청을 처리 가능
warning:
requests_per_second: 300
Prometheus 지표¶
metrics 기능이 활성화된 경우 부하 관리 관련 지표가 노출됩니다:
| 지표 | 유형 | 설명 |
|---|---|---|
smart_routing_load_state | Gauge | 현재 부하 상태: 0=Normal, 1=Warning, 2=Critical |
smart_routing_tier_degradation_total | Counter | 부하로 인해 티어가 하향된 횟수 |
smart_routing_load_transitions_total | Counter | 부하 상태 전환 횟수(from_state, to_state, reason 레이블 포함) |
reason은 전환을 일으킨 규칙을 가리키며 네 가지 고정 값 중 하나입니다. router_metrics(라우터 측 전역 임계값), engine_stats(엔진 측 전역 임계값), tier_threshold(어떤 신호로 작성했든 tier_thresholds 재정의. 고쳐야 할 설정 블록을 가리키기 위함), recovery(더 이상 걸리는 임계값이 없음). 값 집합이 닫혀 있으므로 백엔드나 모델을 아무리 많이 설정해도 시계열 개수는 늘어나지 않습니다.
LLM 분류기는 추가로 6개의 지표를 노출합니다:
| 지표 | 유형 | 설명 |
|---|---|---|
smart_routing_llm_classifier_calls_total | Counter | LLM 분류기 호출 총 횟수 |
smart_routing_llm_classifier_cache_hits_total | Counter | 캐시에서 반환된 분류 결과 수 |
smart_routing_llm_classifier_duration_seconds | Histogram | LLM 분류 전체 지연 시간 |
smart_routing_llm_classifier_fallbacks_total | Counter | LLM 결과를 버리고 규칙 기반 결과를 사용한 횟수 |
smart_routing_llm_classifier_parse_errors_total | Counter | 재시도 전 응답 파싱 실패 횟수 |
smart_routing_llm_classifier_retries_total | Counter | 초기 파싱 실패 후 재시도 횟수 |
디버그 응답 헤더¶
debug_headers: true로 설정하면 HTTP 응답 헤더에 스마트 라우팅 결정 정보가 포함됩니다. 개발 및 스테이징 환경에서 사용하기 위한 기능입니다.
활성화하면 스마트 라우팅된 응답에 다음 헤더가 추가됩니다:
| 헤더 | 설명 |
|---|---|
X-Smart-Route-Source | 요청된 원본 모델 (예: auto) |
X-Smart-Route-Target | 선택된 모델 (예: gpt-5.6-terra) |
X-Smart-Route-Complexity | 분류된 복잡도 수준 |
X-Smart-Route-Domain | 분류된 도메인 |
X-Smart-Route-Policy | 매칭된 정책 |
X-Smart-Route-Load-State | 라우팅 시점의 부하 상태 |
X-Smart-Route-Classifier | 사용된 분류기 (rule_based, llm_based, system_one, cached) |
X-Smart-Route-Shadow | classifier.system_one.shadow: true일 때만 존재: System One 질문 묶음의 답과 라우팅된 분류와의 일치 여부 (complexity=<level>;domain=<tag>;confidence=<0.00>;agree=<both|complexity|domain|none>) |
X-Smart-Route-Input-Tokens | 로드된 정책 중 when.input_tokens를 쓰는 정책이 있을 때만 존재: 정책 매칭에 사용한 추정 입력 토큰 수 |
Admin API¶
스마트 라우팅은 관찰 가능성과 관리를 위해 /admin/smart-routing/ 아래에 여러 admin 엔드포인트를 노출합니다. 전체 엔드포인트 참조는 Admin API 문서에서 확인하세요.
주요 엔드포인트:
GET /status-- 전체 상태, 부하 상태, 정책 수POST /classify-- 실제 라우팅 없이 요청 분류 (진단용)POST /simulate-- 전체 라우팅 파이프라인 시뮬레이션GET /policies및PUT /policies-- 정책 조회 및 핫 리로드GET /load-state-- 평가 정보를 포함한 현재 부하 상태. 스냅샷에는 판정 근거가 된 엔진 측 값을 담은engine객체(kv_cache_usage,waiting_requests,fresh_backends)가 함께 들어갑니다. 앞의 두 값은 신선한 스냅샷이 해당 필드를 진술하지 않았을 때null이며 이는 유휴 상태의0과 다릅니다.fresh_backends는 몇 개의 백엔드가 값을 제공했는지 알려줍니다GET /cache/stats및POST /cache/clear-- LLM 분류기 캐시 관리
구조화 로깅¶
모든 스마트 라우팅 결정은 DEBUG 수준으로 구조화된 필드와 함께 기록됩니다:
level=DEBUG msg="Smart routing decision"
source_model="auto"
target_model="gpt-5.6-terra"
complexity="simple"
domain="general"
policy="trivial_to_lightweight"
load_state="normal"
classifier="rule_based"
confidence=0.92
classification_ms=0.3
부하 상태 전환 및 정책 변경은 INFO 수준으로 기록됩니다.
핫 리로드¶
smart_routing 섹션은 설정 파일이 변경될 때 즉시 리로드됩니다. 리로드 후 추론 캐시가 초기화되어 다음 요청부터 새 프로필을 기준으로 재평가합니다. routing_policies와 load_management 설정도 서버 재시작 없이 즉시 반영됩니다. PUT /admin/smart-routing/policies 엔드포인트를 통해 런타임에 정책을 업데이트할 수도 있습니다.
리로드에서 enabled: false로 바꾸면 다음 요청부터 스마트 라우팅이 멈춥니다. auto(intercept_all이면 모든 모델)는 티어 모델로 바뀌지 않고 적힌 그대로 전달됩니다. 다시 true로 바꾸면 리로드된 정책으로 라우팅이 재개됩니다. 스마트 라우팅을 끈 상태로 시작한 라우터도 마찬가지입니다. 스마트 라우터와 부하 모니터는 어느 경우든 시작 시 만들어지므로(비활성 동안에는 유휴 상태), 리로드에서 스마트 라우팅이나 load_management를 켜면 재시작 없이 다음 요청부터 적용됩니다. 유휴 라우터는 요청당 원자적 로드 한 번의 비용만 들며 잠금을 잡지 않습니다. 부하 모니터의 평가 태스크는 부하 관리가 처음 켜질 때 시작되고 그 뒤로는 주기(assessment_interval_ms)가 고정됩니다.
허브가 관리하는 스마트 라우팅¶
control_plane.policy가 활성화되어 Continuum Hub에 연결된 라우터(control-plane 빌드)는 허브에서 스마트 라우팅 정책(라우팅 규칙, 모델 티어 프로필, 가상 모델 이름, intercept_all, 분류기 방식·모델·백엔드 이름)을 수신할 수 있습니다:
- 엄격성 우선 합성 및 로컬 경계: 로컬
enabled: false가 항상 우선하고intercept_all은 양쪽 모두 활성화했을 때만 동작하며, 허브 분류기 백엔드는 이 라우터의 로컬backends[]이름을 참조합니다. 백엔드 URL, 자격 증명, 부하 관리,unprofiled_models, 분류기 세부 튜닝은 로컬 전용으로 유지되며, 등록 요청과 하트비트에는 읽기 전용smart_routing_inventory블록이 포함됩니다. - 상세 문서: 허브 선언 가능 항목, 합성 규칙 및 실제 예시, 로컬 전용 항목, 거부 코드, 허브 상태 보고, Admin API 동작, 인벤토리 스키마는 스마트 라우팅 거버넌스를 참고하세요.
환경별 설정¶
개발 설정¶
# config/development.yaml
server:
bind_address: "127.0.0.1:8080"
backends:
- name: "local-ollama"
url: "http://localhost:11434"
health_checks:
interval: "10s" # 더 빈번한 확인
timeout: "5s"
unhealthy_threshold: 3
healthy_threshold: 2
endpoint: "/health"
logging:
level: "debug" # 상세 로깅
format: "pretty" # 사람이 읽기 쉬운
프로덕션 설정¶
# config/production.yaml
server:
bind_address: "0.0.0.0:8080"
workers: 8 # 프로덕션용 더 많은 워커
connection_pool_size: 300 # 더 큰 연결 풀
backends:
- name: "primary-openai"
url: "https://api.openai.com"
weight: 3
- name: "secondary-azure"
url: "https://azure-openai.example.com"
weight: 2
- name: "fallback-local"
url: "http://internal-llm:11434"
weight: 1
health_checks:
interval: "60s" # 덜 빈번한 확인
timeout: "15s" # 네트워크 지연을 위한 더 긴 타임아웃
unhealthy_threshold: 5 # 더 많은 허용
healthy_threshold: 3
endpoint: "/health"
timeouts:
connection: "10s"
request:
standard:
first_byte: "30s" # 폐기 예정이며 동작하지 않음. 예산은 `total`
total: "120s" # 프로덕션용 제한된 타임아웃
streaming:
first_byte: "120s" # 첫 SSE 청크에 적용되는 실제 기한
chunk_interval: "30s"
total: "600s"
retry:
max_attempts: 3 # 제한된 재시도
initial_delay: "100ms"
max_delay: "10s"
backoff_multiplier: 2.0
jitter: true
retryable_status_codes: [429, 502, 503, 504]
retryable_errors: [ConnectionError, TimeoutError]
timeout: "30s"
logging:
level: "warn" # 덜 상세한 로깅
format: "json" # 구조화된 로깅
컨테이너 설정¶
# config/container.yaml - 컨테이너에 최적화
server:
bind_address: "0.0.0.0:8080"
workers: 0 # 컨테이너 제한에 따라 자동 감지
backends:
- name: "backend-1"
url: "${BACKEND_1_URL}" # 환경 변수 치환
- name: "backend-2"
url: "${BACKEND_2_URL}"
health_checks:
interval: "30s"
timeout: "5s"
unhealthy_threshold: 3
healthy_threshold: 2
endpoint: "/health"
logging:
level: "info" # 필요하면 검증 전에 다른 값으로 렌더링
format: "json" # 컨테이너에서 항상 JSON