mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-24 16:45:45 +08:00
feat(management): add support for static and plugin model definitions retrieval by channel
- Implement fallback logic in model definitions handler to query plugin host and global registry for unknown channels. - Add sorting for registry model definitions by provider and ID to ensure consistent responses. Closes: #6736
This commit is contained in:
@@ -2,9 +2,11 @@ package management
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/router-for-me/CLIProxyAPI/v8/internal/pluginhost"
|
||||
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
|
||||
)
|
||||
|
||||
@@ -21,7 +23,10 @@ func (h *Handler) GetStaticModelDefinitions(c *gin.Context) {
|
||||
}
|
||||
|
||||
models := registry.GetStaticModelDefinitionsByChannel(channel)
|
||||
if models == nil {
|
||||
if len(models) == 0 {
|
||||
models = h.pluginModelDefinitionsByChannel(channel)
|
||||
}
|
||||
if len(models) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown channel", "channel": channel})
|
||||
return
|
||||
}
|
||||
@@ -31,3 +36,30 @@ func (h *Handler) GetStaticModelDefinitions(c *gin.Context) {
|
||||
"models": models,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) pluginModelDefinitionsByChannel(channel string) []*registry.ModelInfo {
|
||||
var host *pluginhost.Host
|
||||
if h != nil {
|
||||
h.mu.Lock()
|
||||
host = h.pluginHost
|
||||
h.mu.Unlock()
|
||||
}
|
||||
if host != nil {
|
||||
if models := host.ModelsForProvider(channel); len(models) > 0 {
|
||||
return models
|
||||
}
|
||||
}
|
||||
models := registry.GetGlobalRegistry().GetAvailableModelsByProvider(channel)
|
||||
if len(models) > 1 {
|
||||
sort.Slice(models, func(i, j int) bool {
|
||||
if models[i] == nil {
|
||||
return models[j] != nil
|
||||
}
|
||||
if models[j] == nil {
|
||||
return false
|
||||
}
|
||||
return models[i].ID < models[j].ID
|
||||
})
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
153
internal/api/handlers/management/model_definitions_test.go
Normal file
153
internal/api/handlers/management/model_definitions_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v8/internal/pluginhost"
|
||||
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
|
||||
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
|
||||
)
|
||||
|
||||
type staticPluginModelProvider struct {
|
||||
provider string
|
||||
models []pluginapi.ModelInfo
|
||||
}
|
||||
|
||||
func (p staticPluginModelProvider) StaticModels(context.Context, pluginapi.StaticModelRequest) (pluginapi.ModelResponse, error) {
|
||||
return pluginapi.ModelResponse{
|
||||
Provider: p.provider,
|
||||
Models: p.models,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p staticPluginModelProvider) ModelsForAuth(context.Context, pluginapi.AuthModelRequest) (pluginapi.ModelResponse, error) {
|
||||
return pluginapi.ModelResponse{}, nil
|
||||
}
|
||||
|
||||
func invokeGetStaticModelDefinitions(t *testing.T, h *Handler, channel string) (*httptest.ResponseRecorder, struct {
|
||||
Channel string `json:"channel"`
|
||||
Error string `json:"error"`
|
||||
Models []registry.ModelInfo `json:"models"`
|
||||
}) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(rec)
|
||||
ctx.Params = gin.Params{{Key: "channel", Value: channel}}
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/v8/management/routing/model-definitions/"+channel, nil)
|
||||
h.GetStaticModelDefinitions(ctx)
|
||||
|
||||
var body struct {
|
||||
Channel string `json:"channel"`
|
||||
Error string `json:"error"`
|
||||
Models []registry.ModelInfo `json:"models"`
|
||||
}
|
||||
if errUnmarshal := json.Unmarshal(rec.Body.Bytes(), &body); errUnmarshal != nil {
|
||||
t.Fatalf("unmarshal response body %q: %v", rec.Body.String(), errUnmarshal)
|
||||
}
|
||||
return rec, body
|
||||
}
|
||||
|
||||
func TestGetStaticModelDefinitions_BuiltInChannel(t *testing.T) {
|
||||
h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, nil)
|
||||
|
||||
rec, body := invokeGetStaticModelDefinitions(t, h, "claude")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if body.Channel != "claude" {
|
||||
t.Fatalf("channel = %q, want %q", body.Channel, "claude")
|
||||
}
|
||||
if len(body.Models) == 0 {
|
||||
t.Fatal("expected non-empty models for built-in channel claude")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStaticModelDefinitions_FallsBackToPluginStaticModels(t *testing.T) {
|
||||
const provider = "workbuddy"
|
||||
reg := registry.GetGlobalRegistry()
|
||||
clientID := "workbuddy-cred-1.json"
|
||||
reg.RegisterClient(clientID, provider, []*registry.ModelInfo{
|
||||
{ID: "workbuddy-from-registry", DisplayName: "WorkBuddy Registry"},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
reg.UnregisterClient(clientID)
|
||||
})
|
||||
|
||||
host := pluginhost.New()
|
||||
pluginID := "workbuddy-plugin"
|
||||
host.RegisterPluginForTest(pluginID, pluginapi.Plugin{
|
||||
Capabilities: pluginapi.Capabilities{
|
||||
ModelProvider: staticPluginModelProvider{
|
||||
provider: provider,
|
||||
models: []pluginapi.ModelInfo{
|
||||
{ID: "workbuddy-pro", DisplayName: "WorkBuddy Pro"},
|
||||
{ID: "workbuddy-lite", DisplayName: "WorkBuddy Lite"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
host.RegisterModels(t.Context(), reg)
|
||||
host.RegisterExecutors(coreauth.NewManager(nil, nil, nil), reg)
|
||||
|
||||
h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, nil)
|
||||
h.SetPluginHost(host)
|
||||
|
||||
rec, body := invokeGetStaticModelDefinitions(t, h, "WorkBuddy")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if body.Channel != provider {
|
||||
t.Fatalf("channel = %q, want %q", body.Channel, provider)
|
||||
}
|
||||
if len(body.Models) != 2 || body.Models[0].ID != "workbuddy-pro" || body.Models[1].ID != "workbuddy-lite" {
|
||||
t.Fatalf("models = %#v, want [workbuddy-pro workbuddy-lite]", body.Models)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStaticModelDefinitions_FallsBackToRegistryModelsForPerAuthPlugin(t *testing.T) {
|
||||
const provider = "qoderwork"
|
||||
reg := registry.GetGlobalRegistry()
|
||||
clientID := "qoderwork-cred-1.json"
|
||||
reg.RegisterClient(clientID, provider, []*registry.ModelInfo{
|
||||
{ID: "qwen3.8-plus-QoderWork", DisplayName: "Qwen 3.8 Plus"},
|
||||
{ID: "qwen3.8-flash-QoderWork", DisplayName: "Qwen 3.8 Flash"},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
reg.UnregisterClient(clientID)
|
||||
})
|
||||
|
||||
h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, nil)
|
||||
h.SetPluginHost(pluginhost.New())
|
||||
|
||||
rec, body := invokeGetStaticModelDefinitions(t, h, "QoderWork")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if body.Channel != provider {
|
||||
t.Fatalf("channel = %q, want %q", body.Channel, provider)
|
||||
}
|
||||
if len(body.Models) != 2 || body.Models[0].ID != "qwen3.8-flash-QoderWork" || body.Models[1].ID != "qwen3.8-plus-QoderWork" {
|
||||
t.Fatalf("models = %#v, want [qwen3.8-flash-QoderWork qwen3.8-plus-QoderWork]", body.Models)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStaticModelDefinitions_RejectsUnknownChannel(t *testing.T) {
|
||||
h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, nil)
|
||||
h.SetPluginHost(pluginhost.New())
|
||||
|
||||
rec, body := invokeGetStaticModelDefinitions(t, h, "nonexistent-provider")
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
||||
}
|
||||
if body.Error != "unknown channel" || body.Channel != "nonexistent-provider" {
|
||||
t.Fatalf("body = %#v, want unknown channel error for nonexistent-provider", body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user