testopenai

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2025 License: Apache-2.0 Imports: 25 Imported by: 0

README

Test OpenAI Server

This package provides a test OpenAI API server for testing AI Gateway functionality without requiring actual API access or credentials.

Pre-recorded OpenAI request/responses are stored as YAML files in the cassettes directory, using the go-vcr v4 format.

Overview

The test server works by:

  1. Automatically loading all pre-recorded API interactions from embedded "cassette" YAML files
  2. Matching incoming requests against recorded interactions based on the X-Cassette-Name header
  3. Replaying the recorded responses with delays faster than real platforms to keep tests fast.

This approach provides:

  • Deterministic testing: Same inputs always produce same outputs
  • No API credentials needed: Tests can run without OpenAI API keys
  • Fast execution: No network calls to external services
  • Cost savings: No API usage charges during testing

Usage

Basic Usage
import (
	"testing"
	"github.com/envoyproxy/ai-gateway/internal/testopenai"
)

func TestMyFeature(t *testing.T) {
	// Create server on random port - cassettes are automatically loaded
	server, err := testopenai.NewServer()
	require.NoError(t, err)
	defer server.Close()

	// Create a request for a specific cassette
	req, err := testopenai.NewRequest(server.URL(), testopenai.CassetteChatBasic)
	require.NoError(t, err)

	// Make the request
	resp, err := http.DefaultClient.Do(req)
	// ... test your code
}

Recording New Cassettes

The test server can record new interactions when:

  • No matching cassette is found
  • OPENAI_API_KEY is set in the environment
  • A cassette name is provided via X-Cassette-Name header

To record a new cassette, follow these steps:

  1. Add a constant for your test scenario to requests.go:

    const (
    	// ... existing constants
    	// CassetteChatFeatureX includes feature X, added to OpenAI version 1.2.3.
    	CassetteChatFeatureX
    	_cassetteNameEnd // Keep this at the end
    )
    

    Note: The constants use iota enumeration, so your new constant must be added before _cassetteNameEnd to be included in the AllCassettes() iteration.

    Also add its string mapping:

    var stringValues = map[CassetteName]string{
    	// ... existing mappings
    	CassetteChatFeatureX: "chat-feature-x",
    }
    
  2. Add the request body for your test to requests.go:

    var requestBodies = map[CassetteName]*openai.ChatCompletionRequest{
    	// ... existing entries
    	CassetteChatFeatureX: {
    		Model: openai.ModelGPT41Nano,
    		Messages: []openai.ChatCompletionMessageParamUnion{
    			{
    				Type: openai.ChatMessageRoleUser,
    				Value: openai.ChatCompletionUserMessageParam{
    					Role: openai.ChatMessageRoleUser,
    					Content: openai.StringOrUserRoleContentUnion{
    						Value: "Your test prompt",
    					},
    				},
    			},
    		},
    		// Add your feature-specific fields here
    	},
    }
    
  3. Run TestNewRequest with your OpenAI API key set:

    cd internal/testopenai
    OPENAI_API_KEY=sk-.. go test -run TestNewRequest -v
    
  4. Use it in tests like chat_completions_test.go

Flowchart of Request Handling

graph TD
    A[Request arrives] --> B{X-Cassette-Name\nheader present?}
    B -->|Yes| C[Search for specific cassette]
    B -->|No| D[Search all cassettes]

    C --> E{Cassette found?}
    D --> F{Match found?}

    E -->|Yes| G{Interaction matches?}
    E -->|No| H{API key set?}
    F -->|Yes| P[Return recorded response]
    F -->|No| I[Return 400 error:\nInclude X-Cassette-Name header]

    G -->|Yes| P
    G -->|No| O[Return 409 error:\nInteraction out of date]

    H -->|Yes| J[Record new interaction]
    H -->|No| K[Return 500 error:\nNo cassette found]

    J --> L[Make real API call]
    L --> M[Save to cassette file\nwith .yaml extension]
    M --> N[Return response to client]

    style I fill:#f96
    style K fill:#f96
    style O fill:#fa6
Future work

OpenAI is not the only inference API supported, but it is special as it is the most common frontend and backend for AI Gateway. This is why we expose the requests, as we will often proxy these even if the backend is not OpenAI compatible.

The recording process would remain consistent for other cloud services, such as Anthropic or Bedrock, though there could be variations in how requests are scrubbed for secrets or handled for request signing. In a future refactoring, we could extract the core recording infrastructure into a separate package, reducing this one to just cassette constants and OpenAI-specific request recording and handling details. Most of the code could be reused for other backends.

For additional insights, refer to OpenTelemetry instrumentation, which often employs VCR for LLM frameworks as well.

Here are key parts of the OpenTelemetry Botocore Bedrock instrumentation that deals with request signing and recording:

Here are key parts of OpenInference Anthropic instrumentation, which handles their endpoint.

Documentation

Overview

Package testopenai provides a test OpenAI API server for testing. It uses VCR (Video Cassette Recorder) pattern to replay pre-recorded API responses, allowing deterministic testing without requiring actual API access or credentials.

Index

Constants

View Source
const CassetteNameHeader = "X-Cassette-Name"

CassetteNameHeader is the header used to specify which cassette to use for matching.

Variables

This section is empty.

Functions

func NewRequest

func NewRequest(ctx context.Context, baseURL string, cassette Cassette) (*http.Request, error)

NewRequest creates a new OpenAI request for the given cassette.

The returned request is an http.MethodPost with the body and CassetteNameHeader according to the pre-recorded cassette.

func ResponseBody

func ResponseBody(cassette Cassette) string

ResponseBody is used in tests to avoid duplicating body content when the proxy serialization matches exactly the upstream (testopenai) server.

Types

type Cassette

type Cassette int

Cassette is an HTTP interaction recording.

Note: At the moment, our tests are optimized for single request/response pairs and do not include scenarios requiring multiple round-trips, such as `cached_tokens`.

const (

	// CassetteChatBasic is the canonical OpenAI chat completion request.
	CassetteChatBasic Cassette = iota
	// CassetteChatJSONMode is a chat completion request with JSON response format.
	CassetteChatJSONMode
	// CassetteChatMultimodal is a multimodal chat request with text and image inputs.
	CassetteChatMultimodal
	// CassetteChatMultiturn is a multi-turn conversation with message history.
	CassetteChatMultiturn
	// CassetteChatNoMessages is a request missing the required messages field.
	CassetteChatNoMessages
	// CassetteChatParallelTools is a chat completion with parallel function calling enabled.
	CassetteChatParallelTools
	// CassetteChatStreaming is the canonical OpenAI chat completion request,
	// with streaming enabled.
	CassetteChatStreaming
	// CassetteChatTools is a chat completion request with function tools.
	CassetteChatTools
	// CassetteChatUnknownModel is a request with a non-existent model.
	CassetteChatUnknownModel
	// CassetteChatBadRequest is a request with multiple validation errors.
	CassetteChatBadRequest
	// CassetteChatReasoning tests capture of reasoning_tokens in completion_tokens_details for O1 models.
	CassetteChatReasoning
	// CassetteChatImageToText tests image input processing showing image token
	// count in usage details.
	CassetteChatImageToText
	// CassetteChatTextToImageTool tests image generation through tool calls since
	// chat completions cannot natively output images.
	CassetteChatTextToImageTool
	// CassetteChatAudioToText tests audio input transcription and audio_tokens
	// in prompt_tokens_details.
	CassetteChatAudioToText
	// CassetteChatTextToAudio tests audio output generation where the model
	// produces audio content, showing audio_tokens in completion_tokens_details.
	CassetteChatTextToAudio
	// CassetteChatDetailedUsage tests capture of all token usage detail fields in a single response.
	CassetteChatDetailedUsage
	// CassetteChatStreamingDetailedUsage tests capture of detailed token usage in streaming responses with include_usage.
	CassetteChatStreamingDetailedUsage
	// CassetteChatWebSearch tests OpenAI Web Search tool with a small URL response, including citations.
	CassetteChatWebSearch
	// CassetteChatStreamingWebSearch is CassetteChatWebSearch except with streaming enabled.
	CassetteChatStreamingWebSearch
	// CassetteChatOpenAIAgentsPython is a real request from OpenAI Agents Python library for financial research.
	// See https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent
	CassetteChatOpenAIAgentsPython

	// CassetteEmbeddingsBasic is the canonical OpenAI embeddings request with a single string input.
	CassetteEmbeddingsBasic
	// CassetteEmbeddingsBase64 tests base64 encoding format for embedding vectors.
	CassetteEmbeddingsBase64
	// CassetteEmbeddingsTokens tests embeddings with token array input instead of text.
	CassetteEmbeddingsTokens
	// CassetteEmbeddingsLargeText tests embeddings with a longer text input.
	CassetteEmbeddingsLargeText
	// CassetteEmbeddingsUnknownModel tests error handling for non-existent model.
	CassetteEmbeddingsUnknownModel
	// CassetteEmbeddingsDimensions tests embeddings with specified output dimensions.
	CassetteEmbeddingsDimensions
	// CassetteEmbeddingsMixedBatch tests batch with varying text lengths.
	CassetteEmbeddingsMixedBatch
	// CassetteEmbeddingsMaxTokens tests input that approaches token limit.
	CassetteEmbeddingsMaxTokens
	// CassetteEmbeddingsWhitespace tests handling of various whitespace patterns.
	CassetteEmbeddingsWhitespace
	// CassetteEmbeddingsBadRequest tests request with multiple validation errors.
	CassetteEmbeddingsBadRequest
)

func ChatCassettes

func ChatCassettes() []Cassette

ChatCassettes returns a slice of all cassettes for chat completions. Unlike image generation—which *requires* an image_generation tool call— audio synthesis is natively supported.

func EmbeddingsCassettes

func EmbeddingsCassettes() []Cassette

EmbeddingsCassettes returns a slice of all cassettes for embeddings.

func (Cassette) String

func (c Cassette) String() string

String returns the string representation of the cassette name.

type Server

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

Server represents a test OpenAI API server that replays cassette recordings.

func NewServer

func NewServer(out io.Writer, port int) (*Server, error)

NewServer creates a new test OpenAI server (use port 0 for random).

func (*Server) Close

func (s *Server) Close()

Close shuts down the server.

func (*Server) Port

func (s *Server) Port() int

Port returns the port the server is listening on.

func (*Server) URL

func (s *Server) URL() string

URL returns the base URL of the server.

Jump to

Keyboard shortcuts

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