vmodel

package
v0.260806.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MPL-2.0 Imports: 5 Imported by: 0

README

vmodel

Virtual models — synthetic, protocol-compliant provider implementations that power the production /virtual/v1/* endpoint (used for onboarding, demos, and dry-runs without configuring a real upstream provider). They are wired into the virtual server at vmodel/virtualserver and shipped in the production binary via server.UseVirtualModelEndpoints.

The same primitives are reused as an in-process LLM substitute by test packages that need wire-format-correct fixtures (see internal/protocoltest).

Design docs. This README is the usage guide. Architecture rationale and per-topic design notes are indexed in .design/vmodel.md.

Layout

vmodel/
├── interface.go        // base VirtualModel interface (provider-neutral)
├── types.go            // VirtualModelType, Model, ToolCallConfig, helpers
├── registry.go         // GenericRegistry[T] — shared thread-safe registry
├── base_mock.go        // BaseMockModel — shared identity/metadata methods
├── stream.go           // ResolveChunkDelay, EmitChunks — shared stream helpers
├── defaults_shared.go  // SharedDefaultMocks() + ExtendedErrorSpecs() — shared specs
├── error_injection.go  // ErrorInjection, ErrorInjectingModel, EmitGate
├── README.md
├── anthropic/          // Anthropic-protocol models + Registry alias
├── openai/             // OpenAI Chat-protocol models + Registry alias
├── virtualserver/      // Production Gin HTTP handler + Service wiring
└── benchmark/          // Load-test client + local server factory
    └── examples/       // Runnable server/client examples

The root package contains provider-neutral primitives shared by all sub-packages. Concrete models, protocol-specific request/response types, stream events, and the Registry alias live in the anthropic and openai sub-packages. The two sub-packages do not import each other.

Positioning & registration discipline

vmodel is a business-first package: it ships in production to back the public /virtual/v1/* endpoint, and it is the single source of truth for synthetic, protocol-compliant model behavior across the codebase. Test packages are secondary consumers that reuse the same primitives.

Role Surface Consumer
Primary (production) /virtual/v1/messages, /virtual/v1/chat/completions, /virtual/openai/v1/responses internal/server mounts virtualserver.Service for end-user demos / onboarding / dry-runs
Secondary (tests) In-process GenericRegistry[T] internal/protocoltest.Scenario, cli/harness --mock

Registration discipline. Anything added to anthropic.RegisterDefaults or openai.RegisterDefaults is visible to end users of the production endpoint. Therefore the defaults registry must contain only user-facing demo entries — protocol-compliant, named clearly, useful for onboarding and dry-runs (echo-model, ask-user-question, virtual-claude-3, virtual-gpt-4, the compact transforms, etc.).

Test-only fixtures (protocol corner cases, wire-format edge cases, scenario-specific stubs) must not be added to RegisterDefaults. Tests that need bespoke synthetic models should construct their own GenericRegistry[T] (the way protocoltest does for Scenario) and register fixtures there — keeping the production defaults clean.

Opt-in fixture sets. Three named registration helpers ship alongside RegisterDefaults for callers that want pre-built fixtures without polluting the production endpoint:

Helper What it registers When to call
RegisterStreamTestMocks(reg) virtual-stream-test, virtual-stream-test-tool — advertise the full usage shape (prompt / completion / cached / cache-creation / reasoning) Streaming-converter tests that need deterministic usage emission
RegisterErrorMocks(reg) virtual-fail-precontent-{429,500}, virtual-fail-midstream-{close,event} — always fail per the configured stage Failover / resilience tests that need a deterministic broken upstream
RegisterExtendedErrorMocks(reg) virtual-fail-auth-{401,403}, virtual-fail-upstream-{502,503,504}, virtual-fail-invalid-400, etc. — comprehensive error scenarios Advanced testing with full error catalog
RegisterAllErrorMocks(reg) Combines both basic and extended error models Convenience helper for full error catalog

Both helpers live in each per-protocol sub-package (anthropic.Register… and openai.Register…), source their specs from the root defaults_shared.go, and are kept out of RegisterDefaults so production registries stay clean.

Design

GenericRegistry

GenericRegistry[T VirtualModel] is a thread-safe, generic registry that underpins all per-protocol registries in this module:

// anthropic.Registry and openai.Registry are type aliases:
type Registry = virtualmodel.GenericRegistry[VirtualModel]

Any package that needs to store objects satisfying virtualmodel.VirtualModel can instantiate its own GenericRegistry directly — protocoltest.Scenario does this for test scenarios.

One registry per protocol
anthropicReg := anthropic.NewRegistry()
anthropic.RegisterDefaults(anthropicReg)

openaiReg := openai.NewRegistry()
openai.RegisterDefaults(openaiReg)

A model registered in anthropic.Registry is callable only via /virtual/v1/messages; a model in openai.Registry is callable via /virtual/v1/chat/completions and /virtual/openai/v1/responses (the Responses surface renders the same HandleOpenAIChat(+Stream) output in Responses wire format — see virtualserver/handler_responses.go). The registry is the protocol context — there is no runtime Protocols() declaration, no byProtocol index, and no protocol type assertions in lookup paths.

When a client requests a model that does not exist in the registry for the endpoint it called, the handler returns 404 Not Found (not 501). A model is either registered for that protocol or it isn't.

The same ID can exist in both registries simultaneously, holding two independent concrete instances. echo-model, ask-user-question, ask-confirmation, and web-search-example are registered in both defaults exactly this way. Each instance only implements its own protocol's interface, so there is no possibility of a model "lying" about which protocols it speaks.

Anthropic v1 vs. beta

The real Anthropic API distinguishes MessageNewParams (v1) and BetaMessageNewParams (beta) on the wire, gated by ?beta=true. The virtual server accepts both and canonicalizes to the beta superset at the HTTP boundary (virtualserver/handler.go). Vmodels see exactly one request type — *protocol.AnthropicBetaMessagesRequest — so the protocol-version distinction does not leak into the VirtualModel interface.

Interfaces

Base (interface.go)
type VirtualModel interface {
    GetID() string
    GetName() string
    GetDescription() string
    GetType() VirtualModelType
    SimulatedDelay() time.Duration
    ToModel() Model
}
Anthropic sub-interface (anthropic/interface.go)
type VirtualModel interface {
    virtualmodel.VirtualModel
    HandleAnthropic(req *protocol.AnthropicBetaMessagesRequest) (VModelResponse, error)
    HandleAnthropicStream(req *protocol.AnthropicBetaMessagesRequest, emit func(any)) error
}
OpenAI sub-interface (openai/interface.go)
type VirtualModel interface {
    virtualmodel.VirtualModel
    HandleOpenAIChat(req *protocol.OpenAIChatCompletionRequest) (VModelResponse, error)
    HandleOpenAIChatStream(req *protocol.OpenAIChatCompletionRequest, emit func(any)) error
}

Shared root-package primitives

BaseMockModel

BaseMockModel implements the six identity/metadata methods of the base VirtualModel interface (GetID, GetName, GetDescription, GetType, SimulatedDelay, ToModel). Protocol-specific mock types embed it and only add their Handle* methods:

type MockModel struct {
    virtualmodel.BaseMockModel
    cfg *MockModelConfig
}
ResolveChunkDelay / EmitChunks

ResolveChunkDelay(totalDelay, chunkCount) distributes a model's simulated latency evenly across stream chunks. EmitChunks is the shared inner loop — it calls the emit closure once per chunk with the appropriate sleep in between. Both the anthropic and openai DefaultStream helpers use these.

SharedDefaultMocks

SharedDefaultMocks() returns the specs for the four mocks that are registered in both default registries. Each protocol's RegisterDefaults calls this and wraps each spec in its own MockModel:

for _, spec := range virtualmodel.SharedDefaultMocks() {
    _ = reg.Register(NewMockModel(&MockModelConfig{
        ID: spec.ID, Name: spec.Name, Content: spec.Content,
        ToolCall: spec.ToolCall, Delay: spec.Delay,
    }))
}

Model categories

VirtualModelType (declared in types.go) tags every vmodel so the extension UI and registry consumers can group by behavior:

Type Meaning Examples
static Returns a fixed text response virtual-claude-3, virtual-gpt-4, echo-model
tool Returns a tool_use / tool_calls block ask-user-question, ask-confirmation, web-search-example
proxy Applies a transform chain (no upstream call; same model also runs in real proxy paths) compact-round-only, claude-code-compact
sequence Walks a configured program of per-request outcomes (status + content) to simulate a flaky upstream virtual-sequence-429

Error models

Error models are virtual models that always fail with a configurable error. They enable testing of failover, resilience, and error handling without requiring a real upstream provider or ad-hoc test servers.

Error model categories

Error models are categorized by ErrorCategory (defined in error_injection.go):

Category Description HTTP Status Retryable
rate_limit Rate limiting (429) 429 Yes
upstream Upstream server errors (5xx) 500, 502, 503, 504 Yes
timeout Request timeout 504 Yes (pre-content), No (mid-stream)
overloaded Service overloaded 503 Yes
invalid Invalid request 400 No
auth Authentication/authorization 401, 403 No
network Network connectivity Various Yes (pre-content), No (mid-stream)
malformed Malformed response Various No
Error stages

Error models operate at two distinct stages:

  1. Pre-content (ErrorStagePreContent): Failure before any response bytes are written

    • Handler returns HTTP error status immediately
    • No streaming starts
    • Failover SHOULD retry (firstChunkGate stays buffered)
  2. Mid-stream (ErrorStageMidStream): Failure after streaming has started

    • Handler emits some content events, then fails
    • Two modes: connection close or error event
    • Failover MUST NOT retry (gate committed, bytes on the wire)
Basic error models (4)

Registered in SharedDefaultMocks() (always available):

Model ID Stage Status Retryable Use case
virtual-fail-429 Pre-content 429 Yes Rate limit testing
virtual-fail-500 Pre-content 500 Yes Upstream error testing
virtual-fail-midstream-close Mid-stream No TCP disconnect testing
virtual-fail-midstream-event Mid-stream No SSE error event testing
Extended error models (5)

Registered by RegisterExtendedErrorMocks():

Model ID Stage Status Category Use case
virtual-fail-auth-401 Pre-content 401 auth Authentication failure
virtual-fail-502 Pre-content 502 upstream Bad gateway
virtual-fail-503 Pre-content 503 overloaded Service unavailable
virtual-fail-400 Pre-content 400 invalid Invalid request
virtual-fail-timeout Mid-stream timeout Mid-stream timeout
Using error models
// In tests - basic error models are already available via SharedDefaultMocks
reg := anthropic.NewRegistry()
anthropic.RegisterDefaults(reg) // Includes virtual-fail-429, virtual-fail-500, etc.

// For extended error models (opt-in)
anthropic.RegisterExtendedErrorMocks(reg)

// In production (for demo/onboarding)
service := virtualserver.NewService(anthropicReg, openaiReg)
// Basic error models ARE included by default
// Extended error models require RegisterExtendedErrorMocks
Error model naming convention

Error model IDs follow the pattern:

virtual-fail-{status}-{variant}
  • status: HTTP status code (429, 500, 401, 502, 503, 400) or type (midstream-close, midstream-event, timeout)
  • variant: Optional disambiguator (close, event, timeout, auth, etc.)

The stage (pre-content vs mid-stream) is implicit from the ErrorStage field, not the ID.

Examples:

  • virtual-fail-429 - Rate limit (pre-content)
  • virtual-fail-midstream-close - Mid-stream connection close
  • virtual-fail-auth-401 - Authentication failure
  • virtual-fail-502 - Bad gateway error

Default model allocation

Model ID Anthropic registry OpenAI registry
virtual-claude-3 X
virtual-gpt-4 X
echo-model X X
ask-user-question X X
ask-confirmation X X
web-search-example X X
compact-round-only X
compact-round-files X
claude-code-compact X
claude-code-strategy X

Compact transforms are Anthropic-only because they operate on the Anthropic message shape. They could be ported to OpenAI by adding an OpenAI-side TransformModel, but no production use case currently calls for it.

The four shared mocks (echo-model, ask-user-question, ask-confirmation, web-search-example) are defined once in defaults_shared.go and registered by both anthropic.RegisterDefaults and openai.RegisterDefaults.

Adding a model

Single protocol
reg := service.GetAnthropicRegistry()
_ = reg.Register(anthropic.NewMockModel(&anthropic.MockModelConfig{
    ID:      "my-mock",
    Name:    "My Mock",
    Content: "fixed reply",
    Delay:   50 * time.Millisecond,
}))
Both protocols (same logical model)

Register two separate concrete instances under the same ID — one per registry. They can share configuration but not state:

cfg := myConfig{...}
_ = anthropicReg.Register(anthropic.NewMockModel(&anthropic.MockModelConfig{
    ID: "my-dual", Content: cfg.Reply,
}))
_ = openaiReg.Register(openai.NewMockModel(&openai.MockModelConfig{
    ID: "my-dual", Content: cfg.Reply,
}))

For richer dual-protocol models with shared logic, factor the logic into a private core type and embed it in two thin wrappers — one in each sub-package — that implement the respective Handle* methods.

Custom (non-mock) model

Implement the relevant sub-interface directly. The sub-package must own the type so it cannot accidentally implement the other protocol's interface.

Streaming

Each sub-package defines its own stream event types, used by the Handle*Stream methods to emit deltas via the emit func(any) callback:

  • anthropic: StreamStartEvent, TextDeltaEvent, ToolUseEvent, DoneEvent
  • openai: DeltaEvent, ToolEvent, DoneEvent

The virtual server (virtualserver/handler.go) translates these into the wire-format SSE frames expected by each protocol.

DefaultStream in each sub-package converts a non-streaming Handle* response into a stream event sequence using the shared EmitChunks helper, so static and tool mocks get streaming for free.

Error injection

A small facility lets a mock simulate an upstream failure without writing a custom handler. It is opt-in per model — set MockModelConfig.Error (or MockScenario.Error) to an ErrorInjection, and the virtual server handler honors it. Models with no Error field set behave exactly as before.

type ErrorInjection struct {
    Stage ErrorStage // ErrorStagePreContent or ErrorStageMidStream

    // Pre-content fields
    Status  int    // HTTP status (defaults to 500)
    Message string // rendered into the protocol-specific error envelope
    Type    string // defaults to "api_error"

    // Mid-stream fields
    AfterEvents   int           // emit N real events first (default 1)
    MidStreamMode MidStreamMode // ConnectionClose (TCP hijack) or ErrorEvent (SSE error frame)
}

The two stages correspond to two distinct gateway paths (and are exactly the cases the priority-routing firstChunkGate must handle differently):

Stage Wire behavior What it exercises
ErrorStagePreContent Handler returns Status + protocol-shaped error envelope before any streaming starts. No SSE frames. Gate stays buffered → retryable; failover MUST retry.
ErrorStageMidStream Handler writes 200 + headers + AfterEvents real chunks, then either hijacks and closes the TCP connection or emits a final SSE error frame. Gate already committed → bytes on the wire; failover MUST NOT retry.
Architecture: model declares, handler enforces

Mock stream loops are kept gate-free: DefaultStream and MockModel.Handle*Stream simply emit every event they would normally emit. The virtualserver handler owns the mid-stream cutoff:

  1. Before invoking Handle*Stream, the handler asks the model whether it implements ErrorInjectingModel and configures an injection.
  2. If a mid-stream injection is configured, the handler wraps the model's emit callback in a counting gate. After AfterEvents events have been admitted, subsequent events (including terminal DoneEvent / UsageEvent) are silently dropped.
  3. Once the model's stream loop returns, the handler applies the configured MidStreamMode (hijackAndClose or applyMidStreamBreak*).

For pre-content injection there's no gate — the handler short-circuits with writePreContentError{OpenAI,Anthropic} before dispatching to the model at all.

This split keeps the failure-injection surface narrow (one small facility, isolated to the handler) and leaves the common mock path uncluttered.

Pre-registered fail mocks

Basic error models (429, 500, midstream-close, midstream-event) are included in SharedDefaultMocks() and registered by default via RegisterDefaults(). Extended error models require opt-in registration via RegisterExtendedErrorMocks().

Model ID Behavior
virtual-fail-429 HTTP 429 + rate_limit_error envelope (retryable)
virtual-fail-500 HTTP 500 + api_error envelope (retryable)
virtual-fail-midstream-close One real chunk then TCP close (not retryable)
virtual-fail-midstream-event One real chunk then SSE error frame (not retryable)

Failover e2e tests (internal/protocoltest) use these directly via SetupFailoverRoute(... primaryFailModel: pt.FailMockPreContent429) instead of standing up ad-hoc httptest.Server instances.

Sequence models

A sequence model simulates a real provider that varies its response from one request to the next — e.g. 200, 200, 429, 200, … — so failover, retry, and backoff logic can be exercised deterministically without a real or ad-hoc upstream. It is the natural complement to the always-fail error models: those fail every time, a sequence model fails on schedule.

Configuration

A vmodel.SequenceConfig is an ordered program of SequenceSteps, each of which is either a success (status 0/200 → returns content) or a pre-content failure (any other status → the matching HTTP error envelope). What happens once the program is consumed is set by OnExhaust:

OnExhaust Behaviour after the last step
ExhaustLoop (default) Wraps back to the first step, repeats forever
ExhaustClamp Keeps serving the last step (200, 503 → 200, 503, 503, …)
ExhaustFail Serves a terminal 410 / sequence_exhausted error for every later request

Status is the only required field. Everything else falls back to a default provided by the module, so you rarely write a struct literal:

  • success content ← step ContentSequenceConfig.DefaultContentvmodel.FallbackSequenceContent
  • error ErrorType/ErrorMessage ← derived from the status code (429 → rate_limit_error, …)

Use the factories — Steps(...) for the common status-only case, Step(status, opts...) for the rest:

// Anthropic; identical API in the openai sub-package.

// Quickest path: a status-only model in one call.
m := anthropic.NewStatusSequence("flaky-provider", "Flaky Provider", 200, 200, 429)

// Equivalent, when you also want delay / description / OnExhaust:
m = anthropic.NewSequenceModel(&vmodel.SequenceConfig{
    ID:    "flaky-provider",
    Name:  "Flaky Provider",
    Steps: vmodel.Steps(200, 200, 429), // 200, 200, 429, looping
})

// Per-step overrides via options when you need them.
m2 := anthropic.NewSequenceModel(&vmodel.SequenceConfig{
    ID:   "burst-then-fail",
    Name: "Burst Then Fail",
    Steps: []vmodel.SequenceStep{
        vmodel.Step(200, vmodel.WithRepeat(5)),                  // succeed 5×
        vmodel.Step(503, vmodel.WithErrorMessage("scheduled outage")), // then fail once
    },
})
_ = reg.Register(m)

The factory ladder, simplest → most explicit:

Factory Use when
anthropic.NewStatusSequence(id, name, statuses...) Status-only program, one call
vmodel.Steps(statuses...) inside a SequenceConfig You also need delay / description / OnExhaust
vmodel.Step(status, opts...) per step Per-step content, repeat, or custom error text
How it works (per-request resolution)

The registry holds one shared instance, but a sequence inherently has a cursor. Rather than thread per-request state through the handler, a SequenceModel implements Snapshotter: the virtualserver handler calls Snapshot() exactly once per request, which atomically advances the cursor and returns a plain stateless MockModel snapshot for that step. From that point on every existing dispatch path — ExtractErrorInjection, the Handle* methods, the mid-stream gate — works unchanged. The atomic cursor is the only shared mutable state, so concurrent requests are safe and each grabs a distinct, monotonically advancing step.

virtual-sequence-429 (200, 200, 429) ships in both default registries as a user-facing demo for failover dry-runs. Construct your own SequenceModel for bespoke programs. See .design/vmodel-sequence.md.

Benchmarking (benchmark/)

benchmark.NewLocalServer() boots a virtualserver.Service with the default registries as an in-process HTTP server. BenchmarkClient drives load against any HTTP endpoint that speaks the virtual server API.

srv := benchmark.NewLocalServer()
defer srv.Close()

client := benchmark.NewBenchmarkClient(srv.URL())
result, _ := client.RunChatBenchmark(ctx, benchmark.BenchmarkConfig{
    Concurrency: 10,
    Requests:    100,
})
fmt.Printf("TPS: %.1f  p99: %v\n", result.TPS, result.P99Latency)

See benchmark/examples/ for runnable server and client programs, and .design/vmodel-benchmark.md for the shared test-bench design.

  • vmodel/virtualserver — Production Gin HTTP handler, routes, request/response shaping. Owns the v1 → beta lift for Anthropic.
  • internal/protocoltest — Test-only consumer that reuses GenericRegistry[Scenario] as a primitive (its Scenario type satisfies vmodel.VirtualModel). Serves pre-rendered byte/SSE payloads for wire-format protocol testing. It does not inherit production defaults from RegisterDefaults; it owns its own registry of test fixtures.
  • internal/protocol/transform — Transform chain types used by anthropic.TransformModel (e.g. compact-round-only).
  • internal/smart_compact — Concrete transform implementations.

Documentation

Overview

Package virtualmodel defines the protocol-agnostic primitives for virtual models. Concrete provider-specific implementations and registries live in the anthropic and openai sub-packages — this top-level package contains only the base interface and shared value types.

These primitives back the production /virtual/v1/* endpoint (onboarding, demos, dry-runs without a real upstream provider) and are also reused as an in-process LLM substitute by test packages such as protocoltest and servertest. The production endpoint is the package's primary surface; test consumers are secondary and use GenericRegistry directly rather than RegisterDefaults — see README.md "Positioning & registration discipline".

Index

Constants

View Source
const DefaultMockDescription = "A virtual model that returns fixed responses for testing"

DefaultMockDescription is the description returned by mock virtual models when their config does not provide an explicit one.

View Source
const DefaultMockOwnedBy = "tingly-box-virtual"

DefaultMockOwnedBy is the OwnedBy value reported by every built-in mock virtual model in the OpenAI-compatible models list.

View Source
const DefaultStreamChunkDelay = 50 * time.Millisecond

DefaultStreamChunkDelay is the per-chunk sleep used by stream helpers when no explicit total simulated delay is configured.

View Source
const FallbackSequenceContent = "Sequenced virtual response."

FallbackSequenceContent is the module-level fallback body for a success step that sets neither its own Content nor SequenceConfig.DefaultContent. It keeps a bare Step(200) useful out of the box. Distinct from SequenceConfig's DefaultContent — that one is per-model, this one is the last resort when a model configures no default at all.

Variables

This section is empty.

Functions

func EmitChunks

func EmitChunks(ctx context.Context, chunks []string, perChunkDelay time.Duration, emit func(index int, chunk string) bool) error

EmitChunks returns the context error if cancellation happens while waiting or if the emit callback stops accepting chunks.

func ResolveChunkDelay

func ResolveChunkDelay(totalDelay time.Duration, chunkCount int) time.Duration

ResolveChunkDelay computes the per-chunk sleep duration for a stream.

  • if totalDelay > 0 and chunkCount > 0 → totalDelay / chunkCount
  • otherwise → DefaultStreamChunkDelay

This is the shared latency-distribution rule used by all mock streams across protocols.

func ToolCallDisplayContent

func ToolCallDisplayContent(args map[string]interface{}) string

ToolCallDisplayContent extracts display text from tool call arguments. It checks for "message" and "question" keys, returning the first non-empty value found.

Types

type BaseMockModel

type BaseMockModel struct {
	ID          string
	Name        string
	Description string
	Type        VirtualModelType
	Delay       time.Duration
}

BaseMockModel is the protocol-neutral half of an in-memory mock virtual model. Protocol-specific mocks embed it and only need to add their own Handle*/Handle*Stream methods. All identity / metadata methods of the VirtualModel interface live here.

func (*BaseMockModel) GetDescription

func (b *BaseMockModel) GetDescription() string

func (*BaseMockModel) GetID

func (b *BaseMockModel) GetID() string

func (*BaseMockModel) GetName

func (b *BaseMockModel) GetName() string

func (*BaseMockModel) GetType

func (b *BaseMockModel) GetType() VirtualModelType

func (*BaseMockModel) SimulatedDelay

func (b *BaseMockModel) SimulatedDelay() time.Duration

func (*BaseMockModel) ToModel

func (b *BaseMockModel) ToModel() Model

ToModel returns the OpenAI-compatible models-list entry for this mock.

type EmitGate added in v0.260604.1

type EmitGate struct {
	// contains filtered or unexported fields
}

EmitGate is a counting gate used by mock streams to honor mid-stream injection. After Allow() has returned true `cutoff` times, all subsequent Allow() calls return false. Callers should bail out of the stream loop the first time Allow() returns false to avoid emitting events past the cutoff.

A cutoff of <= 0 disables the gate (Allow() always returns true).

func NewEmitGate added in v0.260604.1

func NewEmitGate(cutoff int) *EmitGate

NewEmitGate constructs a gate. Pass the return value of MidStreamCutoff (or -1 to disable).

func (*EmitGate) Allow added in v0.260604.1

func (g *EmitGate) Allow() bool

Allow reports whether the next event should be emitted, incrementing the internal counter on success.

func (*EmitGate) Tripped added in v0.260604.1

func (g *EmitGate) Tripped() bool

Tripped reports whether the gate has refused at least one event (or would refuse the next one). Use after a stream loop completes to decide whether to emit terminal events (DoneEvent / message_stop).

type ErrorCategory added in v0.260611.1

type ErrorCategory string

ErrorCategory categorizes error models for filtering and catalog purposes.

const (
	ErrorCategoryRateLimit  ErrorCategory = "rate_limit" // Rate limiting (429)
	ErrorCategoryUpstream   ErrorCategory = "upstream"   // Upstream server errors (5xx)
	ErrorCategoryTimeout    ErrorCategory = "timeout"    // Request timeout
	ErrorCategoryOverloaded ErrorCategory = "overloaded" // Service overloaded
	ErrorCategoryInvalid    ErrorCategory = "invalid"    // Invalid request
	ErrorCategoryAuth       ErrorCategory = "auth"       // Authentication/authorization
	ErrorCategoryNetwork    ErrorCategory = "network"    // Network connectivity
	ErrorCategoryMalformed  ErrorCategory = "malformed"  // Malformed response
)

type ErrorInjectingModel added in v0.260604.1

type ErrorInjectingModel interface {
	ErrorInjection() *ErrorInjection
}

ErrorInjectingModel is the optional interface a mock virtual model implements to declare a synthetic failure. The virtualserver handler type-asserts to this interface before dispatching the request.

type ErrorInjection added in v0.260604.1

type ErrorInjection struct {
	Stage ErrorStage

	// PreContent fields: HTTP status (defaults to 500 if zero) and an
	// optional message that the handler renders into the protocol-specific
	// error envelope (OpenAI: error.message; Anthropic: error.error.message).
	// Type defaults to "api_error" if empty.
	Status  int
	Message string
	Type    string

	// MidStream fields: number of stream events to emit before tripping
	// (default 1; values <= 0 are treated as 1), and how to terminate.
	AfterEvents   int
	MidStreamMode MidStreamMode
}

ErrorInjection describes a synthetic failure that a mock virtual model should simulate. Attach one to MockScenario.Error (or MockModelConfig.Error) and the virtualserver handler honors it without any real upstream involvement.

Two stages are supported, and they exercise different gateway paths:

  • PreContent: HTTP status + error body, no streaming started. The gateway's firstChunkGate stays buffered → failover retries.
  • MidStream: handler writes a 200 + Content-Type + AfterEvents real events, then either closes the TCP connection or emits a final SSE error event. firstChunkGate is already committed → failover MUST NOT retry.

func ExtractErrorInjection added in v0.260604.1

func ExtractErrorInjection(vm any) *ErrorInjection

ExtractErrorInjection reports the model's error injection configuration, or nil if the model does not implement ErrorInjectingModel or has none set.

type ErrorStage added in v0.260604.1

type ErrorStage int

ErrorStage selects when in the request lifecycle a mock model's error injection fires.

const (
	// ErrorStagePreContent is a failure before any response body is written:
	// the handler returns the configured HTTP status with an error JSON envelope
	// and never invokes the model's Handle* methods past that point. This is
	// the case that priority-routing failover MUST retry — the gate stays
	// buffered, the orchestrator sees a retryable status and discards.
	ErrorStagePreContent ErrorStage = iota

	// ErrorStageMidStream is a failure after the handler has already started
	// streaming. The model emits AfterEvents real stream events, then the
	// handler applies MidStreamMode: it either hijacks and closes the TCP
	// connection (truncated stream from the client's POV) or emits a single
	// SSE error event before stopping. This is the case that priority-routing
	// failover MUST NOT retry — the gate has committed, bytes left the process.
	ErrorStageMidStream
)

type ExhaustPolicy added in v0.260723.1

type ExhaustPolicy string

ExhaustPolicy selects what a sequence serves once every step has been consumed once. The zero value loops, which is the common default.

const (
	// ExhaustLoop wraps back to the first step and repeats the program
	// indefinitely. This is the zero value / default.
	ExhaustLoop ExhaustPolicy = ""

	// ExhaustClamp keeps serving the last step forever once the program is
	// exhausted (e.g. 200, 503 → 200, 503, 503, 503, …).
	ExhaustClamp ExhaustPolicy = "clamp"

	// ExhaustFail serves a terminal pre-content error (HTTP 410, type
	// "sequence_exhausted") for every request after the program is exhausted,
	// modelling an upstream whose scripted run is over.
	ExhaustFail ExhaustPolicy = "fail"
)

type GenericRegistry

type GenericRegistry[T VirtualModel] struct {
	// contains filtered or unexported fields
}

GenericRegistry is a thread-safe registry of virtual models indexed by ID, parameterised by a protocol-specific VirtualModel sub-interface T. Each protocol sub-package (anthropic, openai, ...) instantiates its own Registry alias so models cannot leak across protocols.

func NewGenericRegistry

func NewGenericRegistry[T VirtualModel]() *GenericRegistry[T]

NewGenericRegistry creates an empty registry.

func (*GenericRegistry[T]) Clear

func (r *GenericRegistry[T]) Clear()

Clear removes all registered models.

func (*GenericRegistry[T]) Get

func (r *GenericRegistry[T]) Get(id string) T

Get returns the virtual model for id, or the zero value of T if not registered.

func (*GenericRegistry[T]) Has

func (r *GenericRegistry[T]) Has(id string) bool

Has reports whether a model with the given ID is registered.

func (*GenericRegistry[T]) List

func (r *GenericRegistry[T]) List() []T

List returns all registered virtual models.

func (*GenericRegistry[T]) ListModels

func (r *GenericRegistry[T]) ListModels() []Model

ListModels returns all registered models in the OpenAI-compatible Model format.

func (*GenericRegistry[T]) Register

func (r *GenericRegistry[T]) Register(vm T) error

Register adds a virtual model. Returns an error if the ID is already taken.

func (*GenericRegistry[T]) Unregister

func (r *GenericRegistry[T]) Unregister(id string)

Unregister removes a virtual model by ID. No-op if not present.

type MidStreamMode added in v0.260604.1

type MidStreamMode int

MidStreamMode selects how a mid-stream failure terminates the stream after AfterEvents events have been emitted to the wire.

const (
	// MidStreamModeConnectionClose hijacks the underlying TCP connection and
	// closes it. The client sees an EOF / abrupt disconnect mid-stream —
	// exactly the shape an unstable upstream produces when it dies during a
	// long response.
	MidStreamModeConnectionClose MidStreamMode = iota

	// MidStreamModeErrorEvent emits one final protocol-specific error event
	// (SSE "event: error" frame) before returning. The stream is well-formed
	// up to that point; the client sees an in-band error.
	MidStreamModeErrorEvent

	// MidStreamModeCleanEOF ends the HTTP response cleanly (proper chunked
	// terminator) after AfterEvents events, without any terminal protocol
	// event. The client's reader sees a well-formed body that simply stops —
	// the shape an upstream reverse proxy or gateway produces when it times
	// out an idle stream, and the shape behind "stream closed before
	// response.completed" reports (#1384).
	MidStreamModeCleanEOF
)

type MockUsage added in v0.260604.1

type MockUsage struct {
	PromptTokens      int64 // input_tokens / prompt_tokens
	CompletionTokens  int64 // output_tokens / completion_tokens
	CachedInputTokens int64 // OpenAI prompt_tokens_details.cached_tokens / Anthropic cache_read_input_tokens
	// CacheWriteTokens renders as Anthropic cache_creation_input_tokens or
	// OpenAI prompt_tokens_details.cache_write_tokens (gpt-5.6+) — the same
	// premium-rate write cost under two wire names.
	CacheWriteTokens int64
	ReasoningTokens  int64 // OpenAI completion_tokens_details.reasoning_tokens
}

MockUsage carries deterministic token-usage values that a mock model emits over its streaming wire format. All fields are optional; a zero value means "do not advertise this dimension". Used so streaming converters / observers can be tested for completeness (cache and reasoning tokens, not just plain prompt/completion).

type Model

type Model struct {
	ID      string `json:"id"`
	Object  string `json:"object"`
	Created int64  `json:"created"`
	OwnedBy string `json:"owned_by"`
}

Model represents a virtual model in the models list (OpenAI-compatible format).

type ResolvedStep added in v0.260723.1

type ResolvedStep struct {
	Content string
	Error   *ErrorInjection // nil for success steps
}

ResolvedStep is the concrete outcome of advancing a Sequence: either a success (Error == nil, use Content) or a pre-content failure (Error set).

func (ResolvedStep) HTTPStatus added in v0.260723.1

func (r ResolvedStep) HTTPStatus() int

HTTPStatus reports the HTTP status this step serves: 200 for a success step, or the configured status for an error step. Derived from Error rather than stored separately, so there is exactly one source of truth for an error step's status.

type Sequence added in v0.260723.1

type Sequence struct {
	// contains filtered or unexported fields
}

Sequence is the protocol-neutral engine behind the per-protocol SequenceModel wrappers. It owns the expanded step program and an atomic cursor so concurrent requests each grab a distinct, monotonically advancing step without locking.

func NewSequence added in v0.260723.1

func NewSequence(cfg SequenceConfig) *Sequence

NewSequence flattens cfg.Steps (expanding Repeat) and pre-resolves each step so Next() is allocation-free on the hot path. A config with no steps yields a single success step backed by DefaultContent, so the model is always usable.

func (*Sequence) Len added in v0.260723.1

func (s *Sequence) Len() int

Len reports the number of (post-expansion) steps in the program.

func (*Sequence) Next added in v0.260723.1

func (s *Sequence) Next() ResolvedStep

Next atomically advances the cursor and returns the step for this request. It is safe for concurrent use; each caller observes a distinct cursor value. Behaviour past the end of the program is governed by OnExhaust.

type SequenceConfig added in v0.260723.1

type SequenceConfig struct {
	ID          string        `json:"id" yaml:"id"`
	Name        string        `json:"name" yaml:"name"`
	Description string        `json:"description,omitempty" yaml:"description,omitempty"`
	Delay       time.Duration `json:"delay,omitempty" yaml:"delay,omitempty"`

	// DefaultContent backs any success step that does not set its own Content.
	DefaultContent string `json:"default_content,omitempty" yaml:"default_content,omitempty"`

	// Steps is the response program. Each step is expanded by its Repeat
	// count at construction time.
	Steps []SequenceStep `json:"steps,omitempty" yaml:"steps,omitempty"`

	// OnExhaust selects the behaviour after the program is consumed once:
	// ExhaustLoop (default) wraps around, ExhaustClamp repeats the last step,
	// ExhaustFail serves a terminal error.
	OnExhaust ExhaustPolicy `json:"on_exhaust,omitempty" yaml:"on_exhaust,omitempty"`
}

SequenceConfig describes a SequenceModel: an ordered program of steps that is walked one step per request. By default the program loops (wraps back to the first step) so the model is reusable across an unbounded number of requests; set OnExhaust to change what happens once it is consumed.

Tagged like SequenceStep even though nothing in the codebase currently (de)serializes it: both types describe the same potential external surface (a sequence loaded from a config file or management API), and tagging them together keeps that door open without committing to it yet.

func DefaultSequenceConfigs added in v0.260723.1

func DefaultSequenceConfigs() []SequenceConfig

DefaultSequenceConfigs returns the user-facing demo sequence(s) registered into BOTH default registries by each protocol's RegisterDefaults. A demo sequence is genuinely useful for onboarding / dry-runs: it lets users see how the gateway reacts to an intermittently rate-limited upstream without configuring a real provider.

type SequenceStep added in v0.260723.1

type SequenceStep struct {
	// Status is the HTTP status this step serves. 0 and 200 both mean
	// "success" (return Content); anything else is served as a pre-content
	// error with that status code.
	Status int `json:"status" yaml:"status"`

	// Content is the response body for a success step. When empty the
	// SequenceConfig.DefaultContent is used. Ignored for error steps.
	Content string `json:"content,omitempty" yaml:"content,omitempty"`

	// ErrorMessage and ErrorType override the error envelope for an error
	// step. When empty they are derived from Status (see defaultErrorMeta).
	// Ignored for success steps.
	ErrorMessage string `json:"error_message,omitempty" yaml:"error_message,omitempty"`
	ErrorType    string `json:"error_type,omitempty" yaml:"error_type,omitempty"`

	// Repeat serves this step Repeat consecutive times before advancing to
	// the next one. Values <= 0 are treated as 1. Useful for compact configs
	// like "succeed 5×, then fail once".
	Repeat int `json:"repeat,omitempty" yaml:"repeat,omitempty"`
}

SequenceStep is one entry in a SequenceModel's response program. A step is either a success (Status 0 or 200 → a normal content response) or a pre-content failure (any other status → the configured HTTP error envelope).

This lets a single virtual model reproduce the real-world behaviour of a flaky upstream — e.g. "200, 200, 429, 200" — so failover / retry / backoff logic can be exercised deterministically without standing up a real or ad-hoc test provider.

func Step added in v0.260723.1

func Step(status int, opts ...StepOption) SequenceStep

Step builds a SequenceStep from a status code plus optional overrides. Status is the only required input — content for a success step and the error type/message for a failure step are filled from defaults at build time (module FallbackSequenceContent / defaultErrorMeta), so Step(429) and Step(200) are both immediately usable.

func Steps added in v0.260723.1

func Steps(statuses ...int) []SequenceStep

Steps builds a program from a bare list of status codes — the common case, e.g. Steps(200, 200, 429). Each step takes default content / error metadata.

type SharedMockSpec

type SharedMockSpec struct {
	ID       string
	Name     string
	Content  string          // static text response (ignored if ToolCall is set)
	ToolCall *ToolCallConfig // if non-nil, this is a tool model
	Delay    time.Duration
	Usage    *MockUsage      // optional explicit usage to advertise in the stream
	Error    *ErrorInjection // optional synthetic failure for error-injection mocks

	// Error metadata (only meaningful when Error != nil)
	ErrorCategory ErrorCategory // Category of error (rate_limit, upstream, etc.)
	IsRetryable   bool          // Whether failover should retry this error
	Severity      string        // "low", "medium", "high" - for filtering/sorting
}

SharedMockSpec describes a built-in mock that is identical across protocols (anthropic + openai). Each protocol's RegisterDefaults converts a SharedMockSpec into its own protocol-specific MockModelConfig.

Entries returned by SharedDefaultMocks are user-facing demo defaults: they are mounted into the production /virtual/v1/* endpoint and visible to end users via the virtual provider. Test-only fixtures must NOT be added here; tests should build their own GenericRegistry rather than pollute the production defaults set.

func ExtendedErrorSpecs added in v0.260611.1

func ExtendedErrorSpecs() []SharedMockSpec

ExtendedErrorSpecs returns additional error scenarios for testing. These are opt-in - register via RegisterExtendedErrorMocks in per-protocol packages.

func SharedDefaultMocks

func SharedDefaultMocks() []SharedMockSpec

SharedDefaultMocks returns the mocks registered by BOTH the Anthropic and OpenAI default registries. Per-protocol unique entries (e.g. virtual-claude-3 for Anthropic, virtual-gpt-4 for OpenAI, the compact transforms) live in their respective sub-packages.

func StreamTestMockSpecs added in v0.260604.1

func StreamTestMockSpecs() []SharedMockSpec

StreamTestMockSpecs returns deterministic stream-test fixtures (static + tool variants) that advertise the full usage shape — prompt, completion, cached input, cache-creation input, and reasoning tokens. These are opt-in (NOT in SharedDefaultMocks): consumers wire them into their own registry via RegisterStreamTestMocks helpers in the per-protocol sub-packages.

type StepOption added in v0.260723.1

type StepOption func(*SequenceStep)

StepOption customizes a SequenceStep built by Step. Only Status is required; these options override the otherwise-defaulted fields for the uncommon cases.

func WithContent added in v0.260723.1

func WithContent(content string) StepOption

WithContent sets a success step's response body (overrides the config/module default content).

func WithErrorMessage added in v0.260723.1

func WithErrorMessage(message string) StepOption

WithErrorMessage overrides an error step's message (otherwise derived from Status).

func WithErrorType added in v0.260723.1

func WithErrorType(typ string) StepOption

WithErrorType overrides an error step's type (otherwise derived from Status).

func WithRepeat added in v0.260723.1

func WithRepeat(n int) StepOption

WithRepeat serves the step n consecutive times before advancing.

type ToolCallConfig

type ToolCallConfig struct {
	Name      string                 `json:"name" yaml:"name"`
	Arguments map[string]interface{} `json:"arguments" yaml:"arguments"`
}

ToolCallConfig defines a tool call to be returned by the virtual model.

type VirtualModel

type VirtualModel interface {
	GetID() string
	GetName() string
	GetDescription() string
	GetType() VirtualModelType
	SimulatedDelay() time.Duration
	ToModel() Model
}

VirtualModel is the base interface common to all virtual model types. Provider-specific extensions are defined in the anthropic and openai sub-packages, each adding the Handle methods for that protocol.

type VirtualModelType

type VirtualModelType string

VirtualModelType represents the type/category of a virtual model.

const (
	// VirtualModelTypeStatic represents static mock models that return fixed responses.
	VirtualModelTypeStatic VirtualModelType = "static"

	// VirtualModelTypeProxy represents proxy/transform models that modify requests before forwarding.
	VirtualModelTypeProxy VirtualModelType = "proxy"

	// VirtualModelTypeTool represents tool models that return tool_use blocks.
	VirtualModelTypeTool VirtualModelType = "tool"

	// VirtualModelTypeSequence represents sequence models that walk a configured
	// program of per-request outcomes (e.g. 200, 200, 429) to simulate a flaky
	// upstream provider.
	VirtualModelTypeSequence VirtualModelType = "sequence"
)

Directories

Path Synopsis
Package anthropic provides Anthropic-protocol virtual models.
Package anthropic provides Anthropic-protocol virtual models.
Package benchmark is the vmodel benchmark: a shared, real-world mock-provider foundation.
Package benchmark is the vmodel benchmark: a shared, real-world mock-provider foundation.
check
Package check holds the protocol-neutral, reusable check logic for the vmodel benchmark: the RoundTripResult view of a single gateway round trip and the named Assertion library that operates on it.
Package check holds the protocol-neutral, reusable check logic for the vmodel benchmark: the RoundTripResult view of a single gateway round trip and the named Assertion library that operates on it.
examples/client command
Stand-alone benchmark client driver: starts an in-process LocalServer (vmodel-backed) and drives it with the BenchmarkClient against both the OpenAI Chat and Anthropic Messages routes, printing a metrics summary.
Stand-alone benchmark client driver: starts an in-process LocalServer (vmodel-backed) and drives it with the BenchmarkClient against both the OpenAI Chat and Anthropic Messages routes, printing a metrics summary.
examples/server command
Stand-alone benchmark mock server: starts a local HTTP server backed by the production virtualmodel registries (with their default mock models pre-registered) so external benchmark drivers can hit a realistic vmodel surface over loopback.
Stand-alone benchmark mock server: starts a local HTTP server backed by the production virtualmodel registries (with their default mock models pre-registered) so external benchmark drivers can hit a realistic vmodel surface over loopback.
scenario
Package scenario holds the reusable mock-provider fixtures for the vmodel benchmark: named Scenarios, each carrying per-format MockResponseBuilders and a set of check.Assertions.
Package scenario holds the reusable mock-provider fixtures for the vmodel benchmark: named Scenarios, each carrying per-format MockResponseBuilders and a set of check.Assertions.
Package vmodelclient provides in-process implementations of the client interfaces backed by vmodel registries.
Package vmodelclient provides in-process implementations of the client interfaces backed by vmodel registries.
Package openai provides OpenAI-protocol virtual models.
Package openai provides OpenAI-protocol virtual models.
Package virtualserver provides the HTTP handler for virtual model endpoints.
Package virtualserver provides the HTTP handler for virtual model endpoints.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL