Skip to content

feat(search): add Serply as a search provider - #42150

Open
googio wants to merge 2 commits into
BerriAI:mainfrom
googio:litellm_serply_search
Open

googio wants to merge 2 commits into
BerriAI:mainfrom
googio:litellm_serply_search

Conversation

@googio

@googio googio commented Sep 20, 2026 •

Copy link
Copy Markdown

TLDR

Problem this solves:

  • No way to use Serply as a search provider
  • google_pse is the only Google-backed option and Custom Search is sunsetting

How it solves it:

  • Adds serply to the search provider seam, same shape as serper
  • Maps the unified params onto Serply's query string
  • Fills real price rows from Serply's published prepaid pricing

User Flow

Before: an admin who wants Serply behind the gateway cannot configure it, so every call to that tool 500s

  1. They add a search tool with search_provider: "serply" to the proxy config and restart
  2. They send POST http://localhost:4000/v1/search/serply-search with {"query": "litellm unified search api", "max_results": 3, "country": "US"}
  3. It comes back 500 with litellm.APIConnectionError: 'serply' is not a valid SearchProviders
  4. Their only Google-backed option is google_pse, and Google Custom Search is closed to new customers

After: the same config works and the same request returns normalized results

  1. The admin adds the same search tool to the proxy config and restarts
  2. They send the same POST http://localhost:4000/v1/search/serply-search
  3. It comes back 200 with a results array of title, url, snippet and date, the same shape every other search provider returns
  4. search_domain_filter narrows a query to a site, and Serply's own tbs reaches the provider untouched for time-ranged searches

Relevant issues

Affected release

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Shared setup, identical on both sides. SERPLY_API_KEY is a real key and every call below bills real Serply credits

# serply_proxy_config.yaml
model_list: []

search_tools:
  - search_tool_name: "serply-search"
    litellm_params:
      search_provider: "serply"
  - search_tool_name: "serply-bad-key"
    litellm_params:
      search_provider: "serply"
      api_key: "deliberately-invalid"
export SERPLY_API_KEY=...
python litellm/proxy/proxy_cli.py --config serply_proxy_config.yaml --port 4000

Before (0f7d4dc)

Case 1: basic search

  1. Run
curl -s -w "\nhttp_status=%{http_code}\n" -X POST "http://localhost:4000/v1/search/serply-search" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
  -d '{"query": "litellm unified search api", "max_results": 3, "country": "US"}'
  1. Observed
{"error":{"message":"litellm.APIConnectionError: 'serply' is not a valid SearchProviders","type":"internal_server_error","param":null,"code":"500"}}
http_status=500

Case 2: restrict to a domain

  1. Run
curl -s -w "\nhttp_status=%{http_code}\n" -X POST "http://localhost:4000/v1/search/serply-search" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
  -d '{"query": "structured outputs", "max_results": 3, "search_domain_filter": ["docs.litellm.ai", "-github.com"]}'
  1. Observed
{"error":{"message":"litellm.APIConnectionError: 'serply' is not a valid SearchProviders","type":"internal_server_error","param":null,"code":"500"}}
http_status=500

Case 3: time-ranged search

  1. Run
curl -s -w "\nhttp_status=%{http_code}\n" -X POST "http://localhost:4000/v1/search/serply-search" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
  -d '{"query": "openai news", "max_results": 4, "tbs": "qdr:w"}'
  1. Observed
{"error":{"message":"litellm.APIConnectionError: 'serply' is not a valid SearchProviders","type":"internal_server_error","param":null,"code":"500"}}
http_status=500

After (755aff8)

Case 1: basic search

  1. Run
curl -s -w "\nhttp_status=%{http_code}\n" -X POST "http://localhost:4000/v1/search/serply-search" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
  -d '{"query": "litellm unified search api", "max_results": 3, "country": "US"}'
  1. Observed
{
    "results": [
        {
            "title": "LiteLLM - Getting Started | liteLLM",
            "url": "https://docs.litellm.ai/",
            "snippet": "You can use LiteLLM through either the Proxy Server or Python SDK. Both gives you a unified interface to access multiple LLMs (100+ LLMs).",
            "date": null,
            "last_updated": null,
            "metadata": {"display_url": "docs.litellm.ai"}
        },
        {
            "title": "Getting Started - LiteLLM",
            "url": "https://docs.litellm.ai/docs/",
            "snippet": "LiteLLM is a unified gateway for LLMs, agents, and MCP, so you don't need a separate agent or MCP gateway. One endpoint for 100+ models, A2A agents, and MCP ...",
            "date": null,
            "last_updated": null,
            "metadata": {"display_url": "docs.litellm.ai"}
        },
        {
            "title": "Providers - LiteLLM",
            "url": "https://docs.litellm.ai/docs/providers",
            "snippet": "LiteLLM supports all AI models from CometAPI. CometAPI provides access to 500+ AI models through a unified API interface, including cutting-edge models like GPT ...",
            "date": null,
            "last_updated": null,
            "metadata": {"display_url": "docs.litellm.ai"}
        }
    ],
    "object": "search"
}
http_status=200

Case 2: restrict to a domain

  1. Run
curl -s -w "\nhttp_status=%{http_code}\n" -X POST "http://localhost:4000/v1/search/serply-search" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
  -d '{"query": "structured outputs", "max_results": 3, "search_domain_filter": ["docs.litellm.ai", "-github.com"]}'
  1. Observed, http_status=200, every hit on the requested domain
https://docs.litellm.ai/docs/completion/json_mode
https://docs.litellm.ai/docs/anthropic_unified/structured_output
https://docs.litellm.ai/docs/guides/core_request_response_patterns

Case 3: time-ranged search

  1. Run
curl -s -w "\nhttp_status=%{http_code}\n" -X POST "http://localhost:4000/v1/search/serply-search" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
  -d '{"query": "openai news", "max_results": 4, "tbs": "qdr:w"}'
  1. Observed, http_status=200, tbs reached Serply and every hit is inside the week
date='4 days ago'  OpenAI Discloses Six New Incidents of 'Concerning' A.I. Behavior
date='2 days ago'  Microsoft AI CEO: OpenAI's latest AI revelation a 'serious situation'

Case 4: bad key surfaces an attributed error

  1. Hit the serply-bad-key tool from the shared config above
curl -s -w "\nhttp_status=%{http_code}\n" -X POST "http://localhost:4000/v1/search/serply-bad-key" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"query": "x"}'
  1. Observed, http_status=401, Serply's {"detail": ...} envelope unwrapped instead of leaking raw JSON
litellm.AuthenticationError: SerplyException - Serply Search: Invalid API key. See https://serply.io/docs for details.

Type

🆕 New Feature

Caveats (if any)

max_results is sent as Serply's num without clamping. Serply serves one page of Google results, so a larger value returns about ten rather than failing, and clamping in the adapter would only hide that from the caller

max_tokens_per_page has no Serply equivalent and is dropped. Everything the unified params do not cover is forwarded as-is, so tbs, hl and start stay reachable, which is what Case 3 above exercises

Docs live in BerriAI/litellm-docs, so they are a companion PR: BerriAI/litellm-docs#1592 adds the provider page, the sidebar entry and the SERPLY_API_BASE and SERPLY_API_KEY reference rows. code-quality and documentation are red here until that one merges, both from the same test_env_keys.py failure on SERPLY_API_BASE, and I verified locally that merging it turns them green

I did not add an Admin UI logo entry. Serper has none either, and I would rather not add an image asset to a PR that does not need one. Happy to add both in a follow-up if you want them

Disclosure: I work on Serply, so this adds my own company's API. Same shape as the Nimble and Serper providers that landed before it, and I am glad to make whatever changes you want

Serply is a Google SERP API. This wires it into the unified search seam
alongside the other Google-backed providers.

The config maps the Perplexity-unified params onto Serply's query string:
max_results -> num, country -> gl, and search_domain_filter -> site: and
-site: clauses appended to q. max_tokens_per_page has no equivalent and is
dropped. Everything else is forwarded untouched, so Serply's own surface
(tbs for time ranges, hl, start) stays reachable without LiteLLM tracking it.

Serply is a GET API, so transform_search_request hands its params to
get_complete_url through the request body under a private key, matching
google_pse and searchapi.

A response body that does not match the documented schema raises an
attributed error rather than degrading to a successful empty search, and
Serply's {"detail": ...} error envelope is unwrapped so the raw JSON does
not reach the user.

Price rows use Serply's published prepaid pricing: $2.00/1k queries at the
$5 entry pack, down to $0.75/1k at volume.
@CLAassistant

CLAassistant commented Sep 20, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codspeed

codspeed Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing googio:litellm_serply_search (755aff8) with main (cbcccd5)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (0f7d4dc) during the generation of this report, so cbcccd5 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@greptile-apps

greptile-apps Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The provider behavior appears sound, but the explicit repository requirements for source comments, strong typing, and mirrored test placement must be satisfied before merging

Findings

  1. P2 Routine comments violate policy ▶
  2. P2 Test helpers lack typing ▶
  3. P2 Tests use wrong location ▶

Summary

Adds Serply as a native unified-search provider, including request and response translation, credential handling, endpoint registration, pricing metadata, and mocked sync and async coverage

  • Maps unified search fields to Serply query-string parameters and preserves provider-native options
  • Normalizes Serply results and attributed errors into LiteLLM contracts
  • Keeps provider, endpoint-support, and pricing registries aligned
  • Repository requirements for comments, typing, and test placement still need cleanup

Reviews (1) · Last reviewed commit: "feat(search): add Serply as a search pro..."

Comment thread litellm/llms/serply/search/transformation.py Outdated
Comment thread tests/search_tests/test_serply_search.py Outdated
Comment thread tests/unit/llms/serply/search/test_serply_search_transformation.py
@codecov

codecov Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.78947% with 42 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/serply/search/transformation.py 52.80% 42 Missing ⚠️

📢 Thoughts on this report? Let us know!

@googio

googio commented Sep 20, 2026

Copy link
Copy Markdown
Author

code-quality and documentation are both red on the same single failure, and the fix is in the docs repo rather than this one

Exception: Environment variables read under ./litellm but mentioned nowhere in the docs: ['SERPLY_API_BASE']

tests/documentation_tests/test_env_keys.py checks out BerriAI/litellm-docs into docs/my-website, and SERPLY_API_BASE is read by get_secret_str in get_complete_url, the same way every other search provider reads its own base. So I opened BerriAI/litellm-docs#1592, which adds the provider page, the sidebar entry, the index rows and the SERPLY_API_BASE and SERPLY_API_KEY reference rows

I checked the fix locally by symlinking that branch into docs/my-website and running the failing command:

$ python ./tests/documentation_tests/test_env_keys.py
Every environment variable read under ./litellm is documented

The other three documentation tests pass too. Every other check on this PR is green, and documentation_test_env_keys is the second-to-last step of code-quality, so everything ahead of it had already passed

Merging the docs PR and re-running should turn both green here. Let me know if you would rather I drop the SERPLY_API_BASE override instead, though that would make this the only search provider without one

Addresses the Greptile review on BerriAI#42150: removes the comments that narrate
adjacent code and gives every test helper real parameter and return types
instead of a bare `dict`

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants