shinnmen / internal /runtime /executor /iflow_provider.go
shinmentakezo07
refactor(executor): split providers and preserve legacy entrypoints
3e5ab39
Raw
History Blame Contribute Delete
5.86 kB
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"
)
// IFlowProvider implements ProviderConfig for iFlow API
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)
// Generate session-id
sessionID := "session-" + uuid.New().String()
req.Header.Set("session-id", sessionID)
// Generate timestamp and signature
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
// Set model in payload
body, err = sjson.SetBytes(body, "model", model)
if err != nil {
return body, fmt.Errorf("iflow executor: failed to set model in payload: %w", err)
}
// Preserve reasoning_content in messages for models that support thinking
body = preserveReasoningContentInMessages(body)
// Ensure tools array exists for streaming to avoid provider quirks
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 {
// No transformation needed for iFlow responses
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)
}
// createIFlowSignature generates HMAC-SHA256 signature for iFlow API requests.
// The signature payload format is: userAgent:sessionId:timestamp
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
}
// preserveReasoningContentInMessages checks if reasoning_content from assistant messages
// is preserved in conversation history for iFlow models that support thinking.
// This is helpful for multi-turn conversations where the model may benefit from seeing
// its previous reasoning to maintain coherent thought chains.
//
// For GLM-4.6/4.7 and MiniMax M2/M2.1, it is recommended to include the full assistant
// response (including reasoning_content) in message history for better context continuity.
func preserveReasoningContentInMessages(body []byte) []byte {
model := strings.ToLower(gjson.GetBytes(body, "model").String())
// Only apply to models that support thinking with history preservation
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
}
// Check if any assistant message already has reasoning_content preserved
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 // stop iteration
}
}
return true
})
// If reasoning content is already present, the messages are properly formatted
// No need to modify - the client has correctly preserved reasoning in history
if hasReasoningContent {
log.Debugf("iflow executor: reasoning_content found in message history for %s", model)
}
return body
}