| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- package service
- import (
- "strings"
- "github.com/tidwall/gjson"
- )
- // ToolContinuationSignals 聚合工具续链相关信号,避免重复遍历 input。
- type ToolContinuationSignals struct {
- HasFunctionCallOutput bool
- HasFunctionCallOutputMissingCallID bool
- HasToolCallContext bool
- HasItemReference bool
- HasItemReferenceForAllCallIDs bool
- FunctionCallOutputCallIDs []string
- }
- // FunctionCallOutputValidation 汇总 function_call_output 关联性校验结果。
- type FunctionCallOutputValidation struct {
- HasFunctionCallOutput bool
- HasToolCallContext bool
- HasFunctionCallOutputMissingCallID bool
- HasItemReferenceForAllCallIDs bool
- }
- func isCodexToolCallContextItemType(typ string) bool {
- switch strings.TrimSpace(typ) {
- case "tool_call",
- "function_call",
- "local_shell_call",
- "tool_search_call",
- "custom_tool_call",
- "mcp_tool_call":
- return true
- default:
- return false
- }
- }
- func isCodexToolCallOutputItemType(typ string) bool {
- switch strings.TrimSpace(typ) {
- case "function_call_output",
- "tool_search_output",
- "custom_tool_call_output",
- "mcp_tool_call_output":
- return true
- default:
- return false
- }
- }
- // NeedsToolContinuation 判定请求是否需要工具调用续链处理。
- // 满足以下任一信号即视为续链:previous_response_id、input 内包含工具输出/item_reference、
- // 或显式声明 tools/tool_choice。
- func NeedsToolContinuation(reqBody map[string]any) bool {
- if reqBody == nil {
- return false
- }
- if hasNonEmptyString(reqBody["previous_response_id"]) {
- return true
- }
- if hasToolsSignal(reqBody) {
- return true
- }
- if hasToolChoiceSignal(reqBody) {
- return true
- }
- input, ok := reqBody["input"].([]any)
- if !ok {
- return false
- }
- for _, item := range input {
- itemMap, ok := item.(map[string]any)
- if !ok {
- continue
- }
- itemType, _ := itemMap["type"].(string)
- if isCodexToolCallItemType(itemType) || itemType == "item_reference" {
- return true
- }
- }
- return false
- }
- // AnalyzeToolContinuationSignals 单次遍历 input,提取工具输出/工具调用上下文/item_reference 相关信号。
- // 字段名保留 FunctionCallOutput 是为了兼容既有调用点;语义覆盖 Codex 的所有工具输出
- // (function_call_output/tool_search_output/custom_tool_call_output/mcp_tool_call_output)。
- func AnalyzeToolContinuationSignals(reqBody map[string]any) ToolContinuationSignals {
- signals := ToolContinuationSignals{}
- if reqBody == nil {
- return signals
- }
- input, ok := reqBody["input"].([]any)
- if !ok {
- return signals
- }
- var callIDs map[string]struct{}
- var referenceIDs map[string]struct{}
- for _, item := range input {
- itemMap, ok := item.(map[string]any)
- if !ok {
- continue
- }
- itemType, _ := itemMap["type"].(string)
- switch {
- case isCodexToolCallContextItemType(itemType):
- callID, _ := itemMap["call_id"].(string)
- if strings.TrimSpace(callID) != "" {
- signals.HasToolCallContext = true
- }
- case isCodexToolCallOutputItemType(itemType):
- signals.HasFunctionCallOutput = true
- callID, _ := itemMap["call_id"].(string)
- callID = strings.TrimSpace(callID)
- if callID == "" {
- signals.HasFunctionCallOutputMissingCallID = true
- continue
- }
- if callIDs == nil {
- callIDs = make(map[string]struct{})
- }
- callIDs[callID] = struct{}{}
- case itemType == "item_reference":
- signals.HasItemReference = true
- idValue, _ := itemMap["id"].(string)
- idValue = strings.TrimSpace(idValue)
- if idValue == "" {
- continue
- }
- if referenceIDs == nil {
- referenceIDs = make(map[string]struct{})
- }
- referenceIDs[idValue] = struct{}{}
- }
- }
- if len(callIDs) == 0 {
- return signals
- }
- signals.FunctionCallOutputCallIDs = make([]string, 0, len(callIDs))
- allReferenced := len(referenceIDs) > 0
- for callID := range callIDs {
- signals.FunctionCallOutputCallIDs = append(signals.FunctionCallOutputCallIDs, callID)
- if allReferenced {
- if _, ok := referenceIDs[callID]; !ok {
- allReferenced = false
- }
- }
- }
- signals.HasItemReferenceForAllCallIDs = allReferenced
- return signals
- }
- // ValidateFunctionCallOutputContextBytes 基于 raw JSON 校验工具输出续链,避免 handler 预校验阶段全量解码大 input。
- func ValidateFunctionCallOutputContextBytes(body []byte) FunctionCallOutputValidation {
- result := FunctionCallOutputValidation{}
- if len(body) == 0 {
- return result
- }
- // handler 热路径只读扫描 input,避免 GetBytes 为大 Responses body 复制整段 JSON。
- input := parseRawJSONView(body).Get("input")
- if !input.IsArray() {
- return result
- }
- var callIDs map[string]struct{}
- var referenceIDs map[string]struct{}
- input.ForEach(func(_, item gjson.Result) bool {
- if !item.IsObject() {
- return true
- }
- itemType := item.Get("type").String()
- switch {
- case isCodexToolCallOutputItemType(itemType):
- result.HasFunctionCallOutput = true
- callID := strings.TrimSpace(item.Get("call_id").String())
- if callID == "" {
- result.HasFunctionCallOutputMissingCallID = true
- return true
- }
- if callIDs == nil {
- callIDs = make(map[string]struct{})
- }
- callIDs[callID] = struct{}{}
- case isCodexToolCallContextItemType(itemType):
- if strings.TrimSpace(item.Get("call_id").String()) != "" {
- result.HasToolCallContext = true
- }
- case itemType == "item_reference":
- idValue := strings.TrimSpace(item.Get("id").String())
- if idValue == "" {
- return true
- }
- if referenceIDs == nil {
- referenceIDs = make(map[string]struct{})
- }
- referenceIDs[idValue] = struct{}{}
- }
- return !result.HasFunctionCallOutput || !result.HasToolCallContext
- })
- if !result.HasFunctionCallOutput || result.HasToolCallContext || len(callIDs) == 0 || len(referenceIDs) == 0 {
- return result
- }
- allReferenced := true
- for callID := range callIDs {
- if _, ok := referenceIDs[callID]; !ok {
- allReferenced = false
- break
- }
- }
- result.HasItemReferenceForAllCallIDs = allReferenced
- return result
- }
- // ToolCallOutputContextCoverage 描述 input 中工具输出与可重建上下文的覆盖关系,
- // 用于判断剥离 previous_response_id 后上游能否仅凭 input 重建工具续链。
- type ToolCallOutputContextCoverage struct {
- HasFunctionCallOutput bool
- // ContextCoversAllCallIDs 表示每个工具输出的 call_id 都能在 input 内找到
- // 同 call_id 的工具调用上下文项或同 id 的 item_reference,且不存在缺失 call_id 的输出。
- // 任一输出无法由 input 自身重建时为 false,此时剥离 previous_response_id 会导致
- // 上游以 "No tool call found for function call output" 拒绝请求。
- ContextCoversAllCallIDs bool
- }
- // AnalyzeToolCallOutputContextCoverageBytes 全量扫描 input,按 call_id 精确匹配工具输出
- // 与可重建上下文。不能复用 ValidateFunctionCallOutputContextBytes 的 HasToolCallContext:
- // 该标志只代表"存在某一个上下文项",部分覆盖的续链仍会被上游拒绝。
- func AnalyzeToolCallOutputContextCoverageBytes(body []byte) ToolCallOutputContextCoverage {
- coverage := ToolCallOutputContextCoverage{}
- if len(body) == 0 {
- return coverage
- }
- input := parseRawJSONView(body).Get("input")
- if !input.IsArray() && !input.IsObject() {
- return coverage
- }
- missingCallID := false
- var outputCallIDs map[string]struct{}
- var contextIDs map[string]struct{}
- analyzeItem := func(item gjson.Result) {
- if !item.IsObject() {
- return
- }
- itemType := item.Get("type").String()
- switch {
- case isCodexToolCallOutputItemType(itemType):
- coverage.HasFunctionCallOutput = true
- callID := strings.TrimSpace(item.Get("call_id").String())
- if callID == "" {
- missingCallID = true
- return
- }
- if outputCallIDs == nil {
- outputCallIDs = make(map[string]struct{})
- }
- outputCallIDs[callID] = struct{}{}
- case isCodexToolCallContextItemType(itemType):
- callID := strings.TrimSpace(item.Get("call_id").String())
- if callID == "" {
- return
- }
- if contextIDs == nil {
- contextIDs = make(map[string]struct{})
- }
- contextIDs[callID] = struct{}{}
- case itemType == "item_reference":
- idValue := strings.TrimSpace(item.Get("id").String())
- if idValue == "" {
- return
- }
- if contextIDs == nil {
- contextIDs = make(map[string]struct{})
- }
- contextIDs[idValue] = struct{}{}
- }
- }
- if input.IsArray() {
- input.ForEach(func(_, item gjson.Result) bool {
- analyzeItem(item)
- return true
- })
- } else {
- analyzeItem(input)
- }
- if !coverage.HasFunctionCallOutput || missingCallID {
- return coverage
- }
- for callID := range outputCallIDs {
- if _, ok := contextIDs[callID]; !ok {
- return coverage
- }
- }
- coverage.ContextCoversAllCallIDs = true
- return coverage
- }
- // ValidateFunctionCallOutputContext 为 handler 提供低开销校验结果:
- // 1) 无工具输出直接返回
- // 2) 若已存在工具调用上下文则提前返回
- // 3) 仅在无工具上下文时才构建 call_id / item_reference 集合
- // 字段名保留 FunctionCallOutput 是为了兼容既有调用点;语义覆盖所有 Codex 工具输出。
- func ValidateFunctionCallOutputContext(reqBody map[string]any) FunctionCallOutputValidation {
- result := FunctionCallOutputValidation{}
- if reqBody == nil {
- return result
- }
- input, ok := reqBody["input"].([]any)
- if !ok {
- return result
- }
- for _, item := range input {
- itemMap, ok := item.(map[string]any)
- if !ok {
- continue
- }
- itemType, _ := itemMap["type"].(string)
- switch {
- case isCodexToolCallOutputItemType(itemType):
- result.HasFunctionCallOutput = true
- case isCodexToolCallContextItemType(itemType):
- callID, _ := itemMap["call_id"].(string)
- if strings.TrimSpace(callID) != "" {
- result.HasToolCallContext = true
- }
- }
- if result.HasFunctionCallOutput && result.HasToolCallContext {
- return result
- }
- }
- if !result.HasFunctionCallOutput || result.HasToolCallContext {
- return result
- }
- callIDs := make(map[string]struct{})
- referenceIDs := make(map[string]struct{})
- for _, item := range input {
- itemMap, ok := item.(map[string]any)
- if !ok {
- continue
- }
- itemType, _ := itemMap["type"].(string)
- switch {
- case isCodexToolCallOutputItemType(itemType):
- callID, _ := itemMap["call_id"].(string)
- callID = strings.TrimSpace(callID)
- if callID == "" {
- result.HasFunctionCallOutputMissingCallID = true
- continue
- }
- callIDs[callID] = struct{}{}
- case itemType == "item_reference":
- idValue, _ := itemMap["id"].(string)
- idValue = strings.TrimSpace(idValue)
- if idValue == "" {
- continue
- }
- referenceIDs[idValue] = struct{}{}
- }
- }
- if len(callIDs) == 0 || len(referenceIDs) == 0 {
- return result
- }
- allReferenced := true
- for callID := range callIDs {
- if _, ok := referenceIDs[callID]; !ok {
- allReferenced = false
- break
- }
- }
- result.HasItemReferenceForAllCallIDs = allReferenced
- return result
- }
- // HasFunctionCallOutput 判断 input 是否包含任意 Codex 工具输出,用于触发续链校验。
- // 名称保留 function_call_output 是为了兼容既有调用点。
- func HasFunctionCallOutput(reqBody map[string]any) bool {
- return AnalyzeToolContinuationSignals(reqBody).HasFunctionCallOutput
- }
- // HasToolCallContext 判断 input 是否包含带 call_id 的工具调用上下文,
- // 用于判断工具输出是否具备可关联的上下文。
- func HasToolCallContext(reqBody map[string]any) bool {
- return AnalyzeToolContinuationSignals(reqBody).HasToolCallContext
- }
- // FunctionCallOutputCallIDs 提取 input 中工具输出的 call_id 集合。
- // 仅返回非空 call_id,用于与 item_reference.id 做匹配校验。
- func FunctionCallOutputCallIDs(reqBody map[string]any) []string {
- return AnalyzeToolContinuationSignals(reqBody).FunctionCallOutputCallIDs
- }
- // HasFunctionCallOutputMissingCallID 判断是否存在缺少 call_id 的工具输出。
- func HasFunctionCallOutputMissingCallID(reqBody map[string]any) bool {
- return AnalyzeToolContinuationSignals(reqBody).HasFunctionCallOutputMissingCallID
- }
- // HasItemReferenceForCallIDs 判断 item_reference.id 是否覆盖所有 call_id。
- // 用于仅依赖引用项完成续链场景的校验。
- func HasItemReferenceForCallIDs(reqBody map[string]any, callIDs []string) bool {
- if reqBody == nil || len(callIDs) == 0 {
- return false
- }
- input, ok := reqBody["input"].([]any)
- if !ok {
- return false
- }
- referenceIDs := make(map[string]struct{})
- for _, item := range input {
- itemMap, ok := item.(map[string]any)
- if !ok {
- continue
- }
- itemType, _ := itemMap["type"].(string)
- if itemType != "item_reference" {
- continue
- }
- idValue, _ := itemMap["id"].(string)
- idValue = strings.TrimSpace(idValue)
- if idValue == "" {
- continue
- }
- referenceIDs[idValue] = struct{}{}
- }
- if len(referenceIDs) == 0 {
- return false
- }
- for _, callID := range callIDs {
- if _, ok := referenceIDs[strings.TrimSpace(callID)]; !ok {
- return false
- }
- }
- return true
- }
- // hasNonEmptyString 判断字段是否为非空字符串。
- func hasNonEmptyString(value any) bool {
- stringValue, ok := value.(string)
- return ok && strings.TrimSpace(stringValue) != ""
- }
- // hasToolsSignal 判断 tools 字段是否显式声明(存在且不为空)。
- func hasToolsSignal(reqBody map[string]any) bool {
- raw, exists := reqBody["tools"]
- if !exists || raw == nil {
- return false
- }
- if tools, ok := raw.([]any); ok {
- return len(tools) > 0
- }
- return false
- }
- // hasToolChoiceSignal 判断 tool_choice 是否显式声明(非空或非 nil)。
- func hasToolChoiceSignal(reqBody map[string]any) bool {
- raw, exists := reqBody["tool_choice"]
- if !exists || raw == nil {
- return false
- }
- switch value := raw.(type) {
- case string:
- return strings.TrimSpace(value) != ""
- case map[string]any:
- return len(value) > 0
- default:
- return false
- }
- }
|