| package executor |
|
|
| import ( |
| "crypto/hmac" |
| "crypto/sha256" |
| "encoding/hex" |
| "fmt" |
| "net/http" |
| "strings" |
| "time" |
|
|
| "github.com/google/uuid" |
| iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" |
| cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| log "github.com/sirupsen/logrus" |
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| const ( |
| iflowDefaultEndpoint = "/chat/completions" |
| iflowUserAgent = "iFlow-Cli" |
| ) |
|
|
| |
| type IFlowProvider struct{} |
|
|
| func (p *IFlowProvider) GetIdentifier() string { |
| return "iflow" |
| } |
|
|
| func (p *IFlowProvider) GetCredentials(auth *cliproxyauth.Auth) (apiKey, baseURL string) { |
| if auth == nil { |
| return "", iflowauth.DefaultAPIBaseURL |
| } |
|
|
| if auth.Attributes != nil { |
| if v := strings.TrimSpace(auth.Attributes["api_key"]); v != "" { |
| apiKey = v |
| } |
| if v := strings.TrimSpace(auth.Attributes["base_url"]); v != "" { |
| baseURL = v |
| } |
| } |
|
|
| if apiKey == "" && auth.Metadata != nil { |
| if v, ok := auth.Metadata["api_key"].(string); ok { |
| apiKey = strings.TrimSpace(v) |
| } |
| } |
|
|
| if baseURL == "" && auth.Metadata != nil { |
| if v, ok := auth.Metadata["base_url"].(string); ok { |
| baseURL = strings.TrimSpace(v) |
| } |
| } |
|
|
| if baseURL == "" { |
| baseURL = iflowauth.DefaultAPIBaseURL |
| } |
|
|
| return apiKey, baseURL |
| } |
|
|
| func (p *IFlowProvider) GetEndpoint(baseURL, model, action string, stream bool) string { |
| return strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint |
| } |
|
|
| func (p *IFlowProvider) ApplyHeaders(req *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool) { |
| req.Header.Set("Content-Type", "application/json") |
| req.Header.Set("Authorization", "Bearer "+apiKey) |
| req.Header.Set("User-Agent", iflowUserAgent) |
|
|
| |
| sessionID := "session-" + uuid.New().String() |
| req.Header.Set("session-id", sessionID) |
|
|
| |
| timestamp := time.Now().UnixMilli() |
| req.Header.Set("x-iflow-timestamp", fmt.Sprintf("%d", timestamp)) |
|
|
| signature := createIFlowSignature(iflowUserAgent, sessionID, timestamp, apiKey) |
| if signature != "" { |
| req.Header.Set("x-iflow-signature", signature) |
| } |
|
|
| if stream { |
| req.Header.Set("Accept", "text/event-stream") |
| } else { |
| req.Header.Set("Accept", "application/json") |
| } |
| } |
|
|
| func (p *IFlowProvider) GetTranslatorFormat() string { |
| return "openai" |
| } |
|
|
| func (p *IFlowProvider) TransformRequestBody(body []byte, model string, stream bool) ([]byte, error) { |
| var err error |
|
|
| |
| body, err = sjson.SetBytes(body, "model", model) |
| if err != nil { |
| return body, fmt.Errorf("iflow executor: failed to set model in payload: %w", err) |
| } |
|
|
| |
| body = preserveReasoningContentInMessages(body) |
|
|
| |
| if stream { |
| toolsResult := gjson.GetBytes(body, "tools") |
| if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 { |
| body = ensureToolsArray(body) |
| } |
| } |
|
|
| return body, nil |
| } |
|
|
| func (p *IFlowProvider) TransformResponseBody(body []byte) []byte { |
| |
| return body |
| } |
|
|
| func (p *IFlowProvider) ParseUsage(data []byte, stream bool) usageDetail { |
| if stream { |
| if detail, ok := parseOpenAIStreamUsage(data); ok { |
| return detail |
| } |
| return usageDetail{} |
| } |
| return parseOpenAIUsage(data) |
| } |
|
|
| |
| |
| func createIFlowSignature(userAgent, sessionID string, timestamp int64, apiKey string) string { |
| if apiKey == "" { |
| return "" |
| } |
| payload := fmt.Sprintf("%s:%s:%d", userAgent, sessionID, timestamp) |
| h := hmac.New(sha256.New, []byte(apiKey)) |
| h.Write([]byte(payload)) |
| return hex.EncodeToString(h.Sum(nil)) |
| } |
|
|
| func ensureToolsArray(body []byte) []byte { |
| placeholder := `[{"type":"function","function":{"name":"noop","description":"Placeholder tool to stabilise streaming","parameters":{"type":"object"}}}]` |
| updated, err := sjson.SetRawBytes(body, "tools", []byte(placeholder)) |
| if err != nil { |
| return body |
| } |
| return updated |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| func preserveReasoningContentInMessages(body []byte) []byte { |
| model := strings.ToLower(gjson.GetBytes(body, "model").String()) |
|
|
| |
| needsPreservation := strings.HasPrefix(model, "glm-4") || strings.HasPrefix(model, "minimax-m2") |
|
|
| if !needsPreservation { |
| return body |
| } |
|
|
| messages := gjson.GetBytes(body, "messages") |
| if !messages.Exists() || !messages.IsArray() { |
| return body |
| } |
|
|
| |
| hasReasoningContent := false |
| messages.ForEach(func(_, msg gjson.Result) bool { |
| role := msg.Get("role").String() |
| if role == "assistant" { |
| rc := msg.Get("reasoning_content") |
| if rc.Exists() && rc.String() != "" { |
| hasReasoningContent = true |
| return false |
| } |
| } |
| return true |
| }) |
|
|
| |
| |
| if hasReasoningContent { |
| log.Debugf("iflow executor: reasoning_content found in message history for %s", model) |
| } |
|
|
| return body |
| } |
|
|