fix(provider,web,asr): normalize elevenlabs configs and gate default chat models
This commit is contained in:
parent
6a096a0d46
commit
b3520d930e
18 changed files with 424 additions and 210 deletions
|
|
@ -82,7 +82,8 @@ Notes:
|
|||
"model_list": [
|
||||
{
|
||||
"model_name": "elevenlabs-asr",
|
||||
"model": "elevenlabs/scribe_v1"
|
||||
"provider": "elevenlabs",
|
||||
"model": "scribe_v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -130,7 +131,7 @@ PicoClaw currently supports three main ASR routes:
|
|||
|
||||
| Route | Example models | Behavior |
|
||||
| --- | --- | --- |
|
||||
| ElevenLabs ASR | `elevenlabs/scribe_v1` | Uses the ElevenLabs transcription API. |
|
||||
| ElevenLabs ASR | `provider: elevenlabs`, `model: scribe_v1` | Uses the ElevenLabs transcription API. |
|
||||
| Whisper endpoint models | `openai/whisper-1`, `groq/whisper-large-v3` | Uses an OpenAI-compatible `/audio/transcriptions` endpoint. |
|
||||
| Audio-capable chat models **(Under construction)** | `openai/gpt-4o-audio-preview`, `gemini/gemini-2.5-flash` | Sends audio to a multimodal chat model and asks it to transcribe. |
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ If you are unsure which one to pick, choose Groq Whisper or ElevenLabs first.
|
|||
|
||||
1. **Preferred path**: resolve `voice.model_name` against `model_list`.
|
||||
2. If that resolved model is:
|
||||
- `elevenlabs/...`, PicoClaw uses the ElevenLabs transcriber.
|
||||
- an `elevenlabs` provider model, PicoClaw uses the ElevenLabs transcriber.
|
||||
- an OpenAI-compatible Whisper model, PicoClaw uses the Whisper transcriber.
|
||||
- an audio-capable chat model, PicoClaw uses `AudioModelTranscriber`.
|
||||
3. **Fallback path**: if `voice.model_name` is not set, PicoClaw performs a compatibility scan through `model_list` for legacy auto-detected ASR entries.
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ model_list:
|
|||
"model_list": [
|
||||
{
|
||||
"model_name": "elevenlabs-asr",
|
||||
"model": "elevenlabs/scribe_v1"
|
||||
"provider": "elevenlabs",
|
||||
"model": "scribe_v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -130,7 +131,7 @@ PicoClaw 目前主要支持三种 ASR 路径:
|
|||
|
||||
| 路径 | 示例模型 | 行为说明 |
|
||||
| --- | --- | --- |
|
||||
| ElevenLabs ASR | `elevenlabs/scribe_v1` | 使用 ElevenLabs 的语音转录接口。 |
|
||||
| ElevenLabs ASR | `provider: elevenlabs`,`model: scribe_v1` | 使用 ElevenLabs 的语音转录接口。 |
|
||||
| Whisper 接口模型 | `openai/whisper-1`、`groq/whisper-large-v3` | 使用 OpenAI 兼容的 `/audio/transcriptions` 接口。 |
|
||||
| 支持音频的聊天模型 **(重构中)** | `openai/gpt-4o-audio-preview`、`gemini/gemini-2.5-flash` | 把音频发给多模态聊天模型,并要求它返回转录结果。 |
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ PicoClaw 目前主要支持三种 ASR 路径:
|
|||
|
||||
1. **首选路径**:根据 `voice.model_name` 在 `model_list` 中找到对应模型。
|
||||
2. 如果找到的模型属于以下类型:
|
||||
- `elevenlabs/...`,则使用 ElevenLabs transcriber。
|
||||
- `provider=elevenlabs` 的模型,则使用 ElevenLabs transcriber。
|
||||
- OpenAI 兼容的 Whisper 模型,则使用 Whisper transcriber。
|
||||
- 支持音频输入的聊天模型,则使用 `AudioModelTranscriber`。
|
||||
3. **回退路径**:如果没有设置 `voice.model_name`,PicoClaw 会为了兼容旧配置,扫描 `model_list` 中可自动识别的 ASR 条目。
|
||||
|
|
|
|||
|
|
@ -78,21 +78,7 @@ func isElevenLabsTranscriptionModel(modelCfg *config.ModelConfig) bool {
|
|||
}
|
||||
|
||||
protocol, _ := providers.ExtractProtocol(modelCfg)
|
||||
if protocol == "elevenlabs" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(modelCfg.Provider) != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
legacyProvider, legacyModel, found := strings.Cut(strings.TrimSpace(modelCfg.Model), "/")
|
||||
if !found || strings.TrimSpace(legacyModel) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Keep legacy elevenlabs/... ASR configs working even though the shared
|
||||
// provider catalog no longer treats elevenlabs as a general model provider.
|
||||
return providers.NormalizeProvider(legacyProvider) == "elevenlabs"
|
||||
return protocol == "elevenlabs"
|
||||
}
|
||||
|
||||
func transcriberFromModelConfig(modelCfg *config.ModelConfig) Transcriber {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,21 @@ func TestDetectTranscriber(t *testing.T) {
|
|||
},
|
||||
wantName: "elevenlabs",
|
||||
},
|
||||
{
|
||||
name: "explicit elevenlabs provider selects elevenlabs transcriber",
|
||||
cfg: &config.Config{
|
||||
Voice: config.VoiceConfig{ModelName: "my-asr-model"},
|
||||
ModelList: []*config.ModelConfig{
|
||||
{
|
||||
ModelName: "my-asr-model",
|
||||
Provider: "elevenlabs",
|
||||
Model: "scribe_v1",
|
||||
APIKeys: config.SimpleSecureStrings("sk_elevenlabs_test"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantName: "elevenlabs",
|
||||
},
|
||||
{
|
||||
name: "voice model name alias selects whisper transcriber for groq",
|
||||
cfg: &config.Config{
|
||||
|
|
|
|||
|
|
@ -987,6 +987,16 @@ func TestModelProviderOptions(t *testing.T) {
|
|||
} else if !option.CreateAllowed {
|
||||
t.Fatal("bedrock should be creatable and defer credential/build errors to runtime")
|
||||
}
|
||||
if option, ok := seen["elevenlabs"]; !ok {
|
||||
t.Fatal("elevenlabs option missing")
|
||||
} else {
|
||||
if option.DefaultAPIBase != "https://api.elevenlabs.io" {
|
||||
t.Fatalf("elevenlabs default_api_base = %q, want %q", option.DefaultAPIBase, "https://api.elevenlabs.io")
|
||||
}
|
||||
if option.DefaultModelAllowed {
|
||||
t.Fatal("elevenlabs should be ASR-only and therefore not allowed as a default chat model")
|
||||
}
|
||||
}
|
||||
if option, ok := seen["antigravity"]; !ok {
|
||||
t.Fatal("antigravity option missing")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,19 +7,21 @@ import (
|
|||
|
||||
// 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"`
|
||||
DefaultAuthMethod string `json:"default_auth_method,omitempty"`
|
||||
AuthMethodLocked bool `json:"auth_method_locked,omitempty"`
|
||||
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
|
||||
defaultAuthMethod string
|
||||
authMethodLocked bool
|
||||
createAllowed bool
|
||||
defaultModelAllowed bool
|
||||
defaultAuthMethod string
|
||||
authMethodLocked bool
|
||||
}
|
||||
|
||||
// attachedModelProviderMetaByName augments protocolMetaByName for provider
|
||||
|
|
@ -27,20 +29,39 @@ type attachedModelProviderMeta struct {
|
|||
// kept out of the core HTTP metadata map because they have special auth/runtime
|
||||
// semantics.
|
||||
var attachedModelProviderMetaByName = map[string]attachedModelProviderMeta{
|
||||
"azure": {createAllowed: true},
|
||||
"azure": {createAllowed: true, defaultModelAllowed: true},
|
||||
"anthropic": {
|
||||
protocolMeta: protocolMeta{defaultAPIBase: "https://api.anthropic.com/v1"},
|
||||
createAllowed: true,
|
||||
protocolMeta: protocolMeta{defaultAPIBase: "https://api.anthropic.com/v1"},
|
||||
createAllowed: true,
|
||||
defaultModelAllowed: true,
|
||||
},
|
||||
"anthropic-messages": {
|
||||
protocolMeta: protocolMeta{defaultAPIBase: "https://api.anthropic.com/v1"},
|
||||
createAllowed: true,
|
||||
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,
|
||||
},
|
||||
"bedrock": {createAllowed: true},
|
||||
"antigravity": {createAllowed: true, defaultAuthMethod: "oauth", authMethodLocked: true},
|
||||
"claude-cli": {createAllowed: true},
|
||||
"codex-cli": {createAllowed: true},
|
||||
"github-copilot": {protocolMeta: protocolMeta{defaultAPIBase: "localhost:4321"}, createAllowed: true},
|
||||
}
|
||||
|
||||
// ModelProviderOptions returns the canonical provider catalog exposed to the Web UI.
|
||||
|
|
@ -51,10 +72,11 @@ func ModelProviderOptions() []ModelProviderOption {
|
|||
continue
|
||||
}
|
||||
optionsByID[provider] = ModelProviderOption{
|
||||
ID: provider,
|
||||
DefaultAPIBase: DefaultAPIBaseForProtocol(provider),
|
||||
EmptyAPIKeyAllowed: IsEmptyAPIKeyAllowedForProtocol(provider),
|
||||
CreateAllowed: true,
|
||||
ID: provider,
|
||||
DefaultAPIBase: DefaultAPIBaseForProtocol(provider),
|
||||
EmptyAPIKeyAllowed: IsEmptyAPIKeyAllowedForProtocol(provider),
|
||||
CreateAllowed: true,
|
||||
DefaultModelAllowed: true,
|
||||
}
|
||||
}
|
||||
for provider, meta := range attachedModelProviderMetaByName {
|
||||
|
|
@ -62,12 +84,13 @@ func ModelProviderOptions() []ModelProviderOption {
|
|||
continue
|
||||
}
|
||||
optionsByID[provider] = ModelProviderOption{
|
||||
ID: provider,
|
||||
DefaultAPIBase: meta.defaultAPIBase,
|
||||
EmptyAPIKeyAllowed: meta.emptyAPIKeyAllowed,
|
||||
CreateAllowed: meta.createAllowed,
|
||||
DefaultAuthMethod: meta.defaultAuthMethod,
|
||||
AuthMethodLocked: meta.authMethodLocked,
|
||||
ID: provider,
|
||||
DefaultAPIBase: meta.defaultAPIBase,
|
||||
EmptyAPIKeyAllowed: meta.emptyAPIKeyAllowed,
|
||||
CreateAllowed: meta.createAllowed,
|
||||
DefaultModelAllowed: meta.defaultModelAllowed,
|
||||
DefaultAuthMethod: meta.defaultAuthMethod,
|
||||
AuthMethodLocked: meta.authMethodLocked,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +132,21 @@ func IsCreatableModelProvider(provider string) bool {
|
|||
return ok && meta.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
|
||||
}
|
||||
|
||||
// SplitModelProviderAndID separates a legacy "provider/model" string into its
|
||||
// effective provider and canonical model ID. Unknown prefixes are treated as
|
||||
// part of the model ID and fall back to defaultProvider.
|
||||
|
|
|
|||
|
|
@ -382,6 +382,9 @@ func (h *Handler) gatewayStartReady() (bool, string, error) {
|
|||
if modelCfg == nil {
|
||||
return false, fmt.Sprintf("default model %q is invalid", modelName), nil
|
||||
}
|
||||
if !defaultModelAllowedForModelConfig(modelCfg) {
|
||||
return false, fmt.Sprintf("default model %q is not usable for chat", modelName), nil
|
||||
}
|
||||
|
||||
if !hasModelConfiguration(modelCfg) {
|
||||
return false, fmt.Sprintf("default model %q has no credentials configured", modelName), nil
|
||||
|
|
|
|||
|
|
@ -357,6 +357,44 @@ func TestGatewayStartReady_NoDefaultModel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGatewayStartReady_RejectsASROnlyDefaultModel(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "elevenlabs-asr",
|
||||
Provider: "elevenlabs",
|
||||
Model: "scribe_v1",
|
||||
APIKeys: config.SimpleSecureStrings("sk_elevenlabs_test"),
|
||||
}}
|
||||
cfg.Agents.Defaults.ModelName = "elevenlabs-asr"
|
||||
|
||||
err = config.SaveConfig(configPath, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
|
||||
h := NewHandler(configPath)
|
||||
ready, reason, err := h.gatewayStartReady()
|
||||
if err != nil {
|
||||
t.Fatalf("gatewayStartReady() error = %v", err)
|
||||
}
|
||||
if ready {
|
||||
t.Fatal("gatewayStartReady() ready = true, want false")
|
||||
}
|
||||
if reason != `default model "elevenlabs-asr" is not usable for chat` {
|
||||
t.Fatalf(
|
||||
"gatewayStartReady() reason = %q, want %q",
|
||||
reason,
|
||||
`default model "elevenlabs-asr" is not usable for chat`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksLikeGatewayCommandLine(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -44,53 +44,12 @@ type modelResponse struct {
|
|||
ExtraBody map[string]any `json:"extra_body,omitempty"`
|
||||
CustomHeaders map[string]string `json:"custom_headers,omitempty"`
|
||||
// Meta
|
||||
Enabled bool `json:"enabled"`
|
||||
Available bool `json:"available"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsVirtual bool `json:"is_virtual"`
|
||||
}
|
||||
|
||||
func legacyUnsupportedASRProviderAndModel(rawModel string) (provider, modelID string, ok bool) {
|
||||
provider, modelID, found := strings.Cut(strings.TrimSpace(rawModel), "/")
|
||||
if !found {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
provider = providers.NormalizeProvider(provider)
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case "elevenlabs":
|
||||
// Keep the documented legacy ASR-only form elevenlabs/scribe_v1 stable
|
||||
// even though elevenlabs is not part of the general model provider
|
||||
// catalog exposed by the Web model-management UI.
|
||||
return provider, modelID, true
|
||||
default:
|
||||
return "", "", false
|
||||
}
|
||||
}
|
||||
|
||||
func isLegacyUnsupportedASRModelConfig(mc *config.ModelConfig) bool {
|
||||
if mc == nil || strings.TrimSpace(mc.Provider) != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
_, _, ok := legacyUnsupportedASRProviderAndModel(mc.Model)
|
||||
return ok
|
||||
}
|
||||
|
||||
func responseProviderAndModel(mc *config.ModelConfig) (provider, modelID string) {
|
||||
if strings.TrimSpace(mc.Provider) == "" {
|
||||
if legacyProvider, legacyModelID, ok := legacyUnsupportedASRProviderAndModel(mc.Model); ok {
|
||||
return legacyProvider, legacyModelID
|
||||
}
|
||||
}
|
||||
|
||||
return providers.ExtractProtocol(mc)
|
||||
Enabled bool `json:"enabled"`
|
||||
Available bool `json:"available"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsVirtual bool `json:"is_virtual"`
|
||||
DefaultModelAllowed bool `json:"default_model_allowed"`
|
||||
}
|
||||
|
||||
func normalizeStoredModelConfig(mc *config.ModelConfig) bool {
|
||||
|
|
@ -121,9 +80,19 @@ func normalizeStoredModelConfig(mc *config.ModelConfig) bool {
|
|||
mc.Provider = normalizedProvider
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
if isLegacyUnsupportedASRModelConfig(mc) {
|
||||
if mc.Provider == "elevenlabs" {
|
||||
if _, strippedModel, found := strings.Cut(
|
||||
model,
|
||||
"/",
|
||||
); found &&
|
||||
providers.NormalizeProvider(strings.TrimSpace(provider)) == "elevenlabs" {
|
||||
strippedModel = strings.TrimSpace(strippedModel)
|
||||
if strippedModel != "" && strippedModel != mc.Model {
|
||||
mc.Model = strippedModel
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
|
|
@ -151,12 +120,17 @@ func normalizeIncomingModelConfig(mc *config.ModelConfig) {
|
|||
mc.Provider = strings.TrimSpace(mc.Provider)
|
||||
mc.AuthMethod = strings.ToLower(strings.TrimSpace(mc.AuthMethod))
|
||||
if mc.Provider == "" {
|
||||
if isLegacyUnsupportedASRModelConfig(mc) {
|
||||
return
|
||||
}
|
||||
mc.Provider, mc.Model = providers.SplitModelProviderAndID(mc.Model, "openai")
|
||||
} else {
|
||||
mc.Provider = providers.NormalizeProvider(mc.Provider)
|
||||
if mc.Provider == "elevenlabs" {
|
||||
if _, strippedModel, found := strings.Cut(mc.Model, "/"); found {
|
||||
strippedModel = strings.TrimSpace(strippedModel)
|
||||
if strippedModel != "" {
|
||||
mc.Model = strippedModel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if mc.Provider == "antigravity" && mc.AuthMethod == "" {
|
||||
mc.AuthMethod = "oauth"
|
||||
|
|
@ -199,6 +173,11 @@ func modelProviderOptionsForResponse() []providers.ModelProviderOption {
|
|||
return options
|
||||
}
|
||||
|
||||
func defaultModelAllowedForModelConfig(mc *config.ModelConfig) bool {
|
||||
provider, _ := providers.ExtractProtocol(mc)
|
||||
return providers.IsDefaultModelProvider(provider)
|
||||
}
|
||||
|
||||
func validateIncomingModelConfig(mc *config.ModelConfig, existing *config.ModelConfig) error {
|
||||
if mc == nil {
|
||||
return fmt.Errorf("model config is required")
|
||||
|
|
@ -207,9 +186,6 @@ func validateIncomingModelConfig(mc *config.ModelConfig, existing *config.ModelC
|
|||
return err
|
||||
}
|
||||
if strings.TrimSpace(mc.Provider) == "" {
|
||||
if existing != nil && isLegacyUnsupportedASRModelConfig(existing) && isLegacyUnsupportedASRModelConfig(mc) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("provider is required")
|
||||
}
|
||||
if !providers.IsSupportedModelProvider(mc.Provider) {
|
||||
|
|
@ -266,29 +242,30 @@ func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
models := make([]modelResponse, 0, len(cfg.ModelList))
|
||||
for i, m := range cfg.ModelList {
|
||||
provider, modelID := responseProviderAndModel(m)
|
||||
provider, modelID := providers.ExtractProtocol(m)
|
||||
models = append(models, modelResponse{
|
||||
Index: i,
|
||||
ModelName: m.ModelName,
|
||||
Provider: provider,
|
||||
Model: modelID,
|
||||
APIBase: m.APIBase,
|
||||
APIKey: maskAPIKey(m.APIKey()),
|
||||
Proxy: m.Proxy,
|
||||
AuthMethod: m.AuthMethod,
|
||||
ConnectMode: m.ConnectMode,
|
||||
Workspace: m.Workspace,
|
||||
RPM: m.RPM,
|
||||
MaxTokensField: m.MaxTokensField,
|
||||
RequestTimeout: m.RequestTimeout,
|
||||
ThinkingLevel: m.ThinkingLevel,
|
||||
ExtraBody: m.ExtraBody,
|
||||
CustomHeaders: m.CustomHeaders,
|
||||
Enabled: m.Enabled,
|
||||
Available: modelStatuses[i].Available,
|
||||
Status: modelStatuses[i].Status,
|
||||
IsDefault: m.ModelName == defaultModel,
|
||||
IsVirtual: m.IsVirtual(),
|
||||
Index: i,
|
||||
ModelName: m.ModelName,
|
||||
Provider: provider,
|
||||
Model: modelID,
|
||||
APIBase: m.APIBase,
|
||||
APIKey: maskAPIKey(m.APIKey()),
|
||||
Proxy: m.Proxy,
|
||||
AuthMethod: m.AuthMethod,
|
||||
ConnectMode: m.ConnectMode,
|
||||
Workspace: m.Workspace,
|
||||
RPM: m.RPM,
|
||||
MaxTokensField: m.MaxTokensField,
|
||||
RequestTimeout: m.RequestTimeout,
|
||||
ThinkingLevel: m.ThinkingLevel,
|
||||
ExtraBody: m.ExtraBody,
|
||||
CustomHeaders: m.CustomHeaders,
|
||||
Enabled: m.Enabled,
|
||||
Available: modelStatuses[i].Available,
|
||||
Status: modelStatuses[i].Status,
|
||||
IsDefault: m.ModelName == defaultModel,
|
||||
IsVirtual: m.IsVirtual(),
|
||||
DefaultModelAllowed: defaultModelAllowedForModelConfig(m),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -439,34 +416,18 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
|
|||
if strings.TrimSpace(cfg.ModelList[idx].Provider) == "" {
|
||||
existingRawModel := strings.TrimSpace(cfg.ModelList[idx].Model)
|
||||
incomingModel := strings.TrimSpace(mc.Model)
|
||||
if legacyProvider, legacyModelID, ok := legacyUnsupportedASRProviderAndModel(existingRawModel); ok {
|
||||
if incomingModel != "" {
|
||||
if incomingModel == legacyModelID {
|
||||
mc.Model = existingRawModel
|
||||
} else if strings.Contains(incomingModel, "/") && !strings.Contains(legacyModelID, "/") {
|
||||
// Older clients only saw the visible legacy ASR model ID
|
||||
// (for example "scribe_v1"). If they now send an explicit
|
||||
// provider/model string, keep that full intent instead of
|
||||
// silently re-applying the hidden ElevenLabs prefix.
|
||||
mc.Model = incomingModel
|
||||
} else if !strings.HasPrefix(incomingModel, legacyProvider+"/") {
|
||||
mc.Model = legacyProvider + "/" + incomingModel
|
||||
}
|
||||
}
|
||||
} else {
|
||||
existingProtocol, existingModelID := providers.ExtractProtocol(cfg.ModelList[idx])
|
||||
if existingRawModel != "" && existingRawModel != existingModelID && incomingModel != "" {
|
||||
if incomingModel == existingModelID {
|
||||
mc.Model = existingRawModel
|
||||
} else if strings.Contains(incomingModel, "/") && !strings.Contains(existingModelID, "/") {
|
||||
// Older clients never saw the hidden provider prefix for simple
|
||||
// legacy entries such as "openai/gpt-4o". If they now send an
|
||||
// explicit provider/model string, treat it as the caller's full
|
||||
// intent instead of re-applying the old hidden prefix.
|
||||
mc.Model = incomingModel
|
||||
} else if !strings.HasPrefix(incomingModel, existingProtocol+"/") {
|
||||
mc.Model = existingProtocol + "/" + incomingModel
|
||||
}
|
||||
existingProtocol, existingModelID := providers.ExtractProtocol(cfg.ModelList[idx])
|
||||
if existingRawModel != "" && existingRawModel != existingModelID && incomingModel != "" {
|
||||
if incomingModel == existingModelID {
|
||||
mc.Model = existingRawModel
|
||||
} else if strings.Contains(incomingModel, "/") && !strings.Contains(existingModelID, "/") {
|
||||
// Older clients never saw the hidden provider prefix for simple
|
||||
// legacy entries such as "openai/gpt-4o". If they now send an
|
||||
// explicit provider/model string, treat it as the caller's full
|
||||
// intent instead of re-applying the old hidden prefix.
|
||||
mc.Model = incomingModel
|
||||
} else if !strings.HasPrefix(incomingModel, existingProtocol+"/") {
|
||||
mc.Model = existingProtocol + "/" + incomingModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -477,6 +438,12 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, fmt.Sprintf("Validation error: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if cfg.Agents.Defaults.ModelName == cfg.ModelList[idx].ModelName &&
|
||||
!defaultModelAllowedForModelConfig(&mc.ModelConfig) {
|
||||
// Allow users to recover from legacy/invalid defaults by saving the model
|
||||
// and clearing the default chat model reference in the same write.
|
||||
cfg.Agents.Defaults.ModelName = ""
|
||||
}
|
||||
|
||||
cfg.ModelList[idx] = &mc.ModelConfig
|
||||
normalizeStoredModelProviders(cfg)
|
||||
|
|
@ -579,6 +546,19 @@ func (h *Handler) handleSetDefaultModel(w http.ResponseWriter, r *http.Request)
|
|||
http.Error(w, fmt.Sprintf("Cannot set virtual model %q as default", req.ModelName), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for _, m := range cfg.ModelList {
|
||||
if m.ModelName == req.ModelName {
|
||||
if !defaultModelAllowedForModelConfig(m) {
|
||||
http.Error(
|
||||
w,
|
||||
fmt.Sprintf("Model %q cannot be used as the default chat model", req.ModelName),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Agents.Defaults.ModelName = req.ModelName
|
||||
|
||||
|
|
|
|||
|
|
@ -820,7 +820,7 @@ func TestHandleAddModel_AllowsBedrockProvider(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleAddModel_PreservesLegacyElevenLabsASRConfig(t *testing.T) {
|
||||
func TestHandleAddModel_NormalizesLegacyElevenLabsASRConfig(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
|
|
@ -862,11 +862,11 @@ func TestHandleAddModel_PreservesLegacyElevenLabsASRConfig(t *testing.T) {
|
|||
if len(updated.ModelList) != 2 {
|
||||
t.Fatalf("len(model_list) = %d, want 2", len(updated.ModelList))
|
||||
}
|
||||
if got := updated.ModelList[0].Provider; got != "" {
|
||||
t.Fatalf("provider = %q, want preserved empty provider for legacy ElevenLabs ASR config", got)
|
||||
if got := updated.ModelList[0].Provider; got != "elevenlabs" {
|
||||
t.Fatalf("provider = %q, want %q after normalization", got, "elevenlabs")
|
||||
}
|
||||
if got := updated.ModelList[0].Model; got != "elevenlabs/scribe_v1" {
|
||||
t.Fatalf("model = %q, want preserved legacy ElevenLabs model ref", got)
|
||||
if got := updated.ModelList[0].Model; got != "scribe_v1" {
|
||||
t.Fatalf("model = %q, want %q after normalization", got, "scribe_v1")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1208,7 +1208,7 @@ func TestHandleListModels_PreservesExplicitProviderPrefixedModel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleListModels_ExposesLegacyElevenLabsASRProvider(t *testing.T) {
|
||||
func TestHandleListModels_ExposesElevenLabsASRProvider(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
|
|
@ -1247,10 +1247,13 @@ func TestHandleListModels_ExposesLegacyElevenLabsASRProvider(t *testing.T) {
|
|||
t.Fatalf("len(models) = %d, want 1", len(resp.Models))
|
||||
}
|
||||
if got := resp.Models[0].Provider; got != "elevenlabs" {
|
||||
t.Fatalf("provider = %q, want %q for legacy unsupported ASR entry", got, "elevenlabs")
|
||||
t.Fatalf("provider = %q, want %q", got, "elevenlabs")
|
||||
}
|
||||
if got := resp.Models[0].Model; got != "scribe_v1" {
|
||||
t.Fatalf("model = %q, want %q for legacy unsupported ASR entry", got, "scribe_v1")
|
||||
t.Fatalf("model = %q, want %q", got, "scribe_v1")
|
||||
}
|
||||
if resp.Models[0].DefaultModelAllowed {
|
||||
t.Fatal("elevenlabs ASR model should not be allowed as the default chat model")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1324,7 +1327,7 @@ func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmitted(t *test
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateModel_PreservesLegacyElevenLabsASRWhenProviderOmitted(t *testing.T) {
|
||||
func TestHandleUpdateModel_MigratesLegacyElevenLabsASRWhenProviderOmitted(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
|
|
@ -1386,17 +1389,63 @@ func TestHandleUpdateModel_PreservesLegacyElevenLabsASRWhenProviderOmitted(t *te
|
|||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
if got := updated.ModelList[0].Provider; got != "" {
|
||||
t.Fatalf("provider = %q, want preserved empty provider", got)
|
||||
if got := updated.ModelList[0].Provider; got != "elevenlabs" {
|
||||
t.Fatalf("provider = %q, want %q", got, "elevenlabs")
|
||||
}
|
||||
if got := updated.ModelList[0].Model; got != "elevenlabs/scribe_v1" {
|
||||
t.Fatalf("model = %q, want preserved legacy model ref", got)
|
||||
if got := updated.ModelList[0].Model; got != "scribe_v1" {
|
||||
t.Fatalf("model = %q, want %q", got, "scribe_v1")
|
||||
}
|
||||
if got := updated.ModelList[0].APIBase; got != "https://api.elevenlabs.io" {
|
||||
t.Fatalf("api_base = %q, want %q", got, "https://api.elevenlabs.io")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateModel_ClearsDefaultWhenSavingASROnlyModel(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "elevenlabs-asr",
|
||||
Provider: "elevenlabs",
|
||||
Model: "scribe_v1",
|
||||
APIKeys: config.SimpleSecureStrings("sk_elevenlabs_test"),
|
||||
}}
|
||||
cfg.Agents.Defaults.ModelName = "elevenlabs-asr"
|
||||
if err = config.SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
|
||||
h := NewHandler(configPath)
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/models/0", bytes.NewBufferString(`{
|
||||
"model_name":"elevenlabs-asr",
|
||||
"provider":"elevenlabs",
|
||||
"model":"scribe_v1",
|
||||
"api_base":"https://api.elevenlabs.io"
|
||||
}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
updated, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
if got := updated.Agents.Defaults.ModelName; got != "" {
|
||||
t.Fatalf("default model = %q, want cleared default", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmittedAndModelChanges(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
|
@ -1512,6 +1561,16 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
|
|||
} else if option.DefaultAPIBase != "localhost:4321" {
|
||||
t.Fatalf("github-copilot default_api_base = %q, want %q", option.DefaultAPIBase, "localhost:4321")
|
||||
}
|
||||
if option, ok := optionsByID["elevenlabs"]; !ok {
|
||||
t.Fatal("elevenlabs provider option missing")
|
||||
} else {
|
||||
if option.DefaultAPIBase != "https://api.elevenlabs.io" {
|
||||
t.Fatalf("elevenlabs default_api_base = %q, want %q", option.DefaultAPIBase, "https://api.elevenlabs.io")
|
||||
}
|
||||
if option.DefaultModelAllowed {
|
||||
t.Fatal("elevenlabs should be marked as not allowed for default chat model selection")
|
||||
}
|
||||
}
|
||||
if option, ok := optionsByID["lmstudio"]; !ok {
|
||||
t.Fatal("lmstudio provider option missing")
|
||||
} else if !option.EmptyAPIKeyAllowed {
|
||||
|
|
@ -1809,6 +1868,45 @@ func TestHandleSetDefaultModel_RejectsNonexistentModel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleSetDefaultModel_RejectsElevenLabsASRProvider(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{
|
||||
ModelName: "elevenlabs-asr",
|
||||
Provider: "elevenlabs",
|
||||
Model: "scribe_v1",
|
||||
APIKeys: config.SimpleSecureStrings("sk_elevenlabs_test"),
|
||||
},
|
||||
}
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
|
||||
h := NewHandler(configPath)
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/models/default", bytes.NewBufferString(`{
|
||||
"model_name": "elevenlabs-asr"
|
||||
}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "cannot be used as the default chat model") {
|
||||
t.Fatalf("body = %q, want default chat model rejection", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskAPIKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface ModelInfo {
|
|||
status: "available" | "unconfigured" | "unreachable"
|
||||
is_default: boolean
|
||||
is_virtual: boolean
|
||||
default_model_allowed?: boolean
|
||||
}
|
||||
|
||||
export interface ModelProviderOption {
|
||||
|
|
@ -33,6 +34,7 @@ export interface ModelProviderOption {
|
|||
default_api_base: string
|
||||
empty_api_key_allowed: boolean
|
||||
create_allowed: boolean
|
||||
default_model_allowed: boolean
|
||||
default_auth_method?: string
|
||||
auth_method_locked?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,8 @@ export function AddModelSheet({
|
|||
.trim()
|
||||
.toLowerCase()
|
||||
const isOAuth = effectiveAuthMethod === "oauth"
|
||||
const defaultModelAllowed =
|
||||
selectedProviderOption?.default_model_allowed !== false
|
||||
const apiBasePlaceholder =
|
||||
getProviderDefaultAPIBase(form.provider, providerOptions) ||
|
||||
"https://api.example.com/v1"
|
||||
|
|
@ -189,6 +191,10 @@ export function AddModelSheet({
|
|||
}
|
||||
return { ...f, provider: value, authMethod }
|
||||
})
|
||||
const nextOption = findProviderOption(value, providerOptions)
|
||||
if (nextOption?.default_model_allowed === false) {
|
||||
setSetAsDefault(false)
|
||||
}
|
||||
if (fieldErrors.provider) {
|
||||
setFieldErrors((prev) => ({ ...prev, provider: undefined }))
|
||||
}
|
||||
|
|
@ -345,9 +351,14 @@ export function AddModelSheet({
|
|||
|
||||
<SwitchCardField
|
||||
label={t("models.defaultOnSave.label")}
|
||||
hint={t("models.defaultOnSave.description")}
|
||||
hint={
|
||||
defaultModelAllowed
|
||||
? t("models.defaultOnSave.description")
|
||||
: t("models.defaultOnSave.unsupportedProvider")
|
||||
}
|
||||
checked={setAsDefault}
|
||||
onCheckedChange={setSetAsDefault}
|
||||
disabled={!defaultModelAllowed}
|
||||
/>
|
||||
|
||||
<AdvancedSection>
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ export function EditModelSheet({
|
|||
const providerError = selectedProviderOption
|
||||
? ""
|
||||
: t("models.field.providerInvalid")
|
||||
const defaultModelAllowed =
|
||||
selectedProviderOption?.default_model_allowed !== false
|
||||
const willClearDefaultOnSave =
|
||||
model?.is_default === true && defaultModelAllowed === false
|
||||
const apiBasePlaceholder =
|
||||
getProviderDefaultAPIBase(form.provider, providerOptions) ||
|
||||
"https://api.example.com/v1"
|
||||
|
|
@ -167,7 +171,7 @@ export function EditModelSheet({
|
|||
initialForm.authMethod = option.default_auth_method ?? ""
|
||||
}
|
||||
setForm(initialForm)
|
||||
setSetAsDefault(model.is_default)
|
||||
setSetAsDefault(model.is_default && model.default_model_allowed !== false)
|
||||
setError("")
|
||||
}
|
||||
}, [model, providerOptions])
|
||||
|
|
@ -199,6 +203,10 @@ export function EditModelSheet({
|
|||
}
|
||||
return { ...f, provider: value, authMethod }
|
||||
})
|
||||
const nextOption = findProviderOption(value, providerOptions)
|
||||
if (nextOption?.default_model_allowed === false) {
|
||||
setSetAsDefault(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
|
|
@ -358,9 +366,16 @@ export function EditModelSheet({
|
|||
|
||||
<SwitchCardField
|
||||
label={t("models.defaultOnSave.label")}
|
||||
hint={t("models.defaultOnSave.description")}
|
||||
hint={
|
||||
willClearDefaultOnSave
|
||||
? t("models.defaultOnSave.clearOnSave")
|
||||
: defaultModelAllowed
|
||||
? t("models.defaultOnSave.description")
|
||||
: t("models.defaultOnSave.unsupportedProvider")
|
||||
}
|
||||
checked={setAsDefault}
|
||||
onCheckedChange={setSetAsDefault}
|
||||
disabled={!defaultModelAllowed}
|
||||
/>
|
||||
|
||||
<AdvancedSection>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ export function ModelCard({
|
|||
const status = model.status
|
||||
const statusLabel = t(`models.status.${status}`)
|
||||
const canSetDefault =
|
||||
model.available && !model.is_default && !model.is_virtual
|
||||
model.available &&
|
||||
!model.is_default &&
|
||||
!model.is_virtual &&
|
||||
model.default_model_allowed !== false
|
||||
|
||||
const setDefaultLabel = t("models.action.setDefault")
|
||||
const setDefaultDisabledReason = (() => {
|
||||
|
|
@ -45,6 +48,9 @@ export function ModelCard({
|
|||
return t("models.action.setDefaultDisabled.unavailable")
|
||||
if (model.is_default) return t("models.action.setDefaultDisabled.isDefault")
|
||||
if (model.is_virtual) return t("models.action.setDefaultDisabled.isVirtual")
|
||||
if (model.default_model_allowed === false) {
|
||||
return t("models.action.setDefaultDisabled.unsupportedProvider")
|
||||
}
|
||||
return setDefaultLabel
|
||||
})()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useMemo, useState } from "react"
|
|||
|
||||
const PROVIDER_ICON_SLUGS: Record<string, string> = {
|
||||
openai: "openai",
|
||||
elevenlabs: "elevenlabs",
|
||||
anthropic: "anthropic",
|
||||
azure: "microsoftazure",
|
||||
gemini: "googlegemini",
|
||||
|
|
@ -21,6 +22,7 @@ const PROVIDER_ICON_SLUGS: Record<string, string> = {
|
|||
|
||||
const PROVIDER_DOMAINS: Record<string, string> = {
|
||||
openai: "openai.com",
|
||||
elevenlabs: "elevenlabs.io",
|
||||
anthropic: "anthropic.com",
|
||||
azure: "azure.com",
|
||||
gemini: "gemini.google.com",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ModelProviderOption } from "@/api/models"
|
|||
const PROVIDER_LABELS: Record<string, string> = {
|
||||
openai: "OpenAI",
|
||||
bedrock: "AWS Bedrock",
|
||||
elevenlabs: "ElevenLabs ASR",
|
||||
anthropic: "Anthropic",
|
||||
"anthropic-messages": "Anthropic Messages",
|
||||
azure: "Azure OpenAI",
|
||||
|
|
@ -57,39 +58,40 @@ export const PROVIDER_PRIORITY: Record<string, number> = {
|
|||
gemini: 2,
|
||||
anthropic: 3,
|
||||
bedrock: 4,
|
||||
"anthropic-messages": 5,
|
||||
zhipu: 6,
|
||||
deepseek: 7,
|
||||
openrouter: 8,
|
||||
"qwen-portal": 9,
|
||||
"qwen-intl": 10,
|
||||
"qwen-us": 11,
|
||||
moonshot: 12,
|
||||
groq: 13,
|
||||
"coding-plan": 14,
|
||||
"coding-plan-anthropic": 15,
|
||||
"github-copilot": 16,
|
||||
antigravity: 17,
|
||||
nvidia: 18,
|
||||
cerebras: 19,
|
||||
shengsuanyun: 20,
|
||||
venice: 21,
|
||||
vivgrid: 22,
|
||||
minimax: 23,
|
||||
longcat: 24,
|
||||
modelscope: 25,
|
||||
mistral: 26,
|
||||
avian: 27,
|
||||
novita: 28,
|
||||
azure: 29,
|
||||
litellm: 30,
|
||||
ollama: 31,
|
||||
vllm: 32,
|
||||
lmstudio: 33,
|
||||
"claude-cli": 34,
|
||||
"codex-cli": 35,
|
||||
zai: 36,
|
||||
mimo: 37,
|
||||
elevenlabs: 5,
|
||||
"anthropic-messages": 6,
|
||||
zhipu: 7,
|
||||
deepseek: 8,
|
||||
openrouter: 9,
|
||||
"qwen-portal": 10,
|
||||
"qwen-intl": 11,
|
||||
"qwen-us": 12,
|
||||
moonshot: 13,
|
||||
groq: 14,
|
||||
"coding-plan": 15,
|
||||
"coding-plan-anthropic": 16,
|
||||
"github-copilot": 17,
|
||||
antigravity: 18,
|
||||
nvidia: 19,
|
||||
cerebras: 20,
|
||||
shengsuanyun: 21,
|
||||
venice: 22,
|
||||
vivgrid: 23,
|
||||
minimax: 24,
|
||||
longcat: 25,
|
||||
modelscope: 26,
|
||||
mistral: 27,
|
||||
avian: 28,
|
||||
novita: 29,
|
||||
azure: 30,
|
||||
litellm: 31,
|
||||
ollama: 32,
|
||||
vllm: 33,
|
||||
lmstudio: 34,
|
||||
"claude-cli": 35,
|
||||
"codex-cli": 36,
|
||||
zai: 37,
|
||||
mimo: 38,
|
||||
}
|
||||
|
||||
export function getProviderKey(provider?: string): string {
|
||||
|
|
|
|||
|
|
@ -236,7 +236,8 @@
|
|||
"setting": "Setting as default...",
|
||||
"unavailable": "Cannot set unavailable model as default",
|
||||
"isDefault": "Already the default model",
|
||||
"isVirtual": "Cannot set virtual model as default"
|
||||
"isVirtual": "Cannot set virtual model as default",
|
||||
"unsupportedProvider": "This provider is ASR-only and cannot be the default chat model"
|
||||
},
|
||||
"deleteDisabled": {
|
||||
"isDefault": "Cannot delete the default model"
|
||||
|
|
@ -244,7 +245,9 @@
|
|||
},
|
||||
"defaultOnSave": {
|
||||
"label": "Default Model",
|
||||
"description": "Automatically set this model as default after saving."
|
||||
"description": "Automatically set this model as default after saving.",
|
||||
"unsupportedProvider": "This provider can be saved in model_list, but it cannot be used as the default chat model.",
|
||||
"clearOnSave": "Saving this ASR-only model will clear the current default chat model selection."
|
||||
},
|
||||
"add": {
|
||||
"button": "Add Model",
|
||||
|
|
|
|||
|
|
@ -236,7 +236,8 @@
|
|||
"setting": "正在设为默认...",
|
||||
"unavailable": "无法将不可用的模型设为默认",
|
||||
"isDefault": "该模型已是默认模型",
|
||||
"isVirtual": "无法将虚拟模型设为默认"
|
||||
"isVirtual": "无法将虚拟模型设为默认",
|
||||
"unsupportedProvider": "该 Provider 仅用于 ASR,不能设为默认聊天模型"
|
||||
},
|
||||
"deleteDisabled": {
|
||||
"isDefault": "无法删除默认模型"
|
||||
|
|
@ -244,7 +245,9 @@
|
|||
},
|
||||
"defaultOnSave": {
|
||||
"label": "默认模型",
|
||||
"description": "保存后自动将该模型设置为默认模型。"
|
||||
"description": "保存后自动将该模型设置为默认模型。",
|
||||
"unsupportedProvider": "该 Provider 可以保存在 model_list 中,但不能作为默认聊天模型使用。",
|
||||
"clearOnSave": "保存这个仅用于 ASR 的模型后,会清除当前的默认聊天模型设置。"
|
||||
},
|
||||
"add": {
|
||||
"button": "添加模型",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue