virtualmodel

package
v0.260507.1 Latest Latest
Warning

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

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

README

internal/virtualmodel

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 internal/virtualmodel/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/server_validate).

Layout

internal/virtualmodel/
├── 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() — configs shared by both protocols
├── 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

internal/virtualmodel 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 internal/server mounts virtualserver.Service for end-user demos / onboarding / dry-runs
Secondary (tests) In-process GenericRegistry[T] internal/server_validate.Scenario, internal/protocol_validate, 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 server_validate does for Scenario) and register fixtures there — keeping the production defaults 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 — server_validate.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 only via /virtual/v1/chat/completions. 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-thinking, claude-code-compact

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-thinking 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.

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.

  • internal/virtualmodel/virtualserver — Production Gin HTTP handler, routes, request/response shaping. Owns the v1 → beta lift for Anthropic.
  • internal/server_validate — Test-only consumer that reuses GenericRegistry[Scenario] as a primitive (its Scenario type satisfies virtualmodel.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-thinking).
  • 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 server_validate and protocol_validate. 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.

Variables

This section is empty.

Functions

func EmitChunks added in v0.260507.1

func EmitChunks(chunks []string, perChunkDelay time.Duration, emit func(index int, chunk string))

EmitChunks invokes emit for every chunk in order, sleeping perChunkDelay before each emission. It is the protocol-neutral inner loop shared by mock streams. Callers construct their own protocol-specific event types inside the emit closure.

func ResolveChunkDelay added in v0.260507.1

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 added in v0.260414.2000

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 added in v0.260507.1

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 added in v0.260507.1

func (b *BaseMockModel) GetDescription() string

func (*BaseMockModel) GetID added in v0.260507.1

func (b *BaseMockModel) GetID() string

func (*BaseMockModel) GetName added in v0.260507.1

func (b *BaseMockModel) GetName() string

func (*BaseMockModel) GetType added in v0.260507.1

func (b *BaseMockModel) GetType() VirtualModelType

func (*BaseMockModel) SimulatedDelay added in v0.260507.1

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

func (*BaseMockModel) ToModel added in v0.260507.1

func (b *BaseMockModel) ToModel() Model

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

type GenericRegistry added in v0.260507.1

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 added in v0.260507.1

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

NewGenericRegistry creates an empty registry.

func (*GenericRegistry[T]) Clear added in v0.260507.1

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

Clear removes all registered models.

func (*GenericRegistry[T]) Get added in v0.260507.1

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 added in v0.260507.1

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

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

func (*GenericRegistry[T]) List added in v0.260507.1

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

List returns all registered virtual models.

func (*GenericRegistry[T]) ListModels added in v0.260507.1

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

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

func (*GenericRegistry[T]) Register added in v0.260507.1

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 added in v0.260507.1

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

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

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 SharedMockSpec added in v0.260507.1

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
}

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 SharedDefaultMocks added in v0.260507.1

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.

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"
)

Directories

Path Synopsis
Package anthropic provides Anthropic-protocol virtual models.
Package anthropic provides Anthropic-protocol virtual models.
Package benchmark provides a load-testing client and an in-process server factory for the virtualmodel HTTP service.
Package benchmark provides a load-testing client and an in-process server factory for the virtualmodel HTTP service.
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.
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