mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor(models): unify provider metadata around backend catalog (#2896)
* feat(models): unify provider metadata around backend catalog - Move shared provider metadata and alias normalization into backend-owned provider catalog - Expose display, fetch, auth, and default model metadata through /api/models provider_options - Replace frontend static provider registry with catalog-driven selection, validation, grouping, and fallback rendering - Treat provider default api_base as placeholder and effective fetch/test base while keep submitted api_base separate from derived defaults - Add model page retry handling, touched locale updates, and provider metadata assertions in backend tests * fix(models): canonicalize backend provider aliases and common models * fix(models): restore deepseek common model recommendations
This commit is contained in:
@@ -18,54 +18,6 @@ import (
|
||||
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||
)
|
||||
|
||||
type protocolMeta struct {
|
||||
defaultAPIBase string
|
||||
emptyAPIKeyAllowed bool
|
||||
}
|
||||
|
||||
var protocolMetaByName = map[string]protocolMeta{
|
||||
"openai": {defaultAPIBase: "https://api.openai.com/v1"},
|
||||
"venice": {defaultAPIBase: "https://api.venice.ai/api/v1"},
|
||||
"openrouter": {defaultAPIBase: "https://openrouter.ai/api/v1"},
|
||||
"litellm": {defaultAPIBase: "http://localhost:4000/v1"},
|
||||
"lmstudio": {defaultAPIBase: "http://localhost:1234/v1", emptyAPIKeyAllowed: true},
|
||||
"novita": {defaultAPIBase: "https://api.novita.ai/openai"},
|
||||
"groq": {defaultAPIBase: "https://api.groq.com/openai/v1"},
|
||||
"zhipu": {defaultAPIBase: "https://open.bigmodel.cn/api/paas/v4"},
|
||||
"gemini": {defaultAPIBase: "https://generativelanguage.googleapis.com/v1beta"},
|
||||
"nvidia": {defaultAPIBase: "https://integrate.api.nvidia.com/v1"},
|
||||
"ollama": {defaultAPIBase: "http://localhost:11434/v1", emptyAPIKeyAllowed: true},
|
||||
"moonshot": {defaultAPIBase: "https://api.moonshot.cn/v1"},
|
||||
"shengsuanyun": {defaultAPIBase: "https://router.shengsuanyun.com/api/v1"},
|
||||
"siliconflow": {defaultAPIBase: "https://api.siliconflow.cn/v1"},
|
||||
"deepseek": {defaultAPIBase: "https://api.deepseek.com/v1"},
|
||||
"cerebras": {defaultAPIBase: "https://api.cerebras.ai/v1"},
|
||||
"vivgrid": {defaultAPIBase: "https://api.vivgrid.com/v1"},
|
||||
"volcengine": {defaultAPIBase: "https://ark.cn-beijing.volces.com/api/v3"},
|
||||
"qwen": {defaultAPIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1"},
|
||||
"qwen-portal": {defaultAPIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1"},
|
||||
"qwen-intl": {defaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"},
|
||||
"qwen-international": {defaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"},
|
||||
"dashscope-intl": {defaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"},
|
||||
"qwen-us": {defaultAPIBase: "https://dashscope-us.aliyuncs.com/compatible-mode/v1"},
|
||||
"dashscope-us": {defaultAPIBase: "https://dashscope-us.aliyuncs.com/compatible-mode/v1"},
|
||||
"coding-plan": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/v1"},
|
||||
"alibaba-coding": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/v1"},
|
||||
"qwen-coding": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/v1"},
|
||||
"coding-plan-anthropic": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"},
|
||||
"alibaba-coding-anthropic": {defaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"},
|
||||
"zai": {defaultAPIBase: "https://api.z.ai/api/coding/paas/v4"},
|
||||
"vllm": {defaultAPIBase: "http://localhost:8000/v1", emptyAPIKeyAllowed: true},
|
||||
"mistral": {defaultAPIBase: "https://api.mistral.ai/v1"},
|
||||
"avian": {defaultAPIBase: "https://api.avian.io/v1"},
|
||||
"minimax": {defaultAPIBase: "https://api.minimaxi.com/v1"},
|
||||
"longcat": {defaultAPIBase: "https://api.longcat.chat/openai"},
|
||||
"modelscope": {defaultAPIBase: "https://api-inference.modelscope.cn/v1"},
|
||||
"mimo": {defaultAPIBase: "https://api.xiaomimimo.com/v1"},
|
||||
"anthropic": {defaultAPIBase: "https://api.anthropic.com/v1"},
|
||||
"anthropic-messages": {defaultAPIBase: "https://api.anthropic.com/v1"},
|
||||
}
|
||||
|
||||
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
|
||||
func createClaudeAuthProvider() (LLMProvider, error) {
|
||||
cred, err := getCredential("anthropic")
|
||||
@@ -184,7 +136,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
provider.SetProviderName(protocol)
|
||||
return finalizeProviderFromConfig(provider, modelID, cfg)
|
||||
|
||||
case "azure", "azure-openai":
|
||||
case "azure":
|
||||
// Azure OpenAI uses deployment-based URLs, api-key header auth,
|
||||
// and always sends max_completion_tokens.
|
||||
if cfg.APIKey() == "" {
|
||||
@@ -241,9 +193,8 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
|
||||
case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "nvidia", "venice",
|
||||
"ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
|
||||
"vivgrid", "volcengine", "vllm", "qwen", "qwen-portal", "qwen-intl", "qwen-international", "dashscope-intl",
|
||||
"qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita",
|
||||
"coding-plan", "alibaba-coding", "qwen-coding", "zai", "mimo":
|
||||
"vivgrid", "volcengine", "vllm", "qwen-portal", "qwen-intl", "qwen-us", "mistral",
|
||||
"avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo":
|
||||
// All other OpenAI-compatible HTTP providers
|
||||
if cfg.APIKey() == "" && cfg.APIBase == "" && !isEmptyAPIKeyAllowed(protocol) {
|
||||
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
||||
@@ -355,7 +306,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
cfg.RequestTimeout,
|
||||
), modelID, cfg)
|
||||
|
||||
case "coding-plan-anthropic", "alibaba-coding-anthropic":
|
||||
case "alibaba-coding-anthropic":
|
||||
// Alibaba Coding Plan with Anthropic-compatible API
|
||||
apiBase := cfg.APIBase
|
||||
if apiBase == "" {
|
||||
@@ -374,21 +325,21 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
case "antigravity":
|
||||
return finalizeProviderFromConfig(NewAntigravityProvider(), modelID, cfg)
|
||||
|
||||
case "claude-cli", "claudecli":
|
||||
case "claude-cli":
|
||||
workspace := cfg.Workspace
|
||||
if workspace == "" {
|
||||
workspace = "."
|
||||
}
|
||||
return finalizeProviderFromConfig(NewClaudeCliProvider(workspace), modelID, cfg)
|
||||
|
||||
case "codex-cli", "codexcli":
|
||||
case "codex-cli":
|
||||
workspace := cfg.Workspace
|
||||
if workspace == "" {
|
||||
workspace = "."
|
||||
}
|
||||
return finalizeProviderFromConfig(NewCodexCliProvider(workspace), modelID, cfg)
|
||||
|
||||
case "github-copilot", "copilot":
|
||||
case "github-copilot":
|
||||
apiBase := cfg.APIBase
|
||||
if apiBase == "" {
|
||||
apiBase = "localhost:4321"
|
||||
@@ -421,8 +372,8 @@ func finalizeProviderFromConfig(
|
||||
}
|
||||
|
||||
func isEmptyAPIKeyAllowed(protocol string) bool {
|
||||
meta, ok := protocolMetaForName(protocol)
|
||||
return ok && meta.emptyAPIKeyAllowed
|
||||
option, ok := modelProviderOptionForName(protocol)
|
||||
return ok && option.EmptyAPIKeyAllowed
|
||||
}
|
||||
|
||||
// IsEmptyAPIKeyAllowedForProtocol reports whether a protocol allows requests
|
||||
@@ -432,6 +383,16 @@ func IsEmptyAPIKeyAllowedForProtocol(protocol string) bool {
|
||||
return isEmptyAPIKeyAllowed(protocol)
|
||||
}
|
||||
|
||||
// IsHTTPAPIProtocol reports whether a provider uses an HTTP API base in the
|
||||
// model configuration path. This excludes providers such as Bedrock, CLI
|
||||
// bridges, and OAuth-only managed providers even if they do not require an
|
||||
// explicit api_key field.
|
||||
func IsHTTPAPIProtocol(protocol string) bool {
|
||||
protocol = NormalizeProvider(protocol)
|
||||
option, ok := modelProviderOptionsByName[protocol]
|
||||
return ok && option.httpAPI
|
||||
}
|
||||
|
||||
// DefaultAPIBaseForProtocol returns the configured default API base for a protocol.
|
||||
// It returns empty string if the protocol has no default base.
|
||||
func DefaultAPIBaseForProtocol(protocol string) string {
|
||||
@@ -441,19 +402,9 @@ func DefaultAPIBaseForProtocol(protocol string) string {
|
||||
|
||||
// getDefaultAPIBase returns the default API base URL for a given protocol.
|
||||
func getDefaultAPIBase(protocol string) string {
|
||||
meta, ok := protocolMetaForName(protocol)
|
||||
option, ok := modelProviderOptionForName(protocol)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return meta.defaultAPIBase
|
||||
}
|
||||
|
||||
func protocolMetaForName(protocol string) (protocolMeta, bool) {
|
||||
if meta, ok := protocolMetaByName[protocol]; ok {
|
||||
return meta, true
|
||||
}
|
||||
if meta, ok := attachedModelProviderMetaByName[protocol]; ok {
|
||||
return meta.protocolMeta, true
|
||||
}
|
||||
return protocolMeta{}, false
|
||||
return option.DefaultAPIBase
|
||||
}
|
||||
|
||||
@@ -946,7 +946,7 @@ func TestCreateProviderFromConfig_CodingPlanAnthropic(t *testing.T) {
|
||||
if modelID != wantModelID {
|
||||
t.Errorf("modelID = %q, want %q", modelID, wantModelID)
|
||||
}
|
||||
// coding-plan-anthropic uses Anthropic Messages provider
|
||||
// alibaba-coding-anthropic uses Anthropic Messages provider
|
||||
// Verify it's the anthropic messages provider by checking interface
|
||||
var _ LLMProvider = provider
|
||||
})
|
||||
@@ -998,6 +998,13 @@ func TestModelProviderOptions(t *testing.T) {
|
||||
if option, ok := seen["openai"]; ok && !option.CreateAllowed {
|
||||
t.Fatal("openai should be creatable")
|
||||
}
|
||||
if option, ok := seen["openai"]; ok && !option.SupportsFetch {
|
||||
t.Fatal("openai should support upstream model listing")
|
||||
} else if option.DisplayName != "OpenAI" {
|
||||
t.Fatalf("openai display_name = %q, want %q", option.DisplayName, "OpenAI")
|
||||
} else if len(option.CommonModels) == 0 {
|
||||
t.Fatal("openai common_models should not be empty")
|
||||
}
|
||||
if option, ok := seen["lmstudio"]; !ok {
|
||||
t.Fatal("lmstudio option missing")
|
||||
} else if !option.EmptyAPIKeyAllowed {
|
||||
@@ -1052,6 +1059,71 @@ func TestModelProviderOptions(t *testing.T) {
|
||||
t.Fatal("github-copilot option missing")
|
||||
} else if option.DefaultAPIBase != "localhost:4321" {
|
||||
t.Fatalf("github-copilot default_api_base = %q, want %q", option.DefaultAPIBase, "localhost:4321")
|
||||
} else if !option.Local {
|
||||
t.Fatal("github-copilot should be marked local")
|
||||
}
|
||||
if option, ok := seen["qwen-portal"]; !ok {
|
||||
t.Fatal("qwen-portal option missing")
|
||||
} else if len(option.Aliases) == 0 || option.Aliases[0] != "qwen" {
|
||||
t.Fatalf("qwen-portal aliases = %#v, want to include qwen", option.Aliases)
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if len(option.CommonModels) > 6 {
|
||||
t.Fatalf("provider %q exposes %d common_models, want at most 6", option.ID, len(option.CommonModels))
|
||||
}
|
||||
if option.Local && len(option.CommonModels) > 0 {
|
||||
t.Fatalf("local provider %q should not expose common_models", option.ID)
|
||||
}
|
||||
seenModels := make(map[string]struct{}, len(option.CommonModels))
|
||||
for _, model := range option.CommonModels {
|
||||
if strings.TrimSpace(model) == "" {
|
||||
t.Fatalf("provider %q includes an empty common_model entry", option.ID)
|
||||
}
|
||||
if _, exists := seenModels[model]; exists {
|
||||
t.Fatalf("provider %q includes duplicate common_model %q", option.ID, model)
|
||||
}
|
||||
seenModels[model] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildModelProviderAliasMap(t *testing.T) {
|
||||
aliases := buildModelProviderAliasMap()
|
||||
if len(aliases) == 0 {
|
||||
t.Fatal("buildModelProviderAliasMap() returned empty map")
|
||||
}
|
||||
|
||||
seenAliases := make(map[string]string, len(aliases))
|
||||
for provider, option := range modelProviderOptionsByName {
|
||||
got, ok := aliases[provider]
|
||||
if !ok {
|
||||
t.Fatalf("canonical provider %q missing from alias map", provider)
|
||||
}
|
||||
if got != provider {
|
||||
t.Fatalf("canonical provider %q mapped to %q", provider, got)
|
||||
}
|
||||
if existing, ok := seenAliases[provider]; ok {
|
||||
t.Fatalf("canonical provider key %q collides with provider %q", provider, existing)
|
||||
}
|
||||
seenAliases[provider] = provider
|
||||
for _, alias := range option.Aliases {
|
||||
normalized := strings.ToLower(strings.TrimSpace(alias))
|
||||
if normalized == "" {
|
||||
t.Fatalf("provider %q includes empty alias", provider)
|
||||
}
|
||||
if existing, ok := seenAliases[normalized]; ok && existing != provider {
|
||||
t.Fatalf("alias %q for provider %q collides with provider %q", alias, provider, existing)
|
||||
}
|
||||
seenAliases[normalized] = provider
|
||||
got, ok := aliases[normalized]
|
||||
if !ok {
|
||||
t.Fatalf("alias %q for provider %q missing from alias map", alias, provider)
|
||||
}
|
||||
if got != provider {
|
||||
t.Fatalf("alias %q normalized to %q, want %q", alias, got, provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,46 +29,14 @@ func ParseModelRef(raw string, defaultProvider string) *ModelRef {
|
||||
|
||||
// NormalizeProvider normalizes provider identifiers to canonical form.
|
||||
func NormalizeProvider(provider string) string {
|
||||
p := strings.ToLower(strings.TrimSpace(provider))
|
||||
|
||||
switch p {
|
||||
case "z.ai", "z-ai":
|
||||
return "zai"
|
||||
case "opencode-zen":
|
||||
return "opencode"
|
||||
case "qwen":
|
||||
return "qwen-portal"
|
||||
case "kimi-code":
|
||||
return "kimi-coding"
|
||||
case "gpt":
|
||||
return "openai"
|
||||
case "claude":
|
||||
return "anthropic"
|
||||
case "glm":
|
||||
return "zhipu"
|
||||
case "google":
|
||||
return "gemini"
|
||||
case "google-antigravity":
|
||||
return "antigravity"
|
||||
case "alibaba-coding", "qwen-coding":
|
||||
return "coding-plan"
|
||||
case "alibaba-coding-anthropic":
|
||||
return "coding-plan-anthropic"
|
||||
case "qwen-international", "dashscope-intl":
|
||||
return "qwen-intl"
|
||||
case "dashscope-us":
|
||||
return "qwen-us"
|
||||
case "azure-openai":
|
||||
return "azure"
|
||||
case "claudecli":
|
||||
return "claude-cli"
|
||||
case "codexcli":
|
||||
return "codex-cli"
|
||||
case "copilot":
|
||||
return "github-copilot"
|
||||
normalized := strings.ToLower(strings.TrimSpace(provider))
|
||||
if normalized == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return p
|
||||
if canonical, ok := normalizedModelProviderAliasesByName[normalized]; ok {
|
||||
return canonical
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// ModelKey returns a canonical "provider/model" key for deduplication.
|
||||
|
||||
@@ -65,9 +65,7 @@ func TestNormalizeProvider(t *testing.T) {
|
||||
{"z.ai", "zai"},
|
||||
{"z-ai", "zai"},
|
||||
{"Z.AI", "zai"},
|
||||
{"opencode-zen", "opencode"},
|
||||
{"qwen", "qwen-portal"},
|
||||
{"kimi-code", "kimi-coding"},
|
||||
{"gpt", "openai"},
|
||||
{"claude", "anthropic"},
|
||||
{"glm", "zhipu"},
|
||||
@@ -79,9 +77,11 @@ func TestNormalizeProvider(t *testing.T) {
|
||||
{"codexcli", "codex-cli"},
|
||||
{"copilot", "github-copilot"},
|
||||
// Alibaba Coding Plan aliases
|
||||
{"alibaba-coding", "coding-plan"},
|
||||
{"qwen-coding", "coding-plan"},
|
||||
{"alibaba-coding-anthropic", "coding-plan-anthropic"},
|
||||
{"alibaba-coding", "alibaba-coding"},
|
||||
{"coding-plan", "alibaba-coding"},
|
||||
{"qwen-coding", "alibaba-coding"},
|
||||
{"alibaba-coding-anthropic", "alibaba-coding-anthropic"},
|
||||
{"coding-plan-anthropic", "alibaba-coding-anthropic"},
|
||||
// Qwen international aliases
|
||||
{"qwen-international", "qwen-intl"},
|
||||
{"dashscope-intl", "qwen-intl"},
|
||||
|
||||
@@ -5,97 +5,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ModelProviderOption describes a canonical provider entry exposed to the Web UI.
|
||||
type ModelProviderOption struct {
|
||||
ID string `json:"id"`
|
||||
DefaultAPIBase string `json:"default_api_base"`
|
||||
EmptyAPIKeyAllowed bool `json:"empty_api_key_allowed"`
|
||||
CreateAllowed bool `json:"create_allowed"`
|
||||
DefaultModelAllowed bool `json:"default_model_allowed"`
|
||||
DefaultAuthMethod string `json:"default_auth_method,omitempty"`
|
||||
AuthMethodLocked bool `json:"auth_method_locked,omitempty"`
|
||||
}
|
||||
|
||||
type attachedModelProviderMeta struct {
|
||||
protocolMeta
|
||||
createAllowed bool
|
||||
defaultModelAllowed bool
|
||||
defaultAuthMethod string
|
||||
authMethodLocked bool
|
||||
}
|
||||
|
||||
// attachedModelProviderMetaByName augments protocolMetaByName for provider
|
||||
// families that are implemented in CreateProviderFromConfig but intentionally
|
||||
// kept out of the core HTTP metadata map because they have special auth/runtime
|
||||
// semantics.
|
||||
var attachedModelProviderMetaByName = map[string]attachedModelProviderMeta{
|
||||
"azure": {createAllowed: true, defaultModelAllowed: true},
|
||||
"anthropic": {
|
||||
protocolMeta: protocolMeta{defaultAPIBase: "https://api.anthropic.com/v1"},
|
||||
createAllowed: true,
|
||||
defaultModelAllowed: true,
|
||||
},
|
||||
"anthropic-messages": {
|
||||
protocolMeta: protocolMeta{defaultAPIBase: "https://api.anthropic.com/v1"},
|
||||
createAllowed: true,
|
||||
defaultModelAllowed: true,
|
||||
},
|
||||
"bedrock": {createAllowed: true, defaultModelAllowed: true},
|
||||
"antigravity": {
|
||||
createAllowed: true,
|
||||
defaultModelAllowed: true,
|
||||
defaultAuthMethod: "oauth",
|
||||
authMethodLocked: true,
|
||||
},
|
||||
"claude-cli": {createAllowed: true, defaultModelAllowed: true},
|
||||
"codex-cli": {createAllowed: true, defaultModelAllowed: true},
|
||||
"github-copilot": {
|
||||
protocolMeta: protocolMeta{defaultAPIBase: "localhost:4321"},
|
||||
createAllowed: true,
|
||||
defaultModelAllowed: true,
|
||||
},
|
||||
// ElevenLabs is intentionally exposed only as an ASR-capable provider. It
|
||||
// belongs in the shared model catalog because ASR is configured via
|
||||
// model_list, but it must not be selectable as the default chat model.
|
||||
"elevenlabs": {
|
||||
protocolMeta: protocolMeta{defaultAPIBase: "https://api.elevenlabs.io"},
|
||||
createAllowed: true,
|
||||
defaultModelAllowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
// ModelProviderOptions returns the canonical provider catalog exposed to the Web UI.
|
||||
func ModelProviderOptions() []ModelProviderOption {
|
||||
optionsByID := make(map[string]ModelProviderOption, len(protocolMetaByName)+len(attachedModelProviderMetaByName))
|
||||
for provider := range protocolMetaByName {
|
||||
if NormalizeProvider(provider) != provider {
|
||||
continue
|
||||
}
|
||||
optionsByID[provider] = ModelProviderOption{
|
||||
ID: provider,
|
||||
DefaultAPIBase: DefaultAPIBaseForProtocol(provider),
|
||||
EmptyAPIKeyAllowed: IsEmptyAPIKeyAllowedForProtocol(provider),
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
}
|
||||
}
|
||||
for provider, meta := range attachedModelProviderMetaByName {
|
||||
if NormalizeProvider(provider) != provider {
|
||||
continue
|
||||
}
|
||||
optionsByID[provider] = ModelProviderOption{
|
||||
ID: provider,
|
||||
DefaultAPIBase: meta.defaultAPIBase,
|
||||
EmptyAPIKeyAllowed: meta.emptyAPIKeyAllowed,
|
||||
CreateAllowed: meta.createAllowed,
|
||||
DefaultModelAllowed: meta.defaultModelAllowed,
|
||||
DefaultAuthMethod: meta.defaultAuthMethod,
|
||||
AuthMethodLocked: meta.authMethodLocked,
|
||||
}
|
||||
}
|
||||
|
||||
options := make([]ModelProviderOption, 0, len(optionsByID))
|
||||
for _, option := range optionsByID {
|
||||
options := make([]ModelProviderOption, 0, len(modelProviderOptionsByName))
|
||||
for _, option := range modelProviderOptionsByName {
|
||||
options = append(options, option)
|
||||
}
|
||||
sort.Slice(options, func(i, j int) bool {
|
||||
@@ -107,44 +20,30 @@ func ModelProviderOptions() []ModelProviderOption {
|
||||
// IsSupportedModelProvider reports whether provider resolves to a provider ID
|
||||
// returned by ModelProviderOptions.
|
||||
func IsSupportedModelProvider(provider string) bool {
|
||||
normalized := NormalizeProvider(provider)
|
||||
if normalized == "" {
|
||||
return false
|
||||
}
|
||||
if _, ok := protocolMetaByName[normalized]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := attachedModelProviderMetaByName[normalized]
|
||||
_, ok := modelProviderOptionForName(provider)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsModelProviderFetchable reports whether provider supports upstream /models
|
||||
// listing through the launcher fetch endpoint.
|
||||
func IsModelProviderFetchable(provider string) bool {
|
||||
option, ok := modelProviderOptionForName(provider)
|
||||
return ok && option.SupportsFetch
|
||||
}
|
||||
|
||||
// IsCreatableModelProvider reports whether provider can be selected for a new
|
||||
// model entry from the Web UI.
|
||||
func IsCreatableModelProvider(provider string) bool {
|
||||
normalized := NormalizeProvider(provider)
|
||||
if normalized == "" {
|
||||
return false
|
||||
}
|
||||
if _, ok := protocolMetaByName[normalized]; ok {
|
||||
return true
|
||||
}
|
||||
meta, ok := attachedModelProviderMetaByName[normalized]
|
||||
return ok && meta.createAllowed
|
||||
option, ok := modelProviderOptionForName(provider)
|
||||
return ok && option.CreateAllowed
|
||||
}
|
||||
|
||||
// IsDefaultModelProvider reports whether provider can be used as the default
|
||||
// chat model. Some providers such as ASR-only entries are intentionally
|
||||
// exposed in model_list management but cannot drive the gateway default model.
|
||||
func IsDefaultModelProvider(provider string) bool {
|
||||
normalized := NormalizeProvider(provider)
|
||||
if normalized == "" {
|
||||
return false
|
||||
}
|
||||
if _, ok := protocolMetaByName[normalized]; ok {
|
||||
return true
|
||||
}
|
||||
meta, ok := attachedModelProviderMetaByName[normalized]
|
||||
return ok && meta.defaultModelAllowed
|
||||
option, ok := modelProviderOptionForName(provider)
|
||||
return ok && option.DefaultModelAllowed
|
||||
}
|
||||
|
||||
// SplitModelProviderAndID separates a legacy "provider/model" string into its
|
||||
|
||||
@@ -0,0 +1,581 @@
|
||||
package providers
|
||||
|
||||
import "strings"
|
||||
|
||||
// ModelProviderOption describes a canonical provider entry exposed to the Web UI.
|
||||
// It also serves as the backend-owned source of truth for shared provider metadata.
|
||||
type ModelProviderOption struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
IconSlug string `json:"icon_slug,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
DefaultAPIBase string `json:"default_api_base"`
|
||||
EmptyAPIKeyAllowed bool `json:"empty_api_key_allowed"`
|
||||
CreateAllowed bool `json:"create_allowed"`
|
||||
DefaultModelAllowed bool `json:"default_model_allowed"`
|
||||
SupportsFetch bool `json:"supports_fetch,omitempty"`
|
||||
DefaultAuthMethod string `json:"default_auth_method,omitempty"`
|
||||
AuthMethodLocked bool `json:"auth_method_locked,omitempty"`
|
||||
Local bool `json:"local,omitempty"`
|
||||
Priority float64 `json:"priority,omitempty"`
|
||||
CommonModels []string `json:"common_models,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
|
||||
httpAPI bool `json:"-"`
|
||||
}
|
||||
|
||||
var modelProviderOptionsByName = map[string]ModelProviderOption{
|
||||
"openai": {
|
||||
ID: "openai",
|
||||
DisplayName: "OpenAI",
|
||||
IconSlug: "openai",
|
||||
Domain: "openai.com",
|
||||
DefaultAPIBase: "https://api.openai.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 100,
|
||||
CommonModels: []string{"gpt-5.4", "gpt-5.4-mini", "gpt-5.5"},
|
||||
Aliases: []string{"gpt"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"anthropic": {
|
||||
ID: "anthropic",
|
||||
DisplayName: "Anthropic",
|
||||
IconSlug: "anthropic",
|
||||
Domain: "anthropic.com",
|
||||
DefaultAPIBase: "https://api.anthropic.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 95,
|
||||
CommonModels: []string{"claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5"},
|
||||
Aliases: []string{"claude"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"anthropic-messages": {
|
||||
ID: "anthropic-messages",
|
||||
DisplayName: "Anthropic Messages",
|
||||
IconSlug: "anthropic",
|
||||
Domain: "anthropic.com",
|
||||
DefaultAPIBase: "https://api.anthropic.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 94,
|
||||
CommonModels: []string{"claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"gemini": {
|
||||
ID: "gemini",
|
||||
DisplayName: "Google Gemini",
|
||||
IconSlug: "googlegemini",
|
||||
Domain: "gemini.google.com",
|
||||
DefaultAPIBase: "https://generativelanguage.googleapis.com/v1beta",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 90,
|
||||
CommonModels: []string{"gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite"},
|
||||
Aliases: []string{"google"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"deepseek": {
|
||||
ID: "deepseek",
|
||||
DisplayName: "DeepSeek",
|
||||
IconSlug: "deepseek",
|
||||
Domain: "deepseek.com",
|
||||
DefaultAPIBase: "https://api.deepseek.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 85,
|
||||
CommonModels: []string{"deepseek-v4-flash", "deepseek-v4-pro"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"openrouter": {
|
||||
ID: "openrouter",
|
||||
DisplayName: "OpenRouter",
|
||||
IconSlug: "openrouter",
|
||||
Domain: "openrouter.ai",
|
||||
DefaultAPIBase: "https://openrouter.ai/api/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 80,
|
||||
CommonModels: []string{
|
||||
"openai/gpt-5.4",
|
||||
"anthropic/claude-opus-4.7",
|
||||
"google/gemini-3.1-pro-preview",
|
||||
"qwen/qwen3-coder-next",
|
||||
},
|
||||
httpAPI: true,
|
||||
},
|
||||
"qwen-portal": {
|
||||
ID: "qwen-portal",
|
||||
DisplayName: "Qwen",
|
||||
IconSlug: "alibabacloud",
|
||||
Domain: "qwenlm.ai",
|
||||
DefaultAPIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 75,
|
||||
CommonModels: []string{"qwen3.6-max-preview", "qwen3.6-plus", "qwen3.6-flash", "qwen3-coder-next"},
|
||||
Aliases: []string{"qwen"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"qwen-intl": {
|
||||
ID: "qwen-intl",
|
||||
DisplayName: "Qwen International",
|
||||
IconSlug: "alibabacloud",
|
||||
Domain: "alibabacloud.com",
|
||||
DefaultAPIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 74,
|
||||
CommonModels: []string{"qwen3.6-max-preview", "qwen3.6-plus", "qwen3.6-flash", "qwen3-coder-next"},
|
||||
Aliases: []string{"qwen-international", "dashscope-intl"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"moonshot": {
|
||||
ID: "moonshot",
|
||||
DisplayName: "Moonshot",
|
||||
Domain: "moonshot.ai",
|
||||
DefaultAPIBase: "https://api.moonshot.cn/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 70,
|
||||
CommonModels: []string{
|
||||
"kimi-k2.5",
|
||||
"kimi-k2-thinking",
|
||||
"kimi-k2-thinking-turbo",
|
||||
"kimi-k2-turbo-preview",
|
||||
},
|
||||
httpAPI: true,
|
||||
},
|
||||
"volcengine": {
|
||||
ID: "volcengine",
|
||||
DisplayName: "Volcengine",
|
||||
IconSlug: "bytedance",
|
||||
Domain: "volcengine.com",
|
||||
DefaultAPIBase: "https://ark.cn-beijing.volces.com/api/v3",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 69,
|
||||
CommonModels: []string{
|
||||
"doubao-seed-1-6-251015",
|
||||
"doubao-seed-1-6-flash-250828",
|
||||
"doubao-seed-1-6-thinking",
|
||||
},
|
||||
httpAPI: true,
|
||||
},
|
||||
"zhipu": {
|
||||
ID: "zhipu",
|
||||
DisplayName: "Zhipu AI",
|
||||
IconSlug: "zhipu",
|
||||
Domain: "zhipuai.cn",
|
||||
DefaultAPIBase: "https://open.bigmodel.cn/api/paas/v4",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 68,
|
||||
CommonModels: []string{"glm-5", "glm-4.7", "glm-4.5-air", "glm-4-flash-250414"},
|
||||
Aliases: []string{"glm"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"groq": {
|
||||
ID: "groq",
|
||||
DisplayName: "Groq",
|
||||
IconSlug: "groq",
|
||||
Domain: "groq.com",
|
||||
DefaultAPIBase: "https://api.groq.com/openai/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 65,
|
||||
CommonModels: []string{
|
||||
"openai/gpt-oss-120b",
|
||||
"openai/gpt-oss-20b",
|
||||
"llama-3.3-70b-versatile",
|
||||
"qwen/qwen3-32b",
|
||||
},
|
||||
httpAPI: true,
|
||||
},
|
||||
"mistral": {
|
||||
ID: "mistral",
|
||||
DisplayName: "Mistral AI",
|
||||
IconSlug: "mistralai",
|
||||
Domain: "mistral.ai",
|
||||
DefaultAPIBase: "https://api.mistral.ai/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 64,
|
||||
CommonModels: []string{
|
||||
"mistral-large-latest",
|
||||
"mistral-medium-3-5",
|
||||
"mistral-small-latest",
|
||||
"devstral-latest",
|
||||
},
|
||||
httpAPI: true,
|
||||
},
|
||||
"nvidia": {
|
||||
ID: "nvidia",
|
||||
DisplayName: "NVIDIA",
|
||||
IconSlug: "nvidia",
|
||||
Domain: "nvidia.com",
|
||||
DefaultAPIBase: "https://integrate.api.nvidia.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 63,
|
||||
CommonModels: []string{
|
||||
"openai/gpt-oss-120b",
|
||||
"openai/gpt-oss-20b",
|
||||
"qwen/qwen3-coder-480b-a35b-instruct",
|
||||
"qwen/qwen3-next-80b-a3b-thinking",
|
||||
},
|
||||
httpAPI: true,
|
||||
},
|
||||
"cerebras": {
|
||||
ID: "cerebras",
|
||||
DisplayName: "Cerebras",
|
||||
IconSlug: "cerebras",
|
||||
Domain: "cerebras.ai",
|
||||
DefaultAPIBase: "https://api.cerebras.ai/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 62,
|
||||
CommonModels: []string{"gpt-oss-120b", "zai-glm-4.7"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"azure": {
|
||||
ID: "azure",
|
||||
DisplayName: "Azure OpenAI",
|
||||
IconSlug: "microsoftazure",
|
||||
Domain: "azure.com",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 61,
|
||||
Aliases: []string{"azure-openai"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"bedrock": {
|
||||
ID: "bedrock",
|
||||
DisplayName: "AWS Bedrock",
|
||||
IconSlug: "amazonwebservices",
|
||||
Domain: "aws.amazon.com",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 60,
|
||||
},
|
||||
"github-copilot": {
|
||||
ID: "github-copilot",
|
||||
DisplayName: "GitHub Copilot",
|
||||
IconSlug: "githubcopilot",
|
||||
Domain: "github.com",
|
||||
DefaultAPIBase: "localhost:4321",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Local: true,
|
||||
Priority: 55,
|
||||
Aliases: []string{"copilot"},
|
||||
},
|
||||
"antigravity": {
|
||||
ID: "antigravity",
|
||||
DisplayName: "Google Code Assist",
|
||||
Domain: "antigravity.google",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
DefaultAuthMethod: "oauth",
|
||||
AuthMethodLocked: true,
|
||||
Priority: 54,
|
||||
Aliases: []string{"google-antigravity"},
|
||||
},
|
||||
"claude-cli": {
|
||||
ID: "claude-cli",
|
||||
DisplayName: "Claude CLI",
|
||||
IconSlug: "anthropic",
|
||||
Domain: "anthropic.com",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Local: true,
|
||||
Priority: 53,
|
||||
Aliases: []string{"claudecli"},
|
||||
},
|
||||
"codex-cli": {
|
||||
ID: "codex-cli",
|
||||
DisplayName: "Codex CLI",
|
||||
IconSlug: "openai",
|
||||
Domain: "openai.com",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Local: true,
|
||||
Priority: 52,
|
||||
Aliases: []string{"codexcli"},
|
||||
},
|
||||
"ollama": {
|
||||
ID: "ollama",
|
||||
DisplayName: "Ollama",
|
||||
IconSlug: "ollama",
|
||||
Domain: "ollama.com",
|
||||
DefaultAPIBase: "http://localhost:11434/v1",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Local: true,
|
||||
Priority: 50,
|
||||
httpAPI: true,
|
||||
},
|
||||
"vllm": {
|
||||
ID: "vllm",
|
||||
DisplayName: "VLLM",
|
||||
Domain: "vllm.ai",
|
||||
DefaultAPIBase: "http://localhost:8000/v1",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Local: true,
|
||||
Priority: 49,
|
||||
httpAPI: true,
|
||||
},
|
||||
"lmstudio": {
|
||||
ID: "lmstudio",
|
||||
DisplayName: "LM Studio",
|
||||
Domain: "lmstudio.ai",
|
||||
DefaultAPIBase: "http://localhost:1234/v1",
|
||||
EmptyAPIKeyAllowed: true,
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Local: true,
|
||||
Priority: 48,
|
||||
httpAPI: true,
|
||||
},
|
||||
"elevenlabs": {
|
||||
ID: "elevenlabs",
|
||||
DisplayName: "ElevenLabs ASR",
|
||||
IconSlug: "elevenlabs",
|
||||
Domain: "elevenlabs.io",
|
||||
DefaultAPIBase: "https://api.elevenlabs.io",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: false,
|
||||
Priority: 47,
|
||||
httpAPI: true,
|
||||
},
|
||||
"venice": {
|
||||
ID: "venice",
|
||||
DisplayName: "Venice AI",
|
||||
IconSlug: "venice",
|
||||
Domain: "venice.ai",
|
||||
DefaultAPIBase: "https://api.venice.ai/api/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 45,
|
||||
httpAPI: true,
|
||||
},
|
||||
"shengsuanyun": {
|
||||
ID: "shengsuanyun",
|
||||
DisplayName: "ShengsuanYun",
|
||||
Domain: "shengsuanyun.com",
|
||||
DefaultAPIBase: "https://router.shengsuanyun.com/api/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 44,
|
||||
httpAPI: true,
|
||||
},
|
||||
"siliconflow": {
|
||||
ID: "siliconflow",
|
||||
DisplayName: "SiliconFlow",
|
||||
Domain: "siliconflow.cn",
|
||||
DefaultAPIBase: "https://api.siliconflow.cn/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 43.5,
|
||||
httpAPI: true,
|
||||
},
|
||||
"vivgrid": {
|
||||
ID: "vivgrid",
|
||||
DisplayName: "Vivgrid",
|
||||
Domain: "vivgrid.com",
|
||||
DefaultAPIBase: "https://api.vivgrid.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 43,
|
||||
httpAPI: true,
|
||||
},
|
||||
"minimax": {
|
||||
ID: "minimax",
|
||||
DisplayName: "MiniMax",
|
||||
Domain: "minimaxi.com",
|
||||
DefaultAPIBase: "https://api.minimaxi.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 42,
|
||||
httpAPI: true,
|
||||
},
|
||||
"longcat": {
|
||||
ID: "longcat",
|
||||
DisplayName: "LongCat",
|
||||
Domain: "longcat.chat",
|
||||
DefaultAPIBase: "https://api.longcat.chat/openai",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 41,
|
||||
httpAPI: true,
|
||||
},
|
||||
"modelscope": {
|
||||
ID: "modelscope",
|
||||
DisplayName: "ModelScope",
|
||||
Domain: "modelscope.cn",
|
||||
DefaultAPIBase: "https://api-inference.modelscope.cn/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 40,
|
||||
httpAPI: true,
|
||||
},
|
||||
"mimo": {
|
||||
ID: "mimo",
|
||||
DisplayName: "Xiaomi MiMo",
|
||||
IconSlug: "xiaomi",
|
||||
Domain: "xiaomi.com",
|
||||
DefaultAPIBase: "https://api.xiaomimimo.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 39,
|
||||
httpAPI: true,
|
||||
},
|
||||
"avian": {
|
||||
ID: "avian",
|
||||
DisplayName: "Avian",
|
||||
Domain: "avian.io",
|
||||
DefaultAPIBase: "https://api.avian.io/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 38,
|
||||
httpAPI: true,
|
||||
},
|
||||
"zai": {
|
||||
ID: "zai",
|
||||
DisplayName: "Z.ai",
|
||||
Domain: "z.ai",
|
||||
DefaultAPIBase: "https://api.z.ai/api/coding/paas/v4",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 37,
|
||||
CommonModels: []string{"glm-5", "glm-4.7", "glm-4.5-air", "glm-4-flash-250414"},
|
||||
Aliases: []string{"z.ai", "z-ai"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"alibaba-coding": {
|
||||
ID: "alibaba-coding",
|
||||
DisplayName: "Alibaba Coding Plan",
|
||||
IconSlug: "alibabacloud",
|
||||
Domain: "alibabacloud.com",
|
||||
DefaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 36.5,
|
||||
CommonModels: []string{"qwen3.6-plus", "kimi-k2.5", "glm-5", "MiniMax-M2.5"},
|
||||
Aliases: []string{"coding-plan", "qwen-coding"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"alibaba-coding-anthropic": {
|
||||
ID: "alibaba-coding-anthropic",
|
||||
DisplayName: "Alibaba Coding Plan (Anthropic)",
|
||||
IconSlug: "alibabacloud",
|
||||
Domain: "alibabacloud.com",
|
||||
DefaultAPIBase: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 36.25,
|
||||
CommonModels: []string{"qwen3.6-plus", "kimi-k2.5", "glm-5", "MiniMax-M2.5"},
|
||||
Aliases: []string{"coding-plan-anthropic"},
|
||||
httpAPI: true,
|
||||
},
|
||||
"novita": {
|
||||
ID: "novita",
|
||||
DisplayName: "Novita AI",
|
||||
Domain: "novita.ai",
|
||||
DefaultAPIBase: "https://api.novita.ai/openai",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 36,
|
||||
httpAPI: true,
|
||||
},
|
||||
"litellm": {
|
||||
ID: "litellm",
|
||||
DisplayName: "LiteLLM",
|
||||
Domain: "litellm.ai",
|
||||
DefaultAPIBase: "http://localhost:4000/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
SupportsFetch: true,
|
||||
Priority: 35,
|
||||
httpAPI: true,
|
||||
},
|
||||
"qwen-us": {
|
||||
ID: "qwen-us",
|
||||
DisplayName: "Qwen US",
|
||||
IconSlug: "alibabacloud",
|
||||
Domain: "alibabacloud.com",
|
||||
DefaultAPIBase: "https://dashscope-us.aliyuncs.com/compatible-mode/v1",
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
Priority: 34,
|
||||
CommonModels: []string{"qwen3.6-max-preview", "qwen3.6-plus", "qwen3.6-flash", "qwen3-coder-next"},
|
||||
Aliases: []string{"dashscope-us"},
|
||||
httpAPI: true,
|
||||
},
|
||||
}
|
||||
|
||||
var normalizedModelProviderAliasesByName = buildModelProviderAliasMap()
|
||||
|
||||
func buildModelProviderAliasMap() map[string]string {
|
||||
totalAliases := 0
|
||||
for _, option := range modelProviderOptionsByName {
|
||||
totalAliases += len(option.Aliases)
|
||||
}
|
||||
|
||||
aliases := make(map[string]string, len(modelProviderOptionsByName)+totalAliases)
|
||||
for provider, option := range modelProviderOptionsByName {
|
||||
aliases[provider] = provider
|
||||
for _, alias := range option.Aliases {
|
||||
normalized := strings.ToLower(strings.TrimSpace(alias))
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
aliases[normalized] = provider
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
func modelProviderOptionForName(provider string) (ModelProviderOption, bool) {
|
||||
normalized := NormalizeProvider(provider)
|
||||
if normalized == "" {
|
||||
return ModelProviderOption{}, false
|
||||
}
|
||||
option, ok := modelProviderOptionsByName[normalized]
|
||||
return option, ok
|
||||
}
|
||||
Reference in New Issue
Block a user