From 03a676995aadef50179e982ddda19e8b39bbfdee Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:08:24 -0700 Subject: [PATCH] feat(search): add Grounding with Bing Search (bing_grounding) as a search provider --- litellm/llms/azure/search/__init__.py | 3 + litellm/llms/azure/search/transformation.py | 353 ++++++++++++++++++ ...odel_prices_and_context_window_backup.json | 8 + .../bing_grounding_websearch_config.yaml | 40 ++ litellm/types/utils.py | 1 + litellm/utils.py | 2 + model_prices_and_context_window.json | 8 + .../test_bing_grounding_search.py | 187 ++++++++++ .../foundry_responses_web_search_fixture.json | 78 ++++ ...st_bing_grounding_search_transformation.py | 311 +++++++++++++++ .../search/test_base_search_transformation.py | 3 + .../public/assets/logos/bing.png | Bin 0 -> 31955 bytes .../_components/CreateSearchTools.tsx | 2 + 13 files changed, 996 insertions(+) create mode 100644 litellm/llms/azure/search/__init__.py create mode 100644 litellm/llms/azure/search/transformation.py create mode 100644 litellm/proxy/example_config_yaml/bing_grounding_websearch_config.yaml create mode 100644 tests/search_tests/test_bing_grounding_search.py create mode 100644 tests/test_litellm/llms/azure/search/foundry_responses_web_search_fixture.json create mode 100644 tests/test_litellm/llms/azure/search/test_bing_grounding_search_transformation.py create mode 100644 ui/litellm-dashboard/public/assets/logos/bing.png diff --git a/litellm/llms/azure/search/__init__.py b/litellm/llms/azure/search/__init__.py new file mode 100644 index 00000000000..2414ba2b1e8 --- /dev/null +++ b/litellm/llms/azure/search/__init__.py @@ -0,0 +1,3 @@ +from litellm.llms.azure.search.transformation import BingGroundingSearchConfig + +__all__ = ("BingGroundingSearchConfig",) diff --git a/litellm/llms/azure/search/transformation.py b/litellm/llms/azure/search/transformation.py new file mode 100644 index 00000000000..2caee9b50a0 --- /dev/null +++ b/litellm/llms/azure/search/transformation.py @@ -0,0 +1,353 @@ +""" +Calls the Microsoft Foundry Responses API with the `bing_grounding` or `web_search` +tool to search the web (Grounding with Bing Search). + +Microsoft docs: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-grounding + +Setup: + 1. Set BING_GROUNDING_PROJECT_ENDPOINT to the Foundry project endpoint, e.g. + https://.services.ai.azure.com/api/projects/ + 2. Set BING_GROUNDING_MODEL to a model deployment in that project (e.g. gpt-4.1); + it runs the grounded search and its tokens are billed on that deployment + 3. Optional: set BING_GROUNDING_CONNECTION_ID to a Grounding with Bing Search + project connection id to use the `bing_grounding` tool; without it the + project's built-in `web_search` tool is used + 4. Auth: pass api_key, or set BING_GROUNDING_TOKEN to an Entra bearer token for + scope https://ai.azure.com/.default, or configure azure-identity + (AZURE_CLIENT_ID / AZURE_CLIENT_SECRET / AZURE_TENANT_ID, managed identity, + or any DefaultAzureCredential source) and the token is minted automatically + +Usage: + response = litellm.search( + query="latest AI developments", + search_provider="bing_grounding", + max_results=5, + ) +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from types import MappingProxyType +from typing import TYPE_CHECKING, Final, Literal + +import httpx +from pydantic import BaseModel, ConfigDict, ValidationError + +from litellm.llms.base_llm.chat.transformation import BaseLLMException +from litellm.llms.base_llm.search.transformation import ( + BaseSearchConfig, + SearchResponse, + SearchResult, +) +from litellm.secret_managers.main import get_secret_str + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj + +_DOCS_URL: Final = "https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-grounding" + +PROJECT_ENDPOINT_ENV: Final = "BING_GROUNDING_PROJECT_ENDPOINT" +MODEL_ENV: Final = "BING_GROUNDING_MODEL" +CONNECTION_ID_ENV: Final = "BING_GROUNDING_CONNECTION_ID" +TOKEN_ENV: Final = "BING_GROUNDING_TOKEN" + +ENTRA_SCOPE: Final = "https://ai.azure.com/.default" + +_RESPONSES_PATH: Final = "/openai/v1/responses" +_SNIPPET_FALLBACK_LENGTH: Final = 300 + + +class _Annotation(BaseModel): + model_config = ConfigDict(extra="ignore", frozen=True) + + type: str = "" + url: str | None = None + title: str | None = None + start_index: int | None = None + end_index: int | None = None + + +class _ContentPart(BaseModel): + model_config = ConfigDict(extra="ignore", frozen=True) + + type: str = "" + text: str = "" + annotations: tuple[_Annotation, ...] = () + + +class _OutputItem(BaseModel): + model_config = ConfigDict(extra="ignore", frozen=True) + + type: str = "" + content: tuple[_ContentPart, ...] = () + + +class _ResponsesEnvelope(BaseModel): + """A Foundry Responses API body. `output` is required: a body without it is not a + Responses API response and must not be reported as a successful empty search.""" + + model_config = ConfigDict(extra="ignore", frozen=True) + + output: tuple[_OutputItem, ...] + + +class _ErrorBody(BaseModel): + model_config = ConfigDict(extra="ignore", frozen=True) + + message: str | None = None + + +class _ErrorEnvelope(BaseModel): + model_config = ConfigDict(extra="ignore", frozen=True) + + error: _ErrorBody | None = None + + +def _unwrap_error_detail(error_message: str) -> str: + """ + Surface the human-readable message inside Foundry's error envelope. + + Tool failures nest a second JSON document as a string inside `error.message` + (observed live for `bing_grounding` connection errors), so the unwrap runs twice. + Falls back to the raw body for anything else. + """ + try: + envelope: Final = _ErrorEnvelope.model_validate_json(error_message) + except ValidationError: + return error_message + message: Final = envelope.error.message if envelope.error else None + if message is None: + return error_message + try: + nested: Final = _ErrorBody.model_validate_json(message) + except ValidationError: + return message + return nested.message or message + + +def _snippet(text: str, annotation: _Annotation) -> str: + """ + The text a citation supports, not the citation marker itself. + + A url_citation's start/end indices span the inline marker ("([host](url))"), + which follows the claim it backs, so the snippet is the marker's own line up + to where the marker starts. + """ + start: Final = annotation.start_index + marker_start: Final = start if start is not None and 0 <= start <= len(text) else len(text) + claim: Final = text[:marker_start].rsplit("\n", 1)[-1].strip() + if claim: + return claim[-_SNIPPET_FALLBACK_LENGTH:] + return text[:_SNIPPET_FALLBACK_LENGTH] + + +def _citation_results(envelope: _ResponsesEnvelope) -> tuple[SearchResult, ...]: + """One result per cited URL: first occurrence wins, order preserved as answered.""" + cited: Final = tuple( + SearchResult( + title=annotation.title or "", + url=annotation.url or "", + snippet=_snippet(part.text, annotation), + date=None, + last_updated=None, + ) + for item in envelope.output + if item.type == "message" + for part in item.content + if part.type == "output_text" + for annotation in part.annotations + if annotation.type == "url_citation" and annotation.url + ) + first_by_url: Final = MappingProxyType({result.url: result for result in reversed(cited)}) + return tuple(first_by_url[url] for url in dict.fromkeys(result.url for result in cited)) + + +class _SearchConfiguration(BaseModel): + model_config = ConfigDict(frozen=True) + + project_connection_id: str + count: int | None = None + + +class _BingGroundingParams(BaseModel): + model_config = ConfigDict(frozen=True) + + search_configurations: tuple[_SearchConfiguration, ...] + + +class _BingGroundingTool(BaseModel): + model_config = ConfigDict(frozen=True) + + type: Literal["bing_grounding"] = "bing_grounding" + bing_grounding: _BingGroundingParams + + +class _UserLocation(BaseModel): + model_config = ConfigDict(frozen=True) + + type: Literal["approximate"] = "approximate" + country: str + + +class _WebSearchTool(BaseModel): + model_config = ConfigDict(frozen=True) + + type: Literal["web_search"] = "web_search" + user_location: _UserLocation | None = None + + +class _ResponsesRequest(BaseModel): + model_config = ConfigDict(frozen=True) + + model: str + input: str + tools: tuple[_BingGroundingTool | _WebSearchTool, ...] + + +def _search_tool(optional_params: Mapping[str, object]) -> _BingGroundingTool | _WebSearchTool: + connection_id: Final = get_secret_str(CONNECTION_ID_ENV) + max_results: Final = optional_params.get("max_results") + country: Final = optional_params.get("country") + if connection_id: + configuration: Final = _SearchConfiguration( + project_connection_id=connection_id, + count=max_results if isinstance(max_results, int) else None, + ) + return _BingGroundingTool(bing_grounding=_BingGroundingParams(search_configurations=(configuration,))) + location: Final = _UserLocation(country=country.upper()) if isinstance(country, str) else None + return _WebSearchTool(user_location=location) + + +def _default_entra_token_minter() -> str: + from litellm.secret_managers.get_azure_ad_token_provider import get_azure_ad_token_provider + + return get_azure_ad_token_provider(azure_scope=ENTRA_SCOPE)() + + +class BingGroundingSearchConfig(BaseSearchConfig): + def __init__(self, entra_token_minter: Callable[[], str] | None = None) -> None: + super().__init__() + self._entra_token_minter = entra_token_minter + + @staticmethod + def ui_friendly_name() -> str: + return "Grounding with Bing Search" + + def validate_environment( + self, + headers: dict[str, str], # mutable-ok: BaseSearchConfig.validate_environment signature + api_key: str | None = None, + api_base: str | None = None, + **kwargs: object, # kwargs-ok: BaseSearchConfig.validate_environment signature + ) -> dict[str, str]: # mutable-ok: the http handler passes this straight to httpx as headers + """ + Validate environment and return headers. + + Returns a new dict rather than mutating ``headers``: the http handler calls this + a second time after ``litellm/search/main.py`` already did, so it has to be idempotent. + """ + resolved_token: Final = self.resolve_server_api_key( + caller_api_key=api_key, + caller_api_base=api_base, + key_env_vars=(TOKEN_ENV,), + base_env_var=PROJECT_ENDPOINT_ENV, + default_api_base=None, + ) or self._mint_entra_token(api_base) + return { # mutable-ok: httpx requires a plain dict of headers + **headers, + "Authorization": f"Bearer {resolved_token}", + "Content-Type": "application/json", + } + + def _mint_entra_token(self, caller_api_base: str | None) -> str: + self._assert_trusted_api_base_for_server_credential( + caller_api_base, None, PROJECT_ENDPOINT_ENV, "Azure AD token" + ) + minter: Final = self._entra_token_minter or _default_entra_token_minter + try: + return minter() + except Exception as e: + raise ValueError( + f"Grounding with Bing Search: no credential available. Pass api_key, set {TOKEN_ENV} " + f"to an Entra bearer token, or configure azure-identity (AZURE_CLIENT_ID / " + f"AZURE_CLIENT_SECRET / AZURE_TENANT_ID or any DefaultAzureCredential source) " + f"for scope {ENTRA_SCOPE}. Underlying error: {e}" + ) from e + + def get_complete_url( + self, + api_base: str | None, + optional_params: dict[str, object], # mutable-ok: BaseSearchConfig.get_complete_url signature + data: dict[str, object] | list[dict[str, object]] | None = None, # mutable-ok: base signature + **kwargs: object, # kwargs-ok: BaseSearchConfig.get_complete_url signature + ) -> str: + resolved_base: Final = api_base or get_secret_str(PROJECT_ENDPOINT_ENV) + if not resolved_base: + raise ValueError( + f"{PROJECT_ENDPOINT_ENV} is not set. Set it to your Microsoft Foundry project " + f"endpoint, e.g. https://.services.ai.azure.com/api/projects/." + ) + trimmed: Final = resolved_base.rstrip("/") + if trimmed.endswith(_RESPONSES_PATH): + return trimmed + return f"{trimmed}{_RESPONSES_PATH}" + + def transform_search_request( + self, + query: str | list[str], # mutable-ok: BaseSearchConfig.transform_search_request signature + optional_params: dict[str, object], # mutable-ok: base signature + **kwargs: object, # kwargs-ok: BaseSearchConfig.transform_search_request signature + ) -> dict[str, object]: # mutable-ok: the http handler passes this straight to httpx as the JSON body + """ + Transform Search request to the Foundry Responses API format. + + The unified params map as far as the API allows: + - max_results -> the bing_grounding search configuration's `count` (the built-in + web_search tool has no result-count knob, so it is dropped in that mode) + - country -> web_search's approximate `user_location` (bing_grounding's `market` + wants a full locale like en-US, which a bare country code cannot fill) + - search_domain_filter, max_tokens_per_page -> no API equivalent, dropped + """ + model: Final = get_secret_str(MODEL_ENV) + if not model: + raise ValueError( + f"{MODEL_ENV} is not set. Set it to a model deployment in the Foundry project " + f"that runs the grounded search, e.g. gpt-4.1." + ) + request: Final = _ResponsesRequest( + model=model, + input=" ".join(query) if isinstance(query, list) else query, + tools=(_search_tool(optional_params),), + ) + return request.model_dump(mode="json", exclude_none=True) + + def transform_search_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + **kwargs: object, # kwargs-ok: BaseSearchConfig.transform_search_response signature + ) -> SearchResponse: + try: + parsed: Final = _ResponsesEnvelope.model_validate_json(raw_response.content) + except ValidationError as e: + raise self.get_error_class( + error_message=f"response does not match the Foundry Responses API schema: {e}", + status_code=raw_response.status_code, + headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature + ) + results: Final = list(_citation_results(parsed)) # mutable-ok: SearchResponse.results is list[SearchResult] + return SearchResponse(results=results, object="search") + + def get_error_class( + self, + error_message: str, + status_code: int, + headers: dict[str, str], # mutable-ok: BaseSearchConfig.get_error_class signature + ) -> Exception: + detail: Final = _unwrap_error_detail(error_message).rstrip(". ") + return BaseLLMException( + status_code=status_code, + message=f"Grounding with Bing Search: {detail}. See {_DOCS_URL} for details.", + headers=headers, + ) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 3af7d9e5019..9ba8b846e11 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -16890,6 +16890,14 @@ "notes": "Web Search on Amazon Bedrock AgentCore, billed by AWS on the gateway" } }, + "bing_grounding/search": { + "input_cost_per_query": 0.035, + "litellm_provider": "bing_grounding", + "mode": "search", + "metadata": { + "notes": "Grounding with Bing Search (G1 SKU): $35 per 1,000 transactions. Tokens for the Foundry model deployment that runs the grounded search are billed separately on that deployment." + } + }, "tinyfish/search": { "input_cost_per_query": 0.0, "litellm_provider": "tinyfish", diff --git a/litellm/proxy/example_config_yaml/bing_grounding_websearch_config.yaml b/litellm/proxy/example_config_yaml/bing_grounding_websearch_config.yaml new file mode 100644 index 00000000000..5ab18723f7f --- /dev/null +++ b/litellm/proxy/example_config_yaml/bing_grounding_websearch_config.yaml @@ -0,0 +1,40 @@ +# Web search via Microsoft Foundry: Grounding with Bing Search / the built-in +# web_search tool, called through the Foundry Responses API. +# See litellm/llms/azure/search/transformation.py for details. +# +# Required environment variables (the search router forwards only +# search_provider / api_key / api_base from the litellm_params block, so +# provider configuration rides env vars): +# BING_GROUNDING_PROJECT_ENDPOINT: the Foundry project endpoint, e.g. +# https://.services.ai.azure.com/api/projects/ +# BING_GROUNDING_MODEL: a model deployment in that project (e.g. gpt-4.1); +# it runs the grounded search, its tokens are billed on that deployment +# Optional: +# BING_GROUNDING_CONNECTION_ID: a Grounding with Bing Search project +# connection id; set it to use the bing_grounding tool ($35 per 1,000 +# transactions on the G1 SKU). Without it the project's built-in +# web_search tool is used +# BING_GROUNDING_TOKEN: an Entra bearer token for scope +# https://ai.azure.com/.default. Without it (and without api_key below) +# the token is minted via azure-identity (AZURE_CLIENT_ID / +# AZURE_CLIENT_SECRET / AZURE_TENANT_ID, managed identity, or any other +# DefaultAzureCredential source) + +model_list: + - model_name: claude-sonnet + litellm_params: + model: bedrock/us.anthropic.claude-sonnet-5 + aws_region_name: us-east-1 + +search_tools: + - search_tool_name: bing-grounding-search + litellm_params: + search_provider: bing_grounding + # Alternative to BING_GROUNDING_TOKEN / azure-identity: + # api_key: os.environ/BING_GROUNDING_TOKEN + +litellm_settings: + callbacks: ["websearch_interception"] + websearch_interception_params: + enabled_providers: ["bedrock"] + search_tool_name: bing-grounding-search diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 67eae2b4f21..371ec7d3375 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3844,6 +3844,7 @@ class SearchProviders(str, Enum): TINYFISH = "tinyfish" AGENTCORE = "agentcore" NIMBLE = "nimble" + BING_GROUNDING = "bing_grounding" # Create a set of all search provider values for quick lookup diff --git a/litellm/utils.py b/litellm/utils.py index e5ce7157e77..006717df187 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -9111,6 +9111,7 @@ class ProviderConfigManager: from litellm.llms.apiserpent.search.transformation import ( APISerpentSearchConfig, ) + from litellm.llms.azure.search.transformation import BingGroundingSearchConfig from litellm.llms.bedrock.search.transformation import AgentCoreSearchConfig from litellm.llms.brave.search.transformation import BraveSearchConfig from litellm.llms.dataforseo.search.transformation import DataForSEOSearchConfig @@ -9152,6 +9153,7 @@ class ProviderConfigManager: SearchProviders.TINYFISH: TinyfishSearchConfig, SearchProviders.AGENTCORE: AgentCoreSearchConfig, SearchProviders.NIMBLE: NimbleSearchConfig, + SearchProviders.BING_GROUNDING: BingGroundingSearchConfig, } config_class: Final = PROVIDER_TO_CONFIG_MAP.get(provider, None) if config_class is None: diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3af7d9e5019..9ba8b846e11 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -16890,6 +16890,14 @@ "notes": "Web Search on Amazon Bedrock AgentCore, billed by AWS on the gateway" } }, + "bing_grounding/search": { + "input_cost_per_query": 0.035, + "litellm_provider": "bing_grounding", + "mode": "search", + "metadata": { + "notes": "Grounding with Bing Search (G1 SKU): $35 per 1,000 transactions. Tokens for the Foundry model deployment that runs the grounded search are billed separately on that deployment." + } + }, "tinyfish/search": { "input_cost_per_query": 0.0, "litellm_provider": "tinyfish", diff --git a/tests/search_tests/test_bing_grounding_search.py b/tests/search_tests/test_bing_grounding_search.py new file mode 100644 index 00000000000..00e5e382eef --- /dev/null +++ b/tests/search_tests/test_bing_grounding_search.py @@ -0,0 +1,187 @@ +""" +Tests for the Grounding with Bing Search (Microsoft Foundry) integration. +""" + +import json +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +import litellm +from tests.search_tests.base_search_unit_tests import BaseSearchTest + +PROJECT_ENDPOINT = "https://acct.services.ai.azure.com/api/projects/proj" + +_ANSWER_TEXT = ( + "LiteLLM is an open source LLM gateway ([github.com](https://github.com/BerriAI/litellm))\n" + "The docs live on docs.litellm.ai ([docs.litellm.ai](https://docs.litellm.ai/))" +) + + +def _annotation(marker: str, url: str, title: str) -> dict: + start = _ANSWER_TEXT.index(marker) + return { + "type": "url_citation", + "url": url, + "title": title, + "start_index": start, + "end_index": start + len(marker), + } + + +MOCK_BING_GROUNDING_RESPONSE = { + "id": "resp_mock", + "object": "response", + "status": "completed", + "model": "gpt-4.1", + "output": [ + {"type": "web_search_call", "status": "completed"}, + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": _ANSWER_TEXT, + "annotations": [ + _annotation( + "([github.com](https://github.com/BerriAI/litellm))", + "https://github.com/BerriAI/litellm", + "BerriAI/litellm - GitHub", + ), + _annotation( + "([docs.litellm.ai](https://docs.litellm.ai/))", + "https://docs.litellm.ai/", + "LiteLLM Docs", + ), + ], + } + ], + }, + ], + "usage": {"input_tokens": 100, "output_tokens": 50}, +} + + +def _mock_response(): + response = Mock() + response.status_code = 200 + response.headers = {} + response.content = json.dumps(MOCK_BING_GROUNDING_RESPONSE).encode() + return response + + +@pytest.mark.skip(reason="Local only tested search providers") +class TestBingGroundingSearch(BaseSearchTest): + """ + E2E tests for Grounding with Bing Search that make real API calls. + Inherits from BaseSearchTest to run standard search tests. + """ + + def get_search_provider(self) -> str: + return "bing_grounding" + + +class TestBingGroundingSearchTransformation: + """ + Full-stack tests through `litellm.search` / `litellm.asearch` with the HTTP layer mocked. + Transformation details are unit-tested in tests/test_litellm/llms/azure/search/. + """ + + @pytest.fixture(autouse=True) + def _server_env(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_PROJECT_ENDPOINT", PROJECT_ENDPOINT) + monkeypatch.setenv("BING_GROUNDING_MODEL", "gpt-4.1") + monkeypatch.setenv("BING_GROUNDING_TOKEN", "test-entra-token") + monkeypatch.delenv("BING_GROUNDING_CONNECTION_ID", raising=False) + + def test_bing_grounding_search_request_and_response(self): + with patch( # test-quality-ok: litellm.search has no client injection seam + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=_mock_response(), + ) as mock_post: + response = litellm.search( + query="what is litellm", + search_provider="bing_grounding", + max_results=5, + country="us", + ) + + assert mock_post.called + call_kwargs = mock_post.call_args.kwargs + assert call_kwargs["url"] == f"{PROJECT_ENDPOINT}/openai/v1/responses" + assert call_kwargs["headers"]["Authorization"] == "Bearer test-entra-token" + + request_body = call_kwargs["json"] + assert request_body["model"] == "gpt-4.1" + assert request_body["input"] == "what is litellm" + assert request_body["tools"] == [ + {"type": "web_search", "user_location": {"type": "approximate", "country": "US"}} + ] + + assert response.object == "search" + assert len(response.results) == 2 + assert response.results[0].url == "https://github.com/BerriAI/litellm" + assert response.results[0].title == "BerriAI/litellm - GitHub" + assert response.results[0].snippet == "LiteLLM is an open source LLM gateway" + assert response.results[1].url == "https://docs.litellm.ai/" + assert response.results[1].snippet == "The docs live on docs.litellm.ai" + + def test_connection_mode_sends_the_bing_grounding_tool(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv( + "BING_GROUNDING_CONNECTION_ID", + "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.CognitiveServices" + "/accounts/acct/projects/proj/connections/bing-conn", + ) + with patch( # test-quality-ok: litellm.search has no client injection seam + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=_mock_response(), + ) as mock_post: + litellm.search( + query="what is litellm", + search_provider="bing_grounding", + max_results=3, + ) + + request_body = mock_post.call_args.kwargs["json"] + assert request_body["tools"] == [ + { + "type": "bing_grounding", + "bing_grounding": { + "search_configurations": [ + { + "project_connection_id": ( + "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.CognitiveServices" + "/accounts/acct/projects/proj/connections/bing-conn" + ), + "count": 3, + } + ] + }, + } + ] + + @pytest.mark.asyncio + async def test_bing_grounding_asearch(self): + with patch( # test-quality-ok: litellm.asearch has no client injection seam + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + new=AsyncMock(return_value=_mock_response()), + ) as mock_post: + response = await litellm.asearch( + query="what is litellm", + search_provider="bing_grounding", + ) + + assert mock_post.call_args.kwargs["json"]["tools"] == [{"type": "web_search"}] + assert len(response.results) == 2 + + def test_bing_grounding_search_tracks_cost(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + with patch( # test-quality-ok: litellm.search has no client injection seam + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=_mock_response(), + ): + response = litellm.search(query="pricing check", search_provider="bing_grounding") + + assert response._hidden_params["response_cost"] == pytest.approx(0.035) diff --git a/tests/test_litellm/llms/azure/search/foundry_responses_web_search_fixture.json b/tests/test_litellm/llms/azure/search/foundry_responses_web_search_fixture.json new file mode 100644 index 00000000000..2dba691d038 --- /dev/null +++ b/tests/test_litellm/llms/azure/search/foundry_responses_web_search_fixture.json @@ -0,0 +1,78 @@ +{ + "id": "resp_04bcf916b629f9be006a8c7c053e9881959a4efe7118cb3a34", + "object": "response", + "status": "completed", + "model": "gpt-4.1", + "output": [ + { + "type": "web_search_call", + "id": "ws_04bcf916b629f9be006a8c7c080e108195b3366ab048231129", + "response_id": "resp_04bcf916b629f9be006a8c7c053e9881959a4efe7118cb3a34", + "status": "completed", + "action": { + "type": "search", + "queries": [ + "latest LiteLLM release GitHub LiteLLM", + "LiteLLM github releases", + "LiteLLM repo LiteLLM" + ], + "query": "latest LiteLLM release GitHub" + } + }, + { + "type": "message", + "id": "msg_04bcf916b629f9be006a8c7c0c7b448195979bfafb3b28f1ba", + "response_id": "resp_04bcf916b629f9be006a8c7c053e9881959a4efe7118cb3a34", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "You asked: “What is the latest LiteLLM release on GitHub? Answer with the version number.”\n\nI verified the most recent release information from the official GitHub repository for LiteLLM (BerriAI/litellm) using multiple sources, including GitHub itself and secondary trackers:\n\n- On the GitHub **Releases** page for BerriAI/litellm, the topmost entry is **v1.99.0‑rc.1**, marked as a pre‑release, published “yesterday” (relative to today, August 24, 2026) ([github.com](https://github.com/BerriAI/litellm/releases)). This indicates that version **v1.99.0‑rc.1** is the most recent tag available.\n- An external release‑tracking site (ReleaseAlert) confirms: **Latest version: v1.99.0‑rc.1**, last published August 22, 2026 ([releasealert.dev](https://releasealert.dev/github/BerriAI/litellm)).\n- The GitHub API (via `releases/latest`) currently points to **v1.98.0** as the latest **stable** release, with published date August 23, 2026 ([api.github.com](https://api.github.com/repos/BerriAI/litellm/releases/latest)).\n\nTo summarize:\n\n- The absolute **latest** release tag on GitHub is **v1.99.0‑rc.1** (release candidate), published recently (August 22, 2026) ([github.com](https://github.com/BerriAI/litellm/releases)).\n- The most recent **stable** release is **v1.98.0**, published August 23, 2026 ([api.github.com](https://api.github.com/repos/BerriAI/litellm/releases/latest)).\n\nSince you asked for the “latest LiteLLM release on GitHub,” without specifying stable vs. pre‑release, the correct answer is:\n\n**v1.99.0‑rc.1**\n\nLet me know if you'd like details on what's new in that release, or if you'd prefer the latest stable version.", + "annotations": [ + { + "type": "url_citation", + "url": "https://github.com/BerriAI/litellm/releases", + "start_index": 456, + "end_index": 515, + "title": "Releases · BerriAI/litellm - GitHub" + }, + { + "type": "url_citation", + "url": "https://releasealert.dev/github/BerriAI/litellm", + "start_index": 722, + "end_index": 791, + "title": "BerriAI/litellm on GitHub | Release Alert" + }, + { + "type": "url_citation", + "url": "https://api.github.com/repos/BerriAI/litellm/releases/latest", + "start_index": 936, + "end_index": 1016, + "title": "api.github.com" + }, + { + "type": "url_citation", + "url": "https://github.com/BerriAI/litellm/releases", + "start_index": 1160, + "end_index": 1219, + "title": "Releases · BerriAI/litellm - GitHub" + }, + { + "type": "url_citation", + "url": "https://api.github.com/repos/BerriAI/litellm/releases/latest", + "start_index": 1300, + "end_index": 1380, + "title": "api.github.com" + } + ], + "logprobs": [] + } + ], + "status": "completed" + } + ], + "usage": { + "input_tokens": 15195, + "output_tokens": 467 + } +} diff --git a/tests/test_litellm/llms/azure/search/test_bing_grounding_search_transformation.py b/tests/test_litellm/llms/azure/search/test_bing_grounding_search_transformation.py new file mode 100644 index 00000000000..25dfa5fbe2b --- /dev/null +++ b/tests/test_litellm/llms/azure/search/test_bing_grounding_search_transformation.py @@ -0,0 +1,311 @@ +import json +from pathlib import Path +from unittest.mock import Mock + +import pytest + +from litellm.llms.azure.search.transformation import BingGroundingSearchConfig + +REAL_FIXTURE = json.loads((Path(__file__).parent / "foundry_responses_web_search_fixture.json").read_text()) + +RESPONSES_URL = "https://acct.services.ai.azure.com/api/projects/proj/openai/v1/responses" + + +@pytest.fixture(autouse=True) +def _clean_env(monkeypatch: pytest.MonkeyPatch): + for var in ( + "BING_GROUNDING_PROJECT_ENDPOINT", + "BING_GROUNDING_MODEL", + "BING_GROUNDING_CONNECTION_ID", + "BING_GROUNDING_TOKEN", + ): + monkeypatch.delenv(var, raising=False) + + +def _config(entra_token_minter=None) -> BingGroundingSearchConfig: + return BingGroundingSearchConfig(entra_token_minter=entra_token_minter) + + +def _resp(payload, status_code: int = 200): + r = Mock() + r.status_code = status_code + r.headers = {} + r.content = (payload if isinstance(payload, str) else json.dumps(payload)).encode() + return r + + +def _message_response(text: str, annotations: list) -> dict: + return { + "output": [ + {"type": "web_search_call", "status": "completed"}, + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": text, "annotations": annotations}], + }, + ] + } + + +def _citation(url: str, title: str, start: int, end: int) -> dict: + return {"type": "url_citation", "url": url, "title": title, "start_index": start, "end_index": end} + + +def test_ui_friendly_name(): + assert _config().ui_friendly_name() == "Grounding with Bing Search" + + +def test_validate_environment_with_explicit_key(): + headers = _config().validate_environment({}, api_key="explicit-token") + assert headers["Authorization"] == "Bearer explicit-token" + assert headers["Content-Type"] == "application/json" + + +def test_validate_environment_reads_env_token(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_TOKEN", "env-token") + assert _config().validate_environment({})["Authorization"] == "Bearer env-token" + + +def test_validate_environment_falls_back_to_entra_minter(): + headers = _config(entra_token_minter=lambda: "entra-token").validate_environment({}) + assert headers["Authorization"] == "Bearer entra-token" + + +def test_validate_environment_api_key_beats_env_token(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_TOKEN", "env-token") + minter = Mock(return_value="entra-token") + headers = _config(entra_token_minter=minter).validate_environment({}, api_key="explicit-token") + assert headers["Authorization"] == "Bearer explicit-token" + minter.assert_not_called() + + +def test_validate_environment_env_token_beats_entra_minter(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_TOKEN", "env-token") + minter = Mock(return_value="entra-token") + assert _config(entra_token_minter=minter).validate_environment({})["Authorization"] == "Bearer env-token" + minter.assert_not_called() + + +def test_validate_environment_refuses_entra_token_for_caller_api_base(): + minter = Mock(return_value="entra-token") + with pytest.raises(ValueError, match="Refusing to send the server-configured"): + _config(entra_token_minter=minter).validate_environment({}, api_base="https://attacker.example.com") + minter.assert_not_called() + + +def test_validate_environment_entra_minter_failure_names_the_options(): + def failing_minter() -> str: + raise RuntimeError("no az login") + + with pytest.raises(ValueError, match="no credential available") as excinfo: + _config(entra_token_minter=failing_minter).validate_environment({}) + message = str(excinfo.value) + assert "BING_GROUNDING_TOKEN" in message + assert "https://ai.azure.com/.default" in message + assert "no az login" in message + + +def test_validate_environment_does_not_mutate_and_is_idempotent(): + config = _config() + caller_headers = {"X-Custom": "keep-me"} + + once = config.validate_environment(caller_headers, api_key="k") + twice = config.validate_environment(once, api_key="k") + + assert caller_headers == {"X-Custom": "keep-me"} + assert once == twice + assert once["X-Custom"] == "keep-me" + + +def test_get_complete_url_from_api_base(): + url = _config().get_complete_url("https://acct.services.ai.azure.com/api/projects/proj", {}) + assert url == RESPONSES_URL + + +def test_get_complete_url_reads_env_endpoint(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_PROJECT_ENDPOINT", "https://acct.services.ai.azure.com/api/projects/proj/") + assert _config().get_complete_url(None, {}) == RESPONSES_URL + + +def test_get_complete_url_missing_endpoint_raises(): + with pytest.raises(ValueError, match="BING_GROUNDING_PROJECT_ENDPOINT"): + _config().get_complete_url(None, {}) + + +@pytest.mark.parametrize( + "api_base", + [ + "https://acct.services.ai.azure.com/api/projects/proj", + "https://acct.services.ai.azure.com/api/projects/proj/", + "https://acct.services.ai.azure.com/api/projects/proj/openai/v1/responses", + "https://acct.services.ai.azure.com/api/projects/proj/openai/v1/responses/", + ], +) +def test_get_complete_url_appends_responses_path_exactly_once(api_base: str): + assert _config().get_complete_url(api_base, {}) == RESPONSES_URL + + +def test_transform_search_request_missing_model_raises(): + with pytest.raises(ValueError, match="BING_GROUNDING_MODEL"): + _config().transform_search_request("q", {}) + + +def test_transform_search_request_web_search_mode_exact_body(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_MODEL", "gpt-4.1") + body = _config().transform_search_request("latest AI developments", {"max_results": 5}) + assert body == { + "model": "gpt-4.1", + "input": "latest AI developments", + "tools": [{"type": "web_search"}], + } + + +def test_transform_search_request_web_search_mode_maps_country(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_MODEL", "gpt-4.1") + body = _config().transform_search_request("q", {"country": "us"}) + assert body["tools"] == [{"type": "web_search", "user_location": {"type": "approximate", "country": "US"}}] + + +def test_transform_search_request_connection_mode_exact_body(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_MODEL", "gpt-4.1") + monkeypatch.setenv("BING_GROUNDING_CONNECTION_ID", "conn-id") + body = _config().transform_search_request("q", {"max_results": 5}) + assert body == { + "model": "gpt-4.1", + "input": "q", + "tools": [ + { + "type": "bing_grounding", + "bing_grounding": {"search_configurations": [{"project_connection_id": "conn-id", "count": 5}]}, + } + ], + } + + +def test_transform_search_request_connection_mode_omits_count_without_max_results( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setenv("BING_GROUNDING_MODEL", "gpt-4.1") + monkeypatch.setenv("BING_GROUNDING_CONNECTION_ID", "conn-id") + body = _config().transform_search_request("q", {}) + assert body["tools"][0]["bing_grounding"]["search_configurations"] == [{"project_connection_id": "conn-id"}] + + +def test_transform_search_request_joins_list_query(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("BING_GROUNDING_MODEL", "gpt-4.1") + assert _config().transform_search_request(["foo", "bar"], {})["input"] == "foo bar" + + +def test_transform_search_response_real_fixture_dedupes_and_preserves_order(): + resp = _config().transform_search_response(_resp(REAL_FIXTURE), logging_obj=Mock()) + + assert resp.object == "search" + assert [r.url for r in resp.results] == [ + "https://github.com/BerriAI/litellm/releases", + "https://releasealert.dev/github/BerriAI/litellm", + "https://api.github.com/repos/BerriAI/litellm/releases/latest", + ] + assert resp.results[0].title == "Releases · BerriAI/litellm - GitHub" + assert resp.results[1].title == "BerriAI/litellm on GitHub | Release Alert" + + +def test_transform_search_response_real_fixture_snippets_are_the_cited_claims(): + resp = _config().transform_search_response(_resp(REAL_FIXTURE), logging_obj=Mock()) + + assert resp.results[0].snippet.startswith("- On the GitHub **Releases** page for BerriAI/litellm") + assert resp.results[1].snippet.startswith("- An external release") + assert resp.results[2].snippet.startswith("- The GitHub API (via `releases/latest`)") + for result in resp.results: + assert "url_citation" not in result.snippet + assert not result.snippet.startswith("([") + + +def test_transform_search_response_snippet_falls_back_to_text_head_for_leading_citation(): + text = "([example.com](https://example.com)) trailing prose" + payload = _message_response(text, [_citation("https://example.com", "Example", 0, 36)]) + resp = _config().transform_search_response(_resp(payload), logging_obj=Mock()) + assert resp.results[0].snippet == text + + +def test_transform_search_response_snippet_without_indices_uses_last_line(): + payload = _message_response( + "first line\nthe claim on the last line", + [{"type": "url_citation", "url": "https://example.com", "title": "Example"}], + ) + resp = _config().transform_search_response(_resp(payload), logging_obj=Mock()) + assert resp.results[0].snippet == "the claim on the last line" + + +def test_transform_search_response_ignores_non_citation_annotations(): + payload = _message_response("text", [{"type": "file_citation", "url": "https://example.com"}]) + assert _config().transform_search_response(_resp(payload), logging_obj=Mock()).results == [] + + +def test_transform_search_response_ignores_citation_without_url(): + payload = _message_response("text", [{"type": "url_citation", "title": "no url"}]) + assert _config().transform_search_response(_resp(payload), logging_obj=Mock()).results == [] + + +def test_transform_search_response_no_message_output(): + payload = {"output": [{"type": "web_search_call", "status": "completed"}]} + assert _config().transform_search_response(_resp(payload), logging_obj=Mock()).results == [] + + +@pytest.mark.parametrize( + "body", + [ + "502 Bad Gateway", + '{"output": "garbage"}', + '{"output": null}', + "{}", + ], +) +def test_transform_search_response_malformed_body_raises_instead_of_reporting_empty(body: str): + with pytest.raises(Exception, match="Grounding with Bing Search"): + _config().transform_search_response(_resp(body, status_code=502), logging_obj=Mock()) + + +def test_get_error_class_attributes_the_provider(): + error = _config().get_error_class(error_message="quota exceeded", status_code=429, headers={}) + assert error.status_code == 429 + assert "Grounding with Bing Search: quota exceeded" in str(error) + assert "learn.microsoft.com" in str(error) + + +def test_get_error_class_unwraps_the_nested_tool_error(): + nested_tool_error = json.dumps( + { + "error": "Tool_User_Error", + "message": ( + "The specified connection ID 'conn-id' in tool config input was not found " + "in the project or account connections." + ), + "code": "invalid_tool_input", + "tool": "bing_grounding", + } + ) + live_400_shape = json.dumps( + { + "error": { + "message": nested_tool_error, + "type": "invalid_request_error", + "param": None, + "code": "tool_user_error", + } + } + ) + error = _config().get_error_class(error_message=live_400_shape, status_code=400, headers={}) + assert ( + "Grounding with Bing Search: The specified connection ID 'conn-id' in tool config input " + "was not found in the project or account connections" in str(error) + ) + assert "Tool_User_Error" not in str(error) + + +def test_get_error_class_unwraps_a_plain_error_envelope(): + error = _config().get_error_class( + error_message='{"error":{"message":"The api key is invalid.","code":"401"}}', + status_code=401, + headers={}, + ) + assert "Grounding with Bing Search: The api key is invalid" in str(error) diff --git a/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py b/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py index e4402bbec49..e6aad7688d1 100644 --- a/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py +++ b/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py @@ -16,6 +16,7 @@ import pytest import litellm from litellm.llms.apiserpent.search.transformation import APISerpentSearchConfig +from litellm.llms.azure.search.transformation import BingGroundingSearchConfig from litellm.llms.base_llm.search.transformation import ( BaseSearchConfig, _is_trusted_search_api_base, @@ -59,6 +60,7 @@ _BASE_ENV_VARS = ( "TINYFISH_API_BASE", "CRW_API_BASE", "NIMBLE_API_BASE", + "BING_GROUNDING_PROJECT_ENDPOINT", ) @@ -99,6 +101,7 @@ PROVIDERS: Tuple[ProviderSpec, ...] = ( (TinyfishSearchConfig, {"TINYFISH_API_KEY": "srv"}, "caller-key", {}), (FastCRWSearchConfig, {"CRW_API_KEY": "srv"}, "caller-key", {}), (NimbleSearchConfig, {"NIMBLE_API_KEY": "srv"}, "caller-key", {}), + (BingGroundingSearchConfig, {"BING_GROUNDING_TOKEN": "srv"}, "caller-key", {}), ) _IDS = tuple(spec[0].__name__ for spec in PROVIDERS) diff --git a/ui/litellm-dashboard/public/assets/logos/bing.png b/ui/litellm-dashboard/public/assets/logos/bing.png new file mode 100644 index 0000000000000000000000000000000000000000..ab1f4359281421b7500b7f1d77109f69ce99a95d GIT binary patch literal 31955 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 rv)4P(FWJ1_JA#ws6lXV1!V&mC`?$)n8 = { google_pse: googlePseLogo.src, dataforseo: dataforseoLogo.src, nimble: nimbleLogo.src, + bing_grounding: bingLogo.src, }; interface SearchProviderLabelProps {