Repository navigation
Conversation
…ch it would never bill
chat_params is a transparent proxy (permit!), so a request could carry OpenRouter's `plugins: [{id: "web"}]` or
`web_search_options` and get web search run on LevelCode's OpenRouter account. Metering prices tokens at catalog
rates (Levelcode.cost_micros) and never reads OpenRouter's own cost, so that search was never billed. Both
fields are now dropped (AiController::UNBILLED_FIELDS); the editor's web search goes through AiWebController,
which bills it.
Spec: a chat request with both fields reaches the upstream without them.
The editor's agent gets web_search and web_fetch (levelcode docs/WEB-SEARCH.md). Two endpoints:
- POST /api/levelcode/v1/ai/web/search {queries: 1-4, count?, recency?}. Billed like a model turn: the wallet is
checked (a throttle plan over budget stops too, since a search costs real money per query), every query's cost
is reserved, the queries run at the same time (Levelcode::WebSearch.run, 6 s each), only those that answered
are settled, one ledger row (model "web-search"), and the answer carries the chat stream's `levelcode` credits
object. An exception releases the hold.
- POST /api/levelcode/v1/ai/web/fetch {url}: the browser edition's page reader (a tab cannot read another site).
Levelcode::WebFetch refuses a name if ANY of its addresses is private, loopback, link-local or otherwise
local, connects to the address it checked (Net::HTTP#ipaddr=) so a second DNS answer cannot swap one in,
re-checks every redirect, reads text only, at most 2 MB. Free, behind sign-in, the ai:agent scope and the
per-minute cap.
The search provider is configured: LEVELCODE_WEB_SEARCH_PROVIDER (default "brave") with BRAVE_SEARCH_API_KEY or
credentials[:brave_search_api_key]; LEVELCODE_WEB_SEARCH_COST_MICROS is the price per query in COST micro-$
(default 5,000). With no key the search endpoint answers 503 and the editor tells the model search is not
available, so this ships safely before a key is set.
Specs: ai_web_spec.rb (billing, partial failures, no key, scope, caps, the fetch errors), web_search_spec.rb
(parallel run and timeouts, the Brave request and mapping), web_fetch_spec.rb (the address checks, redirects,
charsets, and a real TCP server for reading and the 2 MB cap). 9 one-edit mutations each fail a spec.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gateway side of web search for the editor's agent (levelcode PR: levelcodeai/levelcode#119, design note
docs/WEB-SEARCH.mdthere), and a billing fix found while building it.1. Billing fix: OpenRouter's web-search fields are dropped (own commit)
chat_paramsis a transparent proxy (permit!), so a chat request could carry OpenRouter'splugins: [{id: "web"}]orweb_search_optionsand get web search run on LevelCode's OpenRouter account, unbilled: metering prices tokens at catalog rates and never reads OpenRouter's cost. Both fields are now dropped (AiController::UNBILLED_FIELDS). This hole is open in production until this ships.2. Web search and page fetch
POST /api/levelcode/v1/ai/web/search{queries: 1-4, count?, recency?}model: "web-search"), and the chat stream'slevelcodecredits object in the answer. An exception releases the hold.POST /api/levelcode/v1/ai/web/fetch{url}Net::HTTP#ipaddr=, so DNS rebinding cannot swap one in), re-checks every redirect, text only, 2 MB cap. Free, behind sign-in,ai:agentand the per-minute cap.Configuration (needed before search works in production):
LEVELCODE_WEB_SEARCH_PROVIDER: defaultbrave.BRAVE_SEARCH_API_KEYorcredentials[:brave_search_api_key].LEVELCODE_WEB_SEARCH_COST_MICROS: the price per query in COST micro-$, default 5,000 ($0.005); retail follows the token margin.With no key, search answers 503 and the editor tells the model search is not available, so this is safe to merge before a key exists.
Tested
spec/requests/api/levelcode/v1/ai_web_spec.rb: billing, partial failures, no key, scope, caps, and every fetch error.spec/services/levelcode/web_search_spec.rb: the parallel run, timeouts, and the Brave request and mapping.spec/services/levelcode/web_fetch_spec.rb: the address checks, redirects, charsets, and a real TCP server for reading and the 2 MB cap.ai_spec.rb.developin this environment (static_spec×2,levelcode/web_spec,admin_spec).Not run
A real Brave key against the live API; the endpoints from a real browser tab.