| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- 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))
- canonical := canonicalizeOpenAIModelAliasSpelling(trimmed)
- // GPT-6 is the public alias for Astra. Keep this deliberately scoped to
- // Astra so other GPT-6 families do not inherit Messages compatibility state.
- if canonical == "gpt-6" || canonical == "gpt-6-astra" {
- return true
- }
- // 仅对 Responses 兼容路径支持的 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)
- }
|