feat(api): add OpenAI compatibility for image models
- Introduced OpenAI-compatible image model support in the API, enabling integration through image generation and editing endpoints. - Added registry type for OpenAIImageModelType to classify and validate compatibility. - Implemented request handling for OpenAI-compatible image models, including JSON and multipart formats. - Enhanced executor methods to support OpenAI-compatible image streaming and non-streaming requests. - Included tests to validate model registration, streaming behavior, and multipart payload formatting.
This commit is contained in:
+38
-24
@@ -1208,30 +1208,7 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
||||
}
|
||||
if strings.EqualFold(compat.Name, compatName) {
|
||||
isCompatAuth = true
|
||||
// Convert compatibility models to registry models
|
||||
ms := make([]*ModelInfo, 0, len(compat.Models))
|
||||
for j := range compat.Models {
|
||||
m := compat.Models[j]
|
||||
// Use alias as model ID, fallback to name if alias is empty
|
||||
modelID := m.Alias
|
||||
if modelID == "" {
|
||||
modelID = m.Name
|
||||
}
|
||||
thinking := m.Thinking
|
||||
if thinking == nil {
|
||||
thinking = ®istry.ThinkingSupport{Levels: []string{"low", "medium", "high"}}
|
||||
}
|
||||
ms = append(ms, &ModelInfo{
|
||||
ID: modelID,
|
||||
Object: "model",
|
||||
Created: time.Now().Unix(),
|
||||
OwnedBy: compat.Name,
|
||||
Type: "openai-compatibility",
|
||||
DisplayName: modelID,
|
||||
UserDefined: false,
|
||||
Thinking: thinking,
|
||||
})
|
||||
}
|
||||
ms := buildOpenAICompatibilityConfigModels(compat)
|
||||
// Register and return
|
||||
if len(ms) > 0 {
|
||||
if providerKey == "" {
|
||||
@@ -1578,6 +1555,43 @@ type modelEntry interface {
|
||||
GetAlias() string
|
||||
}
|
||||
|
||||
func buildOpenAICompatibilityConfigModels(compat *config.OpenAICompatibility) []*ModelInfo {
|
||||
if compat == nil || len(compat.Models) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
models := make([]*ModelInfo, 0, len(compat.Models))
|
||||
for i := range compat.Models {
|
||||
model := compat.Models[i]
|
||||
modelID := strings.TrimSpace(model.Alias)
|
||||
if modelID == "" {
|
||||
modelID = strings.TrimSpace(model.Name)
|
||||
}
|
||||
if modelID == "" {
|
||||
continue
|
||||
}
|
||||
modelType := "openai-compatibility"
|
||||
if model.Image {
|
||||
modelType = registry.OpenAIImageModelType
|
||||
}
|
||||
thinking := model.Thinking
|
||||
if thinking == nil && !model.Image {
|
||||
thinking = ®istry.ThinkingSupport{Levels: []string{"low", "medium", "high"}}
|
||||
}
|
||||
models = append(models, &ModelInfo{
|
||||
ID: modelID,
|
||||
Object: "model",
|
||||
Created: now,
|
||||
OwnedBy: compat.Name,
|
||||
Type: modelType,
|
||||
DisplayName: modelID,
|
||||
UserDefined: false,
|
||||
Thinking: thinking,
|
||||
})
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func buildConfigModels[T modelEntry](models []T, ownedBy, modelType string) []*ModelInfo {
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
internalregistry "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
|
||||
)
|
||||
@@ -63,3 +64,71 @@ func TestRegisterModelsForAuth_UsesPreMergedExcludedModelsAttribute(t *testing.T
|
||||
t.Fatal("expected global excluded model to be present when attribute override is set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterModelsForAuth_OpenAICompatibilityImageModelType(t *testing.T) {
|
||||
service := &Service{
|
||||
cfg: &config.Config{
|
||||
OpenAICompatibility: []config.OpenAICompatibility{
|
||||
{
|
||||
Name: "images",
|
||||
BaseURL: "https://example.com/v1",
|
||||
Models: []config.OpenAICompatibilityModel{
|
||||
{Name: "upstream-image", Alias: "compat-image", Image: true},
|
||||
{Name: "upstream-chat", Alias: "compat-chat"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
auth := &coreauth.Auth{
|
||||
ID: "auth-openai-compat-image",
|
||||
Provider: "openai-compatibility",
|
||||
Status: coreauth.StatusActive,
|
||||
Attributes: map[string]string{
|
||||
"auth_kind": "api_key",
|
||||
"compat_name": "images",
|
||||
"provider_key": "images",
|
||||
},
|
||||
}
|
||||
|
||||
modelRegistry := internalregistry.GetGlobalRegistry()
|
||||
modelRegistry.UnregisterClient(auth.ID)
|
||||
t.Cleanup(func() {
|
||||
modelRegistry.UnregisterClient(auth.ID)
|
||||
})
|
||||
|
||||
service.registerModelsForAuth(auth)
|
||||
|
||||
models := modelRegistry.GetModelsForClient(auth.ID)
|
||||
var imageModel *internalregistry.ModelInfo
|
||||
var chatModel *internalregistry.ModelInfo
|
||||
for _, model := range models {
|
||||
if model == nil {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(model.ID) {
|
||||
case "compat-image":
|
||||
imageModel = model
|
||||
case "compat-chat":
|
||||
chatModel = model
|
||||
}
|
||||
}
|
||||
if imageModel == nil {
|
||||
t.Fatal("expected compat-image to be registered")
|
||||
}
|
||||
if imageModel.Type != internalregistry.OpenAIImageModelType {
|
||||
t.Fatalf("image model type = %q, want %q", imageModel.Type, internalregistry.OpenAIImageModelType)
|
||||
}
|
||||
if imageModel.Thinking != nil {
|
||||
t.Fatalf("image model thinking = %+v, want nil", imageModel.Thinking)
|
||||
}
|
||||
if chatModel == nil {
|
||||
t.Fatal("expected compat-chat to be registered")
|
||||
}
|
||||
if chatModel.Type != "openai-compatibility" {
|
||||
t.Fatalf("chat model type = %q, want openai-compatibility", chatModel.Type)
|
||||
}
|
||||
if chatModel.Thinking == nil {
|
||||
t.Fatal("expected chat model to keep default thinking support")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user