]> git.giacomolli.uk - onechat/blobdiff - model_test.go
Name and order the model client code
[onechat] / model_test.go
similarity index 66%
rename from responses_test.go
rename to model_test.go
index 9a1355a8d0098bb2ee18b01a1d0b9e3d0a8d665e..b90eaddca24bec8012cee558e4194585cc6942e2 100644 (file)
@@ -16,28 +16,28 @@ import (
        "github.com/stretchr/testify/require"
 )
 
        "github.com/stretchr/testify/require"
 )
 
-// testKey is a fake API key.
-const testKey = "sk-test-not-a-real-key"
+// modelTestKey is a fake API key.
+const modelTestKey = "sk-test-not-a-real-key"
 
 func TestClientSendsTheFixedRequestFields(t *testing.T) {
 
 func TestClientSendsTheFixedRequestFields(t *testing.T) {
-       server := newFakeResponses(t)
-       client := newTestClient(t, server)
+       server := newModelFake(t)
+       client := newTestModelClient(t, server)
 
 
-       _, err := client.Create(t.Context(), Request{
+       _, err := client.Create(t.Context(), ModelRequest{
                Model:        "gpt-test",
                Effort:       "high",
                Instructions: "You are a test.",
                Model:        "gpt-test",
                Effort:       "high",
                Instructions: "You are a test.",
-               Tools: []Tool{{
+               Tools: []ModelTool{{
                        Type:       "function",
                        Name:       "date",
                        Parameters: map[string]any{"type": "object"},
                }},
                        Type:       "function",
                        Name:       "date",
                        Parameters: map[string]any{"type": "object"},
                }},
-               Input: []any{userMessage("<chat>\n</chat>\n", "hello")},
+               Input: []any{modelUserMessage("<chat>\n</chat>\n", "hello")},
        })
        require.NoError(t, err)
 
        call := server.Call(0)
        })
        require.NoError(t, err)
 
        call := server.Call(0)
-       require.Equal(t, "Bearer "+testKey, call.header.Get("Authorization"))
+       require.Equal(t, "Bearer "+modelTestKey, call.header.Get("Authorization"))
        require.Equal(t, "text/event-stream", call.header.Get("Accept"))
        require.Equal(t, "application/json", call.header.Get("Content-Type"))
 
        require.Equal(t, "text/event-stream", call.header.Get("Accept"))
        require.Equal(t, "application/json", call.header.Get("Content-Type"))
 
@@ -68,7 +68,7 @@ func TestClientSendsTheSessionHeaderToChatGPT(t *testing.T) {
 
        testCases := []struct {
                name string
 
        testCases := []struct {
                name string
-               auth authorizer
+               auth ModelAuthorizer
                want string
        }{
                {
                want string
        }{
                {
@@ -78,16 +78,16 @@ func TestClientSendsTheSessionHeaderToChatGPT(t *testing.T) {
                },
                {
                        name: "OpenAI API",
                },
                {
                        name: "OpenAI API",
-                       auth: apiKey(testKey),
+                       auth: ModelAPIKey(modelTestKey),
                        want: "",
                },
        }
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
                        want: "",
                },
        }
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
-                       server := newFakeResponses(t)
-                       client := newClient(server.URL, tc.auth, "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
-                       _, err := client.Create(t.Context(), testRequest())
+                       server := newModelFake(t)
+                       client := newModelClient(server.URL, tc.auth, "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
+                       _, err := client.Create(t.Context(), modelTestRequest())
                        require.NoError(t, err)
 
                        call := server.Call(0)
                        require.NoError(t, err)
 
                        call := server.Call(0)
@@ -98,24 +98,24 @@ func TestClientSendsTheSessionHeaderToChatGPT(t *testing.T) {
 }
 
 func TestClientReadsTheStream(t *testing.T) {
 }
 
 func TestClientReadsTheStream(t *testing.T) {
-       server := newFakeResponses(t)
-       server.reply = func(w http.ResponseWriter, call fakeCall) {
-               writeEvents(w,
+       server := newModelFake(t)
+       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+               modelWriteEvents(w,
                        map[string]any{"type": "response.created", "response": map[string]any{}},
                        map[string]any{"type": "response.output_item.done", "item": map[string]any{"type": "reasoning", "encrypted_content": "opaque"}},
                        map[string]any{"type": "response.output_text.delta", "delta": "Hel"},
                        map[string]any{"type": "response.created", "response": map[string]any{}},
                        map[string]any{"type": "response.output_item.done", "item": map[string]any{"type": "reasoning", "encrypted_content": "opaque"}},
                        map[string]any{"type": "response.output_text.delta", "delta": "Hel"},
-                       map[string]any{"type": "response.output_item.done", "item": messageItem("Hello")},
-                       map[string]any{"type": "response.output_item.done", "item": messageItem(" world")},
-                       completedEvent(nil, 1200, 1024, 30),
+                       map[string]any{"type": "response.output_item.done", "item": modelMessageItem("Hello")},
+                       map[string]any{"type": "response.output_item.done", "item": modelMessageItem(" world")},
+                       modelCompletedEvent(nil, 1200, 1024, 30),
                )
        }
 
                )
        }
 
-       response, err := newTestClient(t, server).Create(t.Context(), testRequest())
+       response, err := newTestModelClient(t, server).Create(t.Context(), modelTestRequest())
        require.NoError(t, err)
        require.Equal(t, "Hello world", response.Text)
        require.Len(t, response.Output, 3)
        require.JSONEq(t, `{"type": "reasoning", "encrypted_content": "opaque"}`, string(response.Output[0]))
        require.NoError(t, err)
        require.Equal(t, "Hello world", response.Text)
        require.Len(t, response.Output, 3)
        require.JSONEq(t, `{"type": "reasoning", "encrypted_content": "opaque"}`, string(response.Output[0]))
-       require.Equal(t, Usage{
+       require.Equal(t, ModelUsage{
                Input:  1200,
                Cached: 1024,
                Output: 30,
                Input:  1200,
                Cached: 1024,
                Output: 30,
@@ -123,19 +123,19 @@ func TestClientReadsTheStream(t *testing.T) {
 }
 
 func TestClientTakesTheOutputOfTheCompletedResponse(t *testing.T) {
 }
 
 func TestClientTakesTheOutputOfTheCompletedResponse(t *testing.T) {
-       server := newFakeResponses(t)
-       server.reply = func(w http.ResponseWriter, call fakeCall) {
-               writeEvents(w, completedEvent([]any{messageItem("only here")}, 10, 0, 2))
+       server := newModelFake(t)
+       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+               modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("only here")}, 10, 0, 2))
        }
 
        }
 
-       response, err := newTestClient(t, server).Create(t.Context(), testRequest())
+       response, err := newTestModelClient(t, server).Create(t.Context(), modelTestRequest())
        require.NoError(t, err)
        require.Equal(t, "only here", response.Text)
 }
 
 func TestClientRecordsEachCall(t *testing.T) {
        require.NoError(t, err)
        require.Equal(t, "only here", response.Text)
 }
 
 func TestClientRecordsEachCall(t *testing.T) {
-       server := newFakeResponses(t)
-       server.reply = func(w http.ResponseWriter, call fakeCall) {
+       server := newModelFake(t)
+       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
                w.Header().Set("X-Codex-Primary-Used-Percent", "12")
                w.Header().Set("X-Codex-Plan-Type", "plus")
                w.Header().Set("X-Other", "not quota")
                w.Header().Set("X-Codex-Primary-Used-Percent", "12")
                w.Header().Set("X-Codex-Plan-Type", "plus")
                w.Header().Set("X-Other", "not quota")
@@ -147,22 +147,22 @@ func TestClientRecordsEachCall(t *testing.T) {
                        return
                }
 
                        return
                }
 
-               writeEvents(w, completedEvent([]any{messageItem("ok")}, 500, 400, 7))
+               modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 500, 400, 7))
        }
 
        }
 
-       client := newTestClient(t, server)
-       request := testRequest()
+       client := newTestModelClient(t, server)
+       request := modelTestRequest()
        request.Node = &nodeKey{2, 5}
        _, err := client.Create(t.Context(), request)
        require.NoError(t, err)
        require.NotContains(t, server.Call(0).body, "node")
        require.NotContains(t, string(server.Call(0).raw), "[2,5]")
 
        request.Node = &nodeKey{2, 5}
        _, err := client.Create(t.Context(), request)
        require.NoError(t, err)
        require.NotContains(t, server.Call(0).body, "node")
        require.NotContains(t, string(server.Call(0).raw), "[2,5]")
 
-       _, err = client.Create(t.Context(), testRequest())
+       _, err = client.Create(t.Context(), modelTestRequest())
        require.ErrorContains(t, err, "HTTP 429")
        require.ErrorContains(t, err, "slow down")
 
        require.ErrorContains(t, err, "HTTP 429")
        require.ErrorContains(t, err, "slow down")
 
-       records := readUsage(t, client.usage.path)
+       records := modelReadUsage(t, client.usage.path)
        require.Len(t, records, 2)
 
        quota := map[string]string{
        require.Len(t, records, 2)
 
        quota := map[string]string{
@@ -190,7 +190,7 @@ func TestClientRecordsEachCall(t *testing.T) {
 
        data, err := os.ReadFile(client.usage.path)
        require.NoError(t, err)
 
        data, err := os.ReadFile(client.usage.path)
        require.NoError(t, err)
-       require.NotContains(t, string(data), testKey)
+       require.NotContains(t, string(data), modelTestKey)
 
        info, err := os.Stat(client.usage.path)
        require.NoError(t, err)
 
        info, err := os.Stat(client.usage.path)
        require.NoError(t, err)
@@ -198,21 +198,21 @@ func TestClientRecordsEachCall(t *testing.T) {
 }
 
 func TestClientRefusesCallsOverTheLimit(t *testing.T) {
 }
 
 func TestClientRefusesCallsOverTheLimit(t *testing.T) {
-       server := newFakeResponses(t)
-       client := newTestClient(t, server)
+       server := newModelFake(t)
+       client := newTestModelClient(t, server)
        client.LimitCalls(2)
 
        for range 2 {
                require.False(t, client.LimitReached())
        client.LimitCalls(2)
 
        for range 2 {
                require.False(t, client.LimitReached())
-               _, err := client.Create(t.Context(), testRequest())
+               _, err := client.Create(t.Context(), modelTestRequest())
                require.NoError(t, err)
        }
 
        require.True(t, client.LimitReached())
                require.NoError(t, err)
        }
 
        require.True(t, client.LimitReached())
-       _, err := client.Create(t.Context(), testRequest())
-       require.ErrorIs(t, err, errCallLimit)
+       _, err := client.Create(t.Context(), modelTestRequest())
+       require.ErrorIs(t, err, errModelCallLimit)
        require.Equal(t, 2, server.Calls())
        require.Equal(t, 2, server.Calls())
-       require.Len(t, readUsage(t, client.usage.path), 2)
+       require.Len(t, modelReadUsage(t, client.usage.path), 2)
 }
 
 func TestClientFailsWhenTheResponseFails(t *testing.T) {
 }
 
 func TestClientFailsWhenTheResponseFails(t *testing.T) {
@@ -238,69 +238,70 @@ func TestClientFailsWhenTheResponseFails(t *testing.T) {
                },
                {
                        name:   "stream ends early",
                },
                {
                        name:   "stream ends early",
-                       events: []any{map[string]any{"type": "response.output_item.done", "item": messageItem("cut")}},
+                       events: []any{map[string]any{"type": "response.output_item.done", "item": modelMessageItem("cut")}},
                        want:   "ended before the response completed",
                },
        }
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
                        want:   "ended before the response completed",
                },
        }
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
-                       server := newFakeResponses(t)
-                       server.reply = func(w http.ResponseWriter, call fakeCall) {
-                               writeEvents(w, tc.events...)
+                       server := newModelFake(t)
+                       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+                               modelWriteEvents(w, tc.events...)
                        }
 
                        }
 
-                       client := newTestClient(t, server)
-                       _, err := client.Create(t.Context(), testRequest())
+                       client := newTestModelClient(t, server)
+                       _, err := client.Create(t.Context(), modelTestRequest())
                        require.ErrorContains(t, err, tc.want)
 
                        require.ErrorContains(t, err, tc.want)
 
-                       records := readUsage(t, client.usage.path)
+                       records := modelReadUsage(t, client.usage.path)
                        require.Len(t, records, 1)
                        require.Contains(t, records[0].Error, tc.want)
                })
        }
 }
 
                        require.Len(t, records, 1)
                        require.Contains(t, records[0].Error, tc.want)
                })
        }
 }
 
-// fakeResponses is an httptest Responses endpoint. It keeps each call, and
-// reply writes the response. The default reply is one short message.
-type fakeResponses struct {
-       *httptest.Server
-       mutex sync.Mutex
-       calls []fakeCall
-       reply func(http.ResponseWriter, fakeCall)
-}
-
-// fakeCall is one call to a fake endpoint, numbered from 0.
-type fakeCall struct {
+// 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
 }
 
        number int
        header http.Header
        raw    []byte
        body   map[string]any
 }
 
-func newFakeResponses(t *testing.T) *fakeResponses {
-       t.Helper()
+// 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)
+}
 
 
-       f := &fakeResponses{
-               reply: func(w http.ResponseWriter, call fakeCall) {
-                       writeEvents(w, completedEvent([]any{messageItem("a line")}, 100, 0, 5))
-               },
-       }
+// Call returns call number n.
+func (f *ModelFake) Call(n int) ModelFakeCall {
+       f.mutex.Lock()
+       defer f.mutex.Unlock()
 
 
-       f.Server = httptest.NewServer(http.HandlerFunc(f.serve))
-       t.Cleanup(f.Close)
+       return f.calls[n]
+}
 
 
-       return f
+// Calls returns the number of calls.
+func (f *ModelFake) Calls() int {
+       f.mutex.Lock()
+       defer f.mutex.Unlock()
+
+       return len(f.calls)
 }
 
 }
 
-func (f *fakeResponses) serve(w http.ResponseWriter, r *http.Request) {
+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
        }
 
        raw, err := io.ReadAll(r.Body)
        if err != nil {
                http.Error(w, err.Error(), http.StatusBadRequest)
                return
        }
 
-       call := fakeCall{
+       call := ModelFakeCall{
                header: r.Header.Clone(),
                raw:    raw,
        }
                header: r.Header.Clone(),
                raw:    raw,
        }
@@ -320,38 +321,37 @@ func (f *fakeResponses) serve(w http.ResponseWriter, r *http.Request) {
        reply(w, call)
 }
 
        reply(w, call)
 }
 
-// Call returns call number n.
-func (f *fakeResponses) Call(n int) fakeCall {
-       f.mutex.Lock()
-       defer f.mutex.Unlock()
+func newModelFake(t *testing.T) *ModelFake {
+       t.Helper()
 
 
-       return f.calls[n]
-}
+       f := &ModelFake{
+               reply: func(w http.ResponseWriter, call ModelFakeCall) {
+                       modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("a line")}, 100, 0, 5))
+               },
+       }
 
 
-// Calls returns the number of calls.
-func (f *fakeResponses) Calls() int {
-       f.mutex.Lock()
-       defer f.mutex.Unlock()
+       f.Server = httptest.NewServer(http.HandlerFunc(f.serve))
+       t.Cleanup(f.Close)
 
 
-       return len(f.calls)
+       return f
 }
 
 }
 
-func newTestClient(t *testing.T, server *fakeResponses) *Client {
+func newTestModelClient(t *testing.T, server *ModelFake) *ModelClient {
        t.Helper()
 
        t.Helper()
 
-       return newClient(server.URL, apiKey(testKey), "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
+       return newModelClient(server.URL, ModelAPIKey(modelTestKey), "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
 }
 
 }
 
-func testRequest() Request {
-       return Request{
+func modelTestRequest() ModelRequest {
+       return ModelRequest{
                Model:  "gpt-test",
                Effort: "low",
                Model:  "gpt-test",
                Effort: "low",
-               Input:  []any{userMessage("hello")},
+               Input:  []any{modelUserMessage("hello")},
        }
 }
 
        }
 }
 
-// writeEvents writes events as a server-sent event stream.
-func writeEvents(w http.ResponseWriter, events ...any) {
+// 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)
        w.Header().Set("Content-Type", "text/event-stream")
        for _, event := range events {
                data, err := json.Marshal(event)
@@ -366,7 +366,7 @@ func writeEvents(w http.ResponseWriter, events ...any) {
        }
 }
 
        }
 }
 
-func messageItem(text string) map[string]any {
+func modelMessageItem(text string) map[string]any {
        return map[string]any{
                "type": "message",
                "role": "assistant",
        return map[string]any{
                "type": "message",
                "role": "assistant",
@@ -376,7 +376,7 @@ func messageItem(text string) map[string]any {
        }
 }
 
        }
 }
 
-func completedEvent(output []any, input, cached, out int) map[string]any {
+func modelCompletedEvent(output []any, input, cached, out int) map[string]any {
        if output == nil {
                output = []any{}
        }
        if output == nil {
                output = []any{}
        }
@@ -394,7 +394,7 @@ func completedEvent(output []any, input, cached, out int) map[string]any {
        }
 }
 
        }
 }
 
-func readUsage(t *testing.T, path string) []usageRecord {
+func modelReadUsage(t *testing.T, path string) []ModelUsageRecord {
        t.Helper()
 
        file, err := os.Open(path)
        t.Helper()
 
        file, err := os.Open(path)
@@ -404,10 +404,10 @@ func readUsage(t *testing.T, path string) []usageRecord {
                require.NoError(t, file.Close())
        }()
 
                require.NoError(t, file.Close())
        }()
 
-       var records []usageRecord
+       var records []ModelUsageRecord
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
-               var record usageRecord
+               var record ModelUsageRecord
                require.NoError(t, json.Unmarshal(scanner.Bytes(), &record))
                records = append(records, record)
        }
                require.NoError(t, json.Unmarshal(scanner.Bytes(), &record))
                records = append(records, record)
        }
@@ -416,3 +416,22 @@ func readUsage(t *testing.T, path string) []usageRecord {
 
        return records
 }
 
        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,
+       }
+}