fix(web,asr): preserve legacy elevenlabs transcription configs
This commit is contained in:
parent
e76527d932
commit
6a096a0d46
2 changed files with 249 additions and 13 deletions
|
|
@ -51,6 +51,48 @@ type modelResponse struct {
|
|||
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)
|
||||
}
|
||||
|
||||
func normalizeStoredModelConfig(mc *config.ModelConfig) bool {
|
||||
if mc == nil {
|
||||
return false
|
||||
|
|
@ -81,6 +123,9 @@ func normalizeStoredModelConfig(mc *config.ModelConfig) bool {
|
|||
}
|
||||
return changed
|
||||
}
|
||||
if isLegacyUnsupportedASRModelConfig(mc) {
|
||||
return changed
|
||||
}
|
||||
|
||||
effectiveProvider, modelID := providers.SplitModelProviderAndID(model, "openai")
|
||||
if effectiveProvider == "" {
|
||||
|
|
@ -106,6 +151,9 @@ 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)
|
||||
|
|
@ -159,6 +207,9 @@ 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) {
|
||||
|
|
@ -215,7 +266,7 @@ 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 := providers.ExtractProtocol(m)
|
||||
provider, modelID := responseProviderAndModel(m)
|
||||
models = append(models, modelResponse{
|
||||
Index: i,
|
||||
ModelName: m.ModelName,
|
||||
|
|
@ -386,20 +437,36 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
|
|||
// This keeps provider-omitted updates backward-compatible even when an
|
||||
// older client edits the visible model ID.
|
||||
if strings.TrimSpace(cfg.ModelList[idx].Provider) == "" {
|
||||
existingProtocol, existingModelID := providers.ExtractProtocol(cfg.ModelList[idx])
|
||||
existingRawModel := strings.TrimSpace(cfg.ModelList[idx].Model)
|
||||
incomingModel := strings.TrimSpace(mc.Model)
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -820,6 +820,56 @@ func TestHandleAddModel_AllowsBedrockProvider(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleAddModel_PreservesLegacyElevenLabsASRConfig(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",
|
||||
Model: "elevenlabs/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", bytes.NewBufferString(`{
|
||||
"model_name":"new-model",
|
||||
"provider":"openai",
|
||||
"model":"gpt-4o-mini",
|
||||
"api_key":"sk-new-model-key"
|
||||
}`))
|
||||
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 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].Model; got != "elevenlabs/scribe_v1" {
|
||||
t.Fatalf("model = %q, want preserved legacy ElevenLabs model ref", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAddModel_RejectsMissingCLIProviderCommand(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
|
@ -1158,6 +1208,52 @@ func TestHandleListModels_PreservesExplicitProviderPrefixedModel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleListModels_ExposesLegacyElevenLabsASRProvider(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",
|
||||
Model: "elevenlabs/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.MethodGet, "/api/models", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Models []modelResponse `json:"models"`
|
||||
}
|
||||
if err = json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
if len(resp.Models) != 1 {
|
||||
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")
|
||||
}
|
||||
if got := resp.Models[0].Model; got != "scribe_v1" {
|
||||
t.Fatalf("model = %q, want %q for legacy unsupported ASR entry", got, "scribe_v1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmitted(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
|
@ -1228,6 +1324,79 @@ func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmitted(t *test
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateModel_PreservesLegacyElevenLabsASRWhenProviderOmitted(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",
|
||||
Model: "elevenlabs/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)
|
||||
|
||||
recList := httptest.NewRecorder()
|
||||
reqList := httptest.NewRequest(http.MethodGet, "/api/models", nil)
|
||||
mux.ServeHTTP(recList, reqList)
|
||||
|
||||
if recList.Code != http.StatusOK {
|
||||
t.Fatalf("list status = %d, want %d, body=%s", recList.Code, http.StatusOK, recList.Body.String())
|
||||
}
|
||||
|
||||
var listResp struct {
|
||||
Models []modelResponse `json:"models"`
|
||||
}
|
||||
if err = json.Unmarshal(recList.Body.Bytes(), &listResp); err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
if len(listResp.Models) != 1 {
|
||||
t.Fatalf("len(models) = %d, want 1", len(listResp.Models))
|
||||
}
|
||||
if got := listResp.Models[0].Provider; got != "elevenlabs" {
|
||||
t.Fatalf("provider = %q, want %q", got, "elevenlabs")
|
||||
}
|
||||
if got := listResp.Models[0].Model; got != "scribe_v1" {
|
||||
t.Fatalf("model = %q, want %q", got, "scribe_v1")
|
||||
}
|
||||
|
||||
recUpdate := httptest.NewRecorder()
|
||||
reqUpdate := httptest.NewRequest(http.MethodPut, "/api/models/0", bytes.NewBufferString(`{
|
||||
"model_name":"elevenlabs-asr",
|
||||
"model":"scribe_v1",
|
||||
"api_base":"https://api.elevenlabs.io"
|
||||
}`))
|
||||
reqUpdate.Header.Set("Content-Type", "application/json")
|
||||
mux.ServeHTTP(recUpdate, reqUpdate)
|
||||
|
||||
if recUpdate.Code != http.StatusOK {
|
||||
t.Fatalf("update status = %d, want %d, body=%s", recUpdate.Code, http.StatusOK, recUpdate.Body.String())
|
||||
}
|
||||
|
||||
updated, err := config.LoadConfig(configPath)
|
||||
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].Model; got != "elevenlabs/scribe_v1" {
|
||||
t.Fatalf("model = %q, want preserved legacy model ref", got)
|
||||
}
|
||||
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_PreservesLegacyModelPrefixWhenProviderOmittedAndModelChanges(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue