콘텐츠로 이동

백엔드 패스스루 계약

일부 백엔드 타입의 경우, Continuum Router는 요청이 전송 계층에 도달한 시점부터는 채팅 완성 요청 바디를 백엔드별 재구성 없이 그대로 전달합니다. 이는 엣지에서 업스트림까지의 바이트 동등성 보증보다 좁은 범위입니다. 앞단의 핸들러 단계는 make_http_request, make_unix_socket_request, LlamaCppBackend::execute_chat_completion이 페이로드를 보기 전에 여전히 이를 수정할 수 있습니다. 이 문서는 전송 계층 보증을 제공하는 백엔드, 알려진 소비자가 의존하는 필드, 그리고 요청을 변환하는 백엔드와의 경계를 정의합니다.

패스스루 백엔드

다음 백엔드 타입은 채팅 완성 전송 경로에서 백엔드별 변환을 적용하지 않습니다. 요청이 해당 경로에 도달한 뒤에는 비표준 최상위 필드, extra_body, 인식할 수 없는 키가 모두 변경 없이 전달됩니다.

  • OpenAI (아래 클라우드 경계 예외 참고)
  • Llamacpp (llama-server)
  • Mlxcel (LlamaCppBackend 재사용; src/infrastructure/backends/factory/backend_factory.rs 참고)
  • Ollama
  • vLLM
  • SGLang (VLLMBackend 재사용; src/infrastructure/backends/factory/backend_factory.rs 참고)
  • LMStudio
  • LocalAI

패스스루에는 동등한 세 가지 프록시 지점과 팩토리 경유 백엔드 지점 하나가 있습니다.

  • HTTP 경로: src/proxy/backend.rs::make_http_request. 주 패스스루 지점으로, 비-Anthropic, 비-Gemini 분기의 else 블록에서 payload.clone()을 호출해 바디를 그대로 전달합니다.
  • Unix 소켓 경로: 같은 파일의 make_unix_socket_request 내 형제 else 블록으로, backend.transport가 UnixSocket일 때 실행됩니다.
  • 스트리밍 경로: src/http/streaming/handler.rs의 client.post(&backend_url).json(&current_payload) 호출 지점으로, 스트리밍 폴백 루프의 각 시도에서 current_payload를 그대로 전달합니다.
  • 팩토리 경유 llama.cpp / MLxcel 경로: src/infrastructure/backends/llamacpp/backend.rs::LlamaCppBackend::execute_chat_completion. src/infrastructure/backends/factory/backend_factory.rs를 통해 BackendTypeConfig::Llamacpp와 BackendTypeConfig::Mlxcel에 대해 실행됩니다.

아래에서 설명하는 경계 예외를 제외하면, 네 지점 모두 공급자별 변환을 적용하지 않습니다.

클라우드 OpenAI 예외

OpenAI 백엔드 타입은 OpenAI 클라우드와 로컬 OpenAI 호환 엔진을 모두 포괄하는데 두 환경은 알 수 없는 필드를 다르게 처리합니다. api.openai.com은 인식할 수 없는 최상위 키가 하나라도 있으면 HTTP 400으로 거부하는 반면, 로컬 엔진은 해당 필드를 실제로 소비합니다.

따라서 전송 지점들은 조건부 필터를 적용합니다. /v1/chat/completions 요청의 목적지 URL에 api.openai.com이 포함되면, 엔진 전용 필드인 chat_template_kwargs, thinking_budget_tokens, enable_thinking, preserve_thinking, top_k, min_p, repeat_penalty(src/infrastructure/backends/field_filter.rs의 NON_OPENAI_FIELDS)를 전송 전에 제거합니다.

  • 제거는 클라우드 OpenAI로 채팅을 보내는 모든 지점에서 실행됩니다. make_http_request의 비스트리밍 패스스루 분기, 스트리밍 전송, 그리고 mid-stream 폴백과 자동 선택 폴백 루프의 각 홉이 해당합니다. 폴백 과정에서 백엔드가 바뀔 수 있으므로 게이트는 홉마다 다시 평가됩니다.
  • 같은 차단 목록이 클라우드 Gemini 제거에도 쓰입니다(Google의 /v1beta/openai/chat/completions 엔드포인트도 필드 이름을 동일하게 검증합니다). Azure OpenAI와 Anthropic /v1/messages 브리지는 별도 표면이므로 이 게이트의 대상이 아닙니다.
  • 제거는 최상위 필드에만 적용됩니다. extra_body와 reasoning_effort는 변경하지 않습니다. extra_body는 공급자별 설정을 위한 의도된 통로이고 reasoning_effort는 클라우드 OpenAI가 직접 허용합니다(클라우드 Gemini는 thinking_level로 매핑합니다).
  • 로컬 엔진 URL에는 api.openai.com이 포함되지 않으므로 llama.cpp, vLLM, SGLang, MLxcel, Ollama, LM Studio, LocalAI에는 이 필터가 동작하지 않으며 패스스루 보증도 그대로 유지됩니다.

Anthropic 고유 필드 예외

클라이언트 측 OpenAI 인그레스는 표준 chat-completions 본문에 포함된 세 가지 Anthropic 고유 확장 필드인 speed(fast mode)와 확장 사고 쌍 thinking, output_config(src/infrastructure/backends/field_filter.rs의 ANTHROPIC_NATIVE_FIELDS)를 허용합니다. 라우터는 이 필드들을 디스패치의 Anthropic 경로에서 변환합니다.

OpenAI 와이어 대상 중 이 필드 이름을 직접 읽는 엔진은 없습니다. llama.cpp는 chat_template_kwargs와 thinking_budget_tokens를, vLLM과 SGLang은 chat_template_kwargs를 읽으며, 모든 대상이 공통으로 인식하는 추론 표기는 reasoning_effort입니다. 클라우드 OpenAI, Azure OpenAI, 클라우드 Gemini는 알 수 없는 최상위 키에 HTTP 400을 반환하므로 필드가 그대로 전달되면 요청이 실패합니다.

따라서 선택된 백엔드의 backend_type이 OpenAI 와이어를 사용하는 경우 /v1/chat/completions 본문에서 이 필드들을 제거합니다.

  • 적용 전송 지점: make_http_request와 make_unix_socket_request의 비스트리밍 패스스루 분기, 스트리밍 전송의 build_openai_chat_request_core(직접 스트리밍, mid-stream 홉 루프, 자동 백엔드 선택 포함), stream_via_unix_socket, dispatch_native_streaming의 Gemini 분기, GeminiBackend::transform_request에서 모두 실행됩니다.
  • Gemini 스트리밍 스냅샷: Gemini 스트리밍 분기는 요청이 라우팅된 시점의 설정 스냅샷을 기준으로 게이트를 실행하므로 백엔드의 model_configs나 model-metadata.d/ 드롭인에 선언한 reasoning_efforts 어휘가 다른 경로와 동일하게 변환된 effort에 적용됩니다. 타입이 고정된 백엔드 자체의 게이트는 선언을 읽지 않으며 보조 수단으로만 남습니다.
  • 타입 기반 게이트: 게이트 기준은 URL이나 모델 이름이 아니라 설정된 백엔드 타입입니다. 클라우드 OpenAI 제거와 달리 로컬 엔진도 이 이름을 읽지 않으므로 함께 대상에 포함됩니다.
  • 제거 대상 타입: Generic, Openai, Azure, Gemini, Vllm, Ollama, Llamacpp, Mlxcel, Lmstudio, Sglang은 해당 필드를 제거합니다. match 문에 포괄 분기를 두지 않아 새 백엔드 타입이 추가되면 처리 방식을 명시적으로 결정해야 합니다.
  • 유지 대상 타입:
    • Anthropic과 Bedrock은 필드를 유지합니다. 각 변환기가 thinking과 output_config를 직접 읽으며 speed는 백엔드의 anthropic_fast_mode 옵트인 설정을 따릅니다.
    • Continuumrouter도 필드를 유지합니다. 하위 Continuum Router는 동일한 표준 확장 필드를 인식하고 자신의 백엔드에 같은 규칙을 적용하므로, 상위에서 미리 제거하면 Anthropic을 앞단에 둔 하위 라우터로 가는 홉에서 클라이언트의 추론 의도가 손실됩니다.
    • 전체 패스스루는 선택된 백엔드 이름이 전송 지점이 참조하는 설정 스냅샷에 없을 때만 발생합니다(예: 요청 처리 중 핫 리로드로 백엔드가 삭제되거나 이름이 변경된 경우). YAML에서 type:을 생략한 백엔드는 기본값 generic으로 해석되어 다른 OpenAI 와이어 타입과 동일하게 제거됩니다.
  • 적용 범위: 제거는 최상위 필드에만 적용됩니다. 클라우드 OpenAI 제거와 같은 이유로 extra_body와 reasoning_effort는 변경하지 않습니다.
  • 제거 전 변환: 같은 게이트에서 제거 대상 요청에 한해 제거 직전에 thinking 설정을 선택된 대상이 허용하는 reasoning_effort 값으로 먼저 변환합니다. 클라이언트가 보낸 reasoning_effort(또는 reasoning.effort)가 항상 우선하며, 형식이 잘못된 설정은 단순 제거로 처리됩니다. 매핑 표와 대상별 어휘 규칙은 모델 폴백을 참고하십시오.

OpenAI 채팅 완성 호환성 예외

실제 백엔드와 모델을 선택한 뒤, OpenAI 및 Azure OpenAI 채팅 완성 요청은 공급자 경계에서 하나의 멱등 호환성 변환을 거칩니다. o1, o3, 구분자가 있는 GPT-5 계열(예: gpt-5, gpt-5-2025-08-07, gpt-5.6-luna)에서는 레거시 max_tokens 필드 이름을 max_completion_tokens로 바꿉니다. 대소문자를 구분하지 않되 계열 경계를 확인하므로 o10, gpt-50, gpt-5o처럼 관련 없는 식별자는 일치하지 않습니다.

선택된 HTTP 및 Unix 소켓 프록시 전송, 스트리밍 시도와 폴백, Anthropic Messages 호환성 브리지, 타입 기반 OpenAI 채팅 및 스트리밍 호출, 컨트롤 플레인 프로브, OpenAIBackend::transform_request가 모두 같은 변환을 사용합니다. 공개 continuum_router::proxy::transform_payload_for_openai 헬퍼도 이 단일 구현을 감싸는 얇은 래퍼로 유지됩니다.

변환의 우선순위와 보존 규칙은 다음과 같습니다.

  • max_tokens만 있으면 그 값을 max_completion_tokens로 옮깁니다.
  • 두 필드가 모두 있으면 호출자가 제공한 max_completion_tokens 값이 우선하며 max_tokens는 제거합니다.
  • 변환을 다시 적용해도 결과는 달라지지 않습니다. temperature, top_p, logprobs 같은 샘플링 제어와 extra_body 및 관련 없는 모든 필드는 그대로 유지됩니다.
  • 모델 이름만으로는 OpenAI 규칙이 활성화되지 않습니다. Gemini와 자체 호스팅 백엔드는 선택된 모델 이름이 gpt-5.6-luna여도 원래 토큰 필드를 유지합니다.

운영자 설정 요청 확장

request_extensions를 설정한 백엔드(요청 확장 참고)는 공급자별 변환과는 별도로 운영자가 정의한 페이로드 병합을 수행합니다.

  • 병합 순서: 설정한 body.defaults와 body.overrides 조각이 위의 모든 제거와 정규화가 끝난 뒤 완성된 와이어 페이로드에 병합되고 설정한 headers가 추가됩니다.
  • 홉별 실행: src/infrastructure/common/request_extensions.rs의 공유 함수가 각 전송 지점에서 선택된 백엔드 설정으로 호출되어 시도마다, 그리고 폴백 홉마다 적용됩니다.
  • 검증 가드: 설정 로드 단계에서 위의 제거 규칙이 해당 백엔드에서 제거하는 본문 키를 거부하므로, 제거된 필드가 병합 순서 때문에 다시 추가되는 일은 없습니다. 이 블록이 없는 백엔드는 그대로 패스스루됩니다.

추론 stop 시퀀스 예외

추론 요청은 request_extensions 적용 후 시도별 최종 검사를 거쳐 stop, stop_sequences, 네이티브 Gemini의 generationConfig.stopSequences를 제거합니다. 실제 비활성화 제어와 선언된 비추론 기본값은 해당 필드를 유지합니다. 원본 요청은 다음 폴백 시도를 위해 보존합니다. 제거한 stop을 사용자에게 보이는 출력에 다시 적용하지는 않습니다. 추론 요청의 stop 시퀀스를 참고하세요.

모델이 선언한 시스템 메시지 병합

메타데이터에 requires_single_leading_system이 설정된 모델(선두 시스템 메시지 하나만 받는 모델 참고, 내장 카탈로그에서는 Qwen3.8 계열에 설정)은 선두 단일 시스템 메시지만 허용하는 채팅 템플릿을 위해 /v1/chat/completions 페이로드의 모든 system 항목을 선두 system 메시지 하나로 병합합니다.

  • 적용 전송 지점: src/infrastructure/common/system_messages.rs의 공유 함수가 클라이언트 대화를 전달하는 모든 /v1/chat/completions 전송 지점에서 확장 병합 직전에 설정 스냅샷과 선택된 백엔드를 인자로 호출됩니다.
    • make_http_request와 make_unix_socket_request의 비스트리밍 패스스루 분기.
    • 스트리밍 전송의 build_openai_chat_request_core와 stream_via_unix_socket.
    • Responses→Chat 브리지(비스트리밍 및 스트림 서비스)와 ACP 프롬프트 핸들러.
    • Anthropic Messages 인그레스(자체 변환 이후 apply_selected_openai_chat_compat에서 적용).
  • 홉별 플래그 해석: 플래그는 시도마다, 그리고 폴백 홉마다 해당 홉의 백엔드와 모델을 기준으로(model_configs → 메타데이터 계층 → 내장 카탈로그 순) OpenAI 와이어 백엔드 타입에 한해 해석됩니다.
  • 페이로드 격리 및 예외: 페이로드가 단일 시도 이후에도 유지되는 지점은 사본을 수정하므로 mid-stream 릴레이의 다음 홉은 원본 messages 배열에서 출발합니다. 컨트롤 플레인 프로브는 단일 사용자 턴만 보내므로 이 함수를 호출하지 않으며, 플래그가 없는 모델은 그대로 패스스루됩니다.

알려진 소비자

다음 필드들은 실제 운영에서 사용되며 패스스루 과정에서 반드시 변경 없이 전달되어야 합니다.

  • chat_template_kwargs: llama.cpp / MLxcel용 Jinja 템플릿 파라미터 (예: {"enable_thinking": false, "preserve_thinking": true}). Qwen3 계열의 thinking-mode 제어에 필요합니다.
  • thinking_budget_tokens: llama.cpp가 허용하는 요청별 thinking 예산 상한. llama-server를 통한 Qwen3 thinking 모드에 필요합니다.
  • extra_body: OpenAI 클라이언트 라이브러리(예: Python openai SDK의 extra_body kwarg)가 전달하는 추가 파라미터 객체. {"skip_special_tokens": false} 같은 vLLM 전용 설정을 전달하는 데 사용됩니다.

변환 백엔드

다음 백엔드 타입은 요청을 전달하기 전에 공급자별 변환 레이어를 거칩니다. 비표준 필드는 원본 그대로 전달되지 않을 수 있습니다.

  • Anthropic: src/http/handlers/anthropic/transform.rs. 변환 과정에서 thinking 블록(budget_tokens 포함)이 다운스트림 OpenAI 호환 호출용 reasoning_effort로 변환됩니다.
  • Gemini: src/infrastructure/backends/gemini/transform.rs. Gemini 네이티브 요청 재구성을 수행하며 현재 필드 매핑은 변환 소스를 참고하세요.

통합 테스트

전송 계층 패스스루 계약은 다음 통합 테스트로 보호됩니다.

  • tests/providers/llamacpp_passthrough_test.rs: chat_template_kwargs, thinking_budget_tokens, extra_body, 임의의 알 수 없는 필드가 llama-server 엔드포인트에 변경 없이 도달하는지 검증합니다.
  • tests/providers/openai_request_compat_test.rs: 타입 기반 OpenAI 채팅, 스트리밍, 요청 변환, 컨트롤 플레인 프로브를 실제 캡처된 와이어 요청으로 검증합니다.
  • tests/providers/mlxcel_passthrough_test.rs: Mlxcel 백엔드 타입에 대한 별도 커버리지를 유지합니다. 프록시 경로 픽스처와 함께 팩토리 경유 BackendFactory -> LlamaCppBackend::execute_chat_completion 어서션을 포함해서, MLxcel 배선이 달라지면 별도의 실패 테스트로 드러납니다.
  • tests/providers/sglang_passthrough_test.rs와 tests/providers/sglang_streaming_passthrough_test.rs: Sglang 백엔드 타입에 대한 별도 커버리지를 유지합니다. SGLang 요청 확장(top_k, min_p, lora_path, session_params, chat_template_kwargs 등)이 비스트리밍과 스트리밍 양쪽 경로에서 엔진에 변경 없이 도달하는지, 응답 메시지와 스트림 델타에서 reasoning_content가 보존되는지, stream_options.include_usage 설정 시 마지막 usage 청크가 그대로 전달되는지를 검증합니다.
  • tests/anthropic/anthropic_input_test.rs: thinking.budget_tokens가 변환되어 다운스트림 서버에 원시 thinking_budget_tokens 필드로 도달하지 않음을 확인하는 부정 테스트(test_anthropic_thinking_budget_tokens_transforms_to_reasoning_effort)를 포함합니다.
  • src/infrastructure/backends/field_filter.rs의 유닛 테스트: 클라우드 게이트 제거를 커버합니다. 클라우드 OpenAI 채팅 요청에서는 차단 목록의 모든 필드가 제거되고 로컬 엔진 요청은 전부 유지되며 채팅이 아닌 엔드포인트는 변경하지 않습니다.
  • 같은 유닛 테스트 모듈이 Anthropic 고유 필드 게이트도 분기별로 한 건씩 커버합니다. OpenAI 와이어 타입은 모두 제거하고, Anthropic / Bedrock / Continuumrouter, 알 수 없는 타입, 채팅이 아닌 엔드포인트는 유지하며, extra_body와 reasoning_effort도 그대로 유지됩니다.
  • tests/fallback/cross_provider_fallback_payload_test.rs: 모의 백엔드가 실제로 수신한 아웃바운드 본문에 대해 Anthropic 고유 필드 제거와 제거 전 변환을 검증합니다.
    • 아웃바운드 제거 검증: 사용자 지정 별칭에서 vLLM 타입 폴백으로 넘어가는 홉(비스트리밍과 스트리밍), 사용자 지정 별칭을 쓰는 Anthropic 타입 대상이 thinking을 그대로 받는 경우, 그리고 anthropic_fast_mode 옵트인이 있을 때만 speed가 와이어까지 전달되는 Anthropic 간 홉.
    • 제거 전 변환 케이스: effort가 지정된 적응형 사고 설정, budget 구간, xhigh 지원 여부별 대상에 대한 max 매핑, reasoning_effort를 지원하지 않는 대상, 기존 reasoning_effort가 우선하는 경우, 비활성 thinking, 형식이 잘못된 설정, 스트리밍 전송 경로.

이 테스트 중 하나라도 리팩터링으로 인해 실패한다면, 일반적인 테스트 실패가 아닌 공개 계약에 대한 변경으로 취급해야 합니다. 병합 전에 이 문서를 업데이트하여 새로운 동작을 반영하세요.

보증 범위

이 보증은 위에 나열한 전송 지점에 적용됩니다. 업스트림 서버가 클라이언트가 제출한 JSON 바디의 바이트 단위 사본을 항상 받는다는 의미는 아닙니다.

패스스루 지점이 실행되기 전에 chat_completions는 여전히 라우터 수준 전처리를 수행할 수 있습니다.

  • global_prompts가 설정된 경우 글로벌 시스템 프롬프트 주입. 이 경우 messages 배열이 수정됩니다.
  • web_search.enabled가 적용되는 자체 호스팅 백엔드 대상 요청에 라우터 관리 web_search 주입. 업스트림 호출 전에 tools 항목이 추가될 수 있습니다.

선택된 공급자 경계에서는 OpenAI 및 Azure OpenAI 채팅 요청만 위에서 설명한 토큰 필드 정규화를 적용받습니다. OpenAI가 아닌 백엔드는 모델 문자열만을 근거로 다시 작성되지 않습니다. 추가적인 최상위 JSON 키를 주입하지 않으며 해당 정규화와 클라우드 OpenAI 제거, 그리고 모든 OpenAI 와이어 backend_type에 적용되는 Anthropic 고유 필드 제거를 제외하면 패스스루 필드를 걸러내거나 이름을 바꾸지 않습니다.

관련 문서