Documentation
¶
Overview ¶
Package lanestub is a fake router with lanes of a scripted speed: the shared measuring instrument for everything in `internal/lane`.
── WHY ONE INSTRUMENT AND NOT FIVE ─────────────────────────────────────────
Five lanes of work are built against this design at once — the sheet and the belief, the choice, the watch, the surface, the ledger and the bench — and every one of them needs the same three things to exist before it can be tested at all: a sheet with plausible percentiles, a stream that starts when it says it will and writes at the rate it says it will, and a way to see what went out on the wire. Written five times those would be five slightly different routers, and the first disagreement between them would look like a bug in the code under test.
So it is written once, here, and it is deliberately a LITTLE more than a test fixture: it counts requests and cancels per lane, records the routing preference of every ask, and can run on a clock that costs nothing, so a scenario scripted in seconds finishes in microseconds.
── WHAT IT SERVES ──────────────────────────────────────────────────────────
GET /api/v1/models/{author}/{slug}/endpoints the sheet, in the router's
own field names
GET /api/v1/models a minimal catalog
POST /api/v1/chat/completions a streamed answer from
whichever lane the
preference selected
Server.URL is what a client's BaseURL is set to; it already carries the `/api/v1` the router's own URL carries.
── TWO THINGS TO KNOW BEFORE WRITING A TEST ────────────────────────────────
FIRST, THIS STUB IS A ROUTER BECAUSE OF WHAT IT ANSWERS AND NOT BECAUSE OF WHERE IT LIVES, and a test writes nothing to make that true. It serves an endpoints page, and a base that serves one carries a routing preference by the router's own contract (issue #419 for the sheet, #433 for the preference), so a client pointed at the plain Server.URL gets a sheet, a frontier, and `provider.order` or `provider.only` on the wire — which is what [Server.Preference] is here to read back.
That was not true until those two landed. The transport decided both questions from the base URL for `openrouter.ai` or from a model spelled `openrouter/…`, a loopback address is neither, and so every test that wanted to see a preference on the wire had to dress itself up as the shipped router. This stub carried that costume — a second mount under a path spelled `/openrouter.ai`, handed out by a second accessor — and it is gone (#426): there is one address now, and if a preference does not arrive at it, that is the product answering.
SECOND, the fast clock is ONE TIMELINE. Its Wait returns at once and advances a shared offset, which is exactly right for a scripted single stream and meaningless for two streams in flight at the same time. A hedge test — where the whole point is that two requests overlap and one is cancelled — uses the real clock with millisecond-scale profiles, which still runs a full scenario in well under a second.
Index ¶
- Constants
- type Ask
- type Clock
- type Fast
- type Lane
- type Profile
- type Server
- func (s *Server) Alias(alias, target string)
- func (s *Server) Anonymous()
- func (s *Server) Asks() []Ask
- func (s *Server) Cancels(lane string) int
- func (s *Server) Close()
- func (s *Server) Model(model string, lanes ...Lane)
- func (s *Server) PacesTheKey(comeback time.Duration)
- func (s *Server) RefusesEverything()
- func (s *Server) RefusesPreference()
- func (s *Server) RefusesWith(status int, message, code string)
- func (s *Server) Requests(lane string) int
- func (s *Server) Served() []string
- func (s *Server) SetClock(clock Clock)
- func (s *Server) Sheetless()
- func (s *Server) Sheets(model string) int
- func (s *Server) URL() string
Constants ¶
const DefaultTokens = 24
DefaultTokens is how long an answer is when a profile does not say.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Ask ¶
type Ask struct {
Model string
Stream bool
Tools bool
MaxTokens int
PromptTokens int
Sort string
Order []string
Only []string
Ignore []string
// NoFallbacks is `allow_fallbacks: false` on the wire: the router may not
// look past the machines the request named.
NoFallbacks bool
// MaxPrice is the router's price ceiling in dollars per million tokens.
MaxPrice *struct {
Prompt float64
Completion float64
}
// At is when the request arrived, on the wall clock and never the scripted
// one: it is what a test measures a GAP with — how long a run sat between
// two requests — and a gap measured on a clock the stub itself advances
// would be a measurement of the script rather than of the code under it.
At time.Time
}
Ask is one request as the wire spelled it, kept so a test can assert on the preference rather than on the lane that happened to answer.
type Clock ¶
type Clock interface {
// Now is the moment.
Now() time.Time
// Wait passes d, or gives up when ctx is done. It reports whether the wait
// finished: false is a client that went away, which is what a hedge does to
// its loser and the one thing this stub counts most carefully.
Wait(ctx context.Context, d time.Duration) bool
}
Clock is how the stub spends time. It is a seam so that a scenario written in seconds does not have to be waited through.
type Fast ¶
type Fast struct {
// contains filtered or unexported fields
}
Fast is a clock that costs nothing: waiting advances it and returns at once.
It is one timeline shared by every request in flight — see the note at the top of this file about which tests may use it.
func NewFast ¶
NewFast starts a fast clock at a stated moment. Starting it somewhere fixed rather than at time.Now is what makes a scenario's timestamps comparable between runs.
type Lane ¶
type Lane struct {
Name string
Profile
// SheetOnly is a lane the endpoints page PUBLISHES and the completion
// endpoint will not serve.
//
// IT IS THE ONE DISAGREEMENT THIS STUB COULD NOT STAGE, and it is the
// disagreement issue #266 was measured on. A real router publishes a
// model's endpoints page under one id and resolves the completion under
// another, so the machines on the sheet and the machines on the wire are
// two sets that overlap rather than one set read twice: on 2026-09-01
// three of five tool-capable lanes on the sheet were not in the router's
// serving set, a pin was chosen from the sheet, and every request carrying
// it came back `…but your request's provider.only preference permits only:
// coreweave`. Serving the sheet and the completions from one slice made
// that state unreachable, so the defect had no replication a stranger
// could run.
//
// A SheetOnly lane therefore appears in [Lane.row] exactly like any other
// and is invisible to [pick]. A request that merely RANKS it (`order`)
// lands on the next lane and never notices; a request that DEMANDS it
// (`only`) gets the router's real refusal, in the router's own words.
SheetOnly bool
// AccountExcluded is a lane the router SERVES this model from and that the
// ACCOUNT'S OWN SETTINGS take away — the paid-model-training privacy switch
// on openrouter.ai/settings/privacy, measured against the owner's account on
// 2026-09-10 for DeepSeek, Fireworks and Wafer.
//
// IT IS A FACT ABOUT THE ACCOUNT AND THE MACHINE, NOT ABOUT ONE MODEL. The
// router drops the lane from every model's set before it asks anybody, so a
// request that merely ranks it lands on the next lane and never notices,
// while a request whose set it was the whole of — a demand, or a veto list
// that left only it — gets the router's real refusal, body and metadata
// both ([accountRefusal]).
AccountExcluded bool
// Unvetoable is a machine the router GOES ON SERVING however loudly the
// request vetoes it: `provider.ignore` names it and the next body lands
// there again anyway.
//
// IT IS THE ONE STATE A CALL CANNOT ROUTE ITS WAY OUT OF, and it is what the
// live router did on 2026-09-11 14:39: eight consecutive sends on
// deepseek/deepseek-v4.1-flash came back `(via Wafer: … temporarily
// rate-limited upstream)` while six other machines on the same model were
// answering. The name in a relayed refusal is the UPSTREAM's, and an upstream
// label is not always a name the router will route around — so a veto written
// from it can change nothing at all, and the call has to be able to find that
// out rather than spend its whole deadline discovering it eight times.
//
// A test stages it to assert what a call does when its veto does not take.
// Nothing else in this stub cares: [pick] applies every other filter exactly
// as before.
Unvetoable bool
}
Lane is one named machine behind a model.
type Profile ¶
type Profile struct {
// TTFT is the wait before the first token, and Rate how fast tokens come
// after it. Tokens is how long the answer is, defaulting to
// [DefaultTokens] and capped by the request's own max_tokens.
TTFT time.Duration
Rate float64
Tokens int
// Reasoning is how many THINKING deltas this lane writes before its first
// visible word. They are billed, streamed tokens like any other — the
// endpoint IS writing — but nothing a person can read appears while they
// run, which is the whole reason the watch counts them apart from the
// answer ([internal/lane.Watch.Token]).
Reasoning int
// Fenced writes that run of thought on the CONTENT channel, wrapped in
// `<think>` … `</think>`, instead of on the reasoning field.
//
// IT IS A REAL SHAPE AND NOT A CURIOSITY. Several gateways hand back a
// model's working inside the answer channel rather than stripping it, which
// is why `internal/provider`'s answer.go carves it back out — and the
// carving has to reach the waiting policy too, or a model that fences its
// thoughts looks to the controller like a model writing an answer and its
// silence clock never runs.
Fenced bool
// StallAfter and StallFor stage a lane that goes quiet mid-answer:
// after StallAfter deltas — counting the reasoning run first — nothing is
// written for StallFor. A zero StallAfter stalls nothing.
StallAfter int
StallFor time.Duration
// StallUntil holds the stall open on a SIGNAL instead of a duration: after
// StallAfter deltas nothing is written until this channel closes or the
// request is cancelled. It overrides StallFor when set, and its zero value
// is today's behaviour, so nothing that only names a duration changes.
//
// IT EXISTS BECAUSE A SCRIPTED ARM MUST ORDER ITSELF BY A SIGNAL AND NEVER
// BY ELAPSED TIME. A hedge test that wants the rescue to take the answer is
// asserting a rule — the first arm to finish cleanly commits — and a stalled
// arm timed to resume a little after the rescue lands asserts nothing but
// the slack between two wall-clock figures, which a starved machine eats.
// Held on a channel the test never closes in time, the primary CANNOT
// finish first, and the assertion is about the rule again.
StallUntil <-chan struct{}
// FirstTokenUntil is THE SAME RULE FOR THE FIRST TOKEN, which StallUntil
// cannot hold: it only takes hold after StallAfter deltas, so a lane whose
// very first word must land after something else happens has no way to say
// so. With this set, the lane waits out its TTFT as scripted and then waits
// for the channel to close as well — the arm answers no sooner than both.
//
// A primary scripted to answer "a little after" a bound the caller enforces
// is the shape this is for. Sixty milliseconds against a ceiling of fifty
// rests on ten milliseconds of wall clock, which a starved machine eats:
// the caller's timer fires late, the first token arrives first, and the test
// asserts the slack between two figures instead of the rule it was written
// for. Held on a signal the caller itself raises, the order cannot invert.
//
// It holds the whole answer on the unstreamed path, where the first token
// and the last arrive together — a knob that silently did nothing on one of
// the two shapes would be a fixture lying about what it staged.
FirstTokenUntil <-chan struct{}
// FailWith is an HTTP status this lane answers with instead of streaming.
// Zero serves normally.
FailWith int
// TearAfter is how many visible deltas this lane writes before the
// connection goes away underneath the answer: no finish frame, no usage, no
// sentinel, the body simply stops. Zero tears nothing.
//
// IT IS THE ONE ENDING THE OTHER KNOBS COULD NOT STAGE. A refusal is a
// status, a stall is silence the guard eventually acts on, and a cancel is
// this process's own decision — but a reply that was arriving and then was
// not is none of those, and it is 1,884 of 1,887 in-stream failures in the
// 2026-09-10 census. A scenario about what a person is shown when a call
// stops halfway has to be able to say it.
TearAfter int
// Answer, when set, is the text this lane streams instead of the
// generated t0 t1 t2 tokens. It exists so a test can stage a
// degraded winner — the F20 mojibake — without inventing a second
// router. Tokens still bills the length.
Answer string
// Heartbeats emits the router's own comment lines before the first token,
// which is the free signal that tells a dead path apart from a slow lane.
Heartbeats bool
// Paced stages a machine whose pool is full: it ACCEPTS the request — the
// router answers 200 and holds the stream open with its comment lines for
// PacedAfter — and then delivers the upstream's 429 INSIDE that 200, naming
// itself, which is the exact shape the live router produced on 2026-09-10
// (`status 200 … API error (429): Provider returned error (via Io Net)`
// after seventeen seconds of silence).
//
// AND THE ROUTER FALLS BACK PAST IT WHEN IT MAY. A request that demands
// nothing — no `only`, fallbacks not forbidden — is the router's free choice,
// and the router answers an upstream's full queue by asking the next
// machine, which is why a relaxed request lands where a demanded one died.
// The paced lane is still counted as asked; the answer comes from the next
// allowed lane. Only a request with nowhere else to go gets the 429.
//
// A ZERO PacedAfter IS THE OTHER TRANSPORT: the router refuses at once with
// an HTTP 429 naming the pool, before any stream opens — which is how the
// same full queue answered the live router's bare probe on 2026-09-10.
Paced bool
PacedAfter time.Duration
// PacedFor is the comeback time this full pool NAMES, in its `Retry-After`
// header, the way a real one does. Zero is a pool that refuses and says
// nothing about when to come back, which is the commoner shape.
//
// IT IS THE ONLY THING THAT MAKES A SECOND SEND TO THE SAME MACHINE LEGAL
// (docs/design/recovery/DESIGN.md §3), so it is what a scenario about
// repeating a request has to be able to state.
PacedFor time.Duration
// Tools, Quant, Context, MaxOut, Uptime and Caches are the gate facts the
// sheet publishes. Uptime is a percentage.
Tools bool
Quant string
Context int
MaxOut int
Uptime float64
Caches bool
// Status is the router's own health word for the lane: zero is healthy.
Status int
// PriceIn, PriceOut and PriceCache are dollars per token, the unit the
// router publishes.
PriceIn float64
PriceOut float64
PriceCache float64
// TTFTms and Rates are the sheet's four percentiles — p50, p75, p90, p99 —
// in milliseconds and tokens per second. LEAVE THEM ZERO and the sheet
// describes a lane that behaves exactly as scripted, with a plausible
// spread around it; set them to stage a sheet that is WRONG about a lane,
// which is the case worth most of the tests here.
TTFTms [4]float64
Rates [4]float64
}
Profile is how one lane behaves and what the sheet says about it.
The stream half and the sheet half are separate on purpose: a lane that the sheet calls quick and that then takes four seconds is the single most interesting thing this package can stage, and it is how the belief gets tested against the prior it started from.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the fake router.
func New ¶
New starts a router serving one model over the given lanes, in the order they are written: that order is what a request with no preference gets.
func (*Server) Alias ¶
Alias makes the router ANSWER for a floating id without PUBLISHING one.
That asymmetry is the whole point and it is the router's real behaviour: a completion sent with `~deepseek/deepseek-v4-flash-latest` is resolved on OpenRouter's side and served by the machines of whatever it currently points at, while `/models/~deepseek/deepseek-v4-flash-latest/endpoints` is a 404 — the endpoints page exists only under the concrete id. A build that keys its beliefs on the spelling it sent therefore holds a ledger about a model no sheet will ever describe, which is exactly what a test needs to be able to stage.
Both spellings are accepted, with the "~" and without, because the alias marker is a prefix on a name rather than part of one.
func (*Server) Anonymous ¶
func (s *Server) Anonymous()
Anonymous makes this router answer without naming the lane that served, the way a plain OpenAI-compatible endpoint does. The lanes still take their turns and Server.Served still records who answered — what changes is only what the WIRE says, which is what the build under test can see. It is set before any request is made.
func (*Server) Cancels ¶
Cancels is how many of lane's streams the client walked away from. It is the figure a hedge is judged on: the loser must be cancelled, because cancelling is what stops the bill.
func (*Server) PacesTheKey ¶
PacesTheKey makes this router answer every completion with the account's own ceiling — 429, `retry_after` in the body, no pool named — while still publishing the model's whole pool on its endpoints page.
IT IS THE SHAPE THE VETO CANNOT ANSWER. Every machine behind the model is behind the same ceiling, so there is nothing to put in `provider.ignore` and nothing a relaxed shape gets under; the only moves are the comeback it names and then another model. It is set before any request is made.
func (*Server) RefusesEverything ¶
func (s *Server) RefusesEverything()
RefusesEverything makes this base answer 400 to every request, in a sentence that is about nothing in particular. It is what a base whose 400 was never about the routing preference looks like from outside, and it is set before any request is made.
func (*Server) RefusesPreference ¶
func (s *Server) RefusesPreference()
RefusesPreference makes this base answer 400 to any request that carries a `provider` object, in OpenAI's own words for an argument it does not know. The request is still recorded in Server.Asks before the refusal, so a test can see both the ask that was refused and the widened one that followed. It is set before any request is made.
func (*Server) RefusesWith ¶
RefusesWith makes this base answer every completion with one named refusal: this status, this sentence, and this `code` in the error envelope. An empty code leaves the envelope's own default, which is the status.
IT IS FOR THE REFUSALS A LANE CANNOT STAGE. A full pool, an account exclusion and an emptied set are all facts about MACHINES, and this stub stages them by describing machines. A request that did not fit the window is a fact about the REQUEST — no lane is implicated, no lane can be described to produce it — so it is named directly. It is set before any request is made.
func (*Server) Sheetless ¶
func (s *Server) Sheetless()
Sheetless makes this router publish no endpoints route at all: every ask for a page is the 404 a base with no such route answers — an HTML not-found page ([routelessBody]) — while the completions route keeps serving. It is set before any request is made.
THE STUB'S TWO 404S ARE THE LIVE ROUTER'S TWO 404S, because the transport tells them apart by their bodies and a stub that answered the same body for both would be testing nothing (measured 2026-09-02):
GET /api/v1/models/nonexistent/model-xyz/endpoints
→ 404, {"error":{"message":"Not Found","code":404}} an unknown model
GET /api/v1/nonexistent-route/x/endpoints
→ 404, <!DOCTYPE html>…<title>Not Found | OpenRouter</title>… no route
The first is what [serveSheet] already answers for a model this stub does not publish, through [writeError]; the second is what this mode answers for every model. It exists so that a test can stage the base issue #373 is about — one that answers completions and has no sheet — without a second server: the sheet must learn that from the answer and not from the hostname, and this is the answer.