Files
Assistant/adapters/models/openai_compatible_local_test.go
2026-08-29 22:40:54 +03:00

152 lines
5.8 KiB
Go

package models
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type captureRoundTripper struct {
authorization string
body []byte
}
type modelsMetadataRoundTripper struct{}
func (modelsMetadataRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"medium-model","owned_by":"llama.cpp","meta":{}}]}`)),
Header: make(http.Header),
Request: request,
}, nil
}
func TestOpenAICompatibleLocalAdapterNormalizesMinimalModelMetadata(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", modelsMetadataRoundTripper{})
if err != nil {
t.Fatal(err)
}
models, probe := adapter.Discover(context.Background())
if probe.Health != capability.HealthAvailable || len(models) != 1 {
t.Fatalf("unexpected discovery result: probe=%+v models=%+v", probe, models)
}
if models[0].MaxContextBytes != compatibleDefaultContextBytes {
t.Fatalf("context bound=%d, want %d", models[0].MaxContextBytes, compatibleDefaultContextBytes)
}
if len(models[0].Capabilities) != 1 || models[0].Capabilities[0] != capability.CapabilityAdvisory {
t.Fatalf("capabilities=%v, want advisory", models[0].Capabilities)
}
}
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
if err != nil {
t.Fatal(err)
}
if adapter.client.Timeout != 47*time.Second {
t.Fatalf("unexpected client timeout: %s", adapter.client.Timeout)
}
}
func TestOpenAICompatibleLocalAdapterRejectsNonPositiveTimeout(t *testing.T) {
if _, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", nil, 0); err == nil {
t.Fatal("expected non-positive timeout to be rejected")
}
}
func (t *captureRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
t.authorization = request.Header.Get("Authorization")
if request.Body != nil {
t.body, _ = io.ReadAll(request.Body)
}
body := `{"data":[]}`
if strings.HasSuffix(request.URL.Path, "/chat/completions") {
body = `{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"ok"}}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`
}
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header), Request: request}, nil
}
func TestOpenAICompatibleLocalAdapterUsesResolvedAPIKey(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKey("http://gateway/v1", "resolved-key", transport)
if err != nil {
t.Fatal(err)
}
_, _ = adapter.Discover(context.Background())
if transport.authorization != "Bearer resolved-key" {
t.Fatalf("unexpected authorization header: %q", transport.authorization)
}
}
func TestOpenAICompatibleLocalAdapterDoesNotSendAuthorizationWithoutKey(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, _ = adapter.Discover(context.Background())
if transport.authorization != "" {
t.Fatalf("unexpected authorization header")
}
}
func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope", Message: "help", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1, ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("medium-model")})
if err != nil {
t.Fatal(err)
}
var payload struct {
ChatTemplateKwargs struct {
EnableThinking bool `json:"enable_thinking"`
} `json:"chat_template_kwargs"`
}
if err := json.Unmarshal(transport.body, &payload); err != nil {
t.Fatal(err)
}
if payload.ChatTemplateKwargs.EnableThinking {
t.Fatal("expected bounded advisory to disable template thinking")
}
}
func TestOpenAICompatibleLocalAdapterPassesLocalContextToAdvisory(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: "request-context", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope",
Message: "Продолжи разговор", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1,
ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("local-model"),
Context: []cognitive.ContextExcerpt{{ID: "focus", SourceKind: "situation", SourceID: "focus-1", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "Активный проект: Домовой"}},
})
if err != nil {
t.Fatal(err)
}
var payload struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(transport.body, &payload); err != nil {
t.Fatal(err)
}
if len(payload.Messages) != 2 || !strings.Contains(payload.Messages[1].Content, "Активный проект: Домовой") {
t.Fatalf("local context was not passed to advisory: %+v", payload.Messages)
}
}