GOPURA

Web search

Ground any chat model in live web results — append :online, get the answer plus its sources, one search per request, exact cost in-band.

Append :online to any chat model and every request is grounded in a live web search: current events, fresh docs, live prices. The gateway runs exactly one search per request, gives the results to the model with your question, and returns the answer with its sources as annotations. You need no search API keys and no tool-call loop in your code.

r = client.chat.completions.create(
    model="anthropic/claude-sonnet-5:online",   # any chat model + :online
    messages=[{"role": "user", "content": "What changed in the EU AI Act this month?"}],
)
print(r.choices[0].message.content)
for a in r.choices[0].message.annotations or []:
    print("-", a.url_citation.title, a.url_citation.url)
print(r.usage.web_search_count, "search ·", r.usage.cost, "USD total")

:online works on every chat model, including models without tool support, and with the router: gopura/auto:online routes each request to the right tier and grounds it.

Sources

Every result the search returned comes back as a url_citation, in the same shape OpenRouter uses:

"message": {
  "role": "assistant",
  "content": "The Commission published … ([europa.eu](https://europa.eu/…)).",
  "annotations": [
    {
      "type": "url_citation",
      "url_citation": {
        "url": "https://europa.eu/…",
        "title": "AI Act: implementation timeline",
        "content": "The first obligations apply from …",
        "start_index": 42,
        "end_index": 69
      }
    }
  ]
}
  • content is the excerpt the model saw, capped at 1,000 characters.
  • The model sees the results numbered [1], [2], … in the same order as annotations. An answer that cites [2] refers to annotations[1].
  • start_index/end_index appear only when the answer quotes that URL. The list always contains every source, whether or not the answer links to it, so you can show sources even when you tell the model not to link (see search_prompt below).
  • Streaming: the sources arrive as one delta.annotations chunk after the text and before the final usage chunk.

Options

The OpenRouter-compatible plugins field configures the search. :online alone uses the defaults.

{
  "model": "openai/gpt-5.6-terra",
  "plugins": [{
    "id": "web",
    "max_results": 5,
    "search_prompt": "Background research for the request above. Do not include links in your answer.",
    "include_domains": ["europa.eu", "*.gov"]
  }],
  "messages": [ … ]
}
FieldDefaultMeaning
max_results5Results to retrieve, 1–10.
search_promptsee belowThe instruction placed in front of the results. Yours replaces the default entirely.
include_domainsnoneOnly search these domains (example.com, *.substack.com, path prefixes, or .gov).
exclude_domainsnoneNever return these domains. Ignored when include_domains is set.
engineparallelparallel, exa, or perplexity. See the table below.
triggeralwaysalways runs one search on every request. model lets the model decide (0–3 searches, no sources).

The default search_prompt gives the date of the search, tells the model to use the results, and asks it to cite sources as markdown links named by domain, such as [europa.eu](https://…). If your app renders sources from annotations and you don't want links inline, set your own search_prompt.

Malformed fields return a 400. They are never silently ignored.

Engines

EnginePer searchSourcesHow it runs
parallel (default)$0.00125✓The gateway searches, then calls the model
perplexity$0.00625–Search tool inside the upstream, forced to run once
exa$0.00875–Search tool inside the upstream, forced to run once

Prices include margin. parallel with trigger: "model" runs as an upstream search tool and costs $0.00625 per search. Domain filters need parallel. With exa or perplexity, the search still runs on every request, but the upstream returns no source list. If you bring your own function tools on those engines, the search is left to the model, so a tool loop is never forced.

Billing

With trigger: "always" (the default), each request is billed for one search plus tokens. The search results become part of the prompt, so they count as input tokens. The response reports the count and the search-inclusive total:

"usage": {
  "prompt_tokens": 3541,
  "completion_tokens": 452,
  "web_search_count": 1,
  "cost": 0.006886
}

With trigger: "model", you pay per search the model actually runs: typically 0–3, often none. In streams, the count appears on the final usage chunk.

Failures

If the search itself fails or times out, the request fails with a 502 (or 504) and nothing is billed. A request that asked for grounding never quietly gets an ungrounded answer.

Notes

  • Only function-type client tools are accepted. Provider-native search tool blocks are rejected; use :online instead, which is metered honestly.
  • The search query is your latest user message. For follow-up questions that only make sense with earlier context ("and in France?"), make that message self-contained.
  • The results are appended to the end of your last user message, so your system prompt and history stay unchanged and prompt caching keeps working on grounded requests.