라우터 관리 웹 검색¶
Continuum Router는 자체 호스팅 LLM 백엔드(vLLM, SGLang, Ollama, llama.cpp, MLxcel, LM Studio, Generic OpenAI 호환, Continuum Router 원격)로 라우팅되는 채팅 완료 요청에 OpenAI 스타일의 web_search 도구를 투명하게 주입합니다. 클라이언트마다 검색 연동을 따로 구현하지 않아도 자체 호스팅 모델에서 에이전트 워크플로를 그대로 실행할 수 있습니다.
상용 프로바이더(OpenAI, Azure OpenAI, Gemini, Anthropic)는 이미 자체 웹 검색 도구를 제공하므로 라우터가 도구를 주입하지 않고 요청을 그대로 전달합니다. gpt-5.6-sol이나 claude-sonnet-4.6 요청은 변환 없이 통과합니다.
동작 방식¶
채팅 완료 요청마다 라우터는 다음 순서로 동작합니다.
- 모델을 처리할 백엔드를 결정합니다.
- 백엔드가 자체 호스팅이고
web_search기능이 켜져 있으면, 아웃바운드tools[]배열에web_search함수 도구를 주입합니다. - 요청을 디스패치합니다. 모델이
web_search에 대한tool_calls항목으로 응답하면 라우터는 다음을 수행합니다.{"query": "..."}인수를 파싱합니다.- 설정된 검색 프로바이더를 호출합니다.
- JSON으로 인코딩된 검색 결과를 담은
tool역할 메시지를 대화에 추가합니다. - 검색 결과가 추가된 대화로 백엔드를 다시 호출합니다.
- 최대
max_tool_iterations회(기본값5)까지 반복한 뒤 모델이 생성한 최종 응답을 반환합니다.
검색 프로바이더 호출이 도중에 실패하면 라우터는 HTTP 요청 전체를 실패 처리하는 대신 모델에 {"error": "..."} 형태의 도구 결과를 반환합니다. 모델은 이를 바탕으로 안내하거나, 재시도하거나, 검색 없이 답변을 이어갈 수 있습니다.
Anthropic Messages API 지원¶
클라이언트가 /anthropic/v1/messages 엔드포인트로 요청할 때 라우터는 두 가지 형태의 web_search 도구를 지원합니다.
커스텀 web_search 도구¶
클라이언트가 JSON input_schema를 가진 Anthropic 커스텀 도구 형식의 web_search를 보내면 일반 자체 호스팅 요청과 동일하게 처리합니다. 요청을 OpenAI 포맷으로 변환해 도구 실행 루프를 돌린 뒤, 응답을 다시 Anthropic 포맷으로 변환합니다. Claude Code의 메인 대화가 이 경로를 사용합니다.
Anthropic 서버 도구 web_search_20250305¶
Anthropic 네이티브 웹 검색은 {"type": "web_search_20250305", "name": "web_search"}로 식별됩니다. Claude Code의 내장 WebSearch는 이 도구 정의와 함께 tool_choice: {type: "tool", name: "web_search"}를 보내 도구 호출을 강제합니다. 강제된 tool_choice에 일반 도구 실행 루프를 그대로 적용하면 매 라운드마다 도구 호출이 반복되어 종료되지 않으므로, 라우터는 전용 단발(single-shot) 에뮬레이션 경로로 처리합니다.
- 백엔드를 한 턴 호출해 모델의 도구 호출에서
query를 추출합니다(백엔드가 도구 호출을 생성하지 않으면 사용자 프롬프트에서 Claude Code 특유의"Perform a web search for the query: "접두어를 떼어내 쿼리로 사용하는 폴백이 동작합니다). - 설정된 검색 프로바이더를 한 번만 호출합니다(루프 없음).
-
응답을 Anthropic 네이티브 포맷으로 조립합니다.
- 추출한 쿼리를 담은
server_tool_use블록 - 검색 결과를 담은
web_search_tool_result블록(content필드는 결과에 따라 결정):- 성공 시: 순위가 매겨진 결과 객체 배열(검색 결과가 0건일 때의 빈 배열
[]포함) - 프로바이더 실패 시(API 키 미설정, HTTP 4xx/5xx, 타임아웃, 파싱 오류): 단일
{"type": "web_search_tool_result_error", "error_code": "..."}객체.error_code는 HTTP 429일 때"too_many_requests", 그 외 모든 실패일 때"unavailable"입니다.
- 성공 시: 순위가 매겨진 결과 객체 배열(검색 결과가 0건일 때의 빈 배열
각 결과 객체의 스니펫은 base64로 인코딩되어
encrypted_content필드에 저장됩니다. - 추출한 쿼리를 담은
-
스트리밍 클라이언트에는 Claude Code 파서가
api.anthropic.com과 직접 통신할 때 기대하는 것과 동일한 SSE 이벤트 시퀀스(message_start, 두 블록 각각의content_block_start/delta/stop,message_delta,message_stop)를 전송합니다.
상용 네이티브 Anthropic 경로(forward_to_anthropic_native)는 변경되지 않습니다. 네이티브 Anthropic 백엔드로 라우팅할 때는 서버 도구가 그대로 프록시되어 Anthropic이 직접 검색을 실행합니다.
Gemini 네이티브 Google Search 그라운딩¶
요청이 type: gemini 백엔드로 라우팅될 때, Continuum Router는 별도의 라우터 측 web_search 프로바이더 설정 없이도 Google의 네이티브 Search Grounding을 직접 지원합니다.
- Chat Completions (
POST /v1/chat/completions):tools: [{"type": "google_search"}](또는{"type": "web_search"},{"google_search": {}},web_search_options)를 전달하면 자동으로 Gemini NativegenerateContent/streamGenerateContent엔드포인트로 전환하여tools: [{"google_search": {}}]를 호출합니다. 응답의candidates[0].groundingMetadata는 OpenAI 호환choices[0].message.annotations(type: "url_citation")와 원본grounding_metadata필드로 정규화되어 반환됩니다. - Responses API (
POST /v1/responses):tools: [{"type": "web_search"}]를 전달하면 네이티브google_search그라운딩으로 매핑되며output[].content[].annotations에url_citation항목이 채워집니다. - 이미지 생성 및 편집 (
POST /v1/images/generations,POST /v1/images/edits):nano-banana-pro(gemini-3-pro-image-preview) 또는nano-banana-2(gemini-3.1-flash-image-preview) 모델에"google_search": true를 지정하면 검색 그라운딩 기반 이미지 생성이 활성화되며 응답에grounding_metadata가 포함됩니다(nano-banana-2-lite와nano-banana는google_search: true요청 시 HTTP400으로 거부).
자세한 백엔드 설정은 Gemini 이중 API 라우팅 및 전용 기능을 참고하세요.
설정¶
config.yaml에 최상위 web_search 섹션을 추가합니다.
web_search:
enabled: true
provider: serper # serper | exa | brave | searxng
api_key: "${SERPER_API_KEY}" # 환경 변수 치환 지원 (searxng는 사용하지 않음)
# base_url: "https://mirror.internal/serper/search" # 선택: 엔드포인트 재정의 (http(s), 로드 시 검증); searxng는 필수
timeout_ms: 5000 # 요청당 타임아웃 (100 - 60 000 ms)
max_results: 5 # 1 - 20
max_tool_iterations: 5 # 도구 라운드의 하드 캡 (1 - 20)
inject_policy: auto # auto | always | never
tool_name: web_search # 모델에 알려지는 함수 이름
result_char_cap: 4000 # 결과당 스니펫 잘라내기 캡 (바이트)
streaming_enabled: false # 스트리밍 중 도구 실행 (고급)
loop_wall_clock_ms: 60000 # 루프의 총 벽시계 예산
max_total_result_bytes: 32768 # 도구 결과 콘텐츠의 누적 바이트 캡
커스텀 프로바이더 엔드포인트¶
기본적으로 호스팅형 프로바이더는 각자의 공개 엔드포인트를 호출합니다.
- Serper:
https://google.serper.dev/search - Exa:
https://api.exa.ai/search - Brave:
https://api.search.brave.com/res/v1/web/search
폐쇄망(air-gapped)이나 정책 제한 환경에서 내부 미러를 경유하도록 하려면 base_url에 유효한 http(s) URL을 지정합니다.
- 전체 엔드포인트 URL 지정:
base_url은 프로바이더의 경로까지 포함한 전체 URL이어야 하며 설정 로드 및 핫 리로드 시점에 검증됩니다. 생략하면 공개 기본값을 그대로 사용합니다. - 프로바이더별 URL 규칙:
brave,searxng: 라우터가 매 요청마다?q=&count=(Brave) 또는?q=&format=json(SearXNG)을 덧붙이므로 쿼리 문자열이나 프래그먼트 없이 엔드포인트 경로만 적어야 합니다.serper,exa: 지정한base_url을 HTTPPOST대상으로 그대로 사용합니다.
자체 호스팅 SearXNG¶
SearXNG는 자체 호스팅 메타검색 엔진이므로 provider: searxng는 호스팅형 프로바이더와 두 가지 점이 다릅니다.
base_url필수: 공개 기본 엔드포인트가 없으므로 인스턴스의/searchURL을base_url에 반드시 지정해야 합니다(누락 시 설정 로드 실패).api_key무시: SearXNG는 API 키를 사용하지 않으며 라우터도 인증 자격 증명을 전송하지 않습니다.
web_search:
enabled: true
provider: searxng
base_url: "http://searxng.internal:8080/search"
max_results: 5
라우터는 GET <base_url>?q=<query>&format=json을 호출해 JSON results 배열의 각 항목(url, title, content)을 검색 결과로 매핑합니다. SearXNG는 기본적으로 html 포맷만 켜져 있으므로 인스턴스의 settings.yml에서 json 포맷을 활성화해야 합니다.
SearXNG 인스턴스 측 설정 때문에 라우터 오류처럼 보이는 두 가지 증상에 유의하세요.
- 모든 검색에서 HTTP 403 발생:
settings.yml의search.formats에json이 빠져 있는 경우입니다. SearXNG는 비활성화된 출력 포맷이 요청되면 403을 반환하며 API 키나 토큰 문제와는 무관합니다. - 모든 검색에서 HTTP 429 발생:
server.limiter: true설정이 라우터의 비브라우저 요청(HTMLAccept헤더 및gzip미포함)을 봇 트래픽으로 분류한 경우입니다. 사설 인스턴스라면 리미터를 끄거나,limiter.toml의botdetection.ip_lists.pass_ip에 라우터 IP를 등록하세요. Anthropic 에뮬레이션 경로는 HTTP 429를too_many_requests로 보고합니다.
그 밖의 SearXNG 런타임 동작은 다음과 같습니다.
- 결과 개수 제한: SearXNG에는 요청당 결과 수 파라미터가 없으므로(한 페이지에 상위 엔진 집계 결과 20~40건 반환) 라우터가 순위 목록을
max_results개수로 잘라냅니다. - 무응답 엔진 경고 로깅: 검색 결과가 0건인 HTTP
200응답도 정상적인 빈 결과로 처리됩니다. 단, 응답에unresponsive_engines가 함께 보고되면 외부 인터넷에 닿지 못하는 인스턴스가 단순 0건 검색처럼 보이지 않도록 라우터가 해당 엔진 이름들을warn레벨(searxng returned no results and reported unresponsive engines)로 기록합니다. - 인스턴스 기본 설정 사용: 검색 엔진 선택, 언어, 세이프서치 기본값은 SearXNG 인스턴스의
settings.yml설정을 따르며 요청마다 전달되지 않습니다.
주입 정책¶
inject_policy는 아웃바운드 요청에 web_search 도구 정의를 주입하는 조건을 제어합니다.
auto(기본값): 클라이언트가enable_web_search: true를 보내거나 이미 요청에서web_search도구를 이름으로 참조할 때 주입합니다.always:web_search가 활성화된 상태에서 모든 자체 호스팅 백엔드 요청에 주입합니다.never: 도구를 자동 주입하지 않습니다. 전체 기능을 끄지 않고 주입만 일시 중지할 수 있어 A/B 테스트 등에 유용합니다.
백엔드별 재정의¶
web_search:
enabled: true
# ... 전역 설정 ...
per_backend:
sensitive-vllm:
enabled: false # 이 백엔드에 대해서만 비활성화
ollama-external:
inject_policy: always
# canary-vllm:
# enabled: true # 전역 enabled=false 상태에서도 이 백엔드만
# # 개별적으로 켤 수 있습니다
Hub가 전달하는 사용자별 자격 증명¶
control_plane.policy.enabled가 켜진 상태에서 Continuum Hub에 연결된 라우터(control-plane 빌드)는 search_credentials_v1 capability를 광고하고, Hub 정책 엔벨로프를 통해 조직 구성원별 개인 검색 API 키를 전달받습니다.
자격 증명 우선순위 및 게이트¶
Hub가 발급한 API 키로 인증된 요청은 다음 우선순위를 따릅니다.
- 사용자 자격 증명 존재: 키 소유자(
KeyEntry.user_id)에게 전달된 자격 증명이 있으면web_search.api_key대신 해당 자격 증명의 프로바이더와 키로 검색합니다. 자격 증명마다 프로바이더가 지정되므로 구성원마다 서로 다른 검색 프로바이더 키를 사용하더라도 단일 라우터에서 모두 처리할 수 있습니다.base_url미러는 사용자 자격 증명의 프로바이더가 로컬web_search.provider와 일치할 때만 적용됩니다. - 사용자 자격 증명 없음: 배포 환경의
web_search설정 키로 폴백합니다. 단, Hub가 자격 증명 집합을 한 번이라도 전달한 뒤에는 사용자 개인 자격 증명도 없고 배포 설정으로도 인증할 수 없는 경우(API 키가 필요한 프로바이더인데 유효한api_key도base_url미러도 없는 경우) 도구를 주입하지 않습니다. - 티어 게이트: 키가 속한 티어에서
web_search_enabled: false로 설정되어 있으면 어떤 자격 증명이 있더라도 도구를 주입하지 않습니다.
로컬 설정 및 Capability 생명주기¶
- 운영자 옵트인 유지: 전달된 자격 증명은 프로바이더와 API 키만 대체하며 주입 정책은 바꾸지 않습니다. 로컬
web_search섹션이 없거나 비활성화된 라우터는 도구를 주입하지 않습니다. - 로컬 설정 폴백: Hub 키와 일치하지 않는 요청이나 Hub로부터 자격 증명을 전달받지 않은 라우터는 로컬
web_search설정만으로 동작합니다. - Capability 광고 및 철회:
web_search섹션이 존재하는 동안 시작 시점과 설정 리로드마다search_credentials_v1을 광고합니다. 리로드 시web_search섹션이 제거되면 즉시 capability 광고를 중단하고 전달받은 키와 확인 다이제스트를 모두 폐기합니다.
메모리 전용 보관 및 엔벨로프 검증¶
- 메모리 전용 보관: 전달된 키는 메모리에만 보관됩니다. 디스크(키 테이블 사이드카 포함)나 로그에 절대 기록되지 않으며
Debug출력에서도 마스킹됩니다. - 엔벨로프 갱신 규칙: 필드가 생략되면 이전 집합을 유지하고, 값이 있으면 교체하며, 빈 집합이면 전달된 모든 자격 증명을 철회합니다.
- 전체 집합 원자적 검증: 항목 수가 1,000개를 초과하거나, 사용자 ID가 중복되거나,
serper·brave·exa이외의 프로바이더가 포함되거나, 다이제스트 재계산 결과가 일치하지 않으면 해당 업데이트 전체를 거부하고 기존 집합을 유지합니다.
사용량 보고 및 헤더 정책¶
- 사용량 레코드 필드: Hub로 전송되는 사용량 레코드에는 다음 필드가 포함됩니다.
web_search_queries: 실패한 호출을 포함해 해당 요청에서 실행된 검색 프로바이더 호출 횟수web_search_provider:serper,brave,exa중 하나(SearXNG는 생략)web_search_credential_source:user또는deployment
- 세부 집계 규칙:
- Anthropic
web_search_20250305에뮬레이션은 쿼리 추출 턴을 검색 호출과 함께 보고합니다. - 도구 루프의 후속 백엔드 턴이 실패하더라도 이미 실행된 검색은 토큰 수
0인 레코드로 보고됩니다. - 폴백 체인의 여러 시도에 걸쳐서도 각 검색은 정확히 하나의 레코드에만 기록됩니다.
- 백엔드 응답에
usage블록이 없는 에뮬레이션 턴은 사용량 레코드를 생성하지 않습니다.
- Anthropic
x-continuum-websearch헤더 미지원: 요청별 제어 헤더는 두지 않습니다. 도구 주입 여부는 티어 스위치, 백엔드별inject_policy, 클라이언트의tools목록으로 결정됩니다.
핫 리로드¶
API 키를 포함한 모든 web_search 필드는 라우터 설정 리로드 시 다시 읽힙니다. 라우터를 재시작하지 않고도 API 키를 교체하거나 기능을 켜고 끌 수 있습니다.
지원되는 프로바이더¶
| 프로바이더 | 상태 | 엔드포인트 |
|---|---|---|
| Serper | 구현됨 | https://google.serper.dev/search |
| Brave | 구현됨 | https://api.search.brave.com/res/v1/web/search |
| Exa | 구현됨 | https://api.exa.ai/search |
| SearXNG | 구현됨 | 자체 호스팅; base_url 필수, API 키 없음 |
네 프로바이더 모두 동일한 SearchProvider 트레이트를 구현하므로 web_search 설정에서 provider: 한 줄만 바꾸면 전환할 수 있습니다(SearXNG는 base_url 추가 지정).
보안¶
- 비밀 값 보호: API 키는 로드 시점에
${ENV}참조에서 확장되며 로그나 HTTP 오류 응답에 절대 기록되지 않습니다. - 디버그 출력 마스킹:
WebSearchConfig,SerperProvider,BraveProvider,ExaProvider의Debug출력은api_key필드를 마스킹합니다. 자격 증명을 갖지 않는SearxngProvider는 운영자가 검색 대상을 확인할 수 있도록Debug출력에 설정된base_url을 표시합니다. - 콘텐츠 정제: 검색 결과의 제목과 스니펫은 HTML 태그 및 제어 문자를 제거한 뒤
result_char_cap길이로 잘라 모델 컨텍스트에 전달합니다.
관찰 가능성¶
metrics 기능이 컴파일에 포함된 경우 다음 Prometheus 메트릭을 내보냅니다.
web_search_calls_total{provider, outcome}: 실행된 검색 도구 호출당 1씩 증가하는 카운터web_search_call_duration_seconds{provider, outcome}: 검색 실행 지연 시간 히스토그램web_search_iteration_cap_total{component="loop"}:max_tool_iterations상한에 도달할 때마다 증가하는 카운터web_search_injections_total{backend_type}: 라우터가web_search도구를 주입한 요청당 1씩 증가하는 카운터
안전 경계¶
비스트리밍 도구 실행 루프는 네 가지 독립적인 안전장치를 적용합니다.
- 반복 횟수 상한 (
max_tool_iterations): 백엔드 왕복 횟수를 제한합니다(하드 상한, 기본값5). - 벽시계 시간 예산 (
loop_wall_clock_ms): 루프 전체에서 소비되는 총 경과 시간을 제한합니다(기본값60000ms). 시간을 초과하면 클라이언트에 오류를 노출하지 않고 모델의 마지막 응답을 그대로 반환합니다. - 결과 크기 상한 (
max_total_result_bytes): 모든 반복에 걸쳐 대화에 추가되는 도구 결과content의 누적 바이트 크기를 제한합니다(기본값32768바이트). 상한을 초과하면 이후 도구 결과 페이로드를 짧은"tool-result budget exhausted"오류 메시지로 대체하여 모델이 도구 호출 사이클을 정상 종료하고 최종 답변을 생성할 수 있게 합니다. - 미등록 도구 호출 방어: 모델이 설정된
tool_name과 다른 이름의 도구를 호출하면, 다음 라운드에서 백엔드가 짝이 맞지 않는tool_call_id때문에 요청을 거부하지 않도록 구조화된tool역할 오류 메시지로 응답합니다. 라우터는 자신이 처리할 수 없는 도구 호출을 직접 실행하지 않습니다.
모델 별칭 (Claude Code 호환성)¶
Claude Code 같은 클라이언트는 내부 헬퍼 작업에 특정 Anthropic 모델 이름을 하드코딩합니다. 예를 들어 WebFetch의 콘텐츠 요약과 WebSearch의 쿼리 추출 중간 호출에는 claude-haiku-4-5-20251001을 사용합니다. 자체 호스팅 모델만 제공하는 라우터에서는 별칭을 설정하지 않으면 이러한 내부 호출이 ModelNotFound로 실패합니다.
최상위 model_aliases 섹션은 백엔드 선택 전에 요청의 model 필드를 다시 쓰며, cc-switch에서 널리 쓰이는 ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL 관례를 따릅니다.
model_aliases:
haiku: GLM-5_1 # "haiku"를 포함하는 모든 수신 이름
sonnet: GLM-5_1 # "sonnet"을 포함하는 모든 수신 이름
opus: GLM-5_1 # "opus"를 포함하는 모든 수신 이름
reasoning: GLM-5_1 # thinking.type이 enabled | adaptive일 때 사용
default: GLM-5_1 # 폴백; 비활성화하려면 설정하지 않음
exact: # 전체 이름 고정 매핑, 우선 적용
claude-haiku-4-5-20251001: GLM-5_1
주요 별칭 규칙은 다음과 같습니다.
- 매칭 우선순위:
exact->reasoning(thinking이 활성화된 경우) ->haiku->opus->sonnet->default순서로 평가됩니다. - 부분 문자열 매칭: 티어 키(
haiku,opus,sonnet)는 요청 모델 이름에 대해 대소문자 구분 없는 부분 문자열 검사를 수행합니다. 모든 재작성 내역은 운영자가 확인할 수 있도록info레벨로 로깅됩니다. - 적용 엔드포인트: 별칭은
/anthropic/v1/messages에만 적용되며, OpenAI 호환/v1/chat/completions엔드포인트에는 영향을 주지 않습니다. 네이티브 Anthropic 백엔드로 라우팅하면서 모델 이름이 바뀌지 않기를 원한다면model_aliases를 비워 두세요.
주의 사항¶
- 스트리밍 지원 범위: 비스트리밍 도구 실행 루프와 Anthropic
web_search_20250305서버 도구 에뮬레이션 경로(스트리밍 SSE 시퀀스 포함)는 기본 활성화되어 있습니다. 일반 커스텀 도구 루프의 스트리밍 중 도구 실행은 기본값이streaming_enabled: false이므로, 이 설정을 켜지 않으면 일반 루프로 들어온 스트리밍 요청은 라우터 측 도구 실행 없이 업스트림 SSE 스트림을 그대로 통과시킵니다. - 잘못된 도구 인수 처리: 모델이 올바른
{"query": "..."}JSON 대신 잘못된 형식의 인수를 생성하면 HTTP 오류를 반환하는 대신 모델에tool역할 오류 메시지를 전달합니다. - OpenAI 함수 호출 스키마 사용: 주입되는 도구는 OpenAI 함수 호출 포맷(
tools[].function.parameters)을 따르므로 OpenAI 스타일 도구 사용에 맞춰 학습된 자체 호스팅 모델이 안정적으로 동작합니다. - 비암호화
encrypted_content: 에뮬레이션된web_search_tool_result블록의encrypted_content는 암호학적으로 서명된 blob이 아니라 스니펫을 base64로 인코딩한 값입니다. Anthropic 본사는 후속 턴에서 이 필드의 서명을 검증하지만, 에뮬레이션 경로에서는 라우터가 서버 역할을 수행하므로 불투명한 base64 페이로드만으로 Claude Code와의 왕복이 정상 동작합니다.
변환 백엔드의 웹 검색 기록¶
Claude Code는 이전 검색의 server_tool_use와 web_search_tool_result 블록을 이후 모든 턴에 다시 보냅니다. 변환 백엔드(OpenAI 호환, Gemini, responses_only 모델)는 이 블록을 같은 메시지의 같은 위치에서 텍스트로 받습니다. 검색어는 Web search: <query>가 되고, 결과는 Web search results: 다음에 결과마다 - <title> (<url>): <snippet> 한 줄이 됩니다. 스니펫은 encrypted_content를 base64로 디코딩한 값이며 디코딩되지 않으면 생략합니다. 결과 목록이 비어 있으면 Web search results: none, 검색 오류이면 Web search failed: <error_code>가 됩니다. 두 타입 중 형식이 잘못된 블록과 다른 서버 도구의 server_tool_use는 계속 400으로 거부됩니다. 네이티브 Anthropic과 Bedrock 백엔드는 이 블록을 그대로 받습니다.