backend: test that no tools loaded omits tools field
Regression guard for tool-free model compatibility: an agent with an empty tool set must produce requests with no "tools"/"tool_choice" field. Covers OpenAI, OpenRouter, Ollama, and Anthropic.
This commit is contained in:
parent
86db9c797b
commit
eff0880737
|
|
@ -0,0 +1,105 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// These tests lock in the "no tools loaded" compatibility path: an agent with
|
||||
// an empty autoLoadTools set (and no /tool_load) produces an empty tool slice,
|
||||
// and every backend must then emit a request with NO "tools" field. This keeps
|
||||
// Ollie compatible with models that do not support tool/function calling — the
|
||||
// answer for such a model is simply to use a tool-free agent.
|
||||
|
||||
// captureBody spins up an httptest server, records the raw request body, drains
|
||||
// the (empty) stream, and returns the decoded top-level JSON object.
|
||||
func captureBody(t *testing.T, build func(baseURL string) (Backend, error)) map[string]json.RawMessage {
|
||||
t.Helper()
|
||||
var rawBody []byte
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rawBody, _ = io.ReadAll(r.Body)
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
fmt.Fprint(w, "data: [DONE]\n\n")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
b, err := build(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msgs := []Message{{Role: "user", Content: "hi"}}
|
||||
// No tools: the empty slice must yield a request without a tools field.
|
||||
ch, err := b.ChatStream(context.Background(), msgs, nil, GenerationParams{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for range ch { //nolint:revive
|
||||
}
|
||||
|
||||
var top map[string]json.RawMessage
|
||||
json.Unmarshal(rawBody, &top) //nolint:errcheck
|
||||
return top
|
||||
}
|
||||
|
||||
func assertNoToolsField(t *testing.T, top map[string]json.RawMessage) {
|
||||
t.Helper()
|
||||
if raw, ok := top["tools"]; ok {
|
||||
t.Errorf("request must omit \"tools\" when no tools are loaded; got tools=%s", raw)
|
||||
}
|
||||
if raw, ok := top["tool_choice"]; ok {
|
||||
t.Errorf("request must omit \"tool_choice\" when no tools are loaded; got tool_choice=%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoTools_OpenAIOmitsToolsField(t *testing.T) {
|
||||
top := captureBody(t, func(baseURL string) (Backend, error) {
|
||||
b, err := NewOpenAI("openai", baseURL, "key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.SetModel("gpt-4o")
|
||||
return b, nil
|
||||
})
|
||||
assertNoToolsField(t, top)
|
||||
}
|
||||
|
||||
func TestNoTools_OpenRouterOmitsToolsField(t *testing.T) {
|
||||
top := captureBody(t, func(baseURL string) (Backend, error) {
|
||||
b, err := NewOpenAI("openrouter", baseURL, "key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.SetModel("mistral-7b")
|
||||
return b, nil
|
||||
})
|
||||
assertNoToolsField(t, top)
|
||||
}
|
||||
|
||||
func TestNoTools_OllamaOmitsToolsField(t *testing.T) {
|
||||
top := captureBody(t, func(baseURL string) (Backend, error) {
|
||||
return NewOllama(baseURL)
|
||||
})
|
||||
assertNoToolsField(t, top)
|
||||
}
|
||||
|
||||
func TestNoTools_AnthropicOmitsToolsField(t *testing.T) {
|
||||
top := captureBody(t, func(baseURL string) (Backend, error) {
|
||||
b, err := NewAnthropic("key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.baseURL = u
|
||||
return b, nil
|
||||
})
|
||||
assertNoToolsField(t, top)
|
||||
}
|
||||
Loading…
Reference in New Issue