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"`
|
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 {
|
func normalizeStoredModelConfig(mc *config.ModelConfig) bool {
|
||||||
if mc == nil {
|
if mc == nil {
|
||||||
return false
|
return false
|
||||||
|
|
@ -81,6 +123,9 @@ func normalizeStoredModelConfig(mc *config.ModelConfig) bool {
|
||||||
}
|
}
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
if isLegacyUnsupportedASRModelConfig(mc) {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
effectiveProvider, modelID := providers.SplitModelProviderAndID(model, "openai")
|
effectiveProvider, modelID := providers.SplitModelProviderAndID(model, "openai")
|
||||||
if effectiveProvider == "" {
|
if effectiveProvider == "" {
|
||||||
|
|
@ -106,6 +151,9 @@ func normalizeIncomingModelConfig(mc *config.ModelConfig) {
|
||||||
mc.Provider = strings.TrimSpace(mc.Provider)
|
mc.Provider = strings.TrimSpace(mc.Provider)
|
||||||
mc.AuthMethod = strings.ToLower(strings.TrimSpace(mc.AuthMethod))
|
mc.AuthMethod = strings.ToLower(strings.TrimSpace(mc.AuthMethod))
|
||||||
if mc.Provider == "" {
|
if mc.Provider == "" {
|
||||||
|
if isLegacyUnsupportedASRModelConfig(mc) {
|
||||||
|
return
|
||||||
|
}
|
||||||
mc.Provider, mc.Model = providers.SplitModelProviderAndID(mc.Model, "openai")
|
mc.Provider, mc.Model = providers.SplitModelProviderAndID(mc.Model, "openai")
|
||||||
} else {
|
} else {
|
||||||
mc.Provider = providers.NormalizeProvider(mc.Provider)
|
mc.Provider = providers.NormalizeProvider(mc.Provider)
|
||||||
|
|
@ -159,6 +207,9 @@ func validateIncomingModelConfig(mc *config.ModelConfig, existing *config.ModelC
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(mc.Provider) == "" {
|
if strings.TrimSpace(mc.Provider) == "" {
|
||||||
|
if existing != nil && isLegacyUnsupportedASRModelConfig(existing) && isLegacyUnsupportedASRModelConfig(mc) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fmt.Errorf("provider is required")
|
return fmt.Errorf("provider is required")
|
||||||
}
|
}
|
||||||
if !providers.IsSupportedModelProvider(mc.Provider) {
|
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))
|
models := make([]modelResponse, 0, len(cfg.ModelList))
|
||||||
for i, m := range cfg.ModelList {
|
for i, m := range cfg.ModelList {
|
||||||
provider, modelID := providers.ExtractProtocol(m)
|
provider, modelID := responseProviderAndModel(m)
|
||||||
models = append(models, modelResponse{
|
models = append(models, modelResponse{
|
||||||
Index: i,
|
Index: i,
|
||||||
ModelName: m.ModelName,
|
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
|
// This keeps provider-omitted updates backward-compatible even when an
|
||||||
// older client edits the visible model ID.
|
// older client edits the visible model ID.
|
||||||
if strings.TrimSpace(cfg.ModelList[idx].Provider) == "" {
|
if strings.TrimSpace(cfg.ModelList[idx].Provider) == "" {
|
||||||
existingProtocol, existingModelID := providers.ExtractProtocol(cfg.ModelList[idx])
|
|
||||||
existingRawModel := strings.TrimSpace(cfg.ModelList[idx].Model)
|
existingRawModel := strings.TrimSpace(cfg.ModelList[idx].Model)
|
||||||
incomingModel := strings.TrimSpace(mc.Model)
|
incomingModel := strings.TrimSpace(mc.Model)
|
||||||
if existingRawModel != "" && existingRawModel != existingModelID && incomingModel != "" {
|
if legacyProvider, legacyModelID, ok := legacyUnsupportedASRProviderAndModel(existingRawModel); ok {
|
||||||
if incomingModel == existingModelID {
|
if incomingModel != "" {
|
||||||
mc.Model = existingRawModel
|
if incomingModel == legacyModelID {
|
||||||
} else if strings.Contains(incomingModel, "/") && !strings.Contains(existingModelID, "/") {
|
mc.Model = existingRawModel
|
||||||
// Older clients never saw the hidden provider prefix for simple
|
} else if strings.Contains(incomingModel, "/") && !strings.Contains(legacyModelID, "/") {
|
||||||
// legacy entries such as "openai/gpt-4o". If they now send an
|
// Older clients only saw the visible legacy ASR model ID
|
||||||
// explicit provider/model string, treat it as the caller's full
|
// (for example "scribe_v1"). If they now send an explicit
|
||||||
// intent instead of re-applying the old hidden prefix.
|
// provider/model string, keep that full intent instead of
|
||||||
mc.Model = incomingModel
|
// silently re-applying the hidden ElevenLabs prefix.
|
||||||
} else if !strings.HasPrefix(incomingModel, existingProtocol+"/") {
|
mc.Model = incomingModel
|
||||||
mc.Model = existingProtocol + "/" + 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) {
|
func TestHandleAddModel_RejectsMissingCLIProviderCommand(t *testing.T) {
|
||||||
configPath, cleanup := setupOAuthTestEnv(t)
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
defer cleanup()
|
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) {
|
func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmitted(t *testing.T) {
|
||||||
configPath, cleanup := setupOAuthTestEnv(t)
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
defer cleanup()
|
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) {
|
func TestHandleUpdateModel_PreservesLegacyModelPrefixWhenProviderOmittedAndModelChanges(t *testing.T) {
|
||||||
configPath, cleanup := setupOAuthTestEnv(t)
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue