diff --git a/internal/entity/models/funasr.go b/internal/entity/models/funasr.go index e6c7e805c7..e8929061ca 100644 --- a/internal/entity/models/funasr.go +++ b/internal/entity/models/funasr.go @@ -38,9 +38,10 @@ type FunASR struct { func NewFunASRModel(baseURL map[string]string, urlSuffix URLSuffix) *FunASR { return &FunASR{ baseModel: BaseModel{ - BaseURL: baseURL, - URLSuffix: urlSuffix, - httpClient: NewDriverHTTPClient(), + BaseURL: baseURL, + URLSuffix: urlSuffix, + AllowEmptyAPIKey: true, + httpClient: NewDriverHTTPClient(), }, } } @@ -77,6 +78,10 @@ func (f *FunASR) TranscribeAudio(ctx context.Context, modelName *string, file *s if file == nil || *file == "" { return nil, fmt.Errorf("file is missing") } + if modelName == nil || strings.TrimSpace(*modelName) == "" { + return nil, fmt.Errorf("model name is missing") + } + model := strings.TrimSpace(*modelName) resolvedBaseURL, err := f.baseModel.GetBaseURL(apiConfig) if err != nil { @@ -109,7 +114,7 @@ func (f *FunASR) TranscribeAudio(ctx context.Context, modelName *string, file *s } // model field - if err := writer.WriteField("model", *modelName); err != nil { + if err := writer.WriteField("model", model); err != nil { return nil, fmt.Errorf("failed to write model field: %w", err) } @@ -155,7 +160,9 @@ func (f *FunASR) TranscribeAudio(ctx context.Context, modelName *string, file *s return nil, fmt.Errorf("failed to create request: %w", err) } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + if auth := BearerAuth(apiConfig); auth != "" { + req.Header.Set("Authorization", auth) + } req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Accept", "application/json") @@ -210,8 +217,6 @@ func (f *FunASR) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListMo if err := f.baseModel.APIConfigCheck(apiConfig); err != nil { return nil, err } - apiKey := strings.TrimSpace(*apiConfig.ApiKey) - resolvedBaseURL, err := f.baseModel.GetBaseURL(apiConfig) if err != nil { return nil, err @@ -226,7 +231,9 @@ func (f *FunASR) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListMo return nil, fmt.Errorf("failed to create request: %w", err) } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey)) + if auth := BearerAuth(apiConfig); auth != "" { + req.Header.Set("Authorization", auth) + } req.Header.Set("Accept", "application/json") resp, err := f.baseModel.httpClient.Do(req) diff --git a/internal/entity/models/funasr_test.go b/internal/entity/models/funasr_test.go new file mode 100644 index 0000000000..e37975b688 --- /dev/null +++ b/internal/entity/models/funasr_test.go @@ -0,0 +1,130 @@ +package models + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +func newFunASRForTest(baseURL string) *FunASR { + return NewFunASRModel( + map[string]string{"default": baseURL}, + URLSuffix{ + ASR: "audio/transcriptions", + Models: "models", + }, + ) +} + +func writeFunASRTestAudio(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "audio.wav") + if err := os.WriteFile(path, []byte("test audio"), 0o600); err != nil { + t.Fatalf("write test audio: %v", err) + } + return path +} + +func TestFunASRTranscribeAudioWithoutAPIKey(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("method=%s, want POST", r.Method) + } + if r.URL.Path != "/audio/transcriptions" { + t.Errorf("path=%s, want /audio/transcriptions", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "" { + t.Errorf("Authorization=%q, want no header", got) + } + if err := r.ParseMultipartForm(1 << 20); err != nil { + t.Errorf("parse multipart form: %v", err) + return + } + if got := r.FormValue("model"); got != "fun-asr-nano" { + t.Errorf("model=%q, want fun-asr-nano", got) + } + _ = json.NewEncoder(w).Encode(map[string]string{"text": "hello"}) + })) + defer srv.Close() + + modelName := " fun-asr-nano " + file := writeFunASRTestAudio(t) + resp, err := newFunASRForTest(srv.URL).TranscribeAudio( + context.Background(), &modelName, &file, &APIConfig{}, nil, nil, + ) + if err != nil { + t.Fatalf("TranscribeAudio: %v", err) + } + if resp == nil || resp.Text != "hello" { + t.Fatalf("response=%v, want text hello", resp) + } +} + +func TestFunASRTranscribeAudioRequiresModelName(t *testing.T) { + file := writeFunASRTestAudio(t) + apiKey := "test-key" + blankModelName := " " + for _, tc := range []struct { + name string + modelName *string + }{ + {name: "nil", modelName: nil}, + {name: "whitespace", modelName: &blankModelName}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := newFunASRForTest("http://unused").TranscribeAudio( + context.Background(), tc.modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil, + ) + if err == nil || !strings.Contains(err.Error(), "model name is missing") { + t.Fatalf("expected missing-model-name error, got %v", err) + } + }) + } +} + +func TestFunASRListModelsWithoutAPIKey(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("method=%s, want GET", r.Method) + } + if r.URL.Path != "/models" { + t.Errorf("path=%s, want /models", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "" { + t.Errorf("Authorization=%q, want no header", got) + } + _, _ = io.WriteString(w, `{"object":"list","data":[{"id":"fun-asr-nano","owned_by":"funasr"}]}`) + })) + defer srv.Close() + + models, err := newFunASRForTest(srv.URL).ListModels(context.Background(), &APIConfig{}) + if err != nil { + t.Fatalf("ListModels: %v", err) + } + if len(models) != 1 || models[0].Name != "fun-asr-nano@funasr" { + t.Fatalf("models=%v, want fun-asr-nano@funasr", models) + } +} + +func TestFunASRListModelsSendsAuthWhenProvided(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer secret" { + t.Errorf("Authorization=%q, want Bearer secret", got) + } + _, _ = io.WriteString(w, `{"object":"list","data":[{"id":"fun-asr-nano"}]}`) + })) + defer srv.Close() + + apiKey := " secret " + if _, err := newFunASRForTest(srv.URL).ListModels( + context.Background(), &APIConfig{ApiKey: &apiKey}, + ); err != nil { + t.Fatalf("ListModels: %v", err) + } +}