package main import ( "bufio" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "sync" "testing" "github.com/stretchr/testify/require" ) // modelTestKey is a fake API key. const modelTestKey = "sk-test-not-a-real-key" // ModelFakeCall is one call to a fake endpoint, numbered from 0. type ModelFakeCall struct { number int header http.Header raw []byte body map[string]any } // ModelFake is an httptest Responses endpoint. It keeps each call, and reply // writes the response. The default reply is one short message. type ModelFake struct { *httptest.Server mutex sync.Mutex calls []ModelFakeCall reply func(http.ResponseWriter, ModelFakeCall) } // Call returns call number n. func (f *ModelFake) Call(n int) ModelFakeCall { f.mutex.Lock() defer f.mutex.Unlock() return f.calls[n] } // Calls returns the number of calls. func (f *ModelFake) Calls() int { f.mutex.Lock() defer f.mutex.Unlock() return len(f.calls) } func (f *ModelFake) serve(w http.ResponseWriter, r *http.Request) { raw, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } call := ModelFakeCall{ header: r.Header.Clone(), raw: raw, } err = json.Unmarshal(raw, &call.body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } f.mutex.Lock() call.number = len(f.calls) f.calls = append(f.calls, call) reply := f.reply f.mutex.Unlock() reply(w, call) } func newModelFake(t *testing.T) *ModelFake { t.Helper() f := &ModelFake{ reply: func(w http.ResponseWriter, call ModelFakeCall) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("a line")}, 100, 0, 5)) }, } f.Server = httptest.NewServer(http.HandlerFunc(f.serve)) t.Cleanup(f.Close) return f } func modelTestRequest() ModelRequest { return ModelRequest{ Model: "gpt-test", Effort: "low", Input: []any{modelUserMessage("hello")}, } } // modelWriteEvents writes events as a server-sent event stream. func modelWriteEvents(w http.ResponseWriter, events ...any) { w.Header().Set("Content-Type", "text/event-stream") for _, event := range events { data, err := json.Marshal(event) if err != nil { panic(err) } _, err = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.(map[string]any)["type"], data) if err != nil { panic(err) } } } func modelMessageItem(text string) map[string]any { return map[string]any{ "type": "message", "role": "assistant", "content": []any{ map[string]any{"type": "output_text", "text": text}, }, } } func modelCompletedEvent(output []any, input, cached, out int) map[string]any { if output == nil { output = []any{} } return map[string]any{ "type": "response.completed", "response": map[string]any{ "output": output, "usage": map[string]any{ "input_tokens": input, "input_tokens_details": map[string]any{"cached_tokens": cached}, "output_tokens": out, }, }, } } func modelReadUsage(t *testing.T, path string) []ModelUsageRecord { t.Helper() file, err := os.Open(path) require.NoError(t, err) defer func() { require.NoError(t, file.Close()) }() var records []ModelUsageRecord scanner := bufio.NewScanner(file) for scanner.Scan() { var record ModelUsageRecord require.NoError(t, json.Unmarshal(scanner.Bytes(), &record)) records = append(records, record) } require.NoError(t, scanner.Err()) return records } func modelReasoningItem(id, encrypted string) map[string]any { return map[string]any{ "type": "reasoning", "id": id, "encrypted_content": encrypted, "summary": []any{}, } } func modelFunctionCallItem(id, callID, name, arguments string) map[string]any { return map[string]any{ "type": "function_call", "id": id, "call_id": callID, "name": name, "arguments": arguments, } }