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:
Ollie Agent 2026-09-07 13:19:35 +02:00
parent 86db9c797b
commit eff0880737
1 changed files with 105 additions and 0 deletions

View File

@ -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)
}