mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
157 lines
6.5 KiB
Go
157 lines
6.5 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/tidwall/gjson"
|
||
)
|
||
|
||
const (
|
||
openAIWSThreadIDHeader = "thread-id"
|
||
openAIWSWindowIDHeader = "x-codex-window-id"
|
||
|
||
// request_kind 是 codex 放在 x-codex-turn-metadata 里的请求类型,
|
||
// 对应源码枚举 CodexResponsesRequestKind,目前四个取值:
|
||
// turn 用户轮次的采样请求,含工具调用后的每次续采样
|
||
// prewarm 预热请求:会话启动预开的连接会被首个 turn 复用,
|
||
// 新连接上也是先 warmup 再在同一连接发 turn
|
||
// compaction 上下文压缩:在两次采样之间顺序执行,复用当前 turn 的
|
||
// client session
|
||
// memory 记忆整理:独立 root turn,后台并发发起,却沿用会话的
|
||
// thread_id,与用户 turn 同键时互相切断在飞轮次
|
||
// 前三种与用户 turn 是同一条连接或同一段顺序动作,必须同道;
|
||
// 未声明 request_kind 的请求(旧版 codex、只带 thread-id 头)视同 turn,
|
||
// 键与此前完全一致;明确声明但认不出的取值各自成道,宁可只在同类间
|
||
// 互相替换,也不让未知副请求打断用户轮次。
|
||
openAIWSRequestKindTurn = "turn"
|
||
openAIWSRequestKindPrewarm = "prewarm"
|
||
openAIWSRequestKindCompaction = "compaction"
|
||
)
|
||
|
||
// resolveOpenAIWSClientThreadID 提取 codex 客户端的线程标识。codex 多智能体会话里
|
||
// 父线程与全部子智能体共用同一个 session-id 头,只有线程标识能把它们区分开。
|
||
// 优先级:thread-id 头 → x-codex-turn-metadata 头 → x-codex-window-id 头 → 请求体 client_metadata。
|
||
func resolveOpenAIWSClientThreadID(c *gin.Context, body []byte) string {
|
||
if c != nil && c.Request != nil {
|
||
if id := strings.TrimSpace(c.GetHeader(openAIWSThreadIDHeader)); id != "" {
|
||
return id
|
||
}
|
||
if id := codexTurnMetadataThreadID(c.GetHeader(openAIWSTurnMetadataHeader)); id != "" {
|
||
return id
|
||
}
|
||
if window := strings.TrimSpace(c.GetHeader(openAIWSWindowIDHeader)); window != "" {
|
||
if id := strings.TrimSpace(strings.SplitN(window, ":", 2)[0]); id != "" {
|
||
return id
|
||
}
|
||
}
|
||
}
|
||
if len(body) == 0 {
|
||
return ""
|
||
}
|
||
if id := strings.TrimSpace(gjson.GetBytes(body, "client_metadata.thread_id").String()); id != "" {
|
||
return id
|
||
}
|
||
return codexTurnMetadataThreadID(gjson.GetBytes(body, "client_metadata."+openAIWSTurnMetadataHeader).String())
|
||
}
|
||
|
||
// codexTurnMetadata 是 x-codex-turn-metadata JSON 里执行作用域用到的字段。
|
||
type codexTurnMetadata struct {
|
||
ThreadID string `json:"thread_id"`
|
||
RequestKind string `json:"request_kind"`
|
||
}
|
||
|
||
func parseCodexTurnMetadata(raw string) (codexTurnMetadata, bool) {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return codexTurnMetadata{}, false
|
||
}
|
||
var metadata codexTurnMetadata
|
||
if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
|
||
return codexTurnMetadata{}, false
|
||
}
|
||
metadata.ThreadID = strings.TrimSpace(metadata.ThreadID)
|
||
metadata.RequestKind = strings.ToLower(strings.TrimSpace(metadata.RequestKind))
|
||
return metadata, true
|
||
}
|
||
|
||
func codexTurnMetadataThreadID(raw string) string {
|
||
metadata, _ := parseCodexTurnMetadata(raw)
|
||
return metadata.ThreadID
|
||
}
|
||
|
||
// openAIWSExecutionTurnMetadata 取本次连接的 turn 元数据:x-codex-turn-metadata 头优先,
|
||
// 其次请求体 client_metadata 内嵌的同名 JSON(Desktop 走 HTTP 时只有后者)。
|
||
func openAIWSExecutionTurnMetadata(c *gin.Context, body []byte) codexTurnMetadata {
|
||
if c != nil && c.Request != nil {
|
||
if metadata, ok := parseCodexTurnMetadata(c.GetHeader(openAIWSTurnMetadataHeader)); ok {
|
||
return metadata
|
||
}
|
||
}
|
||
if len(body) == 0 {
|
||
return codexTurnMetadata{}
|
||
}
|
||
metadata, _ := parseCodexTurnMetadata(gjson.GetBytes(body, "client_metadata."+openAIWSTurnMetadataHeader).String())
|
||
return metadata
|
||
}
|
||
|
||
func openAIWSExecutionSubagent(c *gin.Context, body []byte) string {
|
||
if c != nil && c.Request != nil {
|
||
if subagent := strings.TrimSpace(c.GetHeader(openAISubagentHeader)); subagent != "" {
|
||
return strings.ToLower(subagent)
|
||
}
|
||
}
|
||
if len(body) == 0 {
|
||
return ""
|
||
}
|
||
return strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "client_metadata."+openAISubagentHeader).String()))
|
||
}
|
||
|
||
// resolveOpenAIWSExecutionLane 派生执行作用域里的“道”,同道才互相抢占、共享会话级状态。
|
||
// 主道(turn / prewarm / compaction / 未声明)返回空串,其余 request_kind 各自成道,取值
|
||
// 语义见上面的常量注释;没有线程元数据但声明了 x-openai-subagent 的(guardian 评分器)
|
||
// 按子智能体值成道,它们与用户 turn 同键时同样会互相切断在飞轮次。
|
||
func resolveOpenAIWSExecutionLane(c *gin.Context, body []byte) string {
|
||
metadata := openAIWSExecutionTurnMetadata(c, body)
|
||
switch metadata.RequestKind {
|
||
case "", openAIWSRequestKindTurn, openAIWSRequestKindPrewarm, openAIWSRequestKindCompaction:
|
||
default:
|
||
return "kind=" + metadata.RequestKind
|
||
}
|
||
if metadata.ThreadID == "" {
|
||
if subagent := openAIWSExecutionSubagent(c, body); subagent != "" {
|
||
return "subagent=" + subagent
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func openAIWSExecutionScopeSeed(apiKeyID int64, identity, value, lane string) string {
|
||
seed := fmt.Sprintf("openai_ws_exec:%d|%s=%s", apiKeyID, identity, value)
|
||
if lane != "" {
|
||
seed += "|" + lane
|
||
}
|
||
return seed
|
||
}
|
||
|
||
// resolveOpenAIWSExecutionScope 派生 WS 接入用于抢占与会话级状态的执行作用域键,
|
||
// 并一并返回解析到的线程标识供日志使用。身份只取客户端声明的线程标识或显式会话标识,
|
||
// 不掺入请求内容,因此同线程重连在输入变化后仍命中同一键;键里带 API key,同组不同 key
|
||
// 互不影响;同线程上与用户 turn 并发的独立请求按 resolveOpenAIWSExecutionLane 另成一道。
|
||
// 两种身份都没有时返回空作用域:按请求内容推导的粘性种子只服务账号亲和,
|
||
// 不是可靠身份,不参与抢占。
|
||
func resolveOpenAIWSExecutionScope(c *gin.Context, body []byte, apiKeyID int64) (scope, threadID string) {
|
||
lane := resolveOpenAIWSExecutionLane(c, body)
|
||
if threadID = resolveOpenAIWSClientThreadID(c, body); threadID != "" {
|
||
scope, _ = deriveOpenAISessionHashes(openAIWSExecutionScopeSeed(apiKeyID, "thread", threadID, lane))
|
||
return scope, threadID
|
||
}
|
||
if sessionID := strings.TrimSpace(explicitOpenAIRequestSessionID(c, body)); sessionID != "" {
|
||
scope, _ = deriveOpenAISessionHashes(openAIWSExecutionScopeSeed(apiKeyID, "session", sessionID, lane))
|
||
return scope, ""
|
||
}
|
||
return "", ""
|
||
}
|