vibe-proxy/backend/internal/api/server_grok_models_test.go
2026-08-24 00:10:41 +02:00

218 lines
8.2 KiB
Go

package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/client/grokbuild"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
)
func TestModelsDispatchByGrokShellUserAgent(t *testing.T) {
modelRegistry := registry.GetGlobalRegistry()
clientID := "test-grok-shell-model-list"
modelRegistry.RegisterClient(clientID, "openai", []*registry.ModelInfo{
{ID: "grok-shell-openai-model", DisplayName: "Grok Shell Model", ContextLength: 256000, Thinking: &registry.ThinkingSupport{Levels: []string{"high"}}},
})
modelRegistry.RegisterClient(clientID+"-claude", "claude", []*registry.ModelInfo{
{ID: "grok-shell-claude-model", DisplayName: "Claude Catalog Model", ContextLength: 200000},
})
t.Cleanup(func() {
modelRegistry.UnregisterClient(clientID)
modelRegistry.UnregisterClient(clientID + "-claude")
})
server := newTestServer(t)
for _, userAgent := range []string{
"grok-shell/0.2.119 (macos; aarch64)",
"grok-pager/0.2.119 grok-shell/0.2.119 (macos; aarch64)",
} {
t.Run(userAgent, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "https://proxy.example.test/v1/models?client_version", nil)
req.Header.Set("Authorization", "Bearer test-key")
req.Header.Set("User-Agent", userAgent)
recorder := httptest.NewRecorder()
server.engine.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
var response struct {
Object string `json:"object"`
Data []struct {
ID string `json:"id"`
Model string `json:"model"`
Name string `json:"name"`
ContextWindow int `json:"context_window"`
APIBackend string `json:"api_backend"`
SupportedInAPI bool `json:"supported_in_api"`
ReasoningEfforts []struct {
Value string `json:"value"`
} `json:"reasoning_efforts"`
} `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("decode response: %v; body=%s", err, recorder.Body.String())
}
if response.Object != "list" {
t.Fatalf("object = %q, want list", response.Object)
}
var foundOpenAI, foundClaude bool
for _, model := range response.Data {
switch model.ID {
case "grok-shell-openai-model":
foundOpenAI = true
if model.Model != model.ID || model.Name != "Grok Shell Model" || model.ContextWindow != 256000 {
t.Fatalf("OpenAI model mapping = %#v", model)
}
if model.APIBackend != "responses" || !model.SupportedInAPI {
t.Fatalf("OpenAI model routing fields = %#v", model)
}
if len(model.ReasoningEfforts) != 1 || model.ReasoningEfforts[0].Value != "high" {
t.Fatalf("OpenAI reasoning efforts = %#v", model.ReasoningEfforts)
}
case "grok-shell-claude-model":
foundClaude = true
if model.Model != model.ID || model.Name != "Claude Catalog Model" || model.ContextWindow != 200000 {
t.Fatalf("Claude model mapping = %#v", model)
}
if len(model.ReasoningEfforts) != 0 {
t.Fatalf("Claude reasoning efforts = %#v, want none", model.ReasoningEfforts)
}
}
}
if !foundOpenAI {
t.Fatalf("registered OpenAI Grok model missing: %s", recorder.Body.String())
}
if !foundClaude {
t.Fatalf("registered Claude Grok model missing: %s", recorder.Body.String())
}
})
}
}
func TestModelsDispatchKeepsOrdinaryOpenAIResponse(t *testing.T) {
modelRegistry := registry.GetGlobalRegistry()
clientID := "test-ordinary-model-list-after-grok"
modelRegistry.RegisterClient(clientID, "openai", []*registry.ModelInfo{{ID: "ordinary-model"}})
t.Cleanup(func() { modelRegistry.UnregisterClient(clientID) })
server := newTestServer(t)
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
req.Header.Set("Authorization", "Bearer test-key")
req.Header.Set("User-Agent", "curl/8.7.1")
recorder := httptest.NewRecorder()
server.engine.ServeHTTP(recorder, req)
var response struct {
Object string `json:"object"`
Data []map[string]any `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Object != "list" {
t.Fatalf("object = %q, want list", response.Object)
}
found := false
for _, model := range response.Data {
if _, exists := model["api_backend"]; exists {
t.Fatalf("ordinary response contains Grok field: %#v", model)
}
if id, ok := model["id"].(string); ok && id == "ordinary-model" {
found = true
}
}
if !found {
t.Fatalf("registered ordinary model missing: %s", recorder.Body.String())
}
}
func TestGrokHomeModelAdapterOmitsReasoning(t *testing.T) {
models := grokModelsFromHomeEntries([]homeModelEntry{
{id: "home-model", displayName: "Home Model", contextLength: 1234},
{id: "home-model-without-context", displayName: "No Context Model"},
})
if len(models) != 2 {
t.Fatalf("Home model count = %d, want 2", len(models))
}
if models[0].ID != "home-model" || models[0].DisplayName != "Home Model" || models[0].ContextLength != 1234 {
t.Fatalf("Home model adapter = %#v", models[0])
}
if models[1].ID != "home-model-without-context" || models[1].DisplayName != "No Context Model" || models[1].ContextLength != 0 {
t.Fatalf("Home zero-context adapter = %#v", models[1])
}
response := grokbuild.BuildResponse(models)
if len(response.Data) != 2 || response.Data[0].ReasoningEfforts != nil || response.Data[1].ReasoningEfforts != nil {
t.Fatalf("Home reasoning efforts = %#v", response.Data)
}
wire, errMarshal := json.Marshal(response)
if errMarshal != nil {
t.Fatalf("marshal Home response: %v", errMarshal)
}
var wireResponse struct {
Data []map[string]json.RawMessage `json:"data"`
}
if errUnmarshal := json.Unmarshal(wire, &wireResponse); errUnmarshal != nil {
t.Fatalf("decode Home response JSON: %v; body=%s", errUnmarshal, wire)
}
if len(wireResponse.Data) != 2 {
t.Fatalf("wire Home model count = %d, want 2; body=%s", len(wireResponse.Data), wire)
}
contextWindow, exists := wireResponse.Data[0]["context_window"]
if !exists {
t.Fatalf("Home model context_window missing from wire response: %s", wire)
}
var gotContextWindow int
if errDecode := json.Unmarshal(contextWindow, &gotContextWindow); errDecode != nil {
t.Fatalf("decode Home context_window: %v", errDecode)
}
if gotContextWindow != 1234 {
t.Fatalf("Home context_window = %d, want 1234", gotContextWindow)
}
if _, exists := wireResponse.Data[0]["reasoning_efforts"]; exists {
t.Fatalf("Home model contains omitted reasoning_efforts: %s", wire)
}
if _, exists := wireResponse.Data[1]["context_window"]; exists {
t.Fatalf("zero-context Home model contains omitted context_window: %s", wire)
}
if _, exists := wireResponse.Data[1]["reasoning_efforts"]; exists {
t.Fatalf("zero-context Home model contains omitted reasoning_efforts: %s", wire)
}
}
func TestGrokModelsPreferHomeOverRegistry(t *testing.T) {
previousHome := home.Current()
home.ClearCurrent()
t.Cleanup(func() { home.SetCurrent(previousHome) })
modelRegistry := registry.GetGlobalRegistry()
clientID := "test-grok-home-source"
modelRegistry.RegisterClient(clientID, "openai", []*registry.ModelInfo{{ID: "local-only-model"}})
t.Cleanup(func() { modelRegistry.UnregisterClient(clientID) })
server := newTestServer(t)
server.cfg.Home.Enabled = true
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
req.Header.Set("Authorization", "Bearer test-key")
req.Header.Set("User-Agent", "grok-shell/0.2.119")
recorder := httptest.NewRecorder()
ginContext, _ := gin.CreateTestContext(recorder)
ginContext.Request = req
server.handleGrokModels(ginContext)
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusServiceUnavailable, recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), "home control center unavailable") {
t.Fatalf("Home failure response missing expected error: %s", recorder.Body.String())
}
if strings.Contains(recorder.Body.String(), "local-only-model") {
t.Fatalf("Home failure response leaked local registry model: %s", recorder.Body.String())
}
}