diff --git a/internal/provider/detect_test.go b/internal/provider/detect_test.go index 34056e52..89334c65 100644 --- a/internal/provider/detect_test.go +++ b/internal/provider/detect_test.go @@ -133,6 +133,81 @@ func TestDetectProtocols(t *testing.T) { } } +// Some Responses relays reject the string shorthand and require message arrays. +func TestResponsesProbesUseArrayInput(t *testing.T) { + detectHome(t) + const model = "gpt-test" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method + " " + r.URL.Path { + case "GET /v1/models": + io.WriteString(w, `{"data":[{"id":"`+model+`"}]}`) + case "POST /v1/responses": + var in struct { + Model string `json:"model"` + Input []struct { + Type string `json:"type"` + Role string `json:"role"` + Content []struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"content"` + } `json:"input"` + } + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + http.Error(w, `{"error":{"message":"input must be an array"}}`, http.StatusBadRequest) + return + } + if in.Model != model || len(in.Input) != 1 || in.Input[0].Type != "message" || + in.Input[0].Role != "user" || len(in.Input[0].Content) != 1 || + in.Input[0].Content[0].Type != "input_text" || in.Input[0].Content[0].Text != "hi" { + http.Error(w, `{"error":{"message":"expected a user text message"}}`, http.StatusBadRequest) + return + } + io.WriteString(w, `{"object":"response","status":"completed","output":[]}`) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + p := Provider{ID: "relay", Key: "sk-relay", Responses: srv.URL + "/v1", Models: []string{model}} + ctx := context.Background() + check := func(t *testing.T, r Result) { + t.Helper() + if !r.OK || r.Status != http.StatusOK || r.Protocol != Responses || r.Model != model { + t.Fatalf("Responses probe: %+v", r) + } + } + t.Run("Detect", func(t *testing.T) { + rs, err := p.Detect(ctx, srv.URL, model) + if err != nil || len(rs) != len(Protocols) { + t.Fatalf("Detect: %v, %+v", err, rs) + } + check(t, rs[1].Result) + }) + t.Run("DetectModels", func(t *testing.T) { + each, sum, err := p.DetectModels(ctx, srv.URL, []string{model}) + if err != nil || len(each) != 1 || len(each[0].Results) != len(Protocols) || len(sum) != len(Protocols) { + t.Fatalf("DetectModels: %v, %+v, %+v", err, each, sum) + } + check(t, each[0].Results[1].Result) + check(t, sum[1].Result) + }) + t.Run("Test", func(t *testing.T) { + rs := p.Test(ctx) + if len(rs) != 1 { + t.Fatalf("Test: %+v", rs) + } + check(t, rs[0]) + }) + t.Run("TestModels", func(t *testing.T) { + rs := p.TestModels(ctx, []string{model}) + if len(rs) != 1 { + t.Fatalf("TestModels: %+v", rs) + } + check(t, rs[0]) + }) +} + // A model typed is the one asked on all three, and the vendor's list isn't // asked for; a URL the provider has for an API is asked rather than the // one typed; no URL at all says so. diff --git a/internal/provider/test.go b/internal/provider/test.go index c9809c8f..5e5adb37 100644 --- a/internal/provider/test.go +++ b/internal/provider/test.go @@ -124,7 +124,7 @@ func tinyBody(q Provider, proto Protocol, model string) (url, body string) { } return q.Chat + "/chat/completions", fmt.Sprintf(`{"model":%q,"messages":[{"role":"user","content":"hi"}],"max_tokens":16}`, model) case Responses: - return q.Responses + "/responses", fmt.Sprintf(`{"model":%q,"input":"hi","max_output_tokens":16}`, model) + return q.Responses + "/responses", fmt.Sprintf(`{"model":%q,"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}],"max_output_tokens":16}`, model) case Anthropic: return q.Anthropic + "/v1/messages", fmt.Sprintf(`{"model":%q,"max_tokens":16,"messages":[{"role":"user","content":"hi"}]}`, model) }