Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
)
|
||||
|
||||
const compatPromptCacheKeyPrefix = "compat_cc_"
|
||||
|
||||
func shouldAutoInjectPromptCacheKeyForCompat(model string) bool {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(model))
|
||||
// 仅对 Codex OAuth 路径支持的 GPT-5 族开启自动注入,避免 normalizeCodexModel
|
||||
// 的默认兜底把任意模型(如 gpt-4o、claude-*)误判为 gpt-5.4。
|
||||
if !strings.Contains(trimmed, "gpt-5") && !strings.Contains(trimmed, "codex") {
|
||||
return false
|
||||
}
|
||||
normalized := strings.TrimSpace(strings.ToLower(normalizeCodexModel(trimmed)))
|
||||
return strings.HasPrefix(normalized, "gpt-5") || strings.Contains(normalized, "codex")
|
||||
}
|
||||
|
||||
func deriveCompatPromptCacheKey(req *apicompat.ChatCompletionsRequest, mappedModel string) string {
|
||||
if req == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
normalizedModel := normalizeCodexModel(strings.TrimSpace(mappedModel))
|
||||
if normalizedModel == "" {
|
||||
normalizedModel = normalizeCodexModel(strings.TrimSpace(req.Model))
|
||||
}
|
||||
if normalizedModel == "" {
|
||||
normalizedModel = strings.TrimSpace(req.Model)
|
||||
}
|
||||
|
||||
seedParts := []string{"model=" + normalizedModel}
|
||||
if req.ReasoningEffort != "" {
|
||||
seedParts = append(seedParts, "reasoning_effort="+strings.TrimSpace(req.ReasoningEffort))
|
||||
}
|
||||
if len(req.ToolChoice) > 0 {
|
||||
seedParts = append(seedParts, "tool_choice="+normalizeCompatSeedJSON(req.ToolChoice))
|
||||
}
|
||||
if len(req.Tools) > 0 {
|
||||
if raw, err := json.Marshal(req.Tools); err == nil {
|
||||
seedParts = append(seedParts, "tools="+normalizeCompatSeedJSON(raw))
|
||||
}
|
||||
}
|
||||
if len(req.Functions) > 0 {
|
||||
if raw, err := json.Marshal(req.Functions); err == nil {
|
||||
seedParts = append(seedParts, "functions="+normalizeCompatSeedJSON(raw))
|
||||
}
|
||||
}
|
||||
|
||||
firstUserCaptured := false
|
||||
for _, msg := range req.Messages {
|
||||
switch strings.TrimSpace(msg.Role) {
|
||||
case "system":
|
||||
seedParts = append(seedParts, "system="+normalizeCompatSeedJSON(msg.Content))
|
||||
case "user":
|
||||
if !firstUserCaptured {
|
||||
seedParts = append(seedParts, "first_user="+normalizeCompatSeedJSON(msg.Content))
|
||||
firstUserCaptured = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return compatPromptCacheKeyPrefix + hashSensitiveValueForLog(strings.Join(seedParts, "|"))
|
||||
}
|
||||
|
||||
func deriveAnthropicCompatPromptCacheKey(req *apicompat.AnthropicRequest, mappedModel string) string {
|
||||
if req == nil {
|
||||
return ""
|
||||
}
|
||||
if anchorKey := deriveAnthropicCacheControlPromptCacheKey(req); anchorKey != "" {
|
||||
return anchorKey
|
||||
}
|
||||
|
||||
normalizedModel := normalizeCodexModel(strings.TrimSpace(mappedModel))
|
||||
if normalizedModel == "" {
|
||||
normalizedModel = normalizeCodexModel(strings.TrimSpace(req.Model))
|
||||
}
|
||||
if normalizedModel == "" {
|
||||
normalizedModel = strings.TrimSpace(req.Model)
|
||||
}
|
||||
|
||||
seedParts := []string{"model=" + normalizedModel}
|
||||
if req.OutputConfig != nil && strings.TrimSpace(req.OutputConfig.Effort) != "" {
|
||||
seedParts = append(seedParts, "effort="+strings.TrimSpace(req.OutputConfig.Effort))
|
||||
}
|
||||
if len(req.ToolChoice) > 0 {
|
||||
seedParts = append(seedParts, "tool_choice="+normalizeCompatSeedJSON(req.ToolChoice))
|
||||
}
|
||||
if len(req.Tools) > 0 {
|
||||
if raw, err := json.Marshal(req.Tools); err == nil {
|
||||
seedParts = append(seedParts, "tools="+normalizeCompatSeedJSON(raw))
|
||||
}
|
||||
}
|
||||
if len(req.System) > 0 {
|
||||
seedParts = append(seedParts, "system="+normalizeCompatSeedJSON(req.System))
|
||||
}
|
||||
|
||||
firstUserCaptured := false
|
||||
for _, msg := range req.Messages {
|
||||
if strings.TrimSpace(msg.Role) != "user" || firstUserCaptured {
|
||||
continue
|
||||
}
|
||||
seedParts = append(seedParts, "first_user="+normalizeCompatSeedJSON(msg.Content))
|
||||
firstUserCaptured = true
|
||||
}
|
||||
|
||||
return compatPromptCacheKeyPrefix + hashSensitiveValueForLog(strings.Join(seedParts, "|"))
|
||||
}
|
||||
|
||||
func deriveAnthropicCacheControlPromptCacheKey(req *apicompat.AnthropicRequest) string {
|
||||
if req == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var parts []string
|
||||
var systemBlocks []apicompat.AnthropicContentBlock
|
||||
if len(req.System) > 0 && json.Unmarshal(req.System, &systemBlocks) == nil {
|
||||
for _, block := range systemBlocks {
|
||||
if block.Type == "text" &&
|
||||
block.CacheControl != nil &&
|
||||
strings.TrimSpace(block.CacheControl.Type) == "ephemeral" &&
|
||||
strings.TrimSpace(block.Text) != "" {
|
||||
parts = append(parts, "system:"+strings.TrimSpace(block.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstUserAnchor := ""
|
||||
for _, msg := range req.Messages {
|
||||
var blocks []apicompat.AnthropicContentBlock
|
||||
if len(msg.Content) == 0 || json.Unmarshal(msg.Content, &blocks) != nil {
|
||||
continue
|
||||
}
|
||||
role := strings.TrimSpace(msg.Role)
|
||||
for _, block := range blocks {
|
||||
if block.Type != "text" ||
|
||||
block.CacheControl == nil ||
|
||||
strings.TrimSpace(block.CacheControl.Type) != "ephemeral" ||
|
||||
strings.TrimSpace(block.Text) == "" {
|
||||
continue
|
||||
}
|
||||
switch role {
|
||||
case "user":
|
||||
if firstUserAnchor == "" {
|
||||
firstUserAnchor = strings.TrimSpace(block.Text)
|
||||
}
|
||||
case "assistant":
|
||||
parts = append(parts, "assistant:"+strings.TrimSpace(block.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstUserAnchor != "" {
|
||||
parts = append(parts, "user_anchor:"+firstUserAnchor)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256([]byte("anthropic-cache:" + strings.Join(parts, "\n")))
|
||||
return fmt.Sprintf("anthropic-cache-%x", sum[:16])
|
||||
}
|
||||
|
||||
func normalizeCompatSeedJSON(v json.RawMessage) string {
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
var tmp any
|
||||
if err := json.Unmarshal(v, &tmp); err != nil {
|
||||
return string(v)
|
||||
}
|
||||
out, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
return string(v)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
Reference in New Issue
Block a user