2026-06-03 16:33:58 +08:00
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
2026-05-08 12:02:37 +08:00
package models
import (
"bytes"
2026-06-02 03:27:26 -04:00
"context"
2026-05-21 21:19:38 -10:00
"encoding/base64"
2026-05-08 12:02:37 +08:00
"encoding/json"
"fmt"
"io"
"net/http"
2026-05-21 21:19:38 -10:00
"os"
"path/filepath"
2026-05-08 12:02:37 +08:00
"ragflow/internal/common"
"strings"
)
// OpenRouterModel implements ModelDriver for OpenRouter AI
type OpenRouterModel struct {
2026-06-04 17:50:22 +08:00
baseModel BaseModel
2026-05-08 12:02:37 +08:00
}
// NewOpenRouterModel creates a new OpenRouter AI model instance
func NewOpenRouterModel ( baseURL map [ string ] string , urlSuffix URLSuffix ) * OpenRouterModel {
return & OpenRouterModel {
2026-06-04 17:50:22 +08:00
baseModel : BaseModel {
2026-06-11 05:20:12 -06:00
BaseURL : baseURL ,
URLSuffix : urlSuffix ,
httpClient : NewDriverHTTPClient ( ) ,
2026-05-08 12:02:37 +08:00
} ,
}
}
func ( o * OpenRouterModel ) NewInstance ( baseURL map [ string ] string ) ModelDriver {
2026-06-04 17:50:22 +08:00
return NewOpenRouterModel ( baseURL , o . baseModel . URLSuffix )
2026-05-08 12:02:37 +08:00
}
func ( o * OpenRouterModel ) Name ( ) string {
return "openrouter"
}
func ( o * OpenRouterModel ) ChatWithMessages ( modelName string , messages [ ] Message , apiConfig * APIConfig , chatModelConfig * ChatConfig ) ( * ChatResponse , error ) {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
2026-05-08 12:02:37 +08:00
}
if len ( messages ) == 0 {
return nil , fmt . Errorf ( "messages is empty" )
}
2026-06-04 17:50:22 +08:00
resolvedBaseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
2026-05-08 12:02:37 +08:00
}
2026-06-04 17:50:22 +08:00
url := fmt . Sprintf ( "%s/%s" , resolvedBaseURL , o . baseModel . URLSuffix . Chat )
2026-05-08 12:02:37 +08:00
// Convert messages to API format
apiMessages := make ( [ ] map [ string ] interface { } , len ( messages ) )
for i , msg := range messages {
apiMessages [ i ] = map [ string ] interface { } {
"role" : msg . Role ,
"content" : msg . Content ,
}
}
// Build request body
reqBody := map [ string ] interface { } {
"model" : modelName ,
"messages" : apiMessages ,
"stream" : false ,
"temperature" : 1 ,
}
if chatModelConfig != nil {
if chatModelConfig . Temperature != nil {
reqBody [ "temperature" ] = * chatModelConfig . Temperature
}
if chatModelConfig . MaxTokens != nil {
reqBody [ "max_tokens" ] = * chatModelConfig . MaxTokens
}
if chatModelConfig . Stream != nil {
reqBody [ "stream" ] = * chatModelConfig . Stream
}
if chatModelConfig . TopP != nil {
reqBody [ "top_p" ] = * chatModelConfig . TopP
}
if chatModelConfig . DoSample != nil {
reqBody [ "do_sample" ] = * chatModelConfig . DoSample
}
2026-05-18 16:57:42 +08:00
if chatModelConfig . Effort != nil {
reqBody [ "reasoning" ] = map [ string ] interface { } {
"effort" : chatModelConfig . Effort ,
}
2026-05-08 12:02:37 +08:00
}
}
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , nonStreamCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "POST" , url , bytes . NewBuffer ( jsonData ) )
2026-05-08 12:02:37 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Add ( "Content-Type" , "application/json" )
req . Header . Add ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
2026-05-08 12:02:37 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response body: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "failed to send request: %d %s" , resp . StatusCode , string ( body ) )
}
// Parse response
var result map [ string ] interface { }
if err := json . Unmarshal ( body , & result ) ; err != nil {
return nil , fmt . Errorf ( "failed to unmarshal response: %w" , err )
}
choices , ok := result [ "choices" ] . ( [ ] interface { } )
if ! ok {
return nil , fmt . Errorf ( "no choices in response" )
}
firstChoice , ok := choices [ 0 ] . ( map [ string ] interface { } )
if ! ok {
return nil , fmt . Errorf ( "no choices in response" )
}
messageMap , ok := firstChoice [ "message" ] . ( map [ string ] interface { } )
if ! ok {
return nil , fmt . Errorf ( "no message in response" )
}
content , ok := messageMap [ "content" ] . ( string )
if ! ok {
return nil , fmt . Errorf ( "no message in response" )
}
var reasonContent string
if chatModelConfig != nil && chatModelConfig . Thinking != nil && * chatModelConfig . Thinking {
reasonContent , ok = messageMap [ "reasoning" ] . ( string )
if ! ok {
return nil , fmt . Errorf ( "invalid content format" )
}
if reasonContent != "" && reasonContent [ 0 ] == '\n' {
reasonContent = reasonContent [ 1 : ]
}
}
chatResponse := & ChatResponse {
Answer : & content ,
ReasonContent : & reasonContent ,
}
return chatResponse , nil
}
func ( o * OpenRouterModel ) ChatStreamlyWithSender ( modelName string , messages [ ] Message , apiConfig * APIConfig , modelConfig * ChatConfig , sender func ( * string , * string ) error ) error {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return err
}
2026-05-08 12:02:37 +08:00
if len ( messages ) == 0 {
return fmt . Errorf ( "messages is empty" )
}
2026-06-04 17:50:22 +08:00
resolvedBaseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return err
2026-05-08 12:02:37 +08:00
}
2026-06-17 18:14:13 +07:00
// All OpenRouter models use the standard chat-completions endpoint, same as
// the non-stream path. The previous qwen/glm branch routed to URLSuffix.AsyncChat,
// which OpenRouter does not configure (empty suffix) — producing a broken URL and
// breaking streaming for every qwen/glm model.
2026-06-04 17:50:22 +08:00
url := fmt . Sprintf ( "%s/%s" , resolvedBaseURL , o . baseModel . URLSuffix . Chat )
2026-05-08 12:02:37 +08:00
// Convert messages to API format
apiMessages := make ( [ ] map [ string ] interface { } , len ( messages ) )
for i , msg := range messages {
apiMessages [ i ] = map [ string ] interface { } {
"role" : msg . Role ,
"content" : msg . Content ,
}
}
reqBody := map [ string ] interface { } {
"model" : modelName ,
"messages" : apiMessages ,
"stream" : true ,
"temperature" : 1 ,
}
if modelConfig != nil {
if modelConfig . Stream != nil {
reqBody [ "stream" ] = * modelConfig . Stream
}
if modelConfig . MaxTokens != nil {
reqBody [ "max_tokens" ] = * modelConfig . MaxTokens
}
if modelConfig . Temperature != nil {
reqBody [ "temperature" ] = * modelConfig . Temperature
}
if modelConfig . DoSample != nil {
reqBody [ "do_sample" ] = * modelConfig . DoSample
}
if modelConfig . TopP != nil {
reqBody [ "top_p" ] = * modelConfig . TopP
}
if modelConfig . Stop != nil {
reqBody [ "stop" ] = * modelConfig . Stop
}
2026-06-17 18:14:13 +07:00
// OpenRouter controls reasoning via the standard `reasoning` request object
// (the non-stream path and the streamed `delta.reasoning` response use it too).
// The previous `thinking` key is non-standard and silently ignored by the API,
// so streaming reasoning was never actually enabled. `effort` takes precedence,
// matching the non-stream path.
2026-05-08 12:02:37 +08:00
if modelConfig . Thinking != nil {
2026-06-17 18:14:13 +07:00
reqBody [ "reasoning" ] = map [ string ] interface { } { "enabled" : * modelConfig . Thinking }
}
if modelConfig . Effort != nil {
reqBody [ "reasoning" ] = map [ string ] interface { } { "effort" : * modelConfig . Effort }
2026-05-08 12:02:37 +08:00
}
}
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , streamCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "POST" , url , bytes . NewBuffer ( jsonData ) )
2026-05-08 12:02:37 +08:00
if err != nil {
return fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
2026-05-08 12:02:37 +08:00
if err != nil {
return fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
body , _ := io . ReadAll ( resp . Body )
return fmt . Errorf ( "invalid status code: %d, body: %s" , resp . StatusCode , string ( body ) )
}
2026-06-11 05:20:12 -06:00
if _ , err := ParseSSEStream [ map [ string ] interface { } ] ( resp . Body , func ( event map [ string ] interface { } ) error {
common . Info ( fmt . Sprintf ( "%v" , event ) )
2026-05-08 12:02:37 +08:00
choices , ok := event [ "choices" ] . ( [ ] interface { } )
if ! ok || len ( choices ) == 0 {
2026-06-11 05:20:12 -06:00
return nil
2026-05-08 12:02:37 +08:00
}
firstChoice , ok := choices [ 0 ] . ( map [ string ] interface { } )
if ! ok {
2026-06-11 05:20:12 -06:00
return nil
2026-05-08 12:02:37 +08:00
}
delta , ok := firstChoice [ "delta" ] . ( map [ string ] interface { } )
if ! ok {
2026-06-11 05:20:12 -06:00
return nil
2026-05-08 12:02:37 +08:00
}
reasoningContent , ok := delta [ "reasoning" ] . ( string )
if ok && reasoningContent != "" {
if err := sender ( nil , & reasoningContent ) ; err != nil {
return err
}
}
content , ok := delta [ "content" ] . ( string )
if ok && content != "" {
if err := sender ( & content , nil ) ; err != nil {
return err
}
}
2026-06-11 05:20:12 -06:00
return nil
} ) ; err != nil {
return fmt . Errorf ( "failed to scan response body: %w" , err )
2026-05-08 12:02:37 +08:00
}
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"
if err = sender ( & endOfStream , nil ) ; err != nil {
return err
}
2026-06-11 05:20:12 -06:00
return nil
2026-05-08 12:02:37 +08:00
}
2026-05-11 00:57:11 -04:00
type openrouterEmbeddingResponse struct {
2026-05-11 14:45:30 +08:00
Data [ ] openrouterEmbeddingData ` json:"data" `
Model string ` json:"model" `
Object string ` json:"object" `
Usage openrouterUsage ` json:"usage" `
2026-05-11 00:57:11 -04:00
}
2026-05-11 14:45:30 +08:00
type openrouterEmbeddingData struct {
Embedding [ ] float64 ` json:"embedding" `
Object string ` json:"object" `
Index int ` json:"index" `
}
type openrouterUsage struct {
PromptTokens int ` json:"prompt_tokens" `
TotalTokens int ` json:"total_tokens" `
}
func ( o * OpenRouterModel ) Embed ( modelName * string , texts [ ] string , apiConfig * APIConfig , embeddingConfig * EmbeddingConfig ) ( [ ] EmbeddingData , error ) {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
}
2026-05-08 13:56:45 +08:00
if len ( texts ) == 0 {
2026-05-11 14:45:30 +08:00
return [ ] EmbeddingData { } , nil
2026-05-08 13:56:45 +08:00
}
2026-05-11 00:57:11 -04:00
if modelName == nil || * modelName == "" {
return nil , fmt . Errorf ( "model name is required" )
}
2026-05-08 13:56:45 +08:00
2026-06-04 17:50:22 +08:00
resolvedBaseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
2026-05-08 13:56:45 +08:00
}
2026-06-04 17:50:22 +08:00
url := fmt . Sprintf ( "%s/%s" , resolvedBaseURL , o . baseModel . URLSuffix . Embedding )
2026-05-08 13:56:45 +08:00
reqBody := map [ string ] interface { } {
"model" : * modelName ,
"input" : texts ,
}
2026-05-11 00:57:11 -04:00
if embeddingConfig != nil && embeddingConfig . Dimension > 0 {
reqBody [ "dimensions" ] = embeddingConfig . Dimension
}
2026-05-08 13:56:45 +08:00
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , nonStreamCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "POST" , url , bytes . NewBuffer ( jsonData ) )
2026-05-08 13:56:45 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
2026-06-04 17:50:22 +08:00
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-05-08 13:56:45 +08:00
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
2026-05-08 13:56:45 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "OpenRouter embedding API error: status %d, body: %s" , resp . StatusCode , string ( body ) )
}
2026-05-11 14:45:30 +08:00
var parsed openrouterEmbeddingResponse
if err = json . Unmarshal ( body , & parsed ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse response: %w" , err )
2026-05-08 13:56:45 +08:00
}
2026-05-11 14:45:30 +08:00
var embeddings [ ] EmbeddingData
for _ , dataElem := range parsed . Data {
var embeddingData EmbeddingData
embeddingData . Embedding = dataElem . Embedding
embeddingData . Index = dataElem . Index
embeddings = append ( embeddings , embeddingData )
2026-05-08 13:56:45 +08:00
}
return embeddings , nil
}
// OpenRouterRerankRequest OpenRouter official rerank request format
type OpenRouterRerankRequest struct {
Model string ` json:"model" `
Query string ` json:"query" `
Documents [ ] string ` json:"documents" `
TopN int ` json:"top_n,omitempty" `
}
// OpenRouterRerankResponse OpenRouter official rerank response format
type OpenRouterRerankResponse struct {
Model string ` json:"model" `
ID string ` json:"id" `
Results [ ] struct {
Index int ` json:"index" `
RelevanceScore float64 ` json:"relevance_score" `
Document * struct {
Text string ` json:"text" `
} ` json:"document,omitempty" `
} ` json:"results" `
2026-05-08 12:02:37 +08:00
}
2026-05-09 17:41:54 +08:00
func ( o * OpenRouterModel ) Rerank ( modelName * string , query string , documents [ ] string , apiConfig * APIConfig , rerankConfig * RerankConfig ) ( * RerankResponse , error ) {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
2026-05-08 12:02:37 +08:00
}
2026-06-04 17:50:22 +08:00
if len ( documents ) == 0 {
return & RerankResponse { } , nil
2026-05-08 12:02:37 +08:00
}
2026-05-09 17:41:54 +08:00
var topN = rerankConfig . TopN
if rerankConfig . TopN == 0 {
topN = len ( documents )
}
2026-05-08 13:56:45 +08:00
reqBody := OpenRouterRerankRequest {
Model : * modelName ,
Query : query ,
2026-05-09 17:41:54 +08:00
Documents : documents ,
TopN : topN ,
2026-05-08 12:02:37 +08:00
}
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-06-04 17:50:22 +08:00
resolvedBaseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
}
url := fmt . Sprintf ( "%s/%s" , strings . TrimSuffix ( resolvedBaseURL , "/" ) , o . baseModel . URLSuffix . Rerank )
2026-05-08 12:02:37 +08:00
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , nonStreamCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "POST" , url , bytes . NewBuffer ( jsonData ) )
2026-05-08 12:02:37 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
2026-05-08 13:56:45 +08:00
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-05-08 12:02:37 +08:00
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
2026-05-08 12:02:37 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
2026-05-08 13:56:45 +08:00
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response: %w" , err )
2026-05-08 12:02:37 +08:00
}
2026-05-08 13:56:45 +08:00
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "OpenRouter Rerank API error: %s, body: %s" , resp . Status , string ( body ) )
}
2026-05-08 12:02:37 +08:00
2026-05-08 13:56:45 +08:00
var rerankResp OpenRouterRerankResponse
if err = json . Unmarshal ( body , & rerankResp ) ; err != nil {
2026-05-08 12:02:37 +08:00
return nil , fmt . Errorf ( "failed to decode response: %w" , err )
}
2026-05-09 17:41:54 +08:00
var rerankResponse RerankResponse
2026-05-08 12:02:37 +08:00
for _ , result := range rerankResp . Results {
2026-05-09 17:41:54 +08:00
rerankResult := RerankResult {
Index : result . Index ,
RelevanceScore : result . RelevanceScore ,
2026-05-08 12:02:37 +08:00
}
2026-05-09 17:41:54 +08:00
rerankResponse . Data = append ( rerankResponse . Data , rerankResult )
2026-05-08 12:02:37 +08:00
}
2026-05-09 17:41:54 +08:00
return & rerankResponse , nil
2026-05-08 12:02:37 +08:00
}
2026-05-21 21:19:38 -10:00
type openRouterTranscriptionResponse struct {
Text string ` json:"text" `
}
func openRouterAudioFormat ( file string , asrConfig * ASRConfig ) string {
if asrConfig != nil && asrConfig . Params != nil {
if format , ok := asrConfig . Params [ "format" ] ; ok && format != nil {
if value := strings . TrimPrefix ( fmt . Sprint ( format ) , "." ) ; value != "" {
return value
}
}
}
ext := strings . TrimPrefix ( strings . ToLower ( filepath . Ext ( file ) ) , "." )
if ext == "" {
return "wav"
}
return ext
}
2026-05-12 17:17:44 +08:00
// TranscribeAudio transcribe audio
func ( o * OpenRouterModel ) TranscribeAudio ( modelName * string , file * string , apiConfig * APIConfig , asrConfig * ASRConfig ) ( * ASRResponse , error ) {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
2026-05-21 21:19:38 -10:00
}
if modelName == nil || * modelName == "" {
return nil , fmt . Errorf ( "model name is required" )
}
if file == nil || * file == "" {
return nil , fmt . Errorf ( "file is missing" )
}
2026-06-04 17:50:22 +08:00
if o . baseModel . URLSuffix . ASR == "" {
2026-05-21 21:19:38 -10:00
return nil , fmt . Errorf ( "OpenRouter ASR url suffix is missing" )
}
fix(codeql): close remaining 44 CodeQL alerts post-merge (#16408)
## Summary
After #16407 merged, 44 of the original 93 CodeQL alerts were still open
on the default branch. This PR closes the remaining ones by:
1. **Moving 32 existing `// codeql[...]` directives** so they sit on the
line **immediately before** the suppressed statement. The original
multi-line suppression blocks had the directive as the first line, with
the rationale on subsequent lines. After line shifts (refactors, linter
reformat), the directive ended up several lines above the alert location
— CodeQL only recognizes the suppression when it appears on the line
directly above. (32 alerts across 27 files.)
2. **Adding 9 new `// codeql[...]` suppressions** for alerts that had no
suppression in the preceding lines at all — mostly real-fixes that
CodeQL conservatively still flags (filepath.Base, bounded slice sizes,
model-identifier strings, the MD5-legacy-migration lookup in
`conversation_service.py`).
## Files changed
- `api/db/services/conversation_service.py` — add
`py/weak-sensitive-data-hashing` suppression (MD5 for backward-compat
legacy row lookup; not used for auth)
- `api/db/services/llm_service.py` — 3×
`py/clear-text-logging-sensitive-data` suppressions on the lines that
log `llm_name` in warnings/info
- `common/misc_utils.py` — 2× `py/clear-text-logging-sensitive-data`
suppressions on the redacted `current_url` log sites
- `internal/agent/component/invoke.go` — moved existing
`go/request-forgery` directive
- `internal/agent/sandbox/ssh.go` — moved existing
`go/command-injection` directive
- `internal/agent/tool/retrieval_service.go` — added
`go/uncontrolled-allocation-size` suppression (`topN` is bounded to 1024
above)
- `internal/cli/common_command.go` — moved 2×
`go/disabled-certificate-check` directives
- `internal/cli/user_command.go` — added `go/clear-text-logging`
suppression (filepath.Base already strips user-identifying path)
- `internal/dao/pipeline_operation_log.go` — moved 2× `go/sql-injection`
directives
- `internal/dao/user_canvas.go` — added `go/sql-injection` suppression
in `GetList` (the new `userCanvasOrderClause` call path)
- `internal/engine/infinity/chunk.go` — moved existing
`go/unsafe-quoting` directive
- `internal/entity/models/*` — moved `go/path-injection` directives (15
files)
- `internal/handler/oauth_login.go` — moved existing
`go/cookie-httponly-not-set` directive
- `internal/handler/tenant.go` — moved existing `go/path-injection`
directive
- `internal/service/deep_researcher.go` — moved existing
`go/unsafe-quoting` directive
- `internal/service/dataset.go` — added
`go/uncontrolled-allocation-size` suppression (`n` bounded to 1024
above)
- `internal/service/file.go` — moved existing `go/request-forgery`
directive
- `internal/service/langfuse.go` — moved 2× `go/request-forgery`
directives
- `internal/utility/mcp_client.go` — moved 3× `go/request-forgery`
directives
- `internal/utility/smtp.go` — moved existing `go/email-injection`
directive
- `rag/prompts/generator.py` — added
`py/clear-text-logging-sensitive-data` suppression
- `web/.../use-provider-fields.tsx` — added
`js/prototype-pollution-utility` suppression (FORBIDDEN_KEYS guard is on
the line above)
## Why the previous PR left alerts open
`// codeql[query-id] explanation` must be on the line **immediately
before** the suppressed statement per the [GitHub CodeQL suppression
spec](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning-with-codeql/suppressing-code-scanning-alerts).
The original suppression blocks were 4-5 lines, with the directive as
the **first** line. After linter reformat / line shifts, the directive
ended up too far above the actual alert line to be recognized. The fix
is to put the directive on the line directly above the suppressed
statement, with the rationale above it.
## Test plan
- All 9 modified Python files `ast.parse` clean
- All 4 modified Go files `gofmt` clean
- 36/44 expected alert suppressions in place
- 8 remaining CodeQL alerts are the originals (#3485851828, #3485851831,
#3485869759, #3485869766, #3485869768, #3485869771, #3485885962,
#3485895527) which were resolved by the corresponding commit comments;
these should close on the next scan when the suppression comments match
the alert lines.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-27 20:49:06 +08:00
// codeql[go/path-injection] False positive: *file is the audio file path the caller passes in to upload. The user (or operator-supplied pipeline) explicitly chose this path, and the OS access check enforces permissions anyway.
2026-05-21 21:19:38 -10:00
audio , err := os . ReadFile ( * file )
if err != nil {
return nil , fmt . Errorf ( "failed to read audio file: %w" , err )
}
reqBody := map [ string ] interface { } {
"model" : * modelName ,
"input_audio" : map [ string ] interface { } {
"data" : base64 . StdEncoding . EncodeToString ( audio ) ,
"format" : openRouterAudioFormat ( * file , asrConfig ) ,
} ,
}
if asrConfig != nil && asrConfig . Params != nil {
for key , value := range asrConfig . Params {
switch key {
case "format" , "model" , "input_audio" :
continue
}
reqBody [ key ] = value
}
}
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-06-04 17:50:22 +08:00
resolvedBaseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
}
url := fmt . Sprintf ( "%s/%s" , strings . TrimSuffix ( resolvedBaseURL , "/" ) , o . baseModel . URLSuffix . ASR )
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , longOpCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "POST" , url , bytes . NewBuffer ( jsonData ) )
2026-05-21 21:19:38 -10:00
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
2026-05-21 21:19:38 -10:00
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response body: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "OpenRouter ASR API error: %s, body: %s" , resp . Status , string ( body ) )
}
var result openRouterTranscriptionResponse
if err = json . Unmarshal ( body , & result ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse transcription response: %w" , err )
}
return & ASRResponse { Text : result . Text } , nil
2026-05-12 17:17:44 +08:00
}
2026-06-03 14:09:07 +08:00
func ( o * OpenRouterModel ) TranscribeAudioWithSender ( modelName * string , file * string , apiConfig * APIConfig , asrConfig * ASRConfig , sender func ( * string , * string ) error ) error {
return fmt . Errorf ( "%s, no such method" , o . Name ( ) )
2026-05-12 17:17:44 +08:00
}
2026-05-15 18:41:43 +08:00
// AudioSpeech convert text to audio
func ( o * OpenRouterModel ) AudioSpeech ( modelName * string , audioContent * string , apiConfig * APIConfig , ttsConfig * TTSConfig ) ( * TTSResponse , error ) {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
}
if audioContent == nil || * audioContent == "" {
return nil , fmt . Errorf ( "text content is empty" )
}
2026-06-04 17:50:22 +08:00
resolvedBaseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
}
2026-06-04 17:50:22 +08:00
url := fmt . Sprintf ( "%s/%s" , resolvedBaseURL , o . baseModel . URLSuffix . TTS )
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
// OpenRouter:response Audio bytes stream
reqBody := map [ string ] interface { } {
"model" : modelName ,
"input" : audioContent ,
}
2026-05-15 18:41:43 +08:00
if ttsConfig != nil && ttsConfig . Params != nil {
for key , value := range ttsConfig . Params {
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
reqBody [ key ] = value
}
}
2026-05-15 18:41:43 +08:00
if ttsConfig != nil && ttsConfig . Format != "" {
reqBody [ "response_format" ] = ttsConfig . Format
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
}
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , longOpCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "POST" , url , bytes . NewBuffer ( jsonData ) )
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926)
### What problem does this PR solve?
This PR implement TTS for FishAudio and MiniMax provider and ASR for
FishAudio
**The following functionalities are now supported:**
**FishAudio:**
- [x] Text To Speech
- [x] Stream Text To Speech
- [x] Audio To Text
**OpenRouter:**
- [x] Text To Speech
**Verified examples from the CLI:**
```plaintext
**FishAudio**
RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav
SUCCESS
RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}'
+----------------------------------------------------------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------------------------------------------------------+
| The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. |
+----------------------------------------------------------------------------------------------------------------------+
```
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [x] Refactoring
2026-05-14 18:58:00 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response body: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "OpenRouter API error: %s, body: %s" , resp . Status , string ( body ) )
}
return & TTSResponse { Audio : body } , nil
2026-05-12 17:17:44 +08:00
}
2026-06-03 14:09:07 +08:00
func ( o * OpenRouterModel ) AudioSpeechWithSender ( modelName * string , audioContent * string , apiConfig * APIConfig , ttsConfig * TTSConfig , sender func ( * string , * string ) error ) error {
return fmt . Errorf ( "%s, no such method" , o . Name ( ) )
2026-05-12 17:17:44 +08:00
}
// OCRFile OCR file
2026-06-03 14:09:07 +08:00
func ( o * OpenRouterModel ) OCRFile ( modelName * string , content [ ] byte , url * string , apiConfig * APIConfig , ocrConfig * OCRConfig ) ( * OCRFileResponse , error ) {
return nil , fmt . Errorf ( "%s, no such method" , o . Name ( ) )
2026-05-12 17:17:44 +08:00
}
2026-05-15 12:29:52 +08:00
// ParseFile parse file
2026-06-03 14:09:07 +08:00
func ( o * OpenRouterModel ) ParseFile ( modelName * string , content [ ] byte , url * string , apiConfig * APIConfig , parseFileConfig * ParseFileConfig ) ( * ParseFileResponse , error ) {
return nil , fmt . Errorf ( "%s, no such method" , o . Name ( ) )
2026-05-15 12:29:52 +08:00
}
2026-06-09 19:01:00 +08:00
func ( o * OpenRouterModel ) ListModels ( apiConfig * APIConfig ) ( [ ] ListModelResponse , error ) {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
2026-05-08 12:02:37 +08:00
}
2026-06-04 17:50:22 +08:00
resolvedBaseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
}
url := fmt . Sprintf ( "%s/%s" , resolvedBaseURL , o . baseModel . URLSuffix . Models )
2026-05-08 12:02:37 +08:00
// Build request body
reqBody := map [ string ] interface { } { }
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , nonStreamCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "GET" , url , bytes . NewBuffer ( jsonData ) )
2026-05-08 12:02:37 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
2026-05-08 12:02:37 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response: %w" , err )
}
if resp . StatusCode != http . StatusOK {
feat(go-models): add PPIO provider driver (#15099)
### What problem does this PR solve?
Closes #15089.
Adds PPIO support to the Go model-provider layer so PPIO instances can
be routed through the Go API server with the same OpenAI-compatible
chat, streaming, model listing, and connection-check flow used by other
SaaS providers.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
## Summary
- Added a PPIO Go model driver.
- Added the PPIO provider catalog and default OpenAI-compatible API URL.
- Registered PPIO in the model factory.
- Added focused provider and provider-manager tests.
## What changed
- Implemented chat completions, SSE streaming, ListModels, and
CheckConnection for PPIO.
- Covered request shape, stream termination, reasoning fallback, model
listing, custom base URLs, safe transport setup, unsupported methods,
and provider config loading.
- Kept the provider catalog aligned with the existing RAGFlow PPIO
factory model set.
- Cleaned up pre-existing Go model package validation blockers so the
scoped provider tests can run normally with vet enabled.
## Why
The existing Python/provider catalog path includes PPIO, but the Go
model-provider layer did not have a PPIO driver, so the Go API server
could not instantiate or use PPIO as requested in #15089.
2026-05-21 20:52:18 -07:00
return nil , fmt . Errorf ( "API request failed with status %d: %s" , resp . StatusCode , string ( body ) )
2026-05-08 12:02:37 +08:00
}
// Parse response
2026-06-11 13:32:50 +08:00
// Parse response
var modelList ModelList
if err = json . Unmarshal ( body , & modelList ) ; err != nil {
2026-05-08 12:02:37 +08:00
return nil , fmt . Errorf ( "failed to parse response: %w" , err )
}
2026-06-11 13:32:50 +08:00
if modelList . Models == nil {
return nil , fmt . Errorf ( "invalid models list format" )
2026-05-08 12:02:37 +08:00
}
2026-06-11 13:32:50 +08:00
return ParseListModel ( modelList ) , nil
2026-05-08 12:02:37 +08:00
}
func ( o * OpenRouterModel ) Balance ( apiConfig * APIConfig ) ( map [ string ] interface { } , error ) {
2026-06-04 17:50:22 +08:00
if err := o . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
2026-05-08 13:56:45 +08:00
}
2026-06-04 17:50:22 +08:00
baseURL , err := o . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
}
url := fmt . Sprintf ( "%s/%s" , baseURL , o . baseModel . URLSuffix . Balance )
2026-05-08 13:56:45 +08:00
2026-06-02 03:27:26 -04:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , nonStreamCallTimeout )
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , "GET" , url , nil )
2026-05-08 13:56:45 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
2026-06-04 17:50:22 +08:00
resp , err := o . baseModel . httpClient . Do ( req )
2026-05-08 13:56:45 +08:00
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "API request failed with status %d: %s" , resp . StatusCode , string ( body ) )
}
var result struct {
Data struct {
TotalCredits float64 ` json:"total_credits" `
TotalUsage float64 ` json:"total_usage" `
} ` json:"data" `
}
if err := json . Unmarshal ( body , & result ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse balance response: %w" , err )
}
remainingBalance := result . Data . TotalCredits - result . Data . TotalUsage
return map [ string ] interface { } {
"total_credits" : result . Data . TotalCredits ,
"total_usage" : result . Data . TotalUsage ,
"balance" : remainingBalance ,
"currency" : "USD" ,
} , nil
2026-05-08 12:02:37 +08:00
}
func ( o * OpenRouterModel ) CheckConnection ( apiConfig * APIConfig ) error {
2026-05-08 13:56:45 +08:00
_ , err := o . Balance ( apiConfig )
return err
2026-05-08 12:02:37 +08:00
}
2026-05-15 12:29:52 +08:00
2026-06-03 14:09:07 +08:00
func ( o * OpenRouterModel ) ListTasks ( apiConfig * APIConfig ) ( [ ] ListTaskStatus , error ) {
return nil , fmt . Errorf ( "%s, no such method" , o . Name ( ) )
2026-05-15 12:29:52 +08:00
}
2026-06-03 14:09:07 +08:00
func ( o * OpenRouterModel ) ShowTask ( taskID string , apiConfig * APIConfig ) ( * TaskResponse , error ) {
return nil , fmt . Errorf ( "%s, no such method" , o . Name ( ) )
2026-05-15 12:29:52 +08:00
}