152 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|