Skip to content

feat(web-search): native web search with billing - #1390

Merged
steebchen merged 18 commits into
mainfrom
terragon/add-native-websearch-support-hpf6au
Jan 4, 2026
Merged

steebchen merged 18 commits into
mainfrom
terragon/add-native-websearch-support-hpf6au

Conversation

@steebchen

@steebchen steebchen commented Jan 2, 2026

Copy link
Copy Markdown
Member

Summary

Adds native web search support with billing across providers. This enables web_search capabilities (via Responses API for non-OpenAI providers and native web search for OpenAI models) and tracks per-search costs, citations, and usage.

Changes

Core flow

  • Extract and handle a native web_search tool from the tools array, removing it from regular tool calls so it’s treated as a dedicated web search capability.
  • Pass a webSearchTool configuration to provider calls and include a webSearchEnabled header flag to enable provider-side web search features when appropriate.
  • Ensure downstream components (parsers, transformers, and logs) are aware of web search usage via new annotations and counters.

Billing & costs

  • Introduce webSearchCost calculation and include it in total cost calculations.
  • Persist webSearchCost in the database schema and in log entries for visibility and billing.

Data model & types

  • Add new unified types: WebSearchTool, WebSearchCitation, and UrlCitationAnnotation.
  • Extend StreamingDelta to optionally include annotations (web search citations).
  • Extend parsed provider response to surface web search citations and a webSearchCount.

Parsing & transformation

  • Parse web search citations from Anthropic, Google grounding metadata, and OpenAI-style responses; accumulate annotations accordingly.
  • Propagate annotations through the transformResponseToOpenai flow so OpenAI-like clients receive citations.
  • Transform streaming deltas to include annotations when web search results are present.

Provider integrations

  • Prepare request bodies to include web_search tooling for responses-based providers and web_search_options for OpenAI search models.
  • Add web search tooling for various providers (Anthropic, Google, ZAI) where applicable.
  • Update provider headers to reflect web search capability via webSearchEnabled flag.

Model & provider configuration

  • Extend provider/model definitions with webSearch and webSearchPrice where supported.
  • Add new OpenAI models with native web search capabilities:
    • gpt-4o-search-preview
    • gpt-4o-mini-search-preview
  • Update existing models to reflect webSearch capabilities and pricing, where relevant.

DB schema & logging

  • Add webSearchCost to log schema to capture per-query web search charges.
  • Update log entries to include webSearchCount and webSearchCost in logs for observability.

Misc

  • Extend tool/types to support a unified WebSearchTool and related annotations.
  • Ensure backward compatibility: if web search is not used, existing behavior remains unchanged.

Documentation

  • Documentation page for web search added: apps/docs/content/features/web-search.mdx. This page explains how to enable web_search, OpenAI web search-enabled models, provider-specific notes, usage examples, and how citations and web search costs are surfaced.

Test plan

  • Run existing unit/integration tests to ensure no regressions.
  • Send a chat request that includes a web_search tool and verify:
    • The web_search tool is extracted and not sent as a regular tool.
    • The provider headers include webSearchEnabled when web search is used.
    • The response contains annotations (url_citation) and a non-null webSearchCount.
    • The transformed OpenAI-compatible response includes annotations and the web_search-related metadata.
  • Validate billing:
    • webSearchCost is calculated when web searches occur and added to totalCost.
    • webSearchCost is persisted in the database and in the cost logs.
  • Validate model configurations:
    • OpenAI models with web search (gpt-4o-search-preview, gpt-4o-mini-search-preview) behave as expected.
    • Non-search models remain unaffected when web search is not requested.

Notes for reviewers

  • This is an opt-in feature: web search is only engaged when a web_search tool (or webSearchEnabled header) is used.
  • The system collects citations as annotations for downstream consumers and OpenAI-like clients.
  • Web search pricing is pluggable per provider via webSearchPrice in provider/model definitions.

Documentation / breaking changes

  • No breaking changes for existing flows. Documentation should be updated to describe the web_search tool, webSearchEnabled headers, and the new billing behavior for web searches.

  • Consider updating UI or client SDKs to expose web search capabilities and citations to end users where applicable.

  • Updated documentation: Added web search feature page at apps/docs/content/features/web-search.mdx with usage guides, model support notes, and billing guidance.

🌿 Generated by Terry


ℹ️ Tag @terragon-labs to ask questions and address PR feedback

Task reference

https://www.terragonlabs.com/task/dafb5a2d-c45b-46cf-aee2-c7165b4556d1

Summary by CodeRabbit

  • New Features

    • Native web search integration with real-time searches across supported models
    • Search citations exposed as annotations in both streaming and final responses
    • Per-search cost tracking and display (included in usage, logs, and UI)
    • Model browser: web-search capability filter and per-provider web-search indicator
  • Documentation

    • New comprehensive web search docs with examples, configuration, streaming, and cost notes

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 2, 2026 14:45
@meet-ploy

meet-ploy Bot commented Jan 2, 2026

Copy link
Copy Markdown

❌ Some deployments failed

Preview URLs


Deployed with Ploy

@coderabbitai

coderabbitai Bot commented Jan 2, 2026

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds native web search tooling: new web_search tool type, provider capability flags/pricing, routing and request-body mapping, provider response parsing for web citations, cost calculation and DB logging for web searches, streaming and non‑streaming annotation propagation, and UI/docs updates.

Changes

Cohort / File(s) Summary
Gateway Chat Core
apps/gateway/src/chat/chat.ts
Add web_search extraction/routing, validate provider webSearch support, thread webSearchTool through prepareRequestBody/getProviderHeaders, propagate annotations and webSearchCount, and include webSearchCost in logs and responses.
Provider Response Parsing & Transformation
apps/gateway/src/chat/tools/parse-provider-response.ts, apps/gateway/src/chat/tools/transform-response-to-openai.ts, apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
Extract web citation annotations and webSearchCount from multiple provider formats, add annotations to streaming deltas and transformed responses, and surface webSearchCost in usage.
Types & Streaming Shape
apps/gateway/src/chat/tools/types.ts, packages/models/src/types.ts, apps/gateway/src/chat/tools/types.ts
Introduce UrlCitationAnnotation / Annotation types, add annotations?: Annotation[] to StreamingDelta, add WebSearchTool / WebSearchCitation / OpenAI web-search tool input types and union tooling.
Request Preparation
packages/models/src/prepare-request-body.ts
Extend prepareRequestBody(webSearchTool?), add isFunctionTool guard, map webSearchTool to provider-specific tool payloads (web_search, web_search_options, google_search, web_search_20250305, etc.).
Provider Metadata & Headers
packages/models/src/models.ts, packages/models/src/models/*.ts (anthropic/google/openai/zai), packages/models/src/get-provider-headers.ts
Add provider.model mapping fields webSearch/webSearchPrice across providers; add ProviderHeaderOptions with webSearchEnabled and include web-search beta header for Anthropic when enabled.
Cost Calculation & DB
apps/gateway/src/lib/costs.ts, packages/db/src/schema.ts, packages/db/migrations/1767485667_faithful_bedlam.sql, packages/db/src/types.ts
Add webSearchCount param and webSearchCost computation to calculateCosts, add web_search_cost column to log schema and migration, and extend DB tool schema to include webSearchTool union.
UI Changes
apps/ui/src/components/models/all-models.tsx, apps/ui/src/components/models/model-provider-card.tsx, apps/ui/src/components/dashboard/log-card.tsx
Add Native Web Search capability filter/icon, show provider webSearch pricing, display webSearchCost in log cards; remove requestPrice sort field.
Docs & Tests/Mocks
apps/docs/content/features/web-search.mdx, apps/gateway/src/test-utils/mock-openai-server.ts
Add comprehensive web-search docs page and mock Responses API handler for testing Responses-style payloads.
Utilities / Endpoint Resolution
packages/models/src/get-provider-endpoint.ts
Enhance OpenAI model lookup fallback to provider modelName to resolve Responses vs Chat endpoints.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Gateway
    participant Provider
    participant WebSearch as WebSearchEngine
    participant DB

    Client->>Gateway: POST /chat (includes tools[], web_search tool)
    activate Gateway
    Gateway->>Gateway: Extract web_search tool\nvalidate provider supports webSearch
    Gateway->>Gateway: prepareRequestBody(..., webSearchTool)
    Gateway->>Provider: Forward provider-specific request\n(include web_search payload, headers with webSearchEnabled)
    deactivate Gateway

    activate Provider
    Provider->>WebSearch: Perform web search(s)
    WebSearch-->>Provider: Return search results + citations
    Provider-->>Gateway: Response with annotations / web_search_count
    deactivate Provider

    activate Gateway
    Gateway->>Gateway: parseProviderResponse -> annotations, webSearchCount
    Gateway->>Gateway: transformResponseToOpenai(..., annotations)
    Gateway->>Gateway: calculateCosts(..., webSearchCount)
    Gateway->>DB: Log request/usage (include web_search_cost, webSearchCount)
    Gateway-->>Client: Return transformed response (includes annotations, costs)
    deactivate Gateway
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • smakosh
  • kmk142789

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(web-search): native web search with billing' clearly summarizes the main feature addition—native web search functionality with associated billing support.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@steebchen
steebchen temporarily deployed to llmgateway-docs--preview January 2, 2026 14:50 — with Meet Ploy Inactive
@github-actions github-actions Bot changed the title Add native web search support with billing across providers feat(web-search): add native support Jan 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/gateway/src/chat/tools/transform-response-to-openai.ts (1)

192-231: Missing annotations propagation for inference.net/together.ai/groq when building new response.

When !transformedResponse.id is true (lines 196-230), the message object is constructed without annotations. Other providers include annotations in similar code paths. Consider adding annotations here for consistency.

🔎 Proposed fix
 message: {
   role: "assistant",
   content: content,
   ...(reasoningContent !== null && {
     reasoning: reasoningContent,
   }),
+  ...(annotations &&
+    annotations.length > 0 && { annotations: annotations }),
 },
apps/gateway/src/chat/chat.ts (1)

3640-3656: Streaming path may be missing webSearchCount in cost calculation.

The non-streaming path passes webSearchCount to calculateCosts (line 4267), but the streaming path's calculateCosts call (lines 3640-3655) does not include webSearchCount. This could result in web search costs not being calculated for streaming responses.

Suggested fix

You'll need to:

  1. Track webSearchCount during streaming (similar to how outputImageCount is tracked)
  2. Pass it to calculateCosts in the streaming finally block

The streaming transformer should accumulate web search count from streaming chunks, then pass it to the cost calculation.

🧹 Nitpick comments (5)
apps/gateway/src/lib/costs.ts (1)

301-307: Consider adding webSearchPrice to the provider type definition.

Using (providerInfo as any).webSearchPrice works but bypasses type safety. Consider extending the provider type definition in @llmgateway/models to include webSearchPrice?: number to avoid the as any cast. This aligns with coding guidelines to avoid any unless absolutely necessary.

Based on coding guidelines: "Never use any or as any unless absolutely necessary in TypeScript code".

apps/gateway/src/chat/tools/parse-provider-response.ts (1)

309-338: Consider if webSearchCount = 1 is appropriate as default for Google grounding.

Setting webSearchCount = 1 as a default when groundingMetadata exists (line 313) before potentially overwriting with webSearchQueries.length (line 336) works, but if webSearchQueries is empty or missing, it defaults to 1 even when no actual search occurred. Consider whether this should be 0 initially and only set to 1 if there are actual grounding chunks.

🔎 Proposed refinement
 // Extract web search citations from Google grounding metadata
 const groundingMetadata = json.candidates?.[0]?.groundingMetadata;
 if (groundingMetadata) {
-  webSearchCount = 1; // Google doesn't report individual search counts
   // Extract from groundingChunks (sources)
   if (
     groundingMetadata.groundingChunks &&
     Array.isArray(groundingMetadata.groundingChunks)
   ) {
+    if (groundingMetadata.groundingChunks.length > 0) {
+      webSearchCount = 1; // Google doesn't report individual search counts
+    }
     for (const chunk of groundingMetadata.groundingChunks) {
packages/models/src/prepare-request-body.ts (2)

237-251: Consider using a typed object instead of any.

The webSearch object is typed as any on line 243. Per coding guidelines, avoid any unless absolutely necessary. Consider defining a local type or interface for the web search tool configuration.

Suggested improvement
-				const webSearch: any = { type: "web_search" };
+				const webSearch: { type: "web_search"; user_location?: WebSearchTool["user_location"]; search_context_size?: WebSearchTool["search_context_size"] } = { type: "web_search" };

352-365: ZAI web search ignores WebSearchTool configuration options.

The ZAI implementation only uses the presence of webSearchTool to enable search but ignores user_location, search_context_size, and max_uses properties. If this is intentional (ZAI doesn't support these options), consider adding a brief comment for clarity.

apps/gateway/src/chat/chat.ts (1)

471-490: Mutating the tools array may have unintended side effects.

The tools.splice(webSearchToolIndex, 1) on line 488 mutates the original array from validationResult.data. While this works for the current flow, it modifies the validated input data in place. Consider creating a filtered copy instead to avoid potential issues if tools is referenced elsewhere.

Suggested approach
 	// Extract web_search tool from tools array if present
 	// The web_search tool is a special tool that enables native web search for providers that support it
 	let webSearchTool: WebSearchTool | undefined;
+	let filteredTools = tools;
 	if (tools && Array.isArray(tools)) {
 		const webSearchToolIndex = tools.findIndex(
 			(tool: any) => tool.type === "web_search",
 		);
 		if (webSearchToolIndex !== -1) {
 			// Cast to any to access properties since the schema allows both function and web_search tools
 			const foundTool = tools[webSearchToolIndex] as any;
 			webSearchTool = {
 				type: "web_search",
 				user_location: foundTool.user_location,
 				search_context_size: foundTool.search_context_size,
 				max_uses: foundTool.max_uses,
 			};
 			// Remove the web_search tool from the tools array so it's not sent as a regular tool
-			tools.splice(webSearchToolIndex, 1);
+			filteredTools = tools.filter((_, i) => i !== webSearchToolIndex);
 		}
 	}

Then use filteredTools instead of tools when passing to prepareRequestBody and other downstream functions.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5dc1507 and e28fab2.

📒 Files selected for processing (15)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/lib/costs.ts
  • packages/db/src/schema.ts
  • packages/models/src/get-provider-headers.ts
  • packages/models/src/models.ts
  • packages/models/src/models/anthropic.ts
  • packages/models/src/models/google.ts
  • packages/models/src/models/openai.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/prepare-request-body.ts
  • packages/models/src/types.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/models/src/models.ts
  • apps/gateway/src/lib/costs.ts
  • packages/models/src/get-provider-headers.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/anthropic.ts
  • packages/db/src/schema.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • packages/models/src/models.ts
  • apps/gateway/src/lib/costs.ts
  • packages/models/src/get-provider-headers.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/anthropic.ts
  • packages/db/src/schema.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • packages/models/src/models.ts
  • apps/gateway/src/lib/costs.ts
  • packages/models/src/get-provider-headers.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/anthropic.ts
  • packages/db/src/schema.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/models/openai.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • packages/models/src/models.ts
  • apps/gateway/src/lib/costs.ts
  • packages/models/src/get-provider-headers.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/anthropic.ts
  • packages/db/src/schema.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/models/openai.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/lib/costs.ts
  • packages/db/src/schema.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM with latest object syntax for database operations

Files:

  • packages/db/src/schema.ts
🧬 Code graph analysis (7)
packages/models/src/get-provider-headers.ts (1)
packages/models/src/providers.ts (1)
  • ProviderId (446-446)
apps/gateway/src/chat/tools/parse-provider-response.ts (1)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
packages/models/src/prepare-request-body.ts (1)
packages/models/src/types.ts (1)
  • WebSearchTool (373-395)
apps/gateway/src/chat/tools/types.ts (1)
packages/models/src/types.ts (1)
  • ToolCall (54-61)
apps/gateway/src/chat/tools/transform-response-to-openai.ts (2)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
packages/db/src/schema.ts (1)
  • message (589-611)
apps/gateway/src/chat/chat.ts (3)
packages/models/src/types.ts (1)
  • WebSearchTool (373-395)
packages/db/src/types.ts (2)
  • tools (44-44)
  • tool (18-21)
packages/models/src/get-provider-headers.ts (1)
  • getProviderHeaders (13-60)
apps/gateway/src/chat/tools/transform-streaming-to-openai.ts (1)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Agent
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: build / run
  • GitHub Check: test / run
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: autofix
🔇 Additional comments (29)
packages/db/src/schema.ts (1)

431-431: LGTM! Web search cost field properly integrated.

The webSearchCost field is correctly typed as real() and nullable, matching the pattern of other optional cost fields (inputCost, outputCost, cachedInputCost, requestCost). Its placement among cost-related fields maintains logical schema organization.

packages/models/src/models.ts (1)

139-146: LGTM! Web search fields well-documented and properly typed.

The optional webSearch and webSearchPrice fields are correctly typed and documented. Making them optional is appropriate since web search is an opt-in feature, ensuring backward compatibility.

apps/gateway/src/chat/tools/types.ts (2)

29-40: LGTM! Annotation types well-structured for extensibility.

The UrlCitationAnnotation interface uses a discriminated union pattern (type: "url_citation") that allows for future annotation types. The nested url_citation object clearly separates metadata from the type discriminator.


49-49: LGTM! Optional annotations field maintains backward compatibility.

Making annotations optional in StreamingDelta correctly reflects the opt-in nature of web search features.

packages/models/src/models/google.ts (1)

530-531: Gemini 3 web search pricing is accurate; remove misleading comparison.

The code correctly specifies $14 per 1,000 queries ($0.014 per query) for Gemini 3 web search, which became effective January 5, 2026. However, the comparison to Gemini 2.5 pricing is misleading: Gemini 2.5 uses per-grounded-prompt billing ($35 per 1,000 prompts) while Gemini 3 switched to per-individual-query billing, making direct price comparison invalid since a single prompt may execute multiple queries.

packages/models/src/models/zai.ts (1)

26-27: Web search enablement is correctly restricted to reasoning models without vision.

Web search (webSearch: true) is enabled only for glm-4.5, glm-4.7, and glm-4.6 (all reasoning models without vision capabilities), and is intentionally excluded from:

  • Vision models: glm-4.5v, glm-4.6v, glm-4.6v-flashx, glm-4.6v-flash
  • Unstable reasoning model: glm-4.5-x

This selective pattern is consistent throughout the configuration.

packages/models/src/get-provider-headers.ts (1)

3-8: LGTM! Clean implementation for conditionally including web search beta header.

The use of a block scope for the anthropic case and dynamic betaFeatures array construction is well-structured.

Also applies to: 19-29

apps/gateway/src/chat/tools/transform-streaming-to-openai.ts (2)

119-153: LGTM! Proper handling of Anthropic web search tool result in streaming.

The implementation correctly:

  • Filters for web_search_result type items
  • Builds annotations array with url_citation format
  • Only includes annotations in delta when non-empty

456-474: LGTM! Google grounding metadata citation extraction is well-implemented.

The code correctly extracts web citations from groundingChunks and transforms them into the unified url_citation annotation format.

packages/models/src/types.ts (1)

373-395: Verify intentional difference in user_location structure between interfaces.

WebSearchTool.user_location has a flat structure with city, region, country directly under type: "approximate", while OpenAIWebSearchOptions.user_location nests these under an additional approximate object. Ensure this aligns with the respective provider API specifications (Anthropic vs OpenAI).

Also applies to: 423-436

apps/gateway/src/lib/costs.ts (1)

89-89: LGTM! Web search cost calculation is correctly integrated.

The implementation properly:

  • Handles null/zero webSearchCount gracefully
  • Applies the discount multiplier consistently
  • Includes webSearchCost in all return paths
  • Adds webSearchCost to the totalCost calculation

Also applies to: 106-106, 191-191, 218-218, 309-313, 320-320

apps/gateway/src/chat/tools/transform-response-to-openai.ts (1)

75-75: LGTM! Annotations are properly propagated across most provider transformations.

The implementation consistently uses conditional spread to include annotations when present and non-empty.

Also applies to: 98-99, 160-161, 296-297, 342-343, 380-383, 431-434

apps/gateway/src/chat/tools/parse-provider-response.ts (4)

103-143: Verify if inline citations should contribute to webSearchCount for Anthropic.

Currently, webSearchCount is set to webSearchBlocks.length (line 109), which counts web search tool result blocks. However, inline citations extracted from text blocks (lines 129-143) do not increment webSearchCount. If inline citations represent additional web search results, this might undercount the actual searches performed.


471-504: LGTM! OpenAI Responses API citation extraction handles multiple sources correctly.

The use of Math.max(webSearchCount, webSearchCalls.length) ensures accurate counting when both annotation-based and web_search_call indicators are present.


584-601: LGTM! ZAI-specific web search result extraction is properly handled.

The code correctly checks for the ZAI provider and extracts web search results from the provider-specific web_search field in the message.


617-618: LGTM! Return values properly expose annotations and webSearchCount.

The conditional null returns (annotations.length > 0 ? annotations : null) ensure clean API responses when no web search data is present.

packages/models/src/prepare-request-body.ts (4)

18-18: LGTM! WebSearchTool import and function signature extension.

The import of WebSearchTool type and the addition of the optional webSearchTool parameter to prepareRequestBody follows the existing pattern for optional parameters in this function.

Also applies to: 147-147


521-535: LGTM! Anthropic web search implementation correctly uses versioned tool type.

The implementation uses web_search_20250305 which aligns with Anthropic's API versioning pattern, and correctly applies only max_uses which is the Anthropic-specific option per the WebSearchTool interface documentation.


830-836: LGTM! Google grounding implementation is correct.

The google_search: {} configuration enables Google's grounding feature. The empty object is appropriate as Google's grounding API typically doesn't require additional configuration options.


276-313: The web_search_options structure is correct and matches OpenAI's API specification.

The nested user_location structure with type: "approximate" and nested approximate object containing city, region, and country matches OpenAI's official documentation for gpt-4o-search-preview and gpt-4o-mini-search-preview models. No changes are needed.

packages/models/src/models/openai.ts (1)

70-95: LGTM! gpt-4o-mini-search-preview model definition.

The configuration follows the same pattern as gpt-4o-search-preview with appropriately lower pricing for the mini variant. The tools: false setting correctly reflects that search models don't support additional tools.

apps/gateway/src/chat/chat.ts (8)

46-46: LGTM! WebSearchTool import added.

The import of WebSearchTool from @llmgateway/models aligns with the type usage in the web search tool extraction logic.


2203-2225: LGTM! webSearchTool correctly passed to prepareRequestBody.

The webSearchTool parameter is properly passed as the last argument, matching the updated function signature. The optional nature handles the case where no web search tool was extracted.


2317-2319: LGTM! Streaming path correctly sets webSearchEnabled header option.

The !!webSearchTool correctly converts to boolean for the header option, enabling Anthropic's web-search beta feature when applicable.


2832-2834: LGTM! Non-streaming path correctly sets webSearchEnabled header option.

Consistent with the streaming path implementation.


4191-4204: LGTM! New annotations and webSearchCount fields correctly extracted from parsed response.

The destructuring properly captures the new web search metadata for use in cost calculation and response transformation.


4274-4304: LGTM! annotations correctly passed to transformResponseToOpenai.

The annotations parameter enables web search citations to be included in the OpenAI-formatted response, maintaining consistency with the provider's web search output.


4404-4404: The webSearchCost field exists in the log schema (packages/db/src/schema.ts:431 as webSearchCost: real()). The insertion at line 4404 is correct and aligns with the schema definition.


4252-4268: The calculateCosts function signature has been correctly updated with the webSearchCount parameter.

The function in apps/gateway/src/lib/costs.ts includes webSearchCount: number | null = null as the final parameter, matching the usage in the code snippet provided.

Comment thread packages/models/src/models/anthropic.ts
Comment on lines +40 to +41
webSearch: true,
webSearchPrice: 0.035, // $35 per 1000 prompts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file to see the current state
wc -l packages/models/src/models/google.ts

Repository: theopenco/llmgateway

Length of output: 105


🏁 Script executed:

# Read the relevant sections of the file
head -n 75 packages/models/src/models/google.ts | tail -n 40

Repository: theopenco/llmgateway

Length of output: 970


🌐 Web query:

Google Gemini 2.5 Pro web search pricing per prompt query

💡 Result:

Gemini 2.5 Pro web (Search) grounding: $35 per 1,000 grounded prompts (≈ $0.035 per grounded prompt). Free daily allotments differ by product page — Google AI developer page shows 1,500 free grounded prompts/day [1], while the Vertex AI (Google Cloud) pricing page lists 10,000 free grounded prompts/day for Gemini 2.5 Pro; overage is $35/1,000 grounded prompts. [1][2]

Sources:
[1] Google AI developer pricing — Gemini grounding details.
[2] Google Cloud Vertex AI pricing — Gemini 2.5 Pro grounding and overage rate.


🌐 Web query:

Google Gemini 3 Pro web search pricing billing unit

💡 Result:

Google’s Vertex AI pricing states Grounding with Google Search / Web Grounding: you get 5,000 search queries per month free (aggregated across Gemini 3 models). Excess queries are billed at $14 per 1,000 search queries (billing counted per individual search query). Billing for this starts January 5, 2026. [1]

Source: Google Cloud Vertex AI pricing — Gemini 3 (Grounding / Web Search section). [1]


🏁 Script executed:

# Check lines 72-73
sed -n '70,75p' packages/models/src/models/google.ts

Repository: theopenco/llmgateway

Length of output: 220


🏁 Script executed:

# Check lines 381-382, 400-401
sed -n '380,410p' packages/models/src/models/google.ts

Repository: theopenco/llmgateway

Length of output: 759


🏁 Script executed:

# Check lines 531, 565, 610, 644 (Gemini 3 models)
sed -n '530,535p; 564,569p; 609,614p; 643,648p' packages/models/src/models/google.ts

Repository: theopenco/llmgateway

Length of output: 647


Clarify web search pricing terminology to match official Google documentation.

Gemini 2.5 comment should say "$35 per 1,000 grounded prompts" to match official pricing terminology. Gemini 3 correctly uses "queries". While both refer to the same billing unit, align Gemini 2.5 comments at lines 40-41, 72-73, 381-382, and 400-401 to include "grounded" for consistency with Google's official terminology.

🤖 Prompt for AI Agents
In packages/models/src/models/google.ts around lines 40-41 (and also update same
phrasing at lines 72-73, 381-382, and 400-401), the Gemini 2.5 comment currently
reads "$35 per 1000 prompts"; change those comments to "$35 per 1,000 grounded
prompts" to match Google's official terminology. Replace the existing comment
text exactly at those locations so Gemini 2.5 uses "grounded" and format the
numeric separator as shown (1,000) for consistency.

Comment thread packages/models/src/models/openai.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive native web search support across multiple LLM providers (OpenAI, Anthropic, Google, ZAI), enabling models to retrieve up-to-date information from the web. The implementation introduces a unified web_search tool interface, provider-specific integrations, cost tracking with per-search billing, and citation/annotation support.

Key Changes:

  • Unified web search API through a WebSearchTool interface that abstracts provider-specific implementations
  • Per-search cost tracking with provider-specific pricing added to model definitions and persisted in database logs
  • Citation extraction and annotation system that surfaces web sources to clients in OpenAI-compatible format

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 21 comments.

Show a summary per file
File Description
packages/models/src/types.ts Defines unified web search types: WebSearchTool, WebSearchCitation, and OpenAIWebSearchOptions interfaces
packages/models/src/prepare-request-body.ts Adds web search tool preparation for each provider with provider-specific formatting (Responses API, Chat Completions, Anthropic, Google grounding, ZAI)
packages/models/src/models/openai.ts Introduces two new search-preview models (gpt-4o-search-preview, gpt-4o-mini-search-preview) with native web search capabilities and pricing
packages/models/src/models/google.ts Adds webSearch capability flag and webSearchPrice to Google models supporting grounding
packages/models/src/models/anthropic.ts Adds webSearch capability flag and webSearchPrice ($0.01 per search) to Anthropic models
packages/models/src/models/zai.ts Adds webSearch capability flag and webSearchPrice ($0.01 per search) to ZAI models
packages/models/src/models.ts Extends ProviderModelMapping interface with optional webSearch and webSearchPrice properties
packages/models/src/get-provider-headers.ts Adds conditional Anthropic beta header for web search feature when enabled
packages/db/src/schema.ts Adds webSearchCost column to log table for persisting per-query web search charges
apps/gateway/src/lib/costs.ts Implements web search cost calculation multiplying search count by provider price, included in total cost
apps/gateway/src/chat/tools/types.ts Defines UrlCitationAnnotation and Annotation types for web search citations, extends StreamingDelta to include annotations
apps/gateway/src/chat/tools/transform-streaming-to-openai.ts Parses web search tool results and grounding metadata from streaming responses, transforms to annotation format
apps/gateway/src/chat/tools/transform-response-to-openai.ts Propagates annotations through response transformation so OpenAI-compatible clients receive web search citations
apps/gateway/src/chat/tools/parse-provider-response.ts Extracts web search citations from provider-specific response formats (Anthropic tool results, Google grounding chunks, OpenAI annotations, ZAI web_search) and counts search queries
apps/gateway/src/chat/chat.ts Extracts web_search tool from tools array, passes to request preparation, integrates web search count into cost calculation, persists web search cost in logs

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

vision: true,
tools: true,
webSearch: true,
webSearchPrice: 0.035, // $35 per 1000 prompts

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pricing comment "$35 per 1000 prompts" is ambiguous and appears in multiple locations. Does this mean $35 per 1000 prompts that use web search, or $35 per 1000 search queries? Based on the implementation in calculateCosts, it appears this should be per search query (multiplied by webSearchCount), not per prompt. The comment should clarify this is "$35 per 1000 searches" or "$0.035 per search" to avoid confusion.

Copilot uses AI. Check for mistakes.
(item: any) => item.type === "web_search_call",
);
if (webSearchCalls.length > 0) {
webSearchCount = Math.max(webSearchCount, webSearchCalls.length);

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webSearchCount is being incremented for each annotation (line 482), but then it's compared with webSearchCalls.length using Math.max (line 503). This logic is inconsistent - if there are multiple citations from a single search call, the count would be inflated. The search count should represent the number of search queries made, not the number of citations. Consider tracking search calls separately from citations, or using only webSearchCalls.length as the authoritative count when available.

Suggested change
webSearchCount = Math.max(webSearchCount, webSearchCalls.length);
// Use the number of web_search_call items as the authoritative search count
webSearchCount = webSearchCalls.length;

Copilot uses AI. Check for mistakes.
Comment on lines +487 to +488
// Remove the web_search tool from the tools array so it's not sent as a regular tool
tools.splice(webSearchToolIndex, 1);

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tools.splice(webSearchToolIndex, 1) operation mutates the original tools array from the request, which could cause issues if the tools array is used elsewhere or if there are multiple attempts to process the same request. This side effect may be unexpected. Consider creating a new filtered array instead of mutating the input.

Suggested change
// Remove the web_search tool from the tools array so it's not sent as a regular tool
tools.splice(webSearchToolIndex, 1);
// Remove the web_search tool from the tools array so it's not sent as a regular tool.
// Use a non-mutating operation to avoid side effects on the original array.
tools = tools.filter((_, index) => index !== webSearchToolIndex);

Copilot uses AI. Check for mistakes.
vision: true,
tools: true,
webSearch: true,
webSearchPrice: 0.035, // $35 per 1000 prompts

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pricing comment "$35 per 1000 prompts" is ambiguous. Does this mean $35 per 1000 prompts that use web search, or $35 per 1000 search queries? Based on the implementation in calculateCosts, it appears this should be per search query (multiplied by webSearchCount), not per prompt. The comment should clarify this is "$35 per 1000 searches" or "$0.035 per search" to avoid confusion.

Copilot uses AI. Check for mistakes.
...(toolResults && { tool_calls: toolResults }),
...(images && images.length > 0 && { images }),
...(annotations &&
annotations.length > 0 && { annotations: annotations }),

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator { annotations: annotations } is redundant. When the property name and variable name are the same, you can use the shorthand { annotations } instead.

Suggested change
annotations.length > 0 && { annotations: annotations }),
annotations.length > 0 && { annotations }),

Copilot uses AI. Check for mistakes.
}),
...(toolResults && { tool_calls: toolResults }),
...(annotations &&
annotations.length > 0 && { annotations: annotations }),

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator { annotations: annotations } is redundant. When the property name and variable name are the same, you can use the shorthand { annotations } instead.

Suggested change
annotations.length > 0 && { annotations: annotations }),
annotations.length > 0 && { annotations }),

Copilot uses AI. Check for mistakes.
Comment on lines +567 to +590
if (annotation.type === "url_citation") {
webSearchCount++;
annotations.push({
type: "url_citation",
url_citation: {
url: annotation.url_citation?.url || annotation.url || "",
title: annotation.url_citation?.title || annotation.title,
start_index:
annotation.url_citation?.start_index ??
annotation.start_index,
end_index:
annotation.url_citation?.end_index ?? annotation.end_index,
},
});
}
}

// For ZAI, extract web search info if present
// ZAI includes web_search content in the response
if (usedProvider === "zai") {
const webSearchResults =
json.choices?.[0]?.message?.web_search || null;
if (webSearchResults && Array.isArray(webSearchResults)) {
webSearchCount = webSearchResults.length;

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the issue in the OpenAI Responses API parsing, webSearchCount is being incremented for each annotation (line 568), which conflates the number of search queries with the number of citations returned. Then at line 590, for ZAI, webSearchCount is overwritten with webSearchResults.length. This inconsistent logic means the count will be unreliable for billing. The count should consistently represent the number of search API calls made, not the number of citations. Consider refactoring to track search calls distinctly from citation results.

Copilot uses AI. Check for mistakes.
maxOutput: 16384,
streaming: true,
vision: true,
tools: false, // Search models don't support additional tools

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates that search models don't support additional tools, but the implementation in prepareRequestBody (lines 280-312) has logic to handle both cases - search models with web_search_options and regular models with web_search as a tool. If search preview models truly don't support additional tools beyond web search, then any user-provided tools should be rejected when using these models, not silently accepted. Consider adding validation to prevent tools from being passed to search-only models, or updating the documentation if tools are actually supported.

Suggested change
tools: false, // Search models don't support additional tools
tools: false, // Disable additional tools via this model definition; web search is handled natively

Copilot uses AI. Check for mistakes.
if (!responsesBody.tools) {
responsesBody.tools = [];
}
const webSearch: any = { type: "web_search" };

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any type for webSearch object bypasses type safety. While the web search tool format may vary by provider, consider defining provider-specific types or at least using a more specific type annotation to catch potential errors at compile time.

Suggested change
const webSearch: any = { type: "web_search" };
const webSearch: WebSearchTool = { type: "web_search" };

Copilot uses AI. Check for mistakes.
}),
...(toolResults && { tool_calls: toolResults }),
...(annotations &&
annotations.length > 0 && { annotations: annotations }),

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator { annotations: annotations } is redundant in multiple places. When the property name and variable name are the same, you can use the shorthand { annotations } instead.

Suggested change
annotations.length > 0 && { annotations: annotations }),
annotations.length > 0 && { annotations }),

Copilot uses AI. Check for mistakes.
@steebchen steebchen changed the title feat(web-search): add native support feat(web-search): add native web search with billing Jan 2, 2026
@steebchen
steebchen had a problem deploying to llmgateway-docs--preview January 2, 2026 20:04 — with Meet Ploy Failure

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/chat.ts (1)

4284-4300: Web search cost not included in streaming path.

The webSearchCount parameter is passed to calculateCosts for non-streaming responses (line 4299), but the streaming path (around line 3672-3687) does not include webSearchCount. This may result in incomplete cost calculations for streaming requests that use web search.

🔎 Verify streaming cost calculation
#!/bin/bash
# Check if webSearchCount is passed in streaming calculateCosts calls
rg -n "calculateCosts" apps/gateway/src/chat/chat.ts -A 15 | head -100
♻️ Duplicate comments (7)
packages/models/src/prepare-request-body.ts (5)

262-262: Use typed interface instead of any.

Per coding guidelines, avoid any unless absolutely necessary. Consider using a more specific type for the web search object.

Based on past review comment on line 262.


301-301: Use typed interface instead of any for web search options.

Per coding guidelines, avoid any unless absolutely necessary. The webSearchOptions object has a known structure that could be typed.

Based on past review comment on lines 301-323.


550-550: Use typed interface for Anthropic web search tool.

Per coding guidelines, avoid any unless absolutely necessary.

Based on past review comment on line 550.


371-384: ZAI web search ignores user-provided options.

The ZAI implementation hardcodes search_engine: "search-prime" and ignores user_location, search_context_size, and max_uses from the webSearchTool parameter. Consider either passing through relevant options or documenting the limitation.

Based on past review comment on lines 377-382.


861-867: Google web search tool structure may need verification.

The code adds { google_search: {} } to the tools array. When function declarations exist, this creates a mixed tools array structure. Verify this is the correct approach per Google's API documentation.

Based on past review comment on lines 861-866.

apps/gateway/src/chat/chat.ts (1)

486-505: Array mutation via splice modifies the original tools array.

The tools.splice(webSearchToolIndex, 1) operation mutates the original tools array from the request. Consider using a non-mutating approach to avoid potential side effects.

Based on past review comment on lines 502-503.

🔎 Suggested non-mutating approach
-			// Remove the web_search tool from the tools array so it's not sent as a regular tool
-			tools.splice(webSearchToolIndex, 1);
+			// Remove the web_search tool from the tools array so it's not sent as a regular tool
+			// Use a non-mutating operation to avoid side effects on the original array
+			tools = tools.filter((_, index) => index !== webSearchToolIndex);

Note: This requires tools to be declared with let instead of destructured from validationResult.data.

packages/models/src/types.ts (1)

441-454: Document the nested structure for OpenAI web search options.

The OpenAIWebSearchOptions.user_location has a nested approximate property, which differs from both WebSearchTool and OpenAIWebSearchToolInput. This is the correct format for OpenAI's API, but the structural difference should be documented to clarify why the transformation in prepare-request-body.ts (lines 303-310) is necessary.

Based on past review comment on lines 444-454.

🧹 Nitpick comments (2)
apps/docs/content/features/web-search.mdx (1)

347-360: Error message mismatch with code.

The error message example mentions "does not support tool calls" but based on the code in chat.ts (lines 816-820), the actual error message is slightly different. Consider aligning the documentation with the actual implementation to avoid confusion.

packages/models/src/types.ts (1)

391-401: Inconsistency between WebSearchTool.user_location and OpenAIWebSearchToolInput.user_location.

WebSearchTool.user_location includes a type: "approximate" field, but OpenAIWebSearchToolInput.user_location (lines 127-132) does not. This creates a type mismatch when extracting web search tools from the request in chat.ts. The extraction at lines 496-501 in chat.ts copies user_location directly, which would not include the type field.

Consider aligning the structures or documenting the transformation that occurs between the API input format and the internal format.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74fc675 and c7e17c8.

⛔ Files ignored due to path filters (3)
  • apps/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (5)
  • apps/docs/content/features/web-search.mdx
  • apps/gateway/src/chat/chat.ts
  • packages/db/src/types.ts
  • packages/models/src/prepare-request-body.ts
  • packages/models/src/types.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/models/src/types.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/prepare-request-body.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • packages/models/src/types.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/prepare-request-body.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • packages/models/src/types.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/prepare-request-body.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • packages/models/src/types.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/prepare-request-body.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM with latest object syntax for database operations

Files:

  • packages/db/src/types.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/db/src/types.ts
  • apps/gateway/src/chat/chat.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/chat/chat.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/chat/chat.ts
🧠 Learnings (1)
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` unless absolutely necessary in TypeScript code

Applied to files:

  • packages/models/src/prepare-request-body.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: lint / run
  • GitHub Check: generate / run
  • GitHub Check: test / run
  • GitHub Check: build / run
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: autofix
🔇 Additional comments (14)
packages/models/src/prepare-request-body.ts (2)

22-29: LGTM!

The isFunctionTool type guard is well-implemented with proper typing and clear logic.


176-183: LGTM!

Clean separation of concerns - filtering function tools from web_search tools before building the request body. The comment clearly explains the reasoning.

apps/gateway/src/chat/chat.ts (7)

196-218: LGTM!

The Zod schema correctly defines the union type for function and web_search tools with appropriate optional fields for web search configuration.


799-821: LGTM!

The validation logic correctly handles the web search capability check. The isWebSearchOnly determination and the conditional validation allowing web-search-only requests for models that support web search is well-implemented.


2349-2351: LGTM!

Provider headers correctly include webSearchEnabled flag for streaming requests.


3864-3866: LGTM!

Provider headers correctly include webSearchEnabled flag for non-streaming requests.


4234-4236: LGTM!

The parseProviderResponse call correctly extracts annotations and webSearchCount from the provider response.


4335-4336: LGTM!

Annotations are correctly passed to transformResponseToOpenai for inclusion in the final response.


4436-4436: LGTM!

Web search cost is correctly persisted in the log entry.

packages/db/src/types.ts (2)

18-37: LGTM!

Clean refactoring of the tool schema to support both function and web_search tool types. The union type approach with functionTool and webSearchTool is well-structured and type-safe.


60-60: LGTM!

The tools array correctly uses the updated union type.

packages/models/src/types.ts (3)

114-140: LGTM!

The function and web search tool input types are well-defined. The union type OpenAIToolInput provides proper type safety for API requests accepting both tool types.


385-413: LGTM!

The WebSearchTool interface is well-documented with JSDoc comments explaining each field's purpose and provider-specific applicability. The type structure correctly represents the unified format.


415-439: LGTM!

The WebSearchCitation interface properly defines the citation structure with all necessary fields for source attribution.

Comment on lines +27 to +32
| Provider | Models | Pricing |
| --------- | -------------------------------------------------------- | ---------------- |
| OpenAI | `gpt-4o-search-preview`, `gpt-4o-mini-search-preview` | $0.03 per search |
| Anthropic | `claude-sonnet-4-20250514` | $0.01 per search |
| Google | `gemini-2.0-flash`, `gemini-2.5-pro`, `gemini-2.5-flash` | Free (grounding) |
| Z.AI | `zai-search` | Varies |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for Anthropic model references in provider configuration
rg -n "claude-sonnet" --type=ts -C3

Repository: theopenco/llmgateway

Length of output: 5439


🏁 Script executed:

# Search for web_search tool/model configuration
rg -n "web_search" --type=ts -C2

Repository: theopenco/llmgateway

Length of output: 15262


🏁 Script executed:

# Find provider configuration files
fd -t f "provider\|anthropic" --type=f | grep -E "\.(ts|js|json)$" | head -20

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

# Look for model name definitions in Anthropic-related files
fd -n "*anthropic*" -t f

Repository: theopenco/llmgateway

Length of output: 294


🏁 Script executed:

# Check OpenAI search models
rg -n "gpt-4o.*search" --type=ts -C2

Repository: theopenco/llmgateway

Length of output: 1802


🏁 Script executed:

# Check Google Gemini models
rg -n "gemini" --type=ts -C2

Repository: theopenco/llmgateway

Length of output: 30669


🏁 Script executed:

# Check Z.AI model
rg -n "zai-search" --type=ts -C2

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

# Search for Z.AI or ZAI in the codebase
rg -i "zai" --type=ts -C2

Repository: theopenco/llmgateway

Length of output: 13493


🏁 Script executed:

# Look for Z.AI in model definitions
rg -i "z\.ai\|zai" --type=ts --type=json

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

cat apps/docs/content/features/web-search.mdx

Repository: theopenco/llmgateway

Length of output: 9119


🏁 Script executed:

# Final verification: Search for any "zai-search" string in codebase
rg "zai-search" --type=ts --type=json --type=mdx

Repository: theopenco/llmgateway

Length of output: 91


🏁 Script executed:

# Search for zai-search in all files
rg "zai-search"

Repository: theopenco/llmgateway

Length of output: 279


Correct the Z.AI model name in the provider table.

The Z.AI entry lists zai-search, but this model does not exist in the codebase. The available Z.AI models are GLM variants (e.g., glm-4.5, glm-4.7, glm-4.6). Update the table to reference an actual supported Z.AI model, or clarify if zai-search is a planned feature.

(The Anthropic model name claude-sonnet-4-20250514 is correct and does not require changes.)

🤖 Prompt for AI Agents
In apps/docs/content/features/web-search.mdx around lines 27 to 32, the Z.AI
model listed as `zai-search` is incorrect; replace it with an actual supported
GLM variant (for example `glm-4.7`) or add a clarifying note if it’s a planned
feature. Update the table cell so the Z.AI row shows a real model name (e.g.,
`glm-4.7`) and adjust the Pricing column text if needed (keep "Varies" if
unknown); do not change the Anthropic entry.

@steebchen
steebchen temporarily deployed to llmgateway-docs--preview January 2, 2026 21:41 — with Meet Ploy Inactive
@steebchen steebchen changed the title feat(web-search): add native web search with billing feat(web-search): native web search with billing Jan 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
packages/models/src/models/openai.ts (2)

44-69: Fix incorrect web search pricing and update release dates.

The webSearchPrice is set to 0.03 ($30 per 1,000 searches), but OpenAI's official pricing is $10.00 per 1,000 web-search calls (equals 0.01 per search). Additionally, the releasedAt and publishedAt dates of 2024-10-01 are outdated; the current model snapshot is gpt-4o-search-preview-2025-03-11.

🔎 Proposed fix
 	{
 		id: "gpt-4o-search-preview",
 		name: "GPT-4o Search Preview",
 		description:
 			"GPT-4o with native web search capabilities for up-to-date information.",
 		family: "openai",
-		releasedAt: new Date("2024-10-01"),
-		publishedAt: new Date("2024-10-01"),
+		releasedAt: new Date("2025-03-11"),
+		publishedAt: new Date("2025-03-11"),
 		providers: [
 			{
 				providerId: "openai",
 				modelName: "gpt-4o-search-preview",
 				inputPrice: 2.5 / 1e6,
 				outputPrice: 10.0 / 1e6,
 				requestPrice: 0,
 				webSearch: true,
-				webSearchPrice: 0.03, // $30 per 1000 searches
+				webSearchPrice: 0.01, // $10 per 1000 searches
 				contextSize: 128000,
 				maxOutput: 16384,
 				streaming: true,
 				vision: true,
 				tools: false, // Search models don't support additional tools
 				jsonOutput: false,
 			},
 		],
 	},

167-169: Add missing webSearchPrice for gpt-4o.

The model has webSearch: true enabled but lacks a webSearchPrice definition. This creates an inconsistency where the cost calculation defaults to 0, making web search free for this model, while all other web-search-enabled models explicitly define pricing.

🔎 Proposed fix
 				streaming: true,
 				vision: true,
 				tools: true,
 				webSearch: true, // Supports web_search tool via Responses API
+				webSearchPrice: 0.01, // $10 per 1000 searches
 				jsonOutputSchema: true,
 				jsonOutput: true,
apps/docs/content/features/web-search.mdx (2)

33-33: Correct the Z.AI model name in the provider table.

The Z.AI entry lists zai-search, but this model does not exist in the codebase. The available Z.AI models are GLM variants (e.g., glm-4.5, glm-4.7, glm-4.6). Update the table to reference an actual supported Z.AI model.

🔎 Proposed fix
 | Google    | `gemini-2.0-flash`, `gemini-2.5-pro`, `gemini-2.5-flash`                 | Free (grounding)     |
-| Z.AI      | `zai-search`                                                             | Varies               |
+| Z.AI      | `glm-4.7` (and other GLM variants)                                       | Varies               |

385-388: Correct Z.AI model reference in provider notes.

The Z.AI section references zai-search, which doesn't exist. Update this to reference actual GLM model variants available in the codebase.

🔎 Proposed fix
 ### Z.AI
 
-	- Dedicated search model (`zai-search`)
-	- Optimized for search queries
+	- GLM model variants (e.g., `glm-4.7`) support web search
+	- Pricing varies by model
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7e17c8 and 7061c92.

📒 Files selected for processing (2)
  • apps/docs/content/features/web-search.mdx
  • packages/models/src/models/openai.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • packages/models/src/models/openai.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • packages/models/src/models/openai.ts
🔇 Additional comments (2)
apps/docs/content/features/web-search.mdx (1)

44-348: LGTM! Well-structured documentation with comprehensive examples.

The documentation provides clear, accurate examples covering basic usage, SDKs (Python/TypeScript), streaming, citations, cost tracking, and combining with function tools. The code snippets correctly demonstrate the web search API and match the implementation in the codebase.

packages/models/src/models/openai.ts (1)

685-687: GPT-5 is now officially released; web search pricing requires documentation review.

GPT-5 was released by OpenAI in 2025, so these models are no longer speculative future releases. However, the specific web search pricing values (0.02–0.05 per search) could not be verified against official OpenAI documentation through public sources. Confirm these values align with current OpenAI pricing documentation for the web search capability in the Responses API.

Also applies to: 752-754, 818-820, 916-918, 982-984, 1012-1014, 1062-1064, 1110-1112, 1175-1177, 1225-1227

Comment on lines +29 to +30
| OpenAI | `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-5.1`, `gpt-5.2`, `gpt-5.2-pro` | $0.02-0.05 / search |
| OpenAI | `gpt-4o-search-preview`, `gpt-4o-mini-search-preview` | $0.025-0.03 / search |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update pricing to match corrected model definitions.

The pricing for OpenAI models should be updated to reflect the correct web search costs. If the pricing fix for gpt-4o-search-preview is applied (changing from $0.03 to $0.01), this table should show $0.01-0.025 / search instead of $0.025-0.03 / search for the search-preview models. Additionally, verify the GPT-5 series pricing range ($0.02-0.05) matches the model definitions.

🔎 Proposed fix (if gpt-4o-search-preview pricing is corrected)
 | Provider  | Models                                                                   | Pricing              |
 | --------- | ------------------------------------------------------------------------ | -------------------- |
 | OpenAI    | `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-5.1`, `gpt-5.2`, `gpt-5.2-pro` | $0.02-0.05 / search  |
-| OpenAI    | `gpt-4o-search-preview`, `gpt-4o-mini-search-preview`                    | $0.025-0.03 / search |
+| OpenAI    | `gpt-4o-search-preview`, `gpt-4o-mini-search-preview`                    | $0.01-0.025 / search |
 | Anthropic | `claude-sonnet-4-20250514`                                               | $0.01 per search     |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| OpenAI | `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-5.1`, `gpt-5.2`, `gpt-5.2-pro` | $0.02-0.05 / search |
| OpenAI | `gpt-4o-search-preview`, `gpt-4o-mini-search-preview` | $0.025-0.03 / search |
| OpenAI | `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-5.1`, `gpt-5.2`, `gpt-5.2-pro` | $0.02-0.05 / search |
| OpenAI | `gpt-4o-search-preview`, `gpt-4o-mini-search-preview` | $0.01-0.025 / search |
🤖 Prompt for AI Agents
In apps/docs/content/features/web-search.mdx around lines 29 to 30, the pricing
row for the OpenAI search-preview models is outdated and should reflect the
corrected `gpt-4o-search-preview` cost; change the second table row's price from
`$0.025-0.03 / search` to `$0.01-0.025 / search`. Also verify the GPT-5 series
row (`gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-5.1`, `gpt-5.2`, `gpt-5.2-pro`)
matches the authoritative model definitions and update its price range if those
definitions show a different range than `$0.02-0.05 / search`.

@steebchen
steebchen had a problem deploying to llmgateway-docs--preview January 2, 2026 22:21 — with Meet Ploy Failure

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7061c92 and 3d3077a.

📒 Files selected for processing (1)
  • apps/ui/src/components/models/all-models.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/ui/src/components/models/all-models.tsx
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/ui/src/components/models/all-models.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/ui/src/components/models/all-models.tsx
apps/{ui,playground}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/{ui,playground}/**/*.{ts,tsx}: Use cookies for user-settings which are not saved in the database to ensure SSR works
Use Next.js App Router with React Server Components for frontend development

Use cookies for user-settings which are not saved in the database to ensure SSR works

Files:

  • apps/ui/src/components/models/all-models.tsx
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/ui/src/components/models/all-models.tsx
apps/{ui,playground,docs}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/link for links and next/navigation's router for programmatic navigation

Files:

  • apps/ui/src/components/models/all-models.tsx
🔇 Additional comments (6)
apps/ui/src/components/models/all-models.tsx (6)

8-8: LGTM! Globe icon import for web search.

The import is correctly added and used consistently throughout the file for web search capability indication.


135-135: LGTM! Consistent filter state initialization.

The webSearch filter initialization follows the same pattern as other capability filters and correctly reads from URL search params.


268-273: LGTM! Consistent filtering logic.

The webSearch filtering logic correctly follows the established pattern for other capability filters and appropriately checks provider-level capabilities.


597-603: LGTM! Web Search capability icon added consistently.

The capability icon definition follows the established pattern and uses an appropriate Globe icon with a distinct color.


618-618: LGTM! Clear filters correctly resets web search.

The webSearch filter is properly reset to false in state and removed from URL parameters, consistent with other capability filters.

Also applies to: 638-639


718-723: LGTM! Web Search filter option added correctly.

The filter checkbox follows the established UI pattern for capability filters and correctly updates both state and URL parameters.

Comment thread apps/ui/src/components/models/all-models.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/ui/src/components/models/all-models.tsx (1)

1203-1218: Verify discount handling for web search pricing.

The Web Search pricing display doesn't apply provider.discount like other price columns do. This creates an inconsistency:

  • Other prices: Use formatPrice(provider.inputPrice, provider.discount) which applies discounts
  • Web Search: Displays provider.webSearchPrice * 1000 without discount consideration

Questions:

  1. Should web search pricing honor provider discounts?
  2. If discounts don't apply to web search, is this intentional?
💡 Optional: Extract web search price formatting to helper function

For consistency and reusability, consider extracting the web search price formatting logic:

const formatWebSearchPrice = (webSearch: boolean | undefined, price: number | undefined, discount?: number) => {
	if (!webSearch) return "—";
	if (price === undefined) return "Free";
	
	const basePrice = price * 1000;
	if (discount) {
		const discountedPrice = (basePrice * (1 - discount)).toFixed(2);
		return (
			<div className="flex flex-col">
				<div className="flex items-center gap-1">
					<span className="line-through text-muted-foreground text-xs">
						${basePrice.toFixed(2)}
					</span>
					<span className="text-green-600 font-semibold">
						${discountedPrice}
					</span>
				</div>
			</div>
		);
	}
	return `$${basePrice.toFixed(2)}/1K`;
};

Then use it in the table cell:

<div className="text-sm font-mono">
	{formatWebSearchPrice(provider.webSearch, provider.webSearchPrice, provider.discount)}
</div>

This would:

  • Apply discounts consistently if they're applicable
  • Make the code more maintainable
  • Match the pattern used for other price columns
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d3077a and b71b8aa.

📒 Files selected for processing (2)
  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/all-models.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/ui/src/components/models/all-models.tsx
  • apps/ui/src/components/dashboard/log-card.tsx
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/ui/src/components/models/all-models.tsx
  • apps/ui/src/components/dashboard/log-card.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/ui/src/components/models/all-models.tsx
  • apps/ui/src/components/dashboard/log-card.tsx
apps/{ui,playground}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/{ui,playground}/**/*.{ts,tsx}: Use cookies for user-settings which are not saved in the database to ensure SSR works
Use Next.js App Router with React Server Components for frontend development

Use cookies for user-settings which are not saved in the database to ensure SSR works

Files:

  • apps/ui/src/components/models/all-models.tsx
  • apps/ui/src/components/dashboard/log-card.tsx
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/ui/src/components/models/all-models.tsx
  • apps/ui/src/components/dashboard/log-card.tsx
apps/{ui,playground,docs}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/link for links and next/navigation's router for programmatic navigation

Files:

  • apps/ui/src/components/models/all-models.tsx
  • apps/ui/src/components/dashboard/log-card.tsx
🧬 Code graph analysis (2)
apps/ui/src/components/models/all-models.tsx (2)
packages/db/src/schema.ts (2)
  • model (658-687)
  • provider (624-656)
apps/ui/src/lib/components/table.tsx (2)
  • TableHead (110-110)
  • TableCell (112-112)
apps/ui/src/components/dashboard/log-card.tsx (1)
packages/db/src/schema.ts (1)
  • log (381-492)
🔇 Additional comments (8)
apps/ui/src/components/dashboard/log-card.tsx (1)

458-463: LGTM! Web Search Cost display follows existing patterns.

The conditional rendering for Web Search Cost is implemented correctly and consistently with the existing cachedInputCost display pattern. The formatting (toFixed(8)), conditional logic, and placement within the Cost Information section are all appropriate.

apps/ui/src/components/models/all-models.tsx (7)

8-8: LGTM: Globe icon import for Web Search.

The import is correctly added for the Web Search capability icon.


135-135: LGTM: Web Search filter initialization.

Correctly initializes the webSearch filter from URL parameters, consistent with other capability filters.


268-273: LGTM: Web Search filtering logic.

The filtering logic correctly checks if any provider supports web search, consistent with other capability filters.


597-603: LGTM: Web Search capability icon.

Correctly adds the Globe icon for Web Search capability with appropriate styling and label.


618-618: LGTM: Web Search filter reset.

Correctly resets the webSearch filter to false and removes it from URL parameters when clearing filters.

Also applies to: 638-639


718-723: LGTM: Web Search filter UI.

The Web Search checkbox is correctly integrated into the capabilities filter panel with consistent styling and behavior.


983-985: LGTM: Web Search table header.

The "Web Search" column header is correctly added with appropriate styling and positioning.

steebchen and others added 6 commits January 2, 2026 23:59
- Introduce WebSearchTool type to unify web search tool config across providers.
- Extract and parse web search citations and counts from provider responses (Anthropic, Google, OpenAI, ZAI).
- Include web search annotations in transformed OpenAI-compatible responses and streaming deltas.
- Add webSearchTool handling in chat core logic for tools and request preparation.
- Extend cost calculations and logging to include web search query costs.
- Update provider models to mark those supporting web search with pricing info.
- Enhance provider headers to support enabling web search beta features.
- Support web search in request body preparations for Anthropic, Google, OpenAI, ZAI.

This enables native web search capabilities for supported models and tracks usage/costs accordingly.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
- Introduce "web_search" tool type alongside existing function tools in API schema
- Support optional user location, search context size, and max uses parameters
- Update request validation and model capability checks to allow web search-only requests
- Modify request preparation to separate web search tool handling from function tools
- Add extensive web search feature documentation in docs/content/features/web-search.mdx
- Add type definitions for web search tools across relevant packages
- Enhance gateway and model packages to support and process web search tools

This enables real-time information retrieval from the internet by models via the new web search tool in the LLM Gateway.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
- Added GPT-5 series models with web search capabilities in openai.ts
- Included pricing and support for Responses API
- Updated web-search.mdx to reflect GPT-5 model support and pricing
- Clarified models that support web search alongside function tools

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
- Include 'webSearch' as a filterable capability for models
- Add Globe icon to represent web search capability
- Update filters and UI to display web search status
- Add 'Web Search' column to models table header

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Added a web search cost display in the LogCard component to show associated costs when present. Also updated the AllModels component to show web search pricing per provider, supporting free, priced, or unavailable statuses.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@steebchen
steebchen force-pushed the terragon/add-native-websearch-support-hpf6au branch from b71b8aa to 71ee641 Compare January 3, 2026 00:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/chat.ts (1)

4481-4532: Add webSearchCost to streaming path logs.

The streaming path's insertLog call (around line 3818) is missing webSearchCost, even though it's calculated via calculateCosts() like the non-streaming path. This creates an inconsistency where web search costs are only tracked for non-streaming requests. Add webSearchCost: costs.webSearchCost to the streaming insertLog call to ensure consistent cost tracking across both request types.

♻️ Duplicate comments (15)
packages/models/src/models/google.ts (1)

40-41: Clarify pricing terminology (duplicate concern).

The comment "$35 per 1000 prompts" has been flagged in previous reviews as ambiguous and inconsistent with Google's official terminology. Google documentation refers to these as "grounded prompts" for Gemini 2.5 models, and the billing is per web search query, not per LLM prompt. This concern has already been raised in prior review cycles.

Also applies to: 72-73, 381-382, 400-401

packages/models/src/models/anthropic.ts (3)

177-199: Missing webSearch configuration (duplicate concern).

According to previous reviews citing Anthropic's web search API documentation, claude-sonnet-4-20250514 should include webSearch: true and webSearchPrice: 0.01. This critical issue was flagged in earlier review cycles but remains unaddressed in the current code.


361-384: Missing webSearch configuration (duplicate concern).

claude-opus-4-20250514 is missing webSearch and webSearchPrice fields. Previous reviews confirmed this model supports web search per Anthropic's official documentation. This critical issue remains unresolved.


406-428: Missing webSearch configuration (duplicate concern).

claude-opus-4-1-20250805 lacks webSearch and webSearchPrice configuration despite supporting web search according to Anthropic's API documentation (as noted in previous reviews). This critical issue needs resolution.

packages/models/src/types.ts (1)

391-454: Structural inconsistency in user_location (duplicate concern).

Previous reviews identified that OpenAIWebSearchOptions (lines 444-454) uses user_location.approximate.city (double-nested), while WebSearchTool (lines 391-413) uses user_location.city with type: "approximate" at the parent level. This structural inconsistency creates confusion about the actual OpenAI API format. The concern has been raised in prior reviews but remains unaddressed.

packages/models/src/models/openai.ts (2)

44-69: Incorrect pricing and release date (duplicate concern).

Previous reviews identified that webSearchPrice should be 0.01 (not 0.03) and the release date should be new Date("2025-03-11") to match OpenAI's official pricing ($10 per 1,000 web-search calls) and the gpt-4o-search-preview-2025-03-11 snapshot. This critical issue was flagged earlier but remains unresolved.


167-169: Missing webSearchPrice despite webSearch enabled.

While Line 167 enables webSearch: true, there is no corresponding webSearchPrice field. Although a previous review comment indicated this was "Addressed in commit 7061c92", the field is absent in the current code. Without this field, the cost calculation defaults to zero, making web search free for gpt-4o, which is inconsistent with all other web-search-enabled models.

🔎 Add webSearchPrice
 webSearch: true, // Supports web_search tool via Responses API
+webSearchPrice: 0.01, // $10 per 1000 searches
 jsonOutputSchema: true,
apps/docs/content/features/web-search.mdx (1)

33-33: Z.AI model zai-search may not exist.

This was flagged in a previous review. Verify if zai-search is a valid model or update to reference an actual supported Z.AI model (e.g., glm-4.5, glm-4.7).

apps/gateway/src/lib/costs.ts (1)

301-307: Remove unnecessary as any cast for webSearchPrice.

The ProviderModelMapping interface should include webSearchPrice as an optional property (per the PR adding webSearchPrice to model definitions). This cast bypasses type safety unnecessarily.

🔎 Proposed fix
-	const webSearchPrice = new Decimal((providerInfo as any).webSearchPrice || 0);
+	const webSearchPrice = new Decimal(providerInfo.webSearchPrice ?? 0);
apps/gateway/src/chat/tools/parse-provider-response.ts (2)

560-583: webSearchCount conflates citations with search calls.

This incrementing webSearchCount for each annotation (line 562) counts citations rather than actual search API calls. The Math.max with webSearchCalls.length (line 583) attempts to correct this but creates unreliable logic. For accurate billing, use only webSearchCalls.length as the authoritative count when available.

This issue was flagged in a previous review.


646-661: Same webSearchCount issue in Chat Completions path.

Similar to the Responses API path, incrementing webSearchCount for each annotation conflates citations with search calls. Consider tracking these separately or using a more reliable indicator of actual search operations.

packages/models/src/prepare-request-body.ts (3)

339-352: Consider defining a typed interface for web search options.

Using any type for the webSearch object bypasses type safety. Per coding guidelines, avoid any unless absolutely necessary.

🔎 Proposed fix
-				const webSearch: any = { type: "web_search" };
+				const webSearch: {
+					type: "web_search";
+					user_location?: WebSearchTool["user_location"];
+					search_context_size?: WebSearchTool["search_context_size"];
+				} = { type: "web_search" };

Based on learnings, avoid any or as any unless absolutely necessary.


455-466: ZAI web search ignores user_location and search_context_size.

The ZAI implementation hardcodes search_engine: "search-prime" and ignores the user_location and search_context_size properties from WebSearchTool. If ZAI supports these options, consider passing them through; otherwise, document which properties are supported per provider.

This was flagged in a previous review.


943-948: Verify Google web search tool structure.

The code pushes { google_search: {} } to the tools array. A previous review noted that Google's API may expect googleSearchRetrieval instead of google_search for grounding. Verify this matches Google's current API documentation.

Google Gemini API google_search vs googleSearchRetrieval grounding tool format
apps/gateway/src/chat/chat.ts (1)

535-554: Array mutation and unnecessary as any casts.

Two issues in this segment:

  1. Array mutation (previously flagged): tools.splice(webSearchToolIndex, 1) mutates the original tools array from the validated request data. This side effect could cause issues if tools is referenced elsewhere.

  2. as any usage: The coding guidelines state "Never use any or as any unless absolutely necessary." A type guard or proper typing would be cleaner.

🔎 Suggested refactor to avoid mutation and `as any`
 	// Extract web_search tool from tools array if present
 	// The web_search tool is a special tool that enables native web search for providers that support it
 	let webSearchTool: WebSearchTool | undefined;
+	let functionTools = tools;
 	if (tools && Array.isArray(tools)) {
-		const webSearchToolIndex = tools.findIndex(
-			(tool: any) => tool.type === "web_search",
+		const webSearchToolEntry = tools.find(
+			(tool): tool is { type: "web_search"; user_location?: WebSearchTool["user_location"]; search_context_size?: WebSearchTool["search_context_size"]; max_uses?: number } =>
+				tool.type === "web_search",
 		);
-		if (webSearchToolIndex !== -1) {
-			// Cast to any to access properties since the schema allows both function and web_search tools
-			const foundTool = tools[webSearchToolIndex] as any;
+		if (webSearchToolEntry) {
 			webSearchTool = {
 				type: "web_search",
-				user_location: foundTool.user_location,
-				search_context_size: foundTool.search_context_size,
-				max_uses: foundTool.max_uses,
+				user_location: webSearchToolEntry.user_location,
+				search_context_size: webSearchToolEntry.search_context_size,
+				max_uses: webSearchToolEntry.max_uses,
 			};
-			// Remove the web_search tool from the tools array so it's not sent as a regular tool
-			tools.splice(webSearchToolIndex, 1);
+			// Filter out web_search tool without mutating the original array
+			functionTools = tools.filter((tool) => tool.type !== "web_search");
 		}
 	}

Then use functionTools instead of tools in subsequent code.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b71b8aa and 71ee641.

⛔ Files ignored due to path filters (3)
  • apps/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (22)
  • apps/docs/content/features/web-search.mdx
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/lib/costs.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/all-models.tsx
  • packages/db/migrations/1767365168_foamy_the_initiative.sql
  • packages/db/migrations/meta/1767365168_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
  • packages/models/src/get-provider-headers.ts
  • packages/models/src/models.ts
  • packages/models/src/models/anthropic.ts
  • packages/models/src/models/google.ts
  • packages/models/src/models/openai.ts
  • packages/models/src/models/zai.ts
  • packages/models/src/prepare-request-body.ts
  • packages/models/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/ui/src/components/dashboard/log-card.tsx
  • packages/db/migrations/1767365168_foamy_the_initiative.sql
  • packages/models/src/models.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • packages/db/src/schema.ts
  • packages/db/migrations/meta/_journal.json
  • packages/models/src/get-provider-headers.ts
  • packages/models/src/models/zai.ts
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/types.ts
  • packages/models/src/models/anthropic.ts
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
  • packages/models/src/models/openai.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/types.ts
  • packages/models/src/models/anthropic.ts
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
  • packages/models/src/models/openai.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/types.ts
  • packages/models/src/models/anthropic.ts
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
  • packages/models/src/models/openai.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • packages/models/src/prepare-request-body.ts
  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • packages/models/src/types.ts
  • packages/models/src/models/google.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/types.ts
  • packages/models/src/models/anthropic.ts
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
  • packages/models/src/models/openai.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/chat.ts
  • packages/db/src/types.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/chat/tools/types.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
apps/{ui,playground}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/{ui,playground}/**/*.{ts,tsx}: Use cookies for user-settings which are not saved in the database to ensure SSR works
Use Next.js App Router with React Server Components for frontend development

Use cookies for user-settings which are not saved in the database to ensure SSR works

Files:

  • apps/ui/src/components/models/all-models.tsx
apps/{ui,playground,docs}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/link for links and next/navigation's router for programmatic navigation

Files:

  • apps/ui/src/components/models/all-models.tsx
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM with latest object syntax for database operations

Files:

  • packages/db/src/types.ts
🧠 Learnings (1)
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` unless absolutely necessary in TypeScript code

Applied to files:

  • packages/models/src/prepare-request-body.ts
🧬 Code graph analysis (4)
apps/gateway/src/chat/tools/transform-streaming-to-openai.ts (1)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
apps/gateway/src/chat/tools/types.ts (1)
packages/models/src/types.ts (1)
  • ToolCall (54-61)
apps/gateway/src/chat/chat.ts (4)
packages/db/src/types.ts (3)
  • webSearchTool (23-35)
  • tools (60-60)
  • tool (37-37)
packages/models/src/types.ts (1)
  • WebSearchTool (391-413)
packages/models/src/models.ts (1)
  • ProviderModelMapping (50-176)
packages/models/src/get-provider-headers.ts (1)
  • getProviderHeaders (13-60)
apps/gateway/src/chat/tools/parse-provider-response.ts (1)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: lint / run
  • GitHub Check: test / run
  • GitHub Check: build / run
  • GitHub Check: generate / run
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: autofix
🔇 Additional comments (20)
packages/db/src/types.ts (1)

18-37: Clean schema definitions for tool types.

The new functionTool and webSearchTool schemas are well-structured and follow Zod conventions. The union type correctly enables both tool types while maintaining type safety.

apps/gateway/src/chat/tools/types.ts (1)

29-49: LGTM! Well-structured annotation types for web search citations.

The UrlCitationAnnotation interface correctly models the citation data structure with appropriate optional fields for provider variations. The type alias Annotation = UrlCitationAnnotation provides good extensibility for future annotation types.

apps/gateway/src/chat/tools/transform-streaming-to-openai.ts (2)

119-153: LGTM! Anthropic web search citation extraction in streaming.

The handler correctly extracts citations from web_search_tool_result blocks and transforms them to the unified url_citation annotation format. The conditional spread ...(annotations.length > 0 && { annotations }) properly avoids adding empty arrays.


456-474: LGTM! Google grounding metadata citation extraction.

The handler correctly extracts web search citations from Google's groundingMetadata.groundingChunks using the proper field names (chunk.web.uri, chunk.web.title).

apps/gateway/src/lib/costs.ts (1)

89-90: LGTM! Web search cost parameter integration.

The webSearchCount parameter is correctly added with a default of null, and the cost calculation properly handles the nullable value with appropriate guards.

apps/gateway/src/chat/tools/parse-provider-response.ts (2)

103-126: LGTM! Anthropic web search extraction logic.

The extraction correctly identifies web_search_tool_result blocks and sets webSearchCount to the number of search blocks (not citations), which accurately represents the number of search operations for billing purposes.


697-698: LGTM! Return statement with new fields.

The return object correctly exposes annotations (null when empty) and webSearchCount (null when zero) for downstream consumption.

apps/ui/src/components/models/all-models.tsx (3)

268-273: LGTM! Web search filter integration.

The filter correctly checks if any provider in the model's provider details supports web search, consistent with other capability filters.


597-603: LGTM! Web search capability icon.

The Globe icon with text-sky-500 color is appropriately added to the capability icons, consistent with the filter panel styling.


1203-1218: LGTM! Web Search column cell implementation.

The cell correctly displays:

  • Price formatted as $/1K searches when webSearch and webSearchPrice are both present
  • "Free" when webSearch is enabled but no price is set (e.g., Google grounding)
  • "—" when web search is not supported
packages/models/src/prepare-request-body.ts (2)

22-29: LGTM! Type guard for function tools.

The isFunctionTool type guard correctly narrows OpenAIToolInput to OpenAIFunctionToolInput, enabling type-safe filtering of tools. This follows the coding guideline to avoid unnecessary any usage.


258-265: LGTM! Filtering tools to function tools only.

Correctly separates function tools from web_search tools, allowing web search to be handled via the dedicated webSearchTool parameter.

apps/gateway/src/chat/chat.ts (8)

46-46: LGTM!

The WebSearchTool type import is correctly placed as a top-level import and aligns with the interface definition in packages/models/src/types.ts.


848-865: LGTM!

The capability checking logic correctly handles the distinction between:

  • Models that support general tool calling
  • Models that only support web search

The isWebSearchOnly determination and the conditional allow logic are well-structured.


2416-2418: LGTM!

The webSearchEnabled flag is correctly propagated to getProviderHeaders, which will add the appropriate beta headers for providers like Anthropic that require them for web search functionality.


2300-2323: LGTM!

The webSearchTool parameter is correctly passed to prepareRequestBody. The undefined value when web search is not requested is the expected behavior.


3931-3933: LGTM!

Consistent with the streaming path implementation. The webSearchEnabled flag is correctly propagated for non-streaming requests.


4289-4303: LGTM!

The new annotations and webSearchCount fields are correctly destructured from parseProviderResponse. These enable proper citation propagation and web search billing.


4364-4380: LGTM!

The webSearchCount is correctly passed to calculateCosts for per-search billing. When web search is not used, this will be undefined or 0, resulting in no additional cost.


4386-4416: LGTM!

The annotations parameter is correctly passed to transformResponseToOpenai, enabling web search citations to be included in the OpenAI-format response returned to users.

Comment on lines +78 to +84
"annotations": [
{
"type": "url_citation",
"url": "https://weather.com/...",
"title": "San Francisco Weather"
}
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation example shows incorrect annotation structure.

The example response shows a flat annotation format, but the actual UrlCitationAnnotation type (in types.ts) uses a nested structure with url_citation object:

 "annotations": [
   {
     "type": "url_citation",
-    "url": "https://weather.com/...",
-    "title": "San Francisco Weather"
+    "url_citation": {
+      "url": "https://weather.com/...",
+      "title": "San Francisco Weather"
+    }
   }
 ]

The same inconsistency appears in the Citations section (lines 210-221). Please update the examples to match the actual API response structure.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"annotations": [
{
"type": "url_citation",
"url": "https://weather.com/...",
"title": "San Francisco Weather"
}
]
"annotations": [
{
"type": "url_citation",
"url_citation": {
"url": "https://weather.com/...",
"title": "San Francisco Weather"
}
}
]
🤖 Prompt for AI Agents
In apps/docs/content/features/web-search.mdx around lines 78-84 and 210-221, the
example JSON shows annotations as a flat object with keys like "type":
"url_citation" and top-level "url"/"title", but the actual UrlCitationAnnotation
type uses a nested structure where the annotation has "type": "url_citation" and
a nested "url_citation" object containing the fields (e.g., url, title). Update
both examples to wrap the url/title inside a "url_citation" object under the
annotation entry to match the API response shape.

Comment on lines +243 to +265
z.union([
z.object({
type: z.literal("function"),
function: z.object({
name: z.string(),
description: z.string().optional(),
parameters: z.record(z.any()).optional(),
}),
}),
}),
z.object({
type: z.literal("web_search"),
user_location: z
.object({
city: z.string().optional(),
region: z.string().optional(),
country: z.string().optional(),
timezone: z.string().optional(),
})
.optional(),
search_context_size: z.enum(["low", "medium", "high"]).optional(),
max_uses: z.number().optional(),
}),
]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and examine the WebSearchTool interface in packages/models
fd types.ts -g 'packages/models/**' | head -5

Repository: theopenco/llmgateway

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Check lines around 545-550 in chat.ts to see how user_location is used
cat -n apps/gateway/src/chat/chat.ts | sed -n '540,555p'

Repository: theopenco/llmgateway

Length of output: 736


🏁 Script executed:

#!/bin/bash
# Search for how webSearchTool is constructed and used
rg -n -B 3 -A 8 'webSearchTool' apps/gateway/src/chat/chat.ts | head -40

Repository: theopenco/llmgateway

Length of output: 1791


🏁 Script executed:

#!/bin/bash
# Find all WebSearchTool references and definitions
rg -n 'WebSearchTool' --type=ts -g '!**/node_modules/**'

Repository: theopenco/llmgateway

Length of output: 546


🏁 Script executed:

#!/bin/bash
# Get the WebSearchTool interface definition
cat -n packages/models/src/types.ts | sed -n '385,410p'

Repository: theopenco/llmgateway

Length of output: 880


🏁 Script executed:

#!/bin/bash
# Also check OpenAIWebSearchToolInput to understand the structure better
cat -n packages/models/src/types.ts | sed -n '120,145p'

Repository: theopenco/llmgateway

Length of output: 849


🏁 Script executed:

#!/bin/bash
# Check the db types definition as mentioned in original comment
fd types.ts | grep packages/db

Repository: theopenco/llmgateway

Length of output: 88


Fix user_location schema to match WebSearchTool interface.

The Zod validation schema accepts user_location with { city?, region?, country?, timezone? }, but the WebSearchTool interface requires { type: "approximate", city?, region?, country? }.

When constructing webSearchTool at lines 545-550, user_location is passed directly without the required type: "approximate" field, and the timezone field is accepted but not part of the interface.

Update the schema to:

user_location: z
  .object({
    type: z.literal("approximate"),
    city: z.string().optional(),
    region: z.string().optional(),
    country: z.string().optional(),
  })
  .optional(),

Remove timezone from the schema and add type: "approximate" to match the WebSearchTool interface.

🤖 Prompt for AI Agents
In apps/gateway/src/chat/chat.ts around lines 243 to 265 (and note webSearchTool
construction at ~lines 545-550), the zod schema for user_location currently
accepts { city?, region?, country?, timezone? } which does not match the
WebSearchTool interface; update the schema so user_location is an optional
object with a required type: z.literal("approximate") and optional city, region,
and country fields, and remove timezone from the schema so the validated shape
matches the WebSearchTool interface used when constructing webSearchTool.

steebchen and others added 5 commits January 3, 2026 19:33
…me fallback

Update model lookup logic in get-provider-endpoint and prepare-request-body to first try matching by model ID and then fall back to matching by provider modelName. This improves compatibility with models identified by provider-specific names, ensuring correct endpoint selection and request preparation.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
- Added checks to ensure that when the web search tool is requested, only model providers that support web search are considered.
- Updated filtering logic for available model providers to respect web search capability.
- Prevent usage of providers that do not support web search when the tool is requested.

This ensures correct provider selection based on web search tool availability, improving reliability when web search is enabled.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
…arch

- Updated documentation to rebrand 'Web Search' as 'Native Web Search' with revised content emphasizing GPT-5.2 as recommended model.
- Simplified provider list into a link to models page filtered for native web search.
- Updated multiple example usages to use model 'openai/gpt-5.2'.
- Adjusted UI labels and tooltips to use 'Native Web Search' instead of 'Web Search' for consistency and clarity.
- Added pricing info to tooltip in model provider card.
- Clarified usage notes to direct users to native web search enabled models only.

This change enhances user understanding and promotes the preferred native web search experience with GPT-5 models.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
…pport-hpf6au' into terragon/add-native-websearch-support-hpf6au

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ui/src/components/models/model-provider-card.tsx (1)

3-16: Align Native Web Search price units with AllModels table

The capability chip implementation and tooltip wiring look solid. However, this tooltip shows ($${provider.webSearchPrice.toFixed(3)}/search) while apps/ui/src/components/models/all-models.tsx renders the same webSearchPrice as $${(provider.webSearchPrice * 1000).toFixed(2)}/1K. One of these is likely misrepresenting the unit.

Recommend deciding whether webSearchPrice is “per search” or “per 1K searches” and updating both places to use the same unit and formatting.

Also applies to: 517-534

♻️ Duplicate comments (2)
apps/ui/src/components/models/all-models.tsx (1)

920-985: Table: Native Web Search column is now structurally and visually consistent (pricing unit mismatch remains)

The table header now includes a “Native Web Search” column and the body renders a corresponding <TableCell> per model row, so the earlier column-count mismatch is resolved. The per-provider cell logic:

provider.webSearch && provider.webSearchPrice
	? `$${(provider.webSearchPrice * 1000).toFixed(2)}/1K`
	: provider.webSearch
		? "Free"
		: "—"

is clear and matches other numeric columns.

However, note this pricing unit disagrees with ModelProviderCard, which renders webSearchPrice as $price/search. One of these two UIs is necessarily wrong about the units. Please align both to the same interpretation of webSearchPrice.

Also applies to: 996-1218

apps/gateway/src/chat/chat.ts (1)

46-47: Web search tool extraction works but uses any and mutates validated data

Functionally this does the right thing: the Zod schema accepts a type: "web_search" tool with user_location, search_context_size, and max_uses, you extract the first such tool into a typed WebSearchTool and keep tools for function tools only.

Two improvement points:

  1. Avoid any and as any
    You can derive the tool type from the schema instead of dropping to any, e.g.:

    type IncomingTool =
    	z.infer<typeof completionsRequestSchema>["tools"] extends (infer T)[]
    		? T
    		: never;
    type IncomingWebSearchTool = Extract<IncomingTool, { type: "web_search" }>;
    
    const webSearchToolIndex = tools.findIndex(
    	(tool): tool is IncomingWebSearchTool => tool.type === "web_search",
    );

    Then foundTool is already strongly typed without as any and matches WebSearchTool.

  2. Avoid mutating tools in-place
    tools.splice(webSearchToolIndex, 1) mutates the parsed request object, which can be surprising and complicates reuse. A non-mutating approach keeps behavior clearer:

    if (webSearchToolIndex !== -1) {
    	const [foundTool] = tools.slice(webSearchToolIndex, webSearchToolIndex + 1);
    	webSearchTool = { ... };
    	tools = tools.filter((_, idx) => idx !== webSearchToolIndex);
    }

    This also gracefully handles any future case where multiple web_search tools might be present.

Neither change is required for correctness, but they tighten type safety and avoid side effects on the validated data.

Check whether TypeScript can narrow the `tools` union from `completionsRequestSchema` sufficiently to avoid `any` by using `z.infer` and `Extract<..., { type: "web_search" }>` patterns.

Also applies to: 242-265, 535-554

🧹 Nitpick comments (1)
apps/ui/src/components/dashboard/log-card.tsx (1)

458-465: Native Web Search cost row looks good; consider zero-cost visibility

The conditional and formatting are consistent with other cost fields. If you ever want to surface free/zero-cost web search usage explicitly (e.g., promotional or bundled search), you might relax the > 0 check to render when webSearchCost is non-null.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b31b685 and 91780a8.

📒 Files selected for processing (5)
  • apps/docs/content/features/web-search.mdx
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/all-models.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/docs/content/features/web-search.mdx
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
apps/{ui,playground}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/{ui,playground}/**/*.{ts,tsx}: Use cookies for user-settings which are not saved in the database to ensure SSR works
Use Next.js App Router with React Server Components for frontend development

Use cookies for user-settings which are not saved in the database to ensure SSR works

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/ui/src/components/models/all-models.tsx
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/models/all-models.tsx
apps/{ui,playground,docs}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/link for links and next/navigation's router for programmatic navigation

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/ui/src/components/models/model-provider-card.tsx
  • apps/ui/src/components/models/all-models.tsx
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/chat/chat.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/chat.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/chat/chat.ts
🧠 Learnings (1)
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{gateway,api}/src/**/*.ts : Use Zod schemas for validation in Hono services

Applied to files:

  • apps/gateway/src/chat/chat.ts
🧬 Code graph analysis (4)
apps/ui/src/components/dashboard/log-card.tsx (1)
packages/db/src/schema.ts (1)
  • log (381-492)
apps/ui/src/components/models/model-provider-card.tsx (2)
packages/db/src/schema.ts (1)
  • provider (624-656)
packages/shared/src/components/ui/tooltip.tsx (3)
  • Tooltip (61-61)
  • TooltipTrigger (61-61)
  • TooltipContent (61-61)
apps/gateway/src/chat/chat.ts (2)
packages/db/src/types.ts (3)
  • webSearchTool (23-35)
  • tools (60-60)
  • tool (37-37)
packages/models/src/get-provider-headers.ts (1)
  • getProviderHeaders (13-60)
apps/ui/src/components/models/all-models.tsx (2)
apps/ui/src/lib/components/providers-icons.tsx (1)
  • getProviderIcon (1213-1226)
apps/ui/src/components/models/model-card.tsx (1)
  • ModelCard (44-478)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: test / run
  • GitHub Check: build / run
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: autofix
🔇 Additional comments (8)
apps/ui/src/components/models/all-models.tsx (4)

83-90: Web Search filter wiring is consistent across state, URL, and UI

Adding webSearch to SortField (for cached pricing), filters.capabilities, clearFilters, URL sync, and the capabilities checklist is coherent. The checkbox correctly toggles both local state and webSearch query param via updateUrlWithFilters, and hasActiveFilters will count it via Object.values(filters.capabilities).

No issues from a behavior or state-sync perspective.

Also applies to: 126-152, 607-652, 666-771


268-273: Web Search capability filter semantics make sense

The capability gate:

if (
	filters.capabilities.webSearch &&
	!model.providerDetails.some((p) => p.provider.webSearch)
) {
	return false;
}

is the right behavior: it keeps only models with at least one provider that supports native web search. This aligns with the rest of the capability filters.


546-605: Shared capability icon helper correctly incorporates Native Web Search

Extending getCapabilityIcons to push a Globe-based “Native Web Search” capability for provider.webSearch and reusing that in both the table (capabilities column) and ModelCard keeps the UI consistent across views.

The ModelCard grid wiring (getCapabilityIcons, shouldShowStabilityWarning, formatPrice) looks correct.

Also applies to: 1303-1318


1330-1331: Header layout and summary cards changes are reasonable

  • Wrapping the main content in an inner container mx-auto py-8 under an outer container ... keeps things centered and doesn’t introduce obvious layout problems.
  • The new “Compare” button linking to /models/compare follows existing patterns for navigation actions alongside “Table/Grid”.
  • Card summaries (models, providers, vision, tools, free models) just tighten padding and reuse modelsWithProviders/filteredProviderCount correctly.

All good from a behavior/UI-consistency standpoint.

Also applies to: 1373-1378, 1449-1507

apps/gateway/src/chat/chat.ts (4)

1177-1183: Routing and fallback correctly restrict web-search requests to web-search-capable providers

These filters:

  • In auto-routing’s suitableProviders:

    if (webSearchTool && (provider as ProviderModelMapping).webSearch !== true) {
    	return false;
    }
  • In low-uptime fallback:

    if (webSearchTool) {
    	return (provider as ProviderModelMapping).webSearch === true;
    }
  • In generic “no usedProvider yet” selection:

    if (webSearchTool) {
    	return (provider as ProviderModelMapping).webSearch === true;
    }

ensure that when routing is involved, web-search-enabled requests only consider providers whose mappings have webSearch === true. That’s exactly what you want for auto/credits/hybrid selection and provider failover.

Looks correct and consistent with the capability model.

Also applies to: 1334-1349, 1455-1465


4391-4407: OpenAI-compatible response now surfaces annotations and keeps cost breakdowns consistent

The additions:

  • Passing webSearchCount into calculateCosts and
  • Passing annotations into transformResponseToOpenai

mean OpenAI-style responses can expose both web-search usage metadata and citations, while shouldIncludeCosts still gates cost fields based on hosted/pro plan.

The way total tokens are computed for the transform (prompt + completion + reasoning) is unchanged and remains consistent with earlier logic.

No issues here.

Also applies to: 4413-4443


2443-2445: webSearchEnabled header flag is threaded correctly into provider headers

Passing webSearchEnabled: !!webSearchTool into getProviderHeaders for both streaming and non-streaming requests is the right hook for providers like Anthropic that toggle web-search betas via headers. The implementation correctly adds web-search-2025-03-05 to the Anthropic beta header when enabled, and unknown providers safely ignore this option, making it a backward-compatible extension.


829-870: Tool support gating: clarify behavior when requests mix web search and function tools

The updated gating logic correctly allows regular tools and web-search-only requests. However, when a request includes both function tools and a web_search tool, the check will pass if the provider supports regular tools (supportsTools=true), even if that provider lacks web search capability (webSearch=false).

This can occur with providers like Deepseek, Meta, XAI, and Cerebras, which declare tools: true but have no webSearch: true flag. While provider selection filtering in auto-routing paths (lines ~1130, ~1300) correctly excludes non-webSearch-capable providers when webSearchTool is present, explicit provider requests bypass these filters. The prepareRequestBody function only handles webSearchTool for OpenAI, Anthropic, Google, and ZAI; other tool-capable providers silently ignore it.

To prevent confusion and silent failures, consider adding an explicit check:

if (webSearchTool && !supportsWebSearch) {
	throw new HTTPException(400, {
		message: `Model ${requestedModel} does not support native web search. Remove the web_search tool or choose a search-capable model.`,
	});
}

This validation should be added before the existing tools capability check to ensure web search requirements are enforced consistently across all code paths.

Comment on lines +4328 to 4330
annotations,
webSearchCount,
} = parseProviderResponse(usedProvider, json, messages);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the calculateCosts function in costs.ts
find . -path "*/apps/gateway/src/lib/costs.ts" -type f

Repository: theopenco/llmgateway

Length of output: 95


🏁 Script executed:

# Also check the structure of the gateway directory
ls -la apps/gateway/src/lib/ 2>/dev/null || echo "Directory not found"

Repository: theopenco/llmgateway

Length of output: 1047


🏁 Script executed:

# Search for calculateCosts function definition
rg "export.*calculateCosts|function calculateCosts" apps/gateway/src/lib/ -A 5

Repository: theopenco/llmgateway

Length of output: 410


🏁 Script executed:

# Read the complete calculateCosts function signature and logic
cat -n apps/gateway/src/lib/costs.ts | head -50

Repository: theopenco/llmgateway

Length of output: 1650


🏁 Script executed:

# Get the full calculateCosts function
rg "export function calculateCosts" apps/gateway/src/lib/costs.ts -A 30

Repository: theopenco/llmgateway

Length of output: 847


🏁 Script executed:

# Get the full calculateCosts function to see how webSearchCount is used
rg "export function calculateCosts" apps/gateway/src/lib/costs.ts -A 100 | head -120

Repository: theopenco/llmgateway

Length of output: 2793


🏁 Script executed:

# Search for webSearchCost calculation in costs.ts
rg "webSearchCost" apps/gateway/src/lib/costs.ts -B 2 -A 2

Repository: theopenco/llmgateway

Length of output: 978


🏁 Script executed:

# Now let's examine the streaming and non-streaming code paths in chat.ts
# First, let's look at the non-streaming path around line 4328-4330
sed -n '4320,4350p' apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 856


🏁 Script executed:

# Look at the streaming code around the mentioned lines
sed -n '4391,4450p' apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 1588


🏁 Script executed:

# Check the final streaming log insert around 4543-4544
sed -n '4535,4555p' apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 669


🏁 Script executed:

# Let's look at the streaming path code. First, check around line 3766-3781 for streaming chunks
sed -n '3760,3795p' apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 848


🏁 Script executed:

# Check the other streaming chunk calculation around 3845-3894
sed -n '3840,3900p' apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 2060


🏁 Script executed:

# Look for calculateCosts calls in streaming context
rg "calculateCosts\(" apps/gateway/src/chat/chat.ts -B 2 -A 8

Repository: theopenco/llmgateway

Length of output: 1554


🏁 Script executed:

# Let's check all calculateCosts calls to see which ones include webSearchCount
rg "calculateCosts\(" apps/gateway/src/chat/chat.ts -B 1 -A 15 | grep -E "(calculateCosts|webSearchCount)" | head -40

Repository: theopenco/llmgateway

Length of output: 267


🏁 Script executed:

# Check if parseProviderResponse returns webSearchCount
rg "export.*parseProviderResponse|function parseProviderResponse" apps/gateway/src/ -A 5

Repository: theopenco/llmgateway

Length of output: 517


🏁 Script executed:

# Look for streaming annotations/deltas to see if webSearchCount is available
rg "webSearchCount" apps/gateway/src/chat/chat.ts -B 3 -A 1

Repository: theopenco/llmgateway

Length of output: 283


🏁 Script executed:

# Let's get a comprehensive view of all calculateCosts calls with context
rg "calculateCosts\(" apps/gateway/src/chat/chat.ts -B 1 -A 12 | head -200

Repository: theopenco/llmgateway

Length of output: 1960


🏁 Script executed:

# Check the streaming log inserts - look for insertLog calls in streaming context
rg "insertLog\(" apps/gateway/src/chat/chat.ts -B 2 -A 30 | grep -E "(insertLog|webSearchCost|streamed.*true)" | head -30

Repository: theopenco/llmgateway

Length of output: 383


🏁 Script executed:

# Let's look at the full streaming insertLog call to confirm webSearchCost is missing
sed -n '3870,3930p' apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 1635


🏁 Script executed:

# Check line 4543-4544 area for the streaming log insert
sed -n '3885,3945p' apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 1503


🏁 Script executed:

# Search for all insertLog calls and check if they include webSearchCost
rg "insertLog\(" apps/gateway/src/chat/chat.ts -A 40 | grep -E "(insertLog|webSearchCost|streamed)" | head -50

Repository: theopenco/llmgateway

Length of output: 522


🏁 Script executed:

# Let's check if parseProviderResponse is called in streaming paths
rg "parseProviderResponse" apps/gateway/src/chat/chat.ts -B 2 -A 2

Repository: theopenco/llmgateway

Length of output: 850


🏁 Script executed:

# Look at the non-streaming insertLog to confirm webSearchCost is included
rg "await insertLog\(" apps/gateway/src/chat/chat.ts -A 45 | grep -A 40 "streamed: false" | head -50

Repository: theopenco/llmgateway

Length of output: 1623


🏁 Script executed:

# Let's check what the streaming path does - look for how streaming annotations are handled
rg "annotations" apps/gateway/src/chat/chat.ts | grep -i stream

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

# Check if there's any web search handling in streaming chunks
rg "webSearch" apps/gateway/src/chat/chat.ts

Repository: theopenco/llmgateway

Length of output: 956


🏁 Script executed:

# Verify the exact line numbers for streaming log insert - check around 3885-3945
sed -n '3880,3920p' apps/gateway/src/chat/chat.ts | cat -n

Repository: theopenco/llmgateway

Length of output: 1450


🏁 Script executed:

# Let's trace the full flow - check if streaming paths call parseProviderResponse
rg "parseProviderResponse" apps/gateway/src/chat/chat.ts -C 5

Repository: theopenco/llmgateway

Length of output: 1581


🏁 Script executed:

# Double-check all streaming insertLog calls to confirm webSearchCost is missing
rg "streamed: true" apps/gateway/src/chat/chat.ts -B 50 | grep -E "(insertLog|webSearchCost|calculateCosts)" | head -40

Repository: theopenco/llmgateway

Length of output: 209


🏁 Script executed:

# Check what happens in streaming chunks - are annotations/deltas parsed?
sed -n '3700,3800p' apps/gateway/src/chat/chat.ts | head -80

Repository: theopenco/llmgateway

Length of output: 2212


🏁 Script executed:

# Let's understand the streaming flow better - check if web search is processed in streaming
rg "webSearchTool" apps/gateway/src/chat/chat.ts -B 3 -A 3 | head -50

Repository: theopenco/llmgateway

Length of output: 1719


🏁 Script executed:

# Check if there's any web search handling in the streaming SSE handler
rg "writeSSE|stream\(" apps/gateway/src/chat/chat.ts -B 2 -A 10 | grep -E "(webSearch|web search|parseProvider)" | head -20

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

# Verify - check the line where streaming log is inserted (around line 3910) for webSearchCost
sed -n '3900,3950p' apps/gateway/src/chat/chat.ts | grep -n "webSearch"

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

# Check the streaming response handling more carefully
sed -n '3600,3700p' apps/gateway/src/chat/chat.ts | cat -n

Repository: theopenco/llmgateway

Length of output: 4289


🏁 Script executed:

# Search for annotation handling in streaming
rg "annotation" apps/gateway/src/chat/chat.ts -C 3

Repository: theopenco/llmgateway

Length of output: 296


🏁 Script executed:

# Check if streaming responses from providers include web search data
rg "chunk|delta" apps/gateway/src/chat/chat.ts | grep -i "web\|search" | head -10

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

# Let's check the full streaming handler to see if webSearchCount could be parsed from streaming chunks
sed -n '3500,3600p' apps/gateway/src/chat/chat.ts | cat -n | head -80

Repository: theopenco/llmgateway

Length of output: 3384


🏁 Script executed:

# Check if parseProviderResponse is somehow used or if annotations are parsed during streaming
rg "streaming.*annotation|chunk.*annotation" apps/gateway/src/chat/chat.ts -C 5

Repository: theopenco/llmgateway

Length of output: 46


🏁 Script executed:

# Let's verify the exact parameters passed to calculateCosts in streaming vs non-streaming
rg "calculateCosts\(" apps/gateway/src/chat/chat.ts | wc -l

Repository: theopenco/llmgateway

Length of output: 65


🏁 Script executed:

# Count how many calculateCosts calls don't include webSearchCount parameter
rg "calculateCosts\(" apps/gateway/src/chat/chat.ts -A 12 | grep -c "webSearchCount\|inputImageCount," | head -20

Repository: theopenco/llmgateway

Length of output: 65


🏁 Script executed:

# Let's verify by checking all 5 calculateCosts calls and their parameters
rg "calculateCosts\(" apps/gateway/src/chat/chat.ts -A 12 | grep -B 12 "inputImageCount"

Repository: theopenco/llmgateway

Length of output: 749


🏁 Script executed:

# Confirm streaming insertLog doesn't have webSearchCost while non-streaming does
rg "await insertLog\(" apps/gateway/src/chat/chat.ts -A 35 | grep -E "(insertLog|webSearchCost|streamed:)" | head -40

Repository: theopenco/llmgateway

Length of output: 522


Streaming requests using web search don't include webSearchCost in billing logs

The non-streaming path correctly wires web search billing:

  • parseProviderResponse returns webSearchCount
  • calculateCosts receives webSearchCount as the final argument
  • insertLog includes webSearchCost: costs.webSearchCost

However, the streaming path omits web search from cost calculations:

  • All streaming calculateCosts calls lack the webSearchCount parameter
  • The streaming insertLog does not set webSearchCost at all
  • Since calculateCosts uses webSearchCount to derive webSearchCost, streaming web-search requests will not be billed for or tracked correctly

If streaming with native web search is a supported combination, the streaming pipeline needs to:

  1. Capture webSearchCount from streaming chunks or responses
  2. Pass it to calculateCosts
  3. Include webSearchCost in the streaming log insert

If streaming + web search is intentionally unsupported, this assumption should be asserted in code or documentation.

🤖 Prompt for AI Agents
In apps/gateway/src/chat/chat.ts around lines 4328 to 4330, the streaming branch
omits webSearchCount when calculating costs and does not include webSearchCost
in the streaming insertLog; update the streaming pipeline to capture
webSearchCount from the provider streaming output (accumulate it from streaming
chunks or obtain it from the final streaming response), pass that webSearchCount
into calculateCosts wherever calculateCosts is invoked in the streaming flow,
and set webSearchCost: costs.webSearchCost in the insertLog for streaming
requests; alternatively, if streaming+web search is unsupported, explicitly
assert or guard against that combination and log/throw accordingly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91780a8 and ee2ff99.

📒 Files selected for processing (3)
  • packages/db/migrations/1767474314_zippy_norman_osborn.sql
  • packages/db/migrations/meta/1767474314_snapshot.json
  • packages/db/migrations/meta/_journal.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/db/migrations/meta/_journal.json

@@ -0,0 +1 @@
ALTER TABLE "log" ADD COLUMN "web_search_cost" real;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all SQL migration files to understand the schema structure
find packages/db/migrations -name "*.sql" -type f | head -20

Repository: theopenco/llmgateway

Length of output: 1183


🏁 Script executed:

#!/bin/bash
# Search for cost-related columns in all SQL files
rg -n 'cost|Cost' packages/db/migrations --type=sql -i

Repository: theopenco/llmgateway

Length of output: 884


🏁 Script executed:

#!/bin/bash
# Look for schema definition files
fd 'schema' packages --type f | grep -E '\.(ts|js|sql)$'

Repository: theopenco/llmgateway

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Examine the schema.ts file to understand how costs are defined
cat packages/db/src/schema.ts

Repository: theopenco/llmgateway

Length of output: 23776


🏁 Script executed:

#!/bin/bash
# Get more context on the data_storage_cost migration that uses numeric
rg -B2 -A2 'data_storage_cost' packages/db/migrations --type=sql

Repository: theopenco/llmgateway

Length of output: 344


Use numeric for web_search_cost to match the pattern established by data_storage_cost.

The real type (4-byte floating-point) can introduce precision errors for financial data. The codebase already addressed this with the data_storage_cost column (migration 1764086480), which uses numeric with fixed precision. This migration should follow that same pattern for consistency and to avoid rounding discrepancies in cost calculations.

Suggested change
ALTER TABLE "log" ADD COLUMN "web_search_cost" numeric DEFAULT '0' NOT NULL;
🤖 Prompt for AI Agents
In packages/db/migrations/1767474314_zippy_norman_osborn.sql around line 1, the
migration adds web_search_cost as type real which can cause precision errors and
diverges from the existing data_storage_cost pattern; change the column to use
numeric with a default and non-null constraint (e.g. ALTER TABLE "log" ADD
COLUMN "web_search_cost" numeric DEFAULT '0' NOT NULL) so it matches the
established cost column type and avoids floating-point rounding issues.

- Update billing count logic to count web_search_call items correctly
- Change pricing for reasoning models to $10 per 1000 searches
- Skip minimal reasoning effort auto-set if web_search tool is present
- Add display of web search cost in UI dashboard log card
- Clarify distinction between citations (display) and calls (billing)

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
apps/gateway/src/chat/tools/parse-provider-response.ts (1)

309-338: Clarify Google web search count logic.

The comment at line 313 states "Google doesn't report individual search counts," yet line 336 overwrites webSearchCount with webSearchQueries.length when available. This creates ambiguity:

  • If Google consistently provides webSearchQueries, the hardcoded 1 is always overwritten and the comment is misleading.
  • If webSearchQueries is sometimes absent, the default should be documented as a fallback assumption.

Suggest:

  1. Remove the misleading comment, OR
  2. Update the comment to clarify: "Default to 1 search; use webSearchQueries.length if available"
🔎 Proposed clarification
-			webSearchCount = 1; // Google doesn't report individual search counts
+			// Default to 1 search; will be updated if webSearchQueries is available
+			webSearchCount = 1;
packages/models/src/models/openai.ts (2)

44-69: Fix incorrect web search pricing for gpt-4o-search-preview.

Line 60 sets webSearchPrice: 0.03 ($30 per 1,000 searches), but OpenAI's official pricing is $10.00 per 1,000 web-search calls, which equals 0.01 per search. The current value is 3x higher and will overcharge users.

Additionally, the release dates (lines 50-51) may be outdated based on the current model snapshot (gpt-4o-search-preview-2025-03-11 as of March 2025).

🔎 Proposed fix
-		webSearchPrice: 0.03, // $30 per 1000 searches
+		webSearchPrice: 0.01, // $10 per 1000 searches (OpenAI official pricing)

Also consider updating the dates if the snapshot has changed:

-		releasedAt: new Date("2024-10-01"),
-		publishedAt: new Date("2024-10-01"),
+		releasedAt: new Date("2025-03-11"),
+		publishedAt: new Date("2025-03-11"),

167-167: Add missing webSearchPrice for gpt-4o or document if free.

Line 167 enables webSearch: true for gpt-4o but lacks a webSearchPrice field. The cost calculation will default to 0, making web search free for this model.

If OpenAI charges for web search on gpt-4o via the Responses API, add the appropriate webSearchPrice. If web search is free for this model, document this explicitly in a comment to clarify the intentional omission.

🔎 Proposed fix if priced
 		tools: true,
 		webSearch: true, // Supports web_search tool via Responses API
+		webSearchPrice: 0.01, // $10 per 1000 searches (verify official pricing)
 		jsonOutputSchema: true,

Or if free:

 		webSearch: true, // Supports web_search tool via Responses API (no additional charge)
apps/gateway/src/chat/chat.ts (2)

243-265: Fix user_location schema to match WebSearchTool interface.

The Zod schema (lines 254-260) accepts { city?, region?, country?, timezone? }, but the WebSearchTool interface requires { type: "approximate", city?, region?, country? } (see packages/models/src/types.ts:395-402).

Mismatches:

  1. Missing required type: "approximate" field
  2. Extra timezone field not in interface

When webSearchTool is constructed at lines 545-550, user_location is passed directly without the required type field.

🔎 Proposed fix
 			z.object({
 				type: z.literal("web_search"),
 				user_location: z
 					.object({
+						type: z.literal("approximate"),
 						city: z.string().optional(),
 						region: z.string().optional(),
 						country: z.string().optional(),
-						timezone: z.string().optional(),
 					})
 					.optional(),
 				search_context_size: z.enum(["low", "medium", "high"]).optional(),
 				max_uses: z.number().optional(),
 			}),

As per coding guidelines, use Zod schemas for validation in Hono services.


535-554: Avoid mutating the tools array; use filter instead.

Line 552 uses tools.splice(webSearchToolIndex, 1) to remove the web_search tool, mutating the original tools array from the request. This side effect could cause issues if the array is used elsewhere.

🔎 Proposed fix
 	// Extract web_search tool from tools array if present
 	let webSearchTool: WebSearchTool | undefined;
 	if (tools && Array.isArray(tools)) {
 		const webSearchToolIndex = tools.findIndex(
 			(tool: any) => tool.type === "web_search",
 		);
 		if (webSearchToolIndex !== -1) {
 			const foundTool = tools[webSearchToolIndex] as any;
 			webSearchTool = {
 				type: "web_search",
 				user_location: foundTool.user_location,
 				search_context_size: foundTool.search_context_size,
 				max_uses: foundTool.max_uses,
 			};
-			// Remove the web_search tool from the tools array so it's not sent as a regular tool
-			tools.splice(webSearchToolIndex, 1);
+			// Filter out the web_search tool to avoid sending it as a regular tool
+			tools = tools.filter((_, index) => index !== webSearchToolIndex);
 		}
 	}
🧹 Nitpick comments (1)
packages/models/src/models/openai.ts (1)

685-687: Verify GPT-5 web search pricing when models are released.

Multiple GPT-5 variants now include webSearch: true with webSearchPrice: 0.01 ($10 per 1,000 searches). The comment specifies this pricing is "for reasoning models."

Since GPT-5 models are not yet released (as of January 2026), confirm this pricing with OpenAI's official documentation when the models become available. The pricing may differ from the current gpt-4o-search-preview pricing.

Also applies to: 752-754, 818-820, 916-918, 982-984, 1012-1014, 1062-1064, 1110-1112, 1175-1177, 1225-1227

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee2ff99 and 43d299b.

📒 Files selected for processing (4)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • packages/models/src/models/openai.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/ui/src/components/dashboard/log-card.tsx
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/models/openai.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/chat.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/chat.ts
  • packages/models/src/models/openai.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/chat.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/chat.ts
🧠 Learnings (1)
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{gateway,api}/src/**/*.ts : Use Zod schemas for validation in Hono services

Applied to files:

  • apps/gateway/src/chat/chat.ts
🧬 Code graph analysis (2)
apps/gateway/src/chat/tools/parse-provider-response.ts (1)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
apps/gateway/src/chat/chat.ts (5)
packages/db/src/types.ts (3)
  • webSearchTool (23-35)
  • tools (60-60)
  • tool (37-37)
packages/models/src/types.ts (1)
  • WebSearchTool (391-413)
packages/db/src/schema.ts (1)
  • provider (624-656)
packages/models/src/models.ts (1)
  • ProviderModelMapping (50-176)
packages/models/src/get-provider-headers.ts (1)
  • getProviderHeaders (13-60)
🔇 Additional comments (10)
apps/gateway/src/chat/tools/parse-provider-response.ts (3)

552-583: LGTM! Clear separation of billing vs display data.

The implementation correctly distinguishes between:

  • Billing: webSearchCalls.length (line 557) counts API calls for cost calculation
  • Display: Annotations (lines 562-582) provide citations for the user

The inline comments clearly document this distinction.


641-666: LGTM! Correct billing for search preview models.

The implementation correctly bills search preview models (gpt-4o-search-preview, gpt-4o-mini-search-preview) at 1 search per request when citations are present, matching OpenAI's billing model as documented in the comment at line 643.


103-143: Implementation correctly aligns with Anthropic's billing model.

Anthropic bills web search at $10 per 1,000 web searches, meaning billing is per search invocation. The code correctly counts webSearchBlocks.length (line 109) as the number of billable search calls, where each web_search_tool_result block represents one search invocation. Inline citations from textBlocks (lines 128-143) are content within search results, not separate billable events, so they appropriately don't increment webSearchCount.

apps/gateway/src/chat/chat.ts (7)

848-870: LGTM! Correct validation for web search tool support.

The implementation correctly validates tool support:

  • Checks if provider supports webSearch capability (lines 849-851)
  • Distinguishes web-search-only requests from mixed tool requests (lines 853-860)
  • Allows requests that either have tool support OR use web search exclusively (lines 862-869)

This properly handles the case where search preview models don't support additional function tools but do support web search.


1177-1183: LGTM! Correct auto-routing filter for web search.

The auto-routing logic correctly filters providers to only those with webSearch === true when a webSearchTool is present (lines 1177-1183). This ensures web search requests are routed to capable providers.


1334-1348: LGTM! Fallback routing respects web search requirements.

When routing away from a low-uptime provider, the code correctly filters to only providers that support web search if webSearchTool is present (lines 1344-1346). This maintains web search capability during fallback routing.


1456-1465: LGTM! Provider selection respects web search capability.

The provider selection logic correctly filters to web-search-capable providers when webSearchTool is present (lines 1461-1463), maintaining consistency with other provider selection paths.


2351-2351: LGTM! Web search tool correctly integrated into request flow.

The webSearchTool is properly:

  • Passed to prepareRequestBody (line 2351) for request body construction
  • Converted to webSearchEnabled flag for headers (lines 2445-2447, 3960-3962)
  • Applied to both streaming and non-streaming code paths

The integration is consistent across all execution paths.

Also applies to: 2445-2447, 3960-3962


4330-4332: LGTM! Non-streaming path correctly handles web search billing.

The non-streaming path properly:

  • Extracts annotations and webSearchCount from parseProviderResponse (lines 4330-4332)
  • Passes webSearchCount to calculateCosts (line 4408)
  • Includes annotations in the transformed response (line 4444)
  • Logs webSearchCost in billing records (line 4545)

The web search billing flow is complete for non-streaming requests.

Note: A past review comment flagged that the streaming path lacks webSearchCost in billing logs (see existing_comments for lines 4330-4332 in past_review_comments). That issue is not addressed in this diff and remains outstanding.

Also applies to: 4408-4408, 4444-4444, 4545-4545


1617-1637: Reconsider the reasoning_effort auto-set logic when web search is present.

The comment states web search is "incompatible with 'minimal' reasoning effort," but this conflates two separate concerns. The actual API limitation is that parallel function tool calls are restricted with minimal reasoning_effort (noted for Azure), not web search itself. Web search is extracted as a separate capability, not invoked as a traditional function tool.

The code should differentiate between:

  • Web search (a native capability) — compatible with minimal reasoning_effort
  • Parallel function tool calls — restricted with minimal reasoning_effort

Consider refining the condition to skip reasoning_effort auto-setting only when the request includes function tools that require parallelization, not whenever web search is present. If web search is used alone or alongside non-parallel tools, auto-setting reasoning_effort should be safe.

Comment on lines +668 to +685
// For ZAI, extract web search info if present
// ZAI includes web_search content in the response
if (usedProvider === "zai") {
const webSearchResults =
json.choices?.[0]?.message?.web_search || null;
if (webSearchResults && Array.isArray(webSearchResults)) {
webSearchCount = webSearchResults.length;
for (const result of webSearchResults) {
annotations.push({
type: "url_citation",
url_citation: {
url: result.link || result.url || "",
title: result.title,
},
});
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

ZAI Z.AI web search API billing model pricing documentation

💡 Result:

Summary — Z.AI (ZAI) Web Search API billing

  • Web Search tool: $0.01 per use. [1]
  • Z.AI overall API pricing is pay-as-you-go; model calls are billed per-token (prices listed per 1M tokens) and some built-in tools (like Web Search) are billed per call. See the pricing table for model-specific token rates and other tool costs. [1]
  • Your account/billing dashboard (Manage → Billing) shows usage, balance, invoices and lets you top up/manage payment methods. [2]

Sources

  • Z.AI pricing documentation (Web Search: $0.01/use; model token rates). [1]
  • Z.AI billing dashboard / account billing page. [2]

Fix web search usage counting logic.

The code counts webSearchResults.length (line 674) as the billable search count. However, ZAI bills Web Search at $0.01 per use/call, not per result returned. A single search call typically returns multiple results in the array. Counting results instead of search calls inflates the reported usage.

Determine the correct count based on how many actual search queries ZAI executed (not results returned), and adjust the webSearchCount assignment accordingly.

Adjusted webSearchPrice from $0.03 to $0.025 for non-reasoning models in openai.ts to reflect accurate pricing.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/models/src/models/openai.ts (2)

70-95: Verify webSearchPrice and release date.

This model has the same webSearchPrice of 0.025 and potentially outdated releasedAt date as gpt-4o-search-preview. Please verify against official OpenAI documentation.


167-167: Add missing webSearchPrice for gpt-4o.

The model has webSearch: true but lacks a webSearchPrice field. A previous review comment (marked as addressed in commit 7061c92) flagged this issue, but the field is still missing in the current code. Without webSearchPrice, cost calculations will default to 0, making web search free for this model and creating an inconsistency with other web-search-enabled models.

🔎 Proposed fix
 webSearch: true, // Supports web_search tool via Responses API
+webSearchPrice: 0.01, // $10 per 1000 searches
 jsonOutputSchema: true,
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43d299b and 3656a9e.

📒 Files selected for processing (1)
  • packages/models/src/models/openai.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • packages/models/src/models/openai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • packages/models/src/models/openai.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • packages/models/src/models/openai.ts
🔇 Additional comments (1)
packages/models/src/models/openai.ts (1)

685-687: Consistent web search implementation across GPT-5 family.

The web search additions across all GPT-5 family models are internally consistent, using webSearchPrice: 0.01 ($10 per 1,000 searches for reasoning models) and supportsResponsesApi: true where applicable. The distinction between reasoning model pricing (0.01) and non-reasoning model pricing (0.025) in the search-preview models suggests an intentional pricing tier, but this should be verified against official OpenAI documentation to ensure accuracy.

Also applies to: 752-754, 818-820, 916-920, 982-984, 1012-1014, 1062-1064, 1110-1114, 1175-1177, 1225-1227

Comment on lines +44 to +69
{
id: "gpt-4o-search-preview",
name: "GPT-4o Search Preview",
description:
"GPT-4o with native web search capabilities for up-to-date information.",
family: "openai",
releasedAt: new Date("2024-10-01"),
publishedAt: new Date("2024-10-01"),
providers: [
{
providerId: "openai",
modelName: "gpt-4o-search-preview",
inputPrice: 2.5 / 1e6,
outputPrice: 10.0 / 1e6,
requestPrice: 0,
webSearch: true,
webSearchPrice: 0.025, // $25 per 1000 searches for non-reasoning models
contextSize: 128000,
maxOutput: 16384,
streaming: true,
vision: true,
tools: false, // Search models don't support additional tools
jsonOutput: false,
},
],
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

OpenAI gpt-4o-search-preview web search pricing per 1000 searches

💡 Result:

gpt-4o-search-preview web-search tool calls: $10.00 per 1,000 calls, plus search content tokens billed at the model’s input-token rates. [1][2]

Sources:
[1] OpenAI API Pricing — Built-in tools (Web search preview (reasoning models) $10.00 / 1K calls + search content tokens billed at model rates).
[2] GPT-4o Search Preview model docs (notes pricing and search-token billing).


🌐 Web query:

OpenAI gpt-4o-search-preview release date model snapshot 2025

💡 Result:

OpenAI released the GPT-4o Search Preview models in March 2025. The API exposes snapshot aliases including gpt-4o-search-preview-2025-03-11 (and the gpt-4o-mini-search-preview equivalent). Sources: OpenAI model docs and changelog. [1][2]

Sources

  1. OpenAI Models docs — "GPT-4o Search Preview" (shows snapshots/gpt-4o-search-preview-2025-03-11).
  2. OpenAI API changelog — March 11, 2025 entry announcing gpt-4o-search-preview and gpt-4o-mini-search-preview.

🏁 Script executed:

sed -n '44,69p' packages/models/src/models/openai.ts

Repository: theopenco/llmgateway

Length of output: 770


Correct webSearchPrice and update release date to match official OpenAI specifications.

The webSearchPrice of 0.025 is incorrect per official OpenAI pricing documentation, which specifies $10.00 per 1,000 search calls (0.01). Additionally, releasedAt and publishedAt should be updated to March 11, 2025 (new Date("2025-03-11")) to match the official model snapshot gpt-4o-search-preview-2025-03-11 released in March 2025.

🤖 Prompt for AI Agents
In packages/models/src/models/openai.ts around lines 44 to 69, update the model
metadata to match official OpenAI specs: change webSearchPrice from 0.025 to
0.01 (representing $10.00 per 1,000 searches) and set releasedAt and publishedAt
to new Date("2025-03-11"); keep all other provider fields the same.

Add a check to ensure that if the web_search tool is requested, the selected model supports it. Throws an HTTP 400 error with an explanatory message if the model does not support native web search, directing users to supported models documentation.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/gateway/src/chat/chat.ts (3)

243-265: [Duplicate] Fix user_location schema to match WebSearchTool interface.

This issue was already flagged in a previous review. The Zod schema for user_location accepts { city?, region?, country?, timezone? } but the WebSearchTool interface requires { type: "approximate", city?, region?, country? }. The schema is missing the required type field and includes an unsupported timezone field.


535-554: [Duplicate] Avoid mutating the original tools array.

This issue was already flagged in a previous review. The tools.splice(webSearchToolIndex, 1) operation mutates the original request's tools array, which can cause issues if the array is used elsewhere. Use a non-mutating operation like filter instead.


3775-3790: [Duplicate] Streaming requests missing web search billing.

This critical issue was already flagged in a previous review. The streaming path does not include web search billing:

  1. The calculateCosts call at line 3775 omits the webSearchCount parameter (compare to line 4415 in non-streaming)
  2. The insertLog call starting at line 3854 does not include the webSearchCost field (compare to line 4552 in non-streaming)

This means streaming requests using web search will not be tracked or billed correctly. Since calculateCosts derives webSearchCost from webSearchCount, and streaming doesn't pass this parameter, web search costs will be zero for streaming requests.

Also applies to: 3854-3903

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3656a9e and 26c2c52.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/chat.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/chat.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/gateway/src/chat/chat.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/gateway/src/chat/chat.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/chat/chat.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/gateway/src/chat/chat.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/chat.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/chat/chat.ts
🧠 Learnings (1)
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{gateway,api}/src/**/*.ts : Use Zod schemas for validation in Hono services

Applied to files:

  • apps/gateway/src/chat/chat.ts
🧬 Code graph analysis (1)
apps/gateway/src/chat/chat.ts (5)
packages/db/src/types.ts (3)
  • webSearchTool (23-35)
  • tools (60-60)
  • tool (37-37)
packages/models/src/types.ts (1)
  • WebSearchTool (391-413)
packages/db/src/schema.ts (1)
  • provider (624-656)
packages/models/src/models.ts (1)
  • ProviderModelMapping (50-176)
packages/models/src/get-provider-headers.ts (1)
  • getProviderHeaders (13-60)
🔇 Additional comments (4)
apps/gateway/src/chat/chat.ts (4)

848-877: LGTM! Clear validation logic for web search support.

The validation logic correctly handles the distinction between function tools and web search tools:

  • Determines if the request is web-search-only (has webSearchTool but no function tools)
  • Allows the request if the model supports regular tools OR (supports web search AND is web-search-only)
  • Provides a clear error message with documentation link when web search is not supported

The error message at lines 873-876 is particularly helpful, directing users to the models page with the webSearch filter.


2358-2358: LGTM! Consistent web search integration in request flow.

The web search tool is correctly integrated into both streaming and non-streaming request flows:

  • webSearchTool is passed to prepareRequestBody to include in provider-specific payloads (line 2358)
  • webSearchEnabled header flag is set consistently using !!webSearchTool (lines 2452-2454 for streaming, 3967-3969 for non-streaming)
  • The boolean coercion pattern matches other similar parameters in the codebase

Also applies to: 2452-2454, 3967-3969


4337-4339: LGTM! Complete web search billing flow in non-streaming path.

The non-streaming path correctly implements the full web search billing flow:

  • Extracts annotations and webSearchCount from provider response (lines 4337-4339)
  • Passes webSearchCount to calculateCosts as the final argument (line 4415)
  • Includes annotations in the transformed OpenAI response (line 4451)
  • Logs webSearchCost in the database (line 4552)

This ensures web search usage is properly tracked and billed for non-streaming requests.

Also applies to: 4415-4415, 4451-4451, 4552-4552


1624-1644: The comment about web search and reasoning effort incompatibility is misleading. The code sends both reasoning_effort and webSearchTool together to the OpenAI Responses API without any validation preventing this combination. Model definitions explicitly list reasoning models with web search support (e.g., webSearchPrice: 0.01, // $10 per 1000 searches for reasoning models). The skip at line 1630 is an auto-routing design choice, not evidence of API incompatibility—explicit reasoning_effort with web search is not rejected anywhere in the codebase and is actively supported by the provider implementations.

Likely an incorrect or invalid review comment.

Comment thread apps/gateway/src/chat/chat.ts
@steebchen
steebchen enabled auto-merge January 4, 2026 00:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/chat.ts (1)

4388-4402: Non‑streaming path correctly wires annotations and web search costs, but surface is incomplete

The non‑streaming flow is mostly well‑integrated:

  • parseProviderResponse now returns annotations and webSearchCount.
  • annotations are passed into transformResponseToOpenai, which attaches them to the assistant message.
  • webSearchCount is passed as the last argument to calculateCosts, so webSearchCost is computed.
  • The final log insert includes webSearchCost: costs.webSearchCost, ensuring per‑search billing is persisted.

Two gaps remain:

  1. usage.web_search_cost not exposed
    Even though webSearchCost is calculated, the CostData passed to transformResponseToOpenai only includes input/output/cached/request/total cost fields. The OpenAPI schema for /v1/chat/completions usage also lacks web_search_cost. If the public API is meant to expose per‑search cost (as docs show), you should:

    • extend the usage schema to add web_search_cost: z.number().nullable().optional(), and
    • extend the CostData/buildUsageObject path (in transform-response-to-openai.ts) to include a web_search_cost field derived from costs.webSearchCost.
  2. Response schema missing annotations field
    The completions route’s response schema defines message without an annotations property, but the transformed responses now include message.annotations when available. To keep OpenAPI accurate, add an optional annotations field (even as z.array(z.record(z.unknown())).optional() if you don’t want to fully model UrlCitationAnnotation in Zod).

Also applies to: 4495-4511, 4518-4547, 4652-4652

♻️ Duplicate comments (4)
apps/gateway/src/chat/chat.ts (2)

3818-3833: Streaming + web search still doesn’t bill or log webSearchCost

The streaming pipeline still omits web search usage from cost calculation and logs:

  • All streaming calculateCosts calls (including cached streaming and non‑cached streaming at lines ~2090 and ~3818) do not pass webSearchCount.
  • There is no variable tracking web search usage/count derived from streaming chunks.
  • The streaming insertLog call at ~3902–3951 sets inputCost, outputCost, etc., but no webSearchCost, so logs don’t record web search billing for streaming requests.

As a result, any request that uses native web search in streaming mode will have:

  • token and request costs accounted for, but
  • web search cost always treated as zero and omitted from logs.

If streaming + web search is a supported combination, the streaming path needs to:

  1. Capture webSearchCount from provider streaming output (e.g., via annotations or dedicated streaming metadata), and
  2. Pass that webSearchCount into calculateCosts everywhere it’s called in the streaming codepath, and
  3. Include webSearchCost: costs.webSearchCost in the streaming insertLog call.

If streaming + web search is intentionally unsupported, you should explicitly guard against stream && webSearchTool (e.g., 400 error) and document that constraint to avoid silent under‑billing.

Also applies to: 3902-3951, 2090-2101, 2213-2224


244-266: Fix web_search tool schema and extraction to match WebSearchTool and avoid any/mutation

Two related problems here:

  1. Schema vs WebSearchTool mismatch
  • Zod schema accepts:

    {
      type: "web_search",
      user_location?: {
        city?: string;
        region?: string;
        country?: string;
        timezone?: string;
      };
      search_context_size?: "low" | "medium" | "high";
      max_uses?: number;
    }
  • But WebSearchTool.user_location (from @llmgateway/models) is:

    user_location?: {
      type: "approximate";
      city?: string;
      region?: string;
      country?: string;
    };
  • The extraction code then does:

    const foundTool = tools[webSearchToolIndex] as any;
    webSearchTool = {
      type: "web_search",
      user_location: foundTool.user_location,
      ...
    };

    This bypasses the required type: "approximate" and silently forwards a shape that doesn’t satisfy the WebSearchTool contract (and carries timezone, which downstream code doesn’t know about). prepareRequestBody and provider integrations will see an object that doesn’t match their declared type.

You should either:

  • Align the request schema with WebSearchTool (recommended for a new feature):

    user_location: z
      .object({
        type: z.literal("approximate"),
        city: z.string().optional(),
        region: z.string().optional(),
        country: z.string().optional(),
      })
      .optional(),

    and drop timezone, or

  • Keep accepting the current shape but normalize it when constructing webSearchTool:

    webSearchTool = {
      type: "web_search",
      user_location: foundTool.user_location
        ? {
            type: "approximate",
            city: foundTool.user_location.city,
            region: foundTool.user_location.region,
            country: foundTool.user_location.country,
          }
        : undefined,
      search_context_size: foundTool.search_context_size,
      max_uses: foundTool.max_uses,
    };
  1. any usage and mutating tools
  • findIndex((tool: any) => tool.type === "web_search") introduces any, violating the guideline to avoid any in TS files.
  • tools.splice(webSearchToolIndex, 1) mutates the validated tools array in place, which is brittle and can surprise future callers that reuse validationResult.data.

A safer, type‑aware pattern is:

Suggested refactor
type ToolsInput = z.infer<typeof completionsRequestSchema>["tools"];

let webSearchTool: WebSearchTool | undefined;
let tools = validationResult.data.tools;

if (tools && Array.isArray(tools)) {
	const webSearchTools = tools.filter(
		(tool): tool is Extract<NonNullable<ToolsInput>[number], { type: "web_search" }> =>
			tool.type === "web_search",
	);

	if (webSearchTools.length > 0) {
		const foundTool = webSearchTools[0];

		webSearchTool = {
			type: "web_search",
			user_location: foundTool.user_location
				? {
						type: "approximate",
						city: foundTool.user_location.city,
						region: foundTool.user_location.region,
						country: foundTool.user_location.country,
					}
				: undefined,
			search_context_size: foundTool.search_context_size,
			max_uses: foundTool.max_uses,
		};

		// Keep `tools` as function tools only, without mutating the original array
		tools = tools.filter((tool) => tool.type !== "web_search");
	}
}

This removes any, avoids in‑place mutation, and produces a WebSearchTool that actually matches the interface used downstream.

Also applies to: 553-572

packages/db/migrations/1767485667_faithful_bedlam.sql (1)

1-1: Use numeric (with default) for web_search_cost instead of real to avoid precision issues

Using real for a cost field will introduce floating‑point rounding and diverges from the existing data_storage_cost pattern, which uses numeric for precise billing. This column should follow the same approach.

Consider:

Suggested migration change
ALTER TABLE "log" ADD COLUMN "web_search_cost" numeric DEFAULT '0' NOT NULL;

This keeps all cost columns consistent and avoids subtle billing discrepancies.

apps/docs/content/features/web-search.mdx (1)

53-83: Align annotations and cost-tracking examples with the actual API types and responses

There are a few mismatches between this doc and the implemented types/behavior:

  1. Annotations shape (lines 53–71 and 197–208)
    The examples show flat annotations:

    {
      "type": "url_citation",
      "url": "https://example.com/article",
      "title": "Article Title"
    }

    but UrlCitationAnnotation is defined as a nested structure with a url_citation object (and includes indices):

    {
      "type": "url_citation",
      "url_citation": {
        "url": "https://example.com/article",
        "title": "Article Title",
        "start_index": 0,
        "end_index": 50
      }
    }

    Both annotation examples here should be updated to match the real UrlCitationAnnotation shape.

  2. web_search_cost in usage (lines 221–231)
    The example shows:

    "usage": {
      "prompt_tokens": 15,
      "completion_tokens": 150,
      "total_tokens": 165,
      "cost_usd_total": 0.0315,
      "cost_usd_input": 0.0015,
      "cost_usd_output": 0.015,
      "web_search_cost": 0.03
    }

    but the /v1/chat/completions OpenAPI schema and buildUsageObject currently expose only cost_usd_* fields (no web_search_cost). Either:

    • extend the gateway response to actually include web_search_cost on usage, or
    • adjust this example to describe web search cost as part of the total/request cost (and make clear that per‑search cost is visible via logs, not usage).
  3. user_location shape in tool config examples (lines 93–103, 307–323)
    Examples accept:

    {
      "type": "web_search",
      "user_location": {
        "city": "San Francisco",
        "region": "California",
        "country": "US",
        "timezone": "America/Los_Angeles"
      }
    }

    while the WebSearchTool interface in packages/models/src/types.ts expects:

    user_location?: {
      type: "approximate";
      city?: string;
      region?: string;
      country?: string;
    };

    Once the server schema is aligned with WebSearchTool, these examples should be updated to include type: "approximate" and drop timezone, or the type/interface should be relaxed if timezone is intended to be supported.

Also applies to: 197-208, 221-231

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26c2c52 and 6ec5efd.

📒 Files selected for processing (8)
  • apps/docs/content/features/web-search.mdx
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • packages/db/migrations/1767485667_faithful_bedlam.sql
  • packages/db/migrations/meta/1767485667_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db/src/schema.ts
  • packages/db/migrations/meta/_journal.json
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/gateway/src/chat/chat.ts
  • apps/ui/src/components/dashboard/log-card.tsx
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
apps/{ui,playground}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/{ui,playground}/**/*.{ts,tsx}: Use cookies for user-settings which are not saved in the database to ensure SSR works
Use Next.js App Router with React Server Components for frontend development

Use cookies for user-settings which are not saved in the database to ensure SSR works

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
apps/{ui,playground,docs}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/link for links and next/navigation's router for programmatic navigation

Files:

  • apps/ui/src/components/dashboard/log-card.tsx
🧠 Learnings (1)
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{gateway,api}/src/**/*.ts : Use Zod schemas for validation in Hono services

Applied to files:

  • apps/gateway/src/chat/chat.ts
🧬 Code graph analysis (3)
apps/gateway/src/chat/chat.ts (4)
packages/db/src/types.ts (3)
  • webSearchTool (23-35)
  • tools (60-60)
  • tool (37-37)
packages/models/src/types.ts (1)
  • WebSearchTool (391-413)
packages/db/src/schema.ts (1)
  • provider (631-663)
packages/models/src/models.ts (1)
  • ProviderModelMapping (50-176)
apps/ui/src/components/dashboard/log-card.tsx (1)
packages/db/src/schema.ts (1)
  • log (381-499)
apps/gateway/src/chat/tools/transform-response-to-openai.ts (2)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
packages/db/src/schema.ts (1)
  • message (596-618)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: build / run
  • GitHub Check: lint / run
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: test / run
  • GitHub Check: e2e-shards (5)
  • GitHub Check: generate / run
  • GitHub Check: autofix
🔇 Additional comments (3)
apps/ui/src/components/dashboard/log-card.tsx (1)

12-12: Native Web Search cost display wiring looks correct

The new webSearchCost handling (conditional rendering, Number(...) casting, and separate cost row + Builtin Tools section) is consistent with how other cost fields are treated and correctly avoids rendering when the value is zero/absent. No issues from a typing or UX perspective.

Also applies to: 470-477, 831-851

apps/gateway/src/chat/chat.ts (2)

866-895: Web search capability gating and routing logic look sound

The updated tool gating and routing logic integrates web search cleanly:

  • For non‑auto/custom models, you now:

    • Allow requests when either:
      • the model supports regular tools, or
      • the model supports webSearch and the request is web‑search‑only (no function tools).
    • Explicitly reject web_search usage when supportsWebSearch is false with a clear error message and link to supported models.
  • In auto‑routing and fallback paths, you correctly filter provider mappings to those with webSearch === true when a webSearchTool is present, ensuring you don’t silently route a web‑search request to a non‑search‑capable provider.

  • Skipping automatic reasoning_effort defaults when webSearchTool is set avoids conflicts with providers that may not support minimal reasoning + web search together.

Overall, the selection logic matches the intended behavior and maintains backward compatibility for non‑web‑search calls.

Also applies to: 1202-1208, 1358-1373, 1481-1490, 1641-1648


2481-2483: Provider headers and request body correctly receive web search context

The new wiring:

  • getProviderHeaders(usedProvider, usedToken, { webSearchEnabled: !!webSearchTool }) in both streaming and non‑streaming paths, and
  • Passing webSearchTool into prepareRequestBody,

is consistent with the models layer design and ensures providers that support native web search get:

  • explicit signaling via headers, and
  • the structured WebSearchTool configuration in the body.

No issues here.

Also applies to: 4015-4017, 2365-2387

@@ -1,4 +1,4 @@
import type { ImageObject } from "./types.js";
import type { Annotation, ImageObject } from "./types.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Annotations wiring is good, but web_search_cost is never exposed in usage

The new annotations plumbed through transformResponseToOpenai for Google, Anthropic, Bedrock, OpenAI responses, and the default branch looks correct and is safely guarded by a non‑empty check.

Two follow‑ups to consider:

  1. Expose web search cost on the usage object
    The CostData interface and buildUsageObject currently only emit:

    • cost_usd_total
    • cost_usd_input
    • cost_usd_output
    • cost_usd_cached_input
    • cost_usd_request

    but docs now show a web_search_cost field in usage. Since calculateCosts computes webSearchCost, you’ll need to:

    • extend CostData with webSearchCost: number | null,
    • include it in buildUsageObject as e.g. "web_search_cost": costs.webSearchCost, and
    • pass webSearchCost: costs.webSearchCost from call sites (e.g. chat.ts).

    Without this, clients never see a per‑search cost on the response despite the documentation.

  2. Minor style nit
    Several blocks use { annotations: annotations }. These can be simplified to { annotations }:

    ...(annotations && annotations.length > 0 && { annotations }),

    This is purely cosmetic and optional.

Also applies to: 75-101, 160-161, 296-297, 425-426, 463-466, 601-604

🤖 Prompt for AI Agents
apps/gateway/src/chat/tools/transform-response-to-openai.ts lines 1, 75-101,
160-161, 296-297, 425-426, 463-466, 601-604: the response `usage` never exposes
`web_search_cost` even though `calculateCosts` computes `webSearchCost`; extend
the CostData interface to include webSearchCost: number | null, update
buildUsageObject to add "web_search_cost": costs.webSearchCost, and update call
sites (e.g. chat.ts and other places that construct CostData) to pass
webSearchCost: costs.webSearchCost so clients see per-search cost; optionally
simplify object shorthand for annotations by replacing `{ annotations:
annotations }` with `{ annotations }` where used.

- Add /v1/responses endpoint to mock OpenAI server
- Include webSearchCost in log entries
- Expose cost_usd_web_search in API usage response
- Update docs with correct web search cost field name

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/tools/transform-response-to-openai.ts (1)

259-276: Missing cost_usd_web_search in usage update for existing responses.

When updating existing responses (not creating new ones), the code manually spreads cost fields but omits cost_usd_web_search. This is inconsistent with buildUsageObject which includes it.

🔎 Proposed fix
 					if (costs !== null) {
 						transformedResponse.usage = {
 							...transformedResponse.usage,
 							cost_usd_total: costs.totalCost,
 							cost_usd_input: costs.inputCost,
 							cost_usd_output: costs.outputCost,
 							cost_usd_cached_input: costs.cachedInputCost,
 							cost_usd_request: costs.requestCost,
+							cost_usd_web_search: costs.webSearchCost,
 						};
 					}

Apply this fix to all similar blocks at lines 259-276, 381-398, 479-496, 562-579, and 612-628.

♻️ Duplicate comments (3)
apps/gateway/src/chat/chat.ts (3)

553-572: Avoid any type casts and mutating the input tools array.

  1. Lines 558 and 562 use as any casts which violate the coding guideline to avoid any unless absolutely necessary.
  2. Line 570 uses tools.splice() which mutates the original tools array from the request. This could cause issues if the array is used elsewhere.
🔎 Proposed refactor using type-safe extraction
 	let webSearchTool: WebSearchTool | undefined;
+	let filteredTools = tools;
 	if (tools && Array.isArray(tools)) {
-		const webSearchToolIndex = tools.findIndex(
-			(tool: any) => tool.type === "web_search",
-		);
-		if (webSearchToolIndex !== -1) {
-			// Cast to any to access properties since the schema allows both function and web_search tools
-			const foundTool = tools[webSearchToolIndex] as any;
-			webSearchTool = {
-				type: "web_search",
-				user_location: foundTool.user_location,
-				search_context_size: foundTool.search_context_size,
-				max_uses: foundTool.max_uses,
-			};
-			// Remove the web_search tool from the tools array so it's not sent as a regular tool
-			tools.splice(webSearchToolIndex, 1);
+		const webSearchTools = tools.filter(
+			(tool): tool is { type: "web_search"; user_location?: unknown; search_context_size?: string; max_uses?: number } =>
+				typeof tool === "object" && tool !== null && "type" in tool && tool.type === "web_search"
+		);
+		if (webSearchTools.length > 0) {
+			const foundTool = webSearchTools[0];
+			webSearchTool = {
+				type: "web_search",
+				user_location: foundTool.user_location as WebSearchTool["user_location"],
+				search_context_size: foundTool.search_context_size as WebSearchTool["search_context_size"],
+				max_uses: foundTool.max_uses,
+			};
+			// Create new array without web_search tools (non-mutating)
+			filteredTools = tools.filter(
+				(tool) => !(typeof tool === "object" && tool !== null && "type" in tool && tool.type === "web_search")
+			);
 		}
 	}

Then use filteredTools instead of tools for subsequent operations.


3823-3838: Streaming path does not capture webSearchCount for cost calculation.

The calculateCosts call here doesn't receive webSearchCount as a parameter (compare with the non-streaming call at line 4519 which passes webSearchCount). This means webSearchCost will always be 0 for streaming requests, even when web search is actually used.

The streaming pipeline needs to:

  1. Accumulate webSearchCount from streaming chunks/responses
  2. Pass it to calculateCosts

This was flagged in past reviews but appears unresolved in the streaming path.


253-265: Schema mismatch with WebSearchTool interface for user_location.

The Zod schema accepts { city?, region?, country?, timezone? } but the WebSearchTool interface requires { type: "approximate", city?, region?, country? }. The timezone field is accepted but not part of the interface, and the required type: "approximate" field is missing.

🧹 Nitpick comments (2)
apps/gateway/src/test-utils/mock-openai-server.ts (2)

44-44: Add error handling for JSON parsing.

Both endpoints parse the request body without error handling. If malformed JSON is sent, it will cause an unhandled promise rejection and potentially flaky tests.

🔎 Proposed fix

For the /v1/responses endpoint:

 mockOpenAIServer.post("/v1/responses", async (c) => {
-	const body = await c.req.json();
+	try {
+		const body = await c.req.json();
+	} catch (error) {
+		c.status(400);
+		return c.json({ error: { message: "Invalid JSON in request body", type: "invalid_request_error" } });
+	}

Similarly for the /v1/chat/completions endpoint at line 92.

Also applies to: 92-92


47-50: Consider adding proper types instead of any.

The code uses any type for message parameters, which violates the coding guideline. While this is test utility code, defining a minimal interface would improve type safety and code clarity.

As per coding guidelines, "Never use any or as any unless absolutely necessary in TypeScript code."

🔎 Proposed fix
+interface MockMessage {
+	role: string;
+	content?: string;
+}
+
+interface MockRequestBody {
+	input?: MockMessage[];
+	model?: string;
+}
+
 mockOpenAIServer.post("/v1/responses", async (c) => {
-	const body = await c.req.json();
+	const body = await c.req.json() as MockRequestBody;

 	// Check if this request should trigger an error response
 	const shouldError = body.input?.some?.(
-		(msg: any) =>
+		(msg: MockMessage) =>
 			msg.role === "user" && msg.content?.includes?.("TRIGGER_ERROR"),
 	);

 	// Get the user's message to include in the response
 	const userMessage =
-		body.input?.find?.((msg: any) => msg.role === "user")?.content || "";
+		body.input?.find?.((msg: MockMessage) => msg.role === "user")?.content || "";

Also applies to: 58-59

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ec5efd and da22350.

📒 Files selected for processing (4)
  • apps/docs/content/features/web-search.mdx
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/gateway/src/test-utils/mock-openai-server.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/docs/content/features/web-search.mdx
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
🧠 Learnings (1)
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{gateway,api}/src/**/*.ts : Use Zod schemas for validation in Hono services

Applied to files:

  • apps/gateway/src/chat/chat.ts
🧬 Code graph analysis (2)
apps/gateway/src/chat/chat.ts (4)
packages/db/src/types.ts (3)
  • webSearchTool (23-35)
  • tools (60-60)
  • tool (37-37)
packages/models/src/types.ts (1)
  • WebSearchTool (391-413)
packages/models/src/models.ts (1)
  • ProviderModelMapping (50-176)
packages/models/src/get-provider-headers.ts (1)
  • getProviderHeaders (13-60)
apps/gateway/src/chat/tools/transform-response-to-openai.ts (2)
apps/gateway/src/chat/tools/types.ts (1)
  • Annotation (40-40)
packages/db/src/schema.ts (1)
  • message (596-618)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: build / run
  • GitHub Check: lint / run
  • GitHub Check: generate / run
  • GitHub Check: test / run
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: autofix
🔇 Additional comments (18)
apps/gateway/src/test-utils/mock-openai-server.ts (1)

62-87: LGTM!

The mock Responses API response structure looks correct and appropriately mirrors the expected format. The implementation is clean and well-suited for testing purposes.

apps/gateway/src/chat/chat.ts (12)

46-47: LGTM!

The WebSearchTool type import is correctly added to support the new web search functionality.


866-894: LGTM!

The web search capability validation logic correctly:

  • Checks provider support for web search
  • Allows web-search-only requests when the provider supports it
  • Provides a helpful error message with a link to supported models when web search is unavailable

1202-1208: LGTM!

The auto-routing filter correctly excludes providers that don't support web search when a web search tool is requested.


1359-1374: LGTM!

The low-uptime fallback logic correctly filters to web-search-capable providers when a web search tool is requested.


1480-1490: LGTM!

The provider selection filter correctly applies web search capability requirements.


1642-1648: LGTM!

Correctly skips auto-setting minimal reasoning effort when web search is present to avoid incompatible parameter combinations.


2482-2484: LGTM!

The webSearchEnabled option is correctly passed to getProviderHeaders based on the presence of the web search tool.


2388-2389: LGTM!

The webSearchTool is correctly passed to prepareRequestBody to include web search configuration in provider requests.


4408-4411: LGTM!

The annotations and webSearchCount are correctly extracted from parseProviderResponse to support web search citations and billing.


4504-4520: LGTM!

The webSearchCount is correctly passed to calculateCosts in the non-streaming path, enabling accurate web search billing.


4526-4557: LGTM!

The webSearchCost is included in the costs object and annotations are passed to transformResponseToOpenai, ensuring web search data is properly included in the response.


4658-4663: LGTM!

The webSearchCost is consistently included in all log entries, with appropriate null values for error cases.

apps/gateway/src/chat/tools/transform-response-to-openai.ts (5)

1-11: LGTM!

The Annotation type import and webSearchCost field in CostData interface are correctly added to support web search functionality.


41-48: LGTM!

The cost_usd_web_search field is correctly added to the usage object when costs are available, allowing clients to see per-search costs.


75-78: LGTM!

The annotations parameter is correctly added with proper typing and a default value for backward compatibility.


192-278: No action needed - annotations correctly omitted for these providers.

The inference.net, together.ai, and groq providers do not have web search capability in their model definitions, so excluding annotations from their response handlers is correct and intentional.


320-401: No action required. Alibaba models do not support web search based on the model configuration, so annotations are not needed for this provider.

Likely an incorrect or invalid review comment.

@steebchen
steebchen added this pull request to the merge queue Jan 4, 2026
Merged via the queue into main with commit fae6d0f Jan 4, 2026
13 of 14 checks passed
@steebchen
steebchen deleted the terragon/add-native-websearch-support-hpf6au branch January 4, 2026 01:59
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