mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 08:00:55 +00:00
llmman (https://github.com/llmmanorg/llmman) is a local model runner serving Ollama- and OpenAI-compatible routes on 127.0.0.1:17434. Register it in the agent provider catalog next to Ollama and extend every Ollama special case (no API key, verification skipped, OpenClaw placeholder key, manual initial model) to cover it as well.
491 lines
18 KiB
Go
491 lines
18 KiB
Go
package provider
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
type APIConfig struct {
|
|
APIType string
|
|
BaseURL string
|
|
EditableBaseURL bool
|
|
DiscoverModels bool
|
|
DefaultAuthMode string
|
|
AuthModes []string
|
|
Models []Model
|
|
}
|
|
|
|
const (
|
|
AuthModeBearer = "bearer"
|
|
AuthModeXAPIKey = "x-api-key"
|
|
)
|
|
|
|
type Model struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
type Meta struct {
|
|
Key string
|
|
DisplayName string
|
|
DisplayNameKey string
|
|
Sort uint
|
|
DefaultAPIType string
|
|
APIConfigs []APIConfig
|
|
EnvKey string
|
|
Models []Model
|
|
}
|
|
|
|
var catalog = map[string]Meta{
|
|
"custom": {
|
|
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
|
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"),
|
|
},
|
|
"ollama": {
|
|
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
|
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
|
|
},
|
|
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
|
"llmman": {
|
|
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
|
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
|
{APIType: "openai-embeddings", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
|
},
|
|
},
|
|
"vllm": {
|
|
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
|
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
|
},
|
|
"deepseek": {
|
|
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"},
|
|
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
|
},
|
|
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
|
},
|
|
"bailian-coding-plan": {
|
|
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
|
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
|
},
|
|
Models: []Model{
|
|
{ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"},
|
|
{ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"},
|
|
{ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"},
|
|
{ID: "glm-4.7", Name: "GLM-4.7"},
|
|
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
|
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
|
{ID: "glm-5", Name: "GLM-5"},
|
|
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
|
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
|
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
|
},
|
|
},
|
|
"ark-coding-plan": {
|
|
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
|
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
|
},
|
|
Models: []Model{
|
|
{ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"},
|
|
{ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
|
{ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"},
|
|
},
|
|
},
|
|
"zai": {
|
|
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
|
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
|
},
|
|
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
|
|
},
|
|
"minimax": {
|
|
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
|
},
|
|
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
|
},
|
|
"xiaomi": {
|
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
|
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"},
|
|
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
|
|
},
|
|
Models: []Model{{ID: "mimo-v2.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
|
|
},
|
|
"kimi": {
|
|
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
|
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}},
|
|
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
|
|
},
|
|
"kimi-coding": {
|
|
Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY",
|
|
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)},
|
|
Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}},
|
|
},
|
|
"openai": {
|
|
Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
|
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
|
|
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"},
|
|
{APIType: "openai-embeddings", BaseURL: "https://api.openai.com/v1", Models: []Model{
|
|
{ID: "text-embedding-3-small", Name: "text-embedding-3-small"},
|
|
{ID: "text-embedding-3-large", Name: "text-embedding-3-large"},
|
|
}},
|
|
},
|
|
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
|
|
},
|
|
"openrouter": {
|
|
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"},
|
|
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"},
|
|
},
|
|
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
|
},
|
|
"anthropic": {
|
|
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
|
|
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)},
|
|
Models: []Model{{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6"}, {ID: "claude-opus-4-6", Name: "Claude Opus 4.6"}, {ID: "claude-opus-4-5", Name: "Claude Opus 4.5"}, {ID: "claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, {ID: "claude-haiku-4-5", Name: "Claude Haiku 4.5"}},
|
|
},
|
|
"gemini": {
|
|
Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY",
|
|
APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}},
|
|
Models: []Model{{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, {ID: "gemini-flash-latest", Name: "Gemini Flash Latest"}, {ID: "gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}},
|
|
},
|
|
"moonshot": {
|
|
Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY",
|
|
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
|
|
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
|
|
},
|
|
"bailian": {
|
|
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{
|
|
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
DiscoverModels: true,
|
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
|
},
|
|
{
|
|
APIType: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
DiscoverModels: true,
|
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
|
},
|
|
{
|
|
APIType: "anthropic-messages", BaseURL: "https://dashscope.aliyuncs.com/apps/anthropic",
|
|
DefaultAuthMode: AuthModeBearer,
|
|
AuthModes: []string{AuthModeBearer},
|
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
|
},
|
|
{
|
|
APIType: "dashscope-images", BaseURL: "https://dashscope.aliyuncs.com",
|
|
Models: []Model{
|
|
{ID: "qwen-image-2.0-pro", Name: "qwen-image-2.0-pro"},
|
|
{ID: "qwen-image-2.0", Name: "qwen-image-2.0"},
|
|
{ID: "wan2.7-image-pro", Name: "wan2.7-image-pro"},
|
|
{ID: "wan2.7-image", Name: "wan2.7-image"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"ark": {
|
|
Key: "ark", DisplayNameKey: "AIProviderArk", Sort: 36, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
|
APIConfigs: []APIConfig{
|
|
{
|
|
APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
|
DiscoverModels: true,
|
|
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
|
|
},
|
|
{
|
|
APIType: "openai-responses", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
|
DiscoverModels: true,
|
|
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
|
|
},
|
|
{
|
|
APIType: "openai-images", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
|
Models: []Model{
|
|
{ID: "doubao-seedream-5-0-260128", Name: "doubao-seedream-5-0-260128"},
|
|
{ID: "doubao-seedream-5-0-lite-260128", Name: "doubao-seedream-5-0-lite-260128"},
|
|
{ID: "doubao-seedream-4-5-251128", Name: "doubao-seedream-4-5-251128"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func editableAPIConfigs(discoverModels bool, apiTypes ...string) []APIConfig {
|
|
configs := make([]APIConfig, 0, len(apiTypes))
|
|
for _, apiType := range apiTypes {
|
|
if apiType == "anthropic-messages" {
|
|
config := anthropicAPIConfig("", AuthModeXAPIKey, AuthModeBearer)
|
|
config.EditableBaseURL = true
|
|
configs = append(configs, config)
|
|
continue
|
|
}
|
|
configs = append(configs, APIConfig{
|
|
APIType: apiType,
|
|
EditableBaseURL: true,
|
|
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
|
|
})
|
|
}
|
|
return configs
|
|
}
|
|
|
|
func anthropicAPIConfig(baseURL, defaultAuthMode string, additionalAuthModes ...string) APIConfig {
|
|
authModes := []string{defaultAuthMode}
|
|
for _, authMode := range additionalAuthModes {
|
|
if authMode != defaultAuthMode {
|
|
authModes = append(authModes, authMode)
|
|
}
|
|
}
|
|
return APIConfig{
|
|
APIType: "anthropic-messages",
|
|
BaseURL: baseURL,
|
|
DefaultAuthMode: defaultAuthMode,
|
|
AuthModes: authModes,
|
|
}
|
|
}
|
|
|
|
func Get(key string) (Meta, bool) {
|
|
meta, ok := catalog[key]
|
|
if !ok {
|
|
return Meta{}, false
|
|
}
|
|
return cloneMeta(meta), true
|
|
}
|
|
|
|
func All() map[string]Meta {
|
|
result := make(map[string]Meta, len(catalog))
|
|
for key, meta := range catalog {
|
|
result[key] = cloneMeta(meta)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func FindAPIConfig(key, apiType string) (APIConfig, bool) {
|
|
meta, ok := catalog[key]
|
|
if !ok {
|
|
return APIConfig{}, false
|
|
}
|
|
target := strings.TrimSpace(apiType)
|
|
if target == "" {
|
|
target = meta.DefaultAPIType
|
|
}
|
|
for _, config := range meta.APIConfigs {
|
|
if config.APIType == target {
|
|
config.AuthModes = append([]string(nil), config.AuthModes...)
|
|
config.Models = append([]Model(nil), config.Models...)
|
|
return config, true
|
|
}
|
|
}
|
|
return APIConfig{}, false
|
|
}
|
|
|
|
func DefaultModels(key, apiType string) []Model {
|
|
meta, ok := catalog[key]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
target := strings.TrimSpace(apiType)
|
|
if target == "" {
|
|
target = meta.DefaultAPIType
|
|
}
|
|
for _, config := range meta.APIConfigs {
|
|
if config.APIType != target {
|
|
continue
|
|
}
|
|
if len(config.Models) > 0 {
|
|
return append([]Model(nil), config.Models...)
|
|
}
|
|
if IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType) {
|
|
return nil
|
|
}
|
|
break
|
|
}
|
|
return append([]Model(nil), meta.Models...)
|
|
}
|
|
|
|
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
|
|
config, ok := FindAPIConfig(provider, apiType)
|
|
if !ok {
|
|
return "", fmt.Errorf("provider %s does not support api type %s", provider, apiType)
|
|
}
|
|
if config.APIType != "anthropic-messages" {
|
|
return "", nil
|
|
}
|
|
authMode := strings.TrimSpace(requested)
|
|
if authMode == "" {
|
|
authMode = config.DefaultAuthMode
|
|
}
|
|
for _, allowed := range config.AuthModes {
|
|
if authMode == allowed {
|
|
return authMode, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("provider %s does not support auth mode %s", provider, authMode)
|
|
}
|
|
|
|
func DefaultBaseURL(key string) (string, bool) {
|
|
config, ok := FindAPIConfig(key, "")
|
|
if !ok || strings.TrimSpace(config.BaseURL) == "" {
|
|
return "", false
|
|
}
|
|
return config.BaseURL, true
|
|
}
|
|
|
|
func DefaultAPIType(key string) string {
|
|
meta, ok := catalog[key]
|
|
if !ok || strings.TrimSpace(meta.DefaultAPIType) == "" {
|
|
return "openai-completions"
|
|
}
|
|
return meta.DefaultAPIType
|
|
}
|
|
|
|
func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
|
config, ok := FindAPIConfig(key, apiType)
|
|
if !ok {
|
|
return "", fmt.Errorf("provider %s does not support api type %s", key, apiType)
|
|
}
|
|
if !config.EditableBaseURL {
|
|
return strings.TrimRight(config.BaseURL, "/"), nil
|
|
}
|
|
baseURL := strings.TrimSpace(requested)
|
|
if baseURL == "" {
|
|
baseURL = config.BaseURL
|
|
}
|
|
if baseURL == "" {
|
|
return "", fmt.Errorf("base url is required")
|
|
}
|
|
parsed, err := url.Parse(baseURL)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid base url")
|
|
}
|
|
if key == "custom" && (IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType)) {
|
|
return baseURL, nil
|
|
}
|
|
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
|
|
parsed.RawQuery = ""
|
|
parsed.Fragment = ""
|
|
return strings.TrimRight(parsed.String(), "/"), nil
|
|
}
|
|
|
|
func normalizeEndpointPath(apiType, value string) string {
|
|
if IsImageAPIType(apiType) {
|
|
return strings.TrimRight(value, "/")
|
|
}
|
|
path := strings.TrimRight(value, "/")
|
|
suffixes := []string{}
|
|
switch apiType {
|
|
case "openai-completions":
|
|
suffixes = []string{"/chat/completions"}
|
|
case "openai-responses":
|
|
suffixes = []string{"/responses"}
|
|
case "anthropic-messages":
|
|
suffixes = []string{"/v1/messages", "/messages"}
|
|
case "openai-embeddings":
|
|
suffixes = []string{"/v1/embeddings", "/embeddings"}
|
|
}
|
|
for _, suffix := range suffixes {
|
|
if strings.HasSuffix(strings.ToLower(path), suffix) {
|
|
return path[:len(path)-len(suffix)]
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
|
|
func IsEmbeddingAPIType(apiType string) bool {
|
|
return apiType == "openai-embeddings"
|
|
}
|
|
|
|
func IsImageAPIType(apiType string) bool {
|
|
switch apiType {
|
|
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func EnvKey(key string) string {
|
|
meta, ok := catalog[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return meta.EnvKey
|
|
}
|
|
|
|
func DisplayName(key string) string {
|
|
meta, ok := catalog[key]
|
|
if !ok || strings.TrimSpace(meta.DisplayName) == "" {
|
|
return key
|
|
}
|
|
return meta.DisplayName
|
|
}
|
|
|
|
func DisplayNameKey(key string) string {
|
|
meta, ok := catalog[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return meta.DisplayNameKey
|
|
}
|
|
|
|
func NormalizeModelID(provider, modelID string) string {
|
|
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
|
for _, prefix := range legacyModelPrefixes[provider] {
|
|
legacyPrefix := prefix + "/"
|
|
if !strings.HasPrefix(target, legacyPrefix) {
|
|
continue
|
|
}
|
|
candidate := strings.TrimSpace(strings.TrimPrefix(target, legacyPrefix))
|
|
if candidate != "" {
|
|
return candidate
|
|
}
|
|
}
|
|
return target
|
|
}
|
|
|
|
var legacyModelPrefixes = map[string][]string{
|
|
"custom": {"custom"},
|
|
"vllm": {"custom"},
|
|
"ollama": {"ollama"},
|
|
"llmman": {"llmman"},
|
|
"deepseek": {"deepseek"},
|
|
"bailian-coding-plan": {"bailian-coding-plan"},
|
|
"ark-coding-plan": {"ark-coding-plan"},
|
|
"zai": {"zai"},
|
|
"minimax": {"minimax"},
|
|
"xiaomi": {"xiaomi"},
|
|
"kimi": {"kimi", "moonshot"},
|
|
"kimi-coding": {"kimi-coding"},
|
|
"openai": {"openai"},
|
|
"anthropic": {"anthropic"},
|
|
"gemini": {"google", "gemini"},
|
|
"moonshot": {"moonshot"},
|
|
}
|
|
|
|
func cloneMeta(meta Meta) Meta {
|
|
clone := meta
|
|
clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs))
|
|
for index, config := range meta.APIConfigs {
|
|
clone.APIConfigs[index] = config
|
|
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
|
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...)
|
|
}
|
|
clone.Models = append([]Model(nil), meta.Models...)
|
|
return clone
|
|
}
|