고급 설정¶
전역 프롬프트¶
전역 프롬프트를 사용하면 모든 요청에 시스템 프롬프트를 주입하여 보안, 규정 준수 및 동작 가이드라인에 대한 중앙 집중식 정책 관리를 제공할 수 있습니다. 프롬프트는 인라인으로 정의하거나 외부 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 토큰 단위 요율이고, 텍스트 요율을 대체하지 않고 더해집니다. cached_image_input_tokens는 0.0-1.0 비율인 cached_input_discount와 달리 가격입니다.
per_image는 또 다른 단위인 생성 이미지 1장당 USD를 씁니다. 이 키가 있는 모델은 input_tokens: 0, output_tokens: 0을 보고하는데, 이 0은 의도한 값입니다. per_image가 있다는 사실이 곧 해당 모델의 과금 축이 토큰이 아니라는 표시입니다.
세 키 모두 선택이고 추가만 하므로, 이 키가 없는 카탈로그 항목은 이전과 똑같이 동작합니다. /v1/models/extended와 /v1/models/{model}에서 제공하며, 자세한 내용은 가격 객체 필드를 참고하세요.
이미지 모델은 위 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은 '해당 없음'을 뜻하며, 가격 블록의 0과 같은 관례입니다. 가격 키와 마찬가지로 토큰 한도 두 개를 제외한 limits 키는 모두 선택이고 추가만 하며, 메타데이터 파일의 알 수 없는 키는 점 표기 경로를 알려주는 로드 경고와 함께 무시됩니다. dall-e-3의 supported_qualities와 supported_styles는 카탈로그에서 원래 qualities와 styles로 쓰였습니다. 옛 이름을 그대로 쓰는 드롭인 파일은 더 이상 조용히 사라지지 않고 같은 알 수 없는 키 경고에 이름이 찍힙니다.
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/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/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은 오른쪽에서 토큰을 하나씩 제거하므로, 중간 형태는 입력에 토큰이 나타난 순서를 그대로 따릅니다. 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 기본 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: 목록과 집계된 라이브 카탈로그에 문자 그대로 대조합니다. 설정된 이름이 모두 백엔드가 제공하는 이름이라면 둘은 일치합니다. 둘이 어긋날 때 라우터는 클라이언트가 보낸 모델 이름으로 백엔드를 고르는 모든 ingress에서 선택 전에 요청을 재작성합니다. 다음 네 조건이 모두 성립해야 합니다.
- 요청된 이름이 활성 백엔드의
model_configs나model-metadata.yaml에 선언된, 다른 정본 id의 정확한 별칭(또는 그models/접두 표기)입니다. 위 우선순위 체인의 퍼지 단계(날짜 접미사, 양자화 접미사, HuggingFace 접두사, 와일드카드 별칭)는 보지 않습니다. 그 단계들은 어떤 항목이 메타데이터를 제공할지 정하는 것이고, 디스패치 정체성은 그보다 강한 주장이기 때문입니다. - 요청된 id를
models:목록에 나열한 모든 활성 백엔드가 라이브 카탈로그에 항목을 하나 이상 가집니다. 검색이 끝나지 않은 백엔드는 아무것도 기여하지 않으며 그 침묵은 증거가 아닙니다. - 라이브 카탈로그(백엔드가
GET /v1/models에서 열거한 id, 또는 검색이 없는 백엔드라면 설정된 이름)에서 어떤 백엔드도 요청된 이름을 제공하지 않습니다. - 사용자 라우팅 가능한 백엔드가 라이브 카탈로그에서 정본 id를 제공합니다.
그때만 model 필드가 정본 id로 바뀝니다. 어떤 백엔드든 문자 그대로 제공하는 이름은 건드리지 않으므로, unsloth/Qwen3.6-35B-A3B-GGUF를 나열하는 로컬 엔진은 정확히 그 id를 계속 받습니다. 아무도 제공할 수 없는 요청은 원래 이름을 유지하고 이전과 같이 실패합니다. 업스트림은 자신이 제공한 id로 응답하므로, 별칭을 요청한 클라이언트는 응답 model 필드에서 정본 id를 보게 되며, 이는 OpenAI가 유동 이름에 응답하는 방식과 같습니다.
알아둘 상호작용이 셋 있습니다. 키별 allowed_models 목록과 request_params 모델 범위는 클라이언트가 요청한 이름으로 판단하므로, 별칭을 허용한 키는 재작성 뒤에도 계속 동작합니다. fallback.fallback_chains는 디스패치된 이름으로 조회하므로 별칭에 키를 건 체인은 발동하지 않습니다. 체인 키를 정본 id로 두십시오. 라우터는 재작성하는 별칭에 체인이 걸려 있으면 요청 시점에 경고를 남깁니다. Hub 귀속 요청, Hub 정확 모델 예산 가드 아래의 요청, AppProxy ingress 고정 요청은 이미 모델 결정을 담고 있으므로 재작성하지 않습니다.
대표 사례는 gemini-3.1-pro입니다. Google이 -preview id만 제공하므로 model-metadata.yaml은 이를 gemini-3.1-pro-preview의 별칭으로 선언하고, gemini-3.1-pro 요청은 Google에 404로 도달하는 대신 gemini-3.1-pro-preview로 디스패치됩니다. 재작성은 요청 이름과 정본 이름과 함께 Dispatching a model alias as the canonical id its backend serves라는 info 레벨 로그로 남습니다.
규칙이 적용되는 ingress는 /v1/chat/completions, /anthropic/v1/messages, /anthropic/v1/messages/count_tokens, /v1/responses, /v1/responses/compact, 네이티브 Gemini 멀티모달 하위 경로를 포함한 /v1/embeddings, /v1/images/generations, /v1/images/edits, /v1/images/variations, realtime 핸드셰이크입니다. ACP만 예외로 문자 그대로 매칭을 유지하는데, ACP 사용법과 ACP 아키텍처가 문자 그대로의 매칭을 약속하기 때문입니다.
적용 대상에 관해 알아둘 점이 둘 있습니다. 재작성 이후에 발생하는 404나 403은 라우터가 해석한 id가 아니라 클라이언트가 요청한 모델 이름을 답합니다. 그리고 이미지 편집과 변형 엔드포인트는 클라이언트가 쓰는 모델 이름의 고정 목록을 재작성 전에 검사하므로, 별칭은 그 목록에 들어 있을 때만 이 두 엔드포인트에 도달합니다.
모델 변형에 별칭 사용¶
별칭은 특히 다음에 유용합니다:
- 다른 양자화:
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경계 안으로 제한합니다. 제한만으로 누락된 필드를 만들지는 않습니다.
최상위 객체는 전역 정책입니다. 각 models.<name> 항목도 같은 세 객체를 정의할 수 있습니다. 모델 필드는 각 모드 안에서 전역 필드를 개별적으로 대체하며, 모델 항목에서 생략한 필드는 해당 전역 값이나 범위를 상속합니다.
런타임 계약은 모델 이름이 로컬 별칭 해석 후 정확히 일치하고, 선택한 범위가 컨트롤 플레인 치환, arbitrage, 백엔드 선택, 재시도 또는 폴백보다 먼저 고정되며 그 과정에서 바뀌지 않아야 함을 요구합니다. control_plane.policy로 전달된 Hub 티어 제한은 limits-only이며 각 경계에서 이 로컬 제한과 교차하므로 어느 쪽도 다른 쪽보다 범위를 넓힐 수 없고 빈 교집합은 요청을 거부합니다. 백엔드 범위, 정규식, 글로브 매칭은 지원하지 않습니다.
지원 파라미터 및 유효성 검사¶
다음 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 동작을 유지합니다. 적격 primary 또는 설정된 fallback 경로가 정책 적용 필드를 보존할 수 없으면, 라우터는 그 값을 조용히 버리는 대신 캐시 조회나 업스트림 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를 해석 | 그 경로 중 하나라도 지원하지 않는 필드는 선택 전에 거부 |
스트리밍 폴백에도 같은 표가 적용됩니다. pre-stream hop은 백엔드별 타입 디스패치로 다시 진입하므로, native Anthropic, Bedrock, Gemini 백엔드로 향하는 hop은 디스패치 시점의 단일 wire 형식 변환과 함께 해당 provider의 native 파이프라인으로 처리됩니다. SSE 출력이 시작된 뒤의 스트림 중간 복구는 OpenAI 호환 wire만 사용하므로, native 프로토콜 또는 Unix 소켓 백엔드로 해석되는 체인 항목은 결정적으로 건너뛰고 다음 항목을 시도하며, 일부만 변환된 정책 요청을 전달하지 않습니다. 각 hop은 canonical effective request에서 다시 만들어지므로 여러 단계의 폴백에서 변환이 누적되거나 복원된 필드가 사라지지 않습니다.
응답 캐시 조회와 저장은 라우팅 결과가 정책 호환 백엔드 하나로 확정되고 폴백 체인이 목적지를 바꿀 수 없을 때만 활성화됩니다. 백엔드 identity도 캐시 namespace에 포함됩니다. 적격 백엔드가 여러 개이거나 폴백 체인이 활성화된 요청은 exact, prefix, streaming 캐시를 우회하므로 한 프로바이더에서 만들어진 항목이 다른 경로의 요청을 충족하지 않습니다.
관측 및 강제 적용¶
정책이 요청 필드를 하나 이상 변경하고 선택된 프로바이더 경로가 유효 요청을 보존할 수 있으면 라우터는 정보 제공용 x-continuum-request-params 응답 헤더를 추가합니다. 쉼표로 구분된 토큰에는 default:max_tokens, limit:temperature처럼 상한이 정해진 동작과 canonical 파라미터 이름만 들어갑니다. 요청 값, 모델 이름, 키, 티어 식별자는 절대 포함하지 않습니다.
이 정책은 운영자가 강제로 적용하는 설정입니다. 클라이언트는 요청 헤더, 쿼리 파라미터, 요청 본문 옵션으로 정책을 끄거나 우회할 수 없습니다. 정보 제공용 응답 헤더는 옵트아웃 수단이 아니며 인증 자격 증명도 아닙니다.
유효한 변경이 일어난 뒤 만들어진 성공 응답, 호환되는 캐시 적중, 업스트림 오류 응답에는 이 헤더가 포함됩니다. 정책이 아무것도 변경하지 않았거나 정책 또는 프로바이더 호환성 검사에서 트랜잭션을 거부하면 헤더를 생략합니다. 거부된 트랜잭션은 롤백되며 적용되었다고 오해하게 하는 헤더를 받지 않습니다. 로그와 Prometheus 메트릭도 같은 상한이 정해진 applied/rejected 결과를 사용합니다.
핫 리로드 및 개인정보 보호¶
request_params는 즉시 핫 리로드되는 섹션입니다. control-plane 빌드에서 control_plane.config_sync.enabled: true이면 Continuum Hub가 selection_strategy, prefix_routing.*, 제한된 kv_cache_index.* 라우팅 스키마에 해당하는 공개 kv_routing leaf도 관리할 수 있습니다. 전체 섹션을 로컬에 고정하려면 request_params_immutable: true 또는 kv_routing_immutable: true를 설정합니다. 파싱과 유효성 검사를 통과한 새 리비전은 게시된 설정 스냅샷을 원자적으로 교체합니다. 거부된 리비전은 마지막으로 검증된 스냅샷을 교체하지 않습니다. 각 요청은 변경 전에 불변 config 스냅샷 하나와 인증된 Hub 티어 정책 하나를 캡처하므로 진행 중인 요청·재시도·폴백에서 정책이 중간에 바뀌지 않습니다.
적용 정책 텔레메트리는 제한된 파라미터 이름, 동작, 조합 출처, 결과만 노출하고 요청 값은 포함하지 않습니다. 설정 및 MCP 화면에는 운영자가 작성한 정책이 표시될 수 있지만 프롬프트 내용이나 임의의 요청 데이터를 직렬화하지 않으며 비밀 타입 설정 필드는 계속 마스킹합니다.
핫 리로드¶
hot-reload 기능과 파일 감시자가 활성화되면 유효한 파일 개정이 새 불변 설정 스냅샷으로 게시됩니다. 유효하지 않은 개정은 이전 스냅샷을 유지합니다. 파싱되는 모든 필드에 실행 중인 런타임 소비자가 있는 것은 아니므로 아래의 보수적인 표를 따르십시오.
재시작 없이 적용¶
- 백엔드 추가/삭제/수정(풀이 조정되고 새 백엔드는 즉시 상태 확인)
- 상태 확인 주기와 임계값
- 해당 서비스가 활성화된 경우 속도 제한 및 서킷 브레이커 정책
- 새 요청이 읽는 전역 프롬프트, 요청 매개변수, 스트리밍 정책
- 폴백 서비스가 이미 존재하는 경우 폴백 체인/정책 수정
- 스마트 라우팅 정책 스냅샷
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.workersserver.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" # 추적 ID 헤더 이름 (기본값)
request_id: "X-Request-ID" # 요청 ID 헤더 이름 (기본값)
correlation_id: "X-Correlation-ID" # 상관 ID 헤더 이름 (기본값)
동작 방식¶
-
추적 ID 추출: 요청이 도착하면 라우터는 다음 우선순위로 헤더에서 추적 ID를 추출합니다:
- W3C
traceparent헤더 (W3C 지원 활성화 시) - 설정된
trace_id헤더 (X-Trace-ID) - 설정된
request_id헤더 (X-Request-ID) - 설정된
correlation_id헤더 (X-Correlation-ID)
- W3C
-
추적 ID 생성: 헤더에서 추적 ID가 발견되지 않으면 새 UUID가 생성됩니다.
-
헤더 전파: 추적 ID는 여러 헤더를 통해 백엔드 서비스로 전파됩니다:
X-Request-ID: 광범위한 호환성을 위함X-Trace-ID: 주요 추적 식별자X-Correlation-ID: 상관관계 추적용traceparent: W3C Trace Context (활성화 시)tracestate: W3C Trace State (원본 요청에 존재 시)
-
재시도 시 보존: 동일한 추적 ID가 모든 재시도 시도에서 보존되어, 단일 클라이언트 요청에 대한 여러 백엔드 요청의 상관관계를 쉽게 파악할 수 있습니다.
구조화된 로깅¶
추적이 활성화되면 모든 로그 메시지에 trace_id 필드가 포함됩니다:
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "info",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"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}) - 송신: 보존된 추적 ID와 새 span ID로
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 섹션(이슈 #1447)은 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
스코어링 항은 접두사 보유 백엔드 가운데 엔진이 보고한 waiting_requests가 적고(신선한 후보 전부가 total_slots를 보고하면 그 값으로 정규화하고, 그렇지 않으면 해당 요청 후보 집합의 최대 waiting_requests로 정규화) kv_cache_usage가 낮은 쪽을 우선합니다. engine_stats.interval * max_staleness_intervals보다 오래된 스냅샷은 없는 것으로 취급해 그 후보는 기본 점수를 유지하고, 모든 후보가 오래되면 패스 전체가 기본 스코어링과 같게 동작합니다. 두 balance 임계값은 SGLang Model Gateway의 balance 게이트를 따른 히스테리시스 데드 밴드를 이루며(둘 다 넘어야 순위를 매김) 큐 진동이 선택을 요동시키지 못하게 합니다. 게이트 판정, 정규화, 스코어러 캐시는 정확한 라이브 후보 집합별로 격리됩니다. 결정은 routing_engine_load_decisions_total{backend,reason}으로 집계됩니다. 메트릭을 참고하십시오.
어드미션 힌트는 스코어러와 독립적으로 공용 선택 지점에서 동작합니다. 모든 정상 후보가 임계값을 넘는 신선한 kv_cache_usage 스냅샷을 보고하면 큐잉하는 대신 선택을 거부합니다. 채팅 경로에서 이 거부는 재시도 가능한 503으로, BackendUnhealthy 트리거를 통해 fallback.fallback_chains에 참여하므로 체인이 설정되어 있으면 클라이언트가 오류를 보는 대신 체인이 이어받습니다. 신선한 스냅샷이 없는 후보는 항상 통과합니다.
백엔드별 재시도 설정¶
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"
모델 폴백¶
최상위 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
model_settings:
gpt-5.4:
fallback_enabled: true
notify_on_fallback: true
일반 프록시 경로는 circuit_breaker_open을 만들지 않습니다. 추론 트래픽에는 능동 상태 확인과 backend_unhealthy를 사용하세요. 검증, 헤더, 메트릭, 공급자 간 변환, 스트림 시작 전/중간 동작의 차이는 모델 폴백을 참고하세요.
미드스트림 폴백¶
미드스트림 폴백은 기본 백엔드가 응답 도중 실패할 때 라우터가 진행 중인 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."
동작 방식¶
- 클라이언트가 스트리밍 채팅 완료 요청을 보냅니다.
- 라우터가 기본 백엔드에서 스트리밍을 시작하면서 응답 내용을 누적합니다.
-
백엔드가 스트림 도중 실패하면 (연결 끊김, 타임아웃, 오류 이벤트):
- 오류는 클라이언트로 전달되지 않습니다.
- 누적된 부분 응답이 캡처됩니다.
- 폴백 체인에서 다음 정상 백엔드가 선택됩니다 (비정상 백엔드는 건너뜀).
- 연속 또는 재시작 요청이 폴백 백엔드로 전송됩니다.
- 클라이언트 연결을 닫지 않은 채 폴백 백엔드에서 스트리밍이 재개됩니다.
-
클라이언트는 전환 중 잠깐의 멈춤만 있을 뿐 끊김 없는 응답을 받습니다.
연속 vs. 재시작 모드¶
min_accumulated_tokens 임계값이 어떤 복구 모드를 사용할지 결정합니다:
| 조건 | 모드 | 동작 |
|---|---|---|
enabled: true (기본값)이고 토큰 수가 min_accumulated_tokens 이상이며 잘리지 않음 | 연속 | 원본 메시지 + 부분 어시스턴트 응답 + 연속 프롬프트 |
enabled: true (기본값)이고 토큰 수가 min_accumulated_tokens 미만 | 재시작 | 원본 요청 재실행 (이어가기에는 컨텍스트 부족) |
enabled: true (기본값)이고 내용이 잘림 (> 100 KB) | 재시작 | 일관성 없는 컨텍스트를 피하기 위해 강제 재시작 |
mid_stream_fallback.enabled: false | 재시작 | 원본 요청을 폴백 백엔드에서 처음부터 재실행 |
연속 모드 (기본값)는 클라이언트에 끊김 없는 출력을 제공합니다. 재시작 모드는 의미 있게 이어가기에는 컨텍스트가 너무 적거나, 누적된 응답이 안전하게 포함하기에는 너무 길 때 자동으로 사용됩니다. enabled: false를 명시적으로 설정하면 무조건 재시작 모드가 강제되며, 클라이언트에 중복되거나 일관성 없는 내용이 보일 수 있습니다.
엣지 케이스 처리¶
미드스트림 폴백 경로는 여러 엣지 케이스를 자동으로 처리합니다:
- 전역 타임아웃 예산: 모든 폴백 시도는 원래 요청의 시작 시각을 공유합니다. 각 시도는 전송 전에 남은 예산을 확인하므로, 체인 전체에 걸쳐 타임아웃이 무한정 누적되지 않습니다.
- 표준 형태 크로스 프로바이더 페이로드: 폴백 모델이 다른 프로바이더에 있어도 (예: OpenAI → Anthropic) 요청은 표준 OpenAI chat-completions 형태를 유지합니다. 홉은 모델 이름만 바꿉니다. 와이어 형식 변환도, 어떤 필드가 와이어까지 나갈 수 있는지에 대한 결정도, 선택된 백엔드의 설정 타입을 기준으로 디스패치에서 한 번만 일어납니다.
- 동시 요청 폭주: 기본 백엔드가 죽으면 그쪽으로 향하던 모든 진행 중 요청이 한꺼번에 실패하고, 그 요청 전부가 같은 구조 백엔드로 홉합니다.
fallback.fallback_policy.max_concurrent_dials_per_backend(기본값50,0= 무제한, 범위0..=10000)는 이 폭주를 백엔드별로 제한합니다. 이 상한은 다이얼만 감쌉니다. 퍼밋은 홉의 아웃바운드 요청을 보내기 직전에 얻고 프로바이더 핸드셰이크가 돌아오는 즉시 (상태 코드와 헤더, 또는 전송 오류) 반납하며, 응답 본문 구간에는 결코 걸치지 않습니다. 본문 구간은 이미server.max_concurrent_requests, 타임아웃, 서킷 브레이커로 제한되고, 스트림 전체를 붙잡으면 장애가 가용성 절벽으로 바뀌기 때문입니다. 네 가지 폴백 경로 모두에 적용됩니다. 비스트리밍 퍼널 (홉의 재시도 각각이 다시 다이얼하고 다시 제한됨), 두mid_stream_enabled모드의 프리스트림 연결 단계, 네이티브 프로토콜 백엔드로의 홉, 그리고 SSE 응답이 커밋된 뒤 릴레이가 수행하는 홉입니다. 제한 대상은 홉뿐입니다. 기본 시도는 퍼밋을 얻지 않으며, 첫 다이얼 전에 백엔드 선택이 폴백 모델로 걸어간 요청도 마찬가지입니다. 그 상태에서는 기본 백엔드가 이미 죽은 것으로 알려져 있고 구조 백엔드가 사실상 기본 백엔드이므로, 이를 제한하면 정상 상태 처리량을 제한하는 셈이 되기 때문입니다. 포화된 백엔드에서는 다이얼이 FIFO로 대기하며, 대기 시간은 유효timeouts.connection까지 (스트리밍 경로에서는 추가로 시도 간 체인 예산의 잔여분까지) 입니다. 만료되면 그 백엔드에 대한 홉은backend_unhealthy트리거 (기본 활성)로 실패하고, 체인은 다음 항목으로 진행하거나 보존된 업스트림 실패로 소진됩니다. 퍼밋은 서킷 브레이커 승인보다 먼저 얻으므로 포화 타임아웃이 half-open 프로브 슬롯을 소비하거나 브레이커에 기록을 남기는 일은 없습니다. 전송 계층에 핸드셰이크 경계가 없어 다이얼 대신 호출 전체 동안 퍼밋을 붙잡는 예외가 둘 있습니다. Unix 소켓을 통한 비스트리밍 홉과 Bedrock runtime 백엔드로의 비스트리밍 홉입니다. 네이티브 프로토콜 백엔드로의 홉에서 발생한 포화 타임아웃도 다른 홉과 마찬가지로backend_unhealthy로 체인을 진행시킵니다. 프로바이더가 응답하기 전에 실패한 네이티브 시도는 같은 체인 탐색에 합류하기 때문입니다 (모델 폴백 참고). 이 설정은 즉시 리로드됩니다. 제한값이 바뀌면 모든 백엔드별 세마포어가 초기화되어 새 다이얼은 새 크기를 쓰고, 이전 세마포어를 이미 보유하거나 대기 중인 다이얼은 그것을 그대로 유지하다 무해하게 반납하므로, 변경이 진행되는 동안 한 백엔드의 상한은 잠시 이전 퍼밋과 새 퍼밋의 합이 됩니다. 포화 타임아웃 횟수는fallback_dial_bound_saturated_total{backend}에 집계됩니다. - 누적기 잘림: 누적된 응답 내용이 100 KB를 초과하면, 일관성 없는 컨텍스트가 폴백 백엔드로 전송되지 않도록 연속 모드가 재시작으로 강제됩니다.
- 헬스 재확인: 체인의 각 폴백 시도 전에 백엔드 헬스를 다시 검증합니다. 비정상 백엔드는 건너뛰고 다음 항목으로 넘어갑니다.
[DONE]마커 누락:[DONE]없이 끝났지만finish_reason: "stop"이 있는 스트림은 정상 완료로 처리되어 불필요한 폴백을 방지합니다.
메트릭¶
세 가지 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.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가 맡습니다. 둘 다 세 갈래 스트리밍 경로(미드스트림 폴백 릴레이, Bedrock 트레이트 스트림 파이프라인, Gemini 스트리밍 파이프라인) 모두에서 강제됩니다. 지연에 민감한 애플리케이션에서는 chunk_interval을 10~15초로 낮추는 것이 좋고, 느린 모델에는 모델별 오버라이드를 두면 됩니다:
timeouts:
request:
streaming:
chunk_interval: 10s # 대부분의 모델에 빠른 감지
model_overrides:
gemini-2.5-pro: # 추론 모델에는 더 긴 간격 필요
streaming:
chunk_interval: 30s
first_byte: 120s
속도 제한¶
설정 가능한 다차원 속도 제한은 속도 제한 문서에서 다룹니다. 그와 별개로 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로 설정하면 모든 요청에 동일한 파이프라인이 적용됩니다.
설정¶
smart_routing:
enabled: true
# 프로필 매칭 실패 및 자동 추론 불가 시 사용할 기본 티어.
# 1 = Flagship, 2 = Standard, 3 = Lightweight. 기본값 2.
default_tier: 2
# 스마트 라우팅을 트리거하는 가상 모델 이름. 기본값 "auto".
virtual_model: "auto"
# true로 설정하면 모델 이름과 관계없이 모든 요청에 스마트 라우팅이 적용됩니다.
intercept_all: false
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은 문제가 된 프로파일을 지목하는 메시지와 함께 기동 단계에서 실패하고, 핫 리로드에서는 이전 설정이 그대로 유지된다.
같은 검증 호출은 기존에 있던 길이 제한도 처음으로 config.yaml 로드 경로에 걸리게 만든다. model과 model_pattern은 각각 200자를 넘을 수 없는데, 이 제한은 원래 어드민 API에서만 적용됐다. 이제는 200자를 넘는 모델명이나 패턴을 적은 config.yaml도 로드 단계에서 실패한다.
모델 선택에 반영되는 값은 입력 비용뿐이다. 스코어러는 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 | 번역 및 다국어 작업 |
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 엔드포인트 호출 시 캐시가 초기화됩니다.
글로브 패턴 문법¶
*만 와일드카드로 사용하며, 여러 개를 중첩할 수 있습니다.
| 패턴 | 매칭 | 미매칭 |
|---|---|---|
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 | 연구 수준 문제, 형식적 증명 | "이 정리를 증명해줘" |
분류 신호¶
| 신호 | 감지 내용 |
|---|---|
message_length | 모든 메시지의 총 토큰 수 |
code_blocks | 펜스 코드 블록 또는 인라인 코드 |
math_notation | LaTeX, 수식, 수학 기호 |
system_prompt_complexity | 시스템 프롬프트의 길이와 복잡도 |
conversation_depth | 대화 턴 수 |
image_attachments | 메시지의 멀티모달 이미지 콘텐츠 |
tool_definitions | 요청의 도구/함수 정의 |
complexity_keywords | "최적화", "설계", "증명" 같은 키워드 |
multilingual | 비라틴 문자. primary_language 언어에서는 억제됩니다 |
creative_markers | "소설", "시", "상상해줘" 같은 표현 |
analysis_markers | "분석해줘", "비교해줘", "평가해줘" 같은 표현 |
code_intent | 코드 마크업 없이 코드 마커가 둘 이상 잡힌 경우 |
감지된 신호가 많을수록 복잡도와 도메인 태그가 올라갑니다. 상충하는 신호(예: 창작 마커와 코드 마커가 동시에 존재)는 신뢰도 점수를 낮춥니다.
도메인은 가장 강한 의도 신호(code_blocks, inline_code, math_notation, creative_markers, analysis_markers, code_intent)로 결정합니다. multilingual은 의도가 아니라 문자 체계를 관찰한 결과이므로, 의도 신호가 하나도 잡히지 않았을 때만 도메인을 정합니다.
code_intent는 코드가 들어 있지 않은 코드 작성 요청을 알아보는 신호입니다. 이 신호가 없던 시절에는 코드 도메인이 오직 문자 그대로의 마크업만 보고 결정됐습니다. 그래서 "이 REST API의 rate limiting 로직을 구현해줘."처럼 백틱이 없는 요청은 코드 신호를 하나도 내지 못하고 general이나 multilingual로 떨어졌고, domain: [code] 정책도 함께 놓쳤습니다. 이 신호는 code 키워드 목록에서 서로 다른 두 개가 잡혀야 발화하며, 이 기준은 조정용 손잡이가 아니라 설계 자체입니다. 마커 하나는 동작 아니면 대상 중 하나일 수밖에 없고 각각 단독으로는 실패합니다. 동작만 보면 "Write a poem about autumn leaves"가 코드 도메인으로 가고, 대상만 보면 "What is a REST API?"가 그렇게 됩니다. 요청에 이미 펜스 블록이나 인라인 백틱이 있으면 이 신호는 아예 건너뜁니다. 그래서 실제 코드 마크업이 있는 요청은 도메인도 신뢰도도 그대로입니다. 강도 0.75는 math_notation(0.8)보다 낮아 수식이 많은 요청은 reasoning으로 남고, multilingual(0.7)보다 높아 마크업 없는 한국어 요청이 문자 체계 관찰로 무너지지 않습니다.
언어 인식¶
분류기는 사용자 메시지 본문의 언어를 감지해 분류 결과에 실어 보냅니다. 감지는 문자 체계를 기준으로 합니다. 한글은 ko, 가나는 ja, 가나 없는 한자는 zh, 라틴 문자는 en, 나머지는 und(미확정)입니다. 한글과 가나는 글자의 10분의 1만 있어도 확정하므로, 영어 기술 용어를 그대로 인용한 한국어 요청도 한국어로 감지됩니다.
감지한 언어는 복잡도·창작·분석 신호가 대조할 키워드 표를 고릅니다. 영어와 한국어 표가 기본 탑재되어 있고, 모든 표는 영어 키워드를 포함합니다. 따라서 여러 언어가 섞인 요청도 영어 목록만 있었을 때 잡히던 신호를 그대로 잡습니다. 자체 표가 없는 언어는 영어 표로 돌아갑니다.
요청 길이는 UTF-8 바이트가 아니라 문자 수를 문자 체계별로 가중해 추정합니다(한글 한 음절과 한자 한 글자는 라틴 문자 두 개로 셉니다). 그래서 한국어 요청과 그 영어 번역이 같은 복잡도 구간에 들어갑니다.
주 언어¶
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와 story 덕분에 간격 규칙을 그대로 씁니다. 이 규칙은 라틴 문자에만 적용합니다. 한국어는 같은 문법 기능을 별도 단어가 아니라 어미와 조사로 표시하므로 영향을 받는 한국어 마커가 없고, 다른 라틴 문자 언어의 관용구를 직접 추가하면 같은 보호를 받지 못합니다.
간격 규칙은 수식어와 합성명사를 구분하지 못합니다. 이는 거친 구석이 아니라 실제 비용입니다. "Write a short story"와 "Write a user story"는 형태가 같아서, 요구사항 산출물을 가리키는 뒤쪽 문장도 이제 creative로 분류됩니다. 이런 표현이 트래픽에 자주 등장하고 라우팅이 어긋나는 것이 문제라면, 해당 마커를 keywords에서 빼고 더 좁은 표현으로 대조하세요.
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 기반 분류기가 소형 저비용 모델에 분류 요청을 보냅니다. classifier.method로 세 가지 동작 방식을 선택할 수 있습니다.
| 방식 | 동작 |
|---|---|
rule | 규칙 기반만 사용 (기본값). LLM 호출 없음. |
llm | 항상 LLM 분류기를 호출하고, 실패 시 규칙 기반으로 폴백. |
hybrid | 규칙 기반을 먼저 실행하고, 신뢰도가 confidence_threshold 미만일 때만 LLM으로 에스컬레이션. |
하이브리드 모드가 권장 운영 설정입니다. 명확한 요청은 마이크로초 단위로 규칙 기반 분류기가 처리하고, 모호한 요청(보통 전체의 10~20%)에만 LLM 호출 지연이 추가됩니다.
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가 가리키는 백엔드가 실제 백엔드 풀에 없으면 기동 시점이 아니라 해당 호출에서만 실패하는데 이때 백엔드 이름을 명시한 오류와 함께 규칙 기반 분류기로 넘어갑니다.
분류기 백엔드 선택¶
분류는 사용자가 입력한 프롬프트 본문을 분류기 백엔드로 보냅니다. 따라서 어떤 백엔드가 선택되는지가 그 내용이 어디로 가는지를 결정합니다. 선택은 요청마다가 아니라 라우터가 상태를 구성할 때 한 번, 그리고 핫 리로드마다 한 번 이루어집니다. 즉 설정이 바뀌기 전까지는 같은 백엔드로 계속 분류 프롬프트가 갑니다.
classifier.llm.backend를 지정한 경우와 생략한 경우는 의도적으로 다르게 처리됩니다.
classifier.llm.backend | 해석 |
|---|---|
| 일반 백엔드 지정 | 그 백엔드를 사용합니다. |
internal: true 백엔드 지정 | 그 백엔드를 사용합니다. 내부 가드 모델을 분류기로 지목하는 것은 운영자의 의도적 선택으로 보며, 어떤 내부 백엔드를 선택했는지 info 수준으로 기록합니다. |
enabled: false 백엔드 지정 | 거부합니다. LLM 분류기를 구성하지 않고 백엔드 이름을 명시한 warn을 남기며 규칙 기반 분류를 유지합니다. 비활성 백엔드는 라우터 내부 트래픽을 포함해 어떤 트래픽도 받지 않습니다. |
| 설정에 없는 이름 지정 | 거부합니다. LLM 분류기를 구성하지 않고 규칙 기반 분류를 유지하며, 아래의 암묵적 선택으로 넘어가지 않습니다. |
| 생략 | backends 순서에서 일반 사용자 트래픽이 닿을 수 있는 첫 번째 백엔드, 즉 internal: true도 enabled: false도 아닌 첫 항목을 사용합니다. 어떤 백엔드를 선택했는지 info 수준으로 기록합니다. 해당하는 백엔드가 없으면 LLM 분류기를 구성하지 않고 규칙 기반 분류를 유지합니다. |
이전 릴리스에서는 생략한 경우가 가시성 검사 없이 말 그대로 backends의 첫 항목이었습니다. 그래서 internal: true 가드 백엔드나 꺼져 있는 enabled: false 예비 백엔드가 첫 자리에 있으면 분류 대상 요청의 프롬프트 본문이 모두 그쪽으로 갔습니다. 숨김 백엔드가 없는 배포에서는 이전과 완전히 동일하게 해석됩니다.
구조화 출력 전략¶
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로 제어). 재시도도 실패하면 규칙 기반 분류기 결과를 사용합니다.
분류 캐시¶
동일한 요청에 대한 반복 LLM 호출을 줄이기 위해 분류 결과를 TTL과 함께 인메모리에 캐시합니다. 캐시 키는 잘린 사용자 메시지의 SHA-256 해시이므로, 의미적으로 동일한 요청은 같은 캐시 결과를 공유합니다. 캐시는 프로세스 단위이며 여러 라우터 인스턴스 간에 공유되지 않습니다.
커스텀 분류 체계¶
복잡도 수준과 도메인 태그 모두 확장 가능합니다. 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)만 지정한 조건은 캐치올이 아닙니다.
정책 필드¶
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) |
신원 조건¶
앞의 세 필드는 요청이 무엇인지를 나타내고, 뒤의 네 필드는 누가 어떤 방식으로 요청했는지를 나타냅니다. 덕분에 하나의 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 필드를 받으므로, 해당 티어의 키를 갖고 있지 않아도 티어 기반 정책을 확인할 수 있습니다.
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) |
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 엔드포인트를 통해 런타임에 정책을 업데이트할 수도 있습니다.
환경별 설정¶
개발 설정¶
# 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