콘텐츠로 이동

요청별 프로바이더 라우팅 선호도: 설계 기록

이 문서는 클라이언트가 표현하는 요청별 라우팅 선호도(OpenRouter의 provider 객체 parity)의 설계를 기록합니다 (이슈 #2396). 기능은 proxy.client_provider_preferences 스위치 뒤에서 출시되었습니다. 사용자 계약은 API 레퍼런스의 클라이언트 provider 기본 설정에 있고, 이 페이지는 설계가 어떻게 확정되었고 원래 계획에서 무엇이 바뀌었는지를 기록합니다.

현재

출시된 기능과 함께 있는 것은 다음과 같습니다.

  • x-backend 헤더: 클라이언트는 이미 모델 라우팅 엔드포인트 전체에서 요청별로 백엔드 하나를 고정할 수 있습니다(src/proxy/selection.rs의 BACKEND_PREFERENCE_HEADER, API 레퍼런스 참고). 여전히 첫 시도 고정이며 provider보다 우선합니다.
  • 패스스루: 스위치가 꺼져 있으면(기본값) 챗 핸들러는 provider 키를 읽지 않으므로, 백엔드 패스스루 규약에 따라 알 수 없는 최상위 필드가 로컬 및 generic OpenAI 호환 백엔드에 그대로 전달됩니다(엄격한 클라우드 OpenAI와 Gemini 경로만 알 수 없는 필드를 제거합니다). 따라서 클라이언트의 provider 객체는 여전히 OpenRouter 같은 애그리게이터 백엔드에 도달하며, 그곳에서 의미를 갖습니다.
  • backends[].request_extensions.body: 백엔드별 기본값과 재정의를 클라이언트 객체에 리프 단위로 병합합니다. 스위치가 켜져 있어도 운영자는 overrides.provider로 이 객체를 이해하는 백엔드에 provider 객체를 보낼 수 있으며, 이 값은 클라이언트 키를 제거한 뒤 디스패치 시점에 적용됩니다.

출시된 설계

  • 설정 표면: proxy.client_provider_preferences, 기본값 false로, /v1/chat/completions 디스패치에서 클라이언트의 provider 객체를 해석합니다. 없거나 비활성화된 경우 아무것도 바뀌지 않으며, provider 키는 이전처럼 그대로 전달됩니다.
  • 파싱과 한도: 원본 값을 serde_json::Value로 유지하고 deny_unknown_fields 대신 직접 검증하므로, 모든 거부가 정확한 경로(provider.<field>, provider.max_price.<key>, 목록 인덱스)를 지목할 수 있습니다. 직렬화한 객체는 16 KiB, 각 이름 목록은 16개 항목, 각 이름은 1~128바이트로 제한하며 앞뒤 공백과 제어 문자를 허용하지 않습니다. 중복은 첫 등장만 남겨 합칩니다.
  • 해석되는 필드: order(소프트 순위 목록), only / ignore(하드 허용/차단 목록), sort(가격, 처리량, 지연), max_price(prompt와 completion의 100만 토큰당 USD 상한)입니다. require_parameters와 data_collection은 문법적으로 받지만 허용적인 값만 받으며, 제한적인 값은 이름을 밝혀 거부합니다. 알 수 없는 필드는 무시하지 않고 400으로 거부합니다.
  • 보안: 후보 집합은 백엔드 가시성, 키의 allowed_models와 allowed_backends, Hub와 정책의 축소로 만든 뒤에야 클라이언트 필터를 적용합니다. 클라이언트의 only/ignore는 키의 허용 목록이나 라우팅 정책을 절대 넓힐 수 없습니다. 항상 교집합을 적용하며 합집합은 적용하지 않습니다. 비어 있지 않은 후보 집합을 비우는 필터는 400으로 응답하므로, 채울 수 없는 기본 설정은 클라이언트 오류이고 클라이언트가 배제한 백엔드로 조용히 바뀌는 일은 없습니다.
  • 바디 위생: 스위치가 켜지면 라우터가 provider 객체를 소비하고 디스패치 전에 제거하므로, 알 수 없는 필드를 거부하는 백엔드도 실패하지 않습니다. 이 제거는 애그리게이터 백엔드로 가는 패스스루를 바꾸므로 스위치는 옵트인이며, 애그리게이터 백엔드는 request_extensions.body.overrides.provider로 전달을 다시 켭니다.
  • 중복 제거: 기본 설정을 담은 요청은 다른 요청의 진행 중인 디스패치에 합류하지 않습니다. 중복 제거 키는 디스패치되는 본문을 해시하는데 객체는 이미 본문에서 제거되어 있으므로, 기본 설정만 다른 두 요청이 합쳐져 한쪽이 자신이 배제한 백엔드의 응답을 받을 수 있기 때문입니다. 객체가 없는 요청은 이전과 똑같이 합쳐집니다.
  • sort용 텔레메트리 시임: 가격은 비용 보고가 쓰는 것과 같은 예상 요청 비용 도우미를, 처리량은 최신 엔진 스탯 스냅숏을, 지연은 풀이 기록한 평균 응답 시간을 사용합니다. 신호가 없는 후보는 뒤의 한 순위 그룹에 합류하므로, 텔레메트리가 없는 플릿은 그룹이 하나가 되고 설정된 selection_strategy가 이전과 똑같이 결정합니다.
  • 그룹화: 확인된 모든 키에서 동률인 후보는 하나의 순위 그룹을 이루고 설정된 전략이 그 안에서 순환하므로, 같은 가격이나 같은 순서 등급이 첫 후보를 고정하지 않습니다.

확정된 질문

  • 기본 sort: 없습니다. sort와 order를 생략하면 설정된 selection_strategy가 그대로 적용됩니다. 스위치 자체 외에 운영자 측 필드 제한은 없습니다. 엄격한 필드 집합과 거부하는 제한적 값들이 그 제한입니다.
  • 애그리게이터 백엔드: 스위치가 켜지면 클라이언트 객체는 어떤 백엔드에도 원문 그대로 도달하지 않습니다. 운영자는 request_extensions.body.overrides.provider로 provider 객체를 의도적으로 전달하며, 이 값이 제거된 클라이언트 키보다 우선하므로 정적 설정이 마지막 결정권을 갖습니다. defaults.provider는 비어 있는 키만 채우므로 제거된 클라이언트 객체를 복원할 수 없습니다.
  • order와 폴백: order는 하나를 고정하지 않고 시도마다 살아남은 후보를 다시 정렬합니다. smart-routing, 할당량, 상태 필터가 먼저 좁히고, order와 sort가 남은 후보의 순위를 정하며, 첫 시도 뒤의 재시도에도 같은 기본 설정을 다시 적용합니다. x-backend는 여전히 첫 시도에서만 이깁니다.
  • 거부 시 폴백: provider 거부는 400 검증 오류로 매핑되고 기본적으로 폴백 트리거가 아닙니다. 스트리밍 경로에서도 이 거부에는 폴백 체인을 차단합니다. 다른 모델의 200은 폴백이 아니라 다른 답이기 때문입니다.
  • Anthropic 인그레스: /anthropic/v1/messages의 최상위 provider 키는 스위치가 켜져 있을 때 조용히 버리지 않고 거부합니다. 이 인그레스는 클라이언트 라우팅 기본 설정을 적용하지 않기 때문입니다.