881 lines · 28.1 KB · Go
RawDownload
package executor import (	"bytes"	"encoding/json"	"fmt"	"net/http"	"sort"	"strings"	"time" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"	"github.com/router-for-me/CLIProxyAPI/v7/internal/signature"	"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"	log "github.com/sirupsen/logrus"	"github.com/tidwall/gjson"	"github.com/tidwall/sjson") // xAI executes these x_search subtools server-side but exposes their trace as// client-style tool calls. Hide the trace so Responses clients do not execute it again.type xaiInternalXSearchResponseFilter struct {	enabled              bool	clientDeclaredTools  map[xaiClientToolKey]struct{}	droppedOutputIndexes map[int64]struct{}	droppedItemIDs       map[string]struct{}} func newXAIInternalXSearchResponseFilter(enabled bool, clientDeclaredTools map[xaiClientToolKey]struct{}) *xaiInternalXSearchResponseFilter {	filter := &xaiInternalXSearchResponseFilter{		enabled:             enabled,		clientDeclaredTools: clientDeclaredTools,	}	if enabled {		filter.droppedOutputIndexes = make(map[int64]struct{})		filter.droppedItemIDs = make(map[string]struct{})	}	return filter} func xaiRequestHasNativeXSearch(body []byte) bool {	if gjson.GetBytes(body, `tools.#(type=="x_search")`).Exists() {		return true	}	// Multipath queries return an array of matches; an empty array still Exists().	// Check the match count instead of Exists() for additional_tools injection.	return len(gjson.GetBytes(body, `input.#(type=="additional_tools")#.tools.#(type=="x_search")`).Array()) > 0} // collectXAIClientDeclaredToolKeys records client-declared function/custom tools// using the Responses post-restore identity (short name + optional namespace) and// the effective upstream tool type after normalizeXAITool. Client custom tools// are normalized to function before being sent to xAI, so keys use function for// both declaration kinds. Must run before normalizeXAITools flattens namespace wrappers.func collectXAIClientDeclaredToolKeys(body []byte) map[xaiClientToolKey]struct{} {	keys := make(map[xaiClientToolKey]struct{})	collect := func(tools gjson.Result) {		if !tools.Exists() || !tools.IsArray() {			return		}		for _, tool := range tools.Array() {			switch toolType := strings.TrimSpace(tool.Get("type").String()); toolType {			case xaiNamespaceToolType:				namespaceName := strings.TrimSpace(tool.Get("name").String())				if namespaceName == "" {					continue				}				for _, nestedTool := range tool.Get("tools").Array() {					nestedType := strings.TrimSpace(nestedTool.Get("type").String())					if nestedType != xaiFunctionToolType && nestedType != xaiCustomToolType {						continue					}					toolName := strings.TrimSpace(nestedTool.Get("name").String())					if toolName == "" {						continue					}					// normalizeXAITool converts custom → function before upstream send.					keys[xaiClientToolKey{namespace: namespaceName, name: toolName, toolType: xaiEffectiveDeclaredToolType(nestedType)}] = struct{}{}				}			case xaiFunctionToolType, xaiCustomToolType:				toolName := strings.TrimSpace(tool.Get("name").String())				if toolName == "" {					continue				}				// normalizeXAITool converts custom → function before upstream send.				keys[xaiClientToolKey{namespace: "", name: toolName, toolType: xaiEffectiveDeclaredToolType(toolType)}] = struct{}{}			}		}	}	collect(gjson.GetBytes(body, "tools"))	input := gjson.GetBytes(body, "input")	if input.Exists() && input.IsArray() {		for _, item := range input.Array() {			if item.Get("type").String() == "additional_tools" {				collect(item.Get("tools"))			}		}	}	return keys} // xaiEffectiveDeclaredToolType returns the tool type actually sent upstream// after normalizeXAITool. Client custom tools are rewritten to function.func xaiEffectiveDeclaredToolType(toolType string) string {	if strings.TrimSpace(toolType) == xaiCustomToolType {		return xaiFunctionToolType	}	return strings.TrimSpace(toolType)} func xaiIsInternalXSearchToolName(name string) bool {	switch strings.TrimSpace(name) {	case "x_user_search", "x_semantic_search", "x_keyword_search", "x_thread_fetch":		return true	default:		return false	}} // xaiResponseCallDeclaredType maps a Responses output call type to the effective// upstream tool declaration kind used when matching client-declared tools.// Client custom tools are normalized to function before upstream send, so only// function_call can match a client-declared same-name tool; custom_tool_call// remains the internal X Search trace shape.func xaiResponseCallDeclaredType(itemType string) string {	switch strings.TrimSpace(itemType) {	case "function_call":		return xaiFunctionToolType	case "custom_tool_call":		return xaiCustomToolType	default:		return ""	}} // xaiIsInternalXSearchCallID reports whether call_id matches the evidenced xAI// X Search server-side trace prefix (xs_call...), as observed in Responses traffic// for native x_search subtools (see issue #4282 / PR #4284 fixtures).func xaiIsInternalXSearchCallID(callID string) bool {	return strings.HasPrefix(strings.TrimSpace(callID), "xs_call")} // xaiIsInternalXSearchCall reports whether an output item is an xAI server-side// X Search subtool trace that should be hidden from Responses clients.//// Evidence from xAI Responses traffic (issue #4282 / PR #4284)://   - native x_search subtools are emitted as custom_tool_call items named//     x_user_search / x_semantic_search / x_keyword_search / x_thread_fetch//   - those traces commonly use call_id values prefixed with "xs_call"//// Client tools that share a short name are preserved only when the response call// kind matches the effective upstream declaration type. Because normalizeXAITool// rewrites client custom → function, a client custom x_keyword_search is keyed as// function and therefore preserves function_call while still filtering genuine// internal custom_tool_call / xs_call* traces. Namespaced restored client tools// are never treated as internal.func xaiIsInternalXSearchCall(item gjson.Result, clientDeclaredTools map[xaiClientToolKey]struct{}) bool {	itemType := strings.TrimSpace(item.Get("type").String())	declaredType := xaiResponseCallDeclaredType(itemType)	if declaredType == "" {		return false	}	name := strings.TrimSpace(item.Get("name").String())	if !xaiIsInternalXSearchToolName(name) {		return false	}	namespace := strings.TrimSpace(item.Get("namespace").String())	// Namespaced calls are restored client tools, never xAI internal X Search traces.	if namespace != "" {		return false	}	// Evidenced internal call_id prefix always identifies server-side X Search traces,	// even when a client tool reuses the same short name.	if xaiIsInternalXSearchCallID(item.Get("call_id").String()) {		return true	}	// Preserve only client tools whose effective upstream declaration kind matches	// this call type (function_call ↔ function after custom normalization).	if _, declared := clientDeclaredTools[xaiClientToolKey{namespace: namespace, name: name, toolType: declaredType}]; declared {		return false	}	return true} func (f *xaiInternalXSearchResponseFilter) apply(eventData []byte) []byte {	if f == nil || !f.enabled || len(eventData) == 0 || !gjson.ValidBytes(eventData) {		return eventData	} 	if item := gjson.GetBytes(eventData, "item"); xaiIsInternalXSearchCall(item, f.clientDeclaredTools) {		f.recordDroppedItem(eventData, item)		return nil	} 	eventData = f.filterCompletedOutput(eventData)	if f.referencesDroppedItem(eventData) {		return nil	}	return f.compactOutputIndex(eventData)} func (f *xaiInternalXSearchResponseFilter) recordDroppedItem(eventData []byte, item gjson.Result) {	if outputIndex := gjson.GetBytes(eventData, "output_index"); outputIndex.Exists() {		f.droppedOutputIndexes[outputIndex.Int()] = struct{}{}	}	for _, path := range []string{"id", "call_id"} {		if id := strings.TrimSpace(item.Get(path).String()); id != "" {			f.droppedItemIDs[id] = struct{}{}		}	}} func (f *xaiInternalXSearchResponseFilter) referencesDroppedItem(eventData []byte) bool {	if outputIndex := gjson.GetBytes(eventData, "output_index"); outputIndex.Exists() {		if _, dropped := f.droppedOutputIndexes[outputIndex.Int()]; dropped {			return true		}	}	for _, path := range []string{"item_id", "call_id"} {		id := strings.TrimSpace(gjson.GetBytes(eventData, path).String())		if _, dropped := f.droppedItemIDs[id]; id != "" && dropped {			return true		}	}	return false} func (f *xaiInternalXSearchResponseFilter) compactOutputIndex(eventData []byte) []byte {	outputIndex := gjson.GetBytes(eventData, "output_index")	if !outputIndex.Exists() {		return eventData	}	original := outputIndex.Int()	removedBefore := int64(0)	for dropped := range f.droppedOutputIndexes {		if dropped < original {			removedBefore++		}	}	if removedBefore == 0 {		return eventData	}	updated, errSet := sjson.SetBytes(eventData, "output_index", original-removedBefore)	if errSet != nil {		return eventData	}	return updated} func (f *xaiInternalXSearchResponseFilter) filterCompletedOutput(eventData []byte) []byte {	output := gjson.GetBytes(eventData, "response.output")	if !output.IsArray() {		return eventData	}	var clientDeclaredTools map[xaiClientToolKey]struct{}	if f != nil {		clientDeclaredTools = f.clientDeclaredTools	}	items := make([]json.RawMessage, 0, len(output.Array()))	changed := false	for _, item := range output.Array() {		if xaiIsInternalXSearchCall(item, clientDeclaredTools) {			changed = true			continue		}		items = append(items, json.RawMessage(item.Raw))	}	if !changed {		return eventData	}	rawOutput, errMarshal := json.Marshal(items)	if errMarshal != nil {		return eventData	}	updated, errSet := sjson.SetRawBytes(eventData, "response.output", rawOutput)	if errSet != nil {		return eventData	}	return updated} func normalizeXAIInputNamespaceToolCalls(body []byte) []byte {	if !gjson.ValidBytes(body) {		return body	}	input := gjson.GetBytes(body, "input")	if !input.Exists() || !input.IsArray() {		return body	}	for index, item := range input.Array() {		if item.Get("type").String() != "function_call" {			continue		}		namespaceName := strings.TrimSpace(item.Get("namespace").String())		toolName := strings.TrimSpace(item.Get("name").String())		qualifiedName := qualifyXAINamespaceToolName(namespaceName, toolName)		if namespaceName == "" || qualifiedName == "" {			continue		}		namePath := fmt.Sprintf("input.%d.name", index)		namespacePath := fmt.Sprintf("input.%d.namespace", index)		updated, errSet := sjson.SetBytes(body, namePath, qualifiedName)		if errSet != nil {			continue		}		updated, errDelete := sjson.DeleteBytes(updated, namespacePath)		if errDelete != nil {			continue		}		body = updated	}	return body} func restoreXAINamespaceToolCalls(data []byte, refs map[string]xaiNamespaceToolRef) []byte {	if len(refs) == 0 || len(data) == 0 || !gjson.ValidBytes(data) {		return data	}	data = restoreXAINamespaceToolCallAtPath(data, "item", refs)	output := gjson.GetBytes(data, "response.output")	if output.Exists() && output.IsArray() {		for index := range output.Array() {			data = restoreXAINamespaceToolCallAtPath(data, fmt.Sprintf("response.output.%d", index), refs)		}	}	return data} func restoreXAINamespaceToolCallAtPath(data []byte, path string, refs map[string]xaiNamespaceToolRef) []byte {	if gjson.GetBytes(data, path+".type").String() != "function_call" {		return data	}	qualifiedName := strings.TrimSpace(gjson.GetBytes(data, path+".name").String())	ref, ok := refs[qualifiedName]	if !ok {		return data	}	updated, errSet := sjson.SetBytes(data, path+".name", ref.name)	if errSet != nil {		return data	}	updated, errSet = sjson.SetBytes(updated, path+".namespace", ref.namespace)	if errSet != nil {		return data	}	return updated} // normalizeXAIObjectRootUnionBranchTypes makes untyped root union branches// explicitly object-only when the parameter root already permits only objects.// This preserves the original schema semantics while satisfying xAI validation.func normalizeXAIObjectRootUnionBranchTypes(tool []byte) ([]byte, bool, bool) {	parameters := gjson.GetBytes(tool, "parameters")	rootType := parameters.Get("type")	if rootType.Type != gjson.String || rootType.String() != "object" {		return tool, false, true	} 	original := tool	changed := false	for _, unionName := range []string{"anyOf", "oneOf"} {		union := parameters.Get(unionName)		if !union.IsArray() {			continue		}		for index, branch := range union.Array() {			if !branch.IsObject() || branch.Get("type").Exists() {				continue			}			updated, errSet := sjson.SetBytes(tool, fmt.Sprintf("parameters.%s.%d.type", unionName, index), "object")			if errSet != nil {				return original, false, false			}			tool = updated			changed = true		}	}	return tool, changed, true} func xaiSchemaTypeIsObjectOnly(schemaType gjson.Result) bool {	if schemaType.Type == gjson.String {		return strings.EqualFold(strings.TrimSpace(schemaType.String()), "object")	}	if !schemaType.IsArray() {		return false	}	types := schemaType.Array()	if len(types) == 0 {		return false	}	for _, schemaTypeItem := range types {		if schemaTypeItem.Type != gjson.String || !strings.EqualFold(strings.TrimSpace(schemaTypeItem.String()), "object") {			return false		}	}	return true} // xaiFunctionParametersNeedSimplification reports whether a function tool, or// a custom tool normalized to a function, has a schema that xAI cannot accept.func xaiFunctionParametersNeedSimplification(tool gjson.Result, namespaceName string) bool {	toolType := strings.TrimSpace(tool.Get("type").String())	isFunction := strings.EqualFold(toolType, xaiFunctionToolType)	isNormalizedCustom := strings.EqualFold(toolType, xaiCustomToolType)	if !isFunction && !isNormalizedCustom {		return false	} 	toolName := strings.TrimSpace(tool.Get("name").String())	qualifiedAutomationName := xaiCodexAppNamespaceName + "__" + xaiAutomationUpdateToolName	if isFunction && (strings.EqualFold(toolName, qualifiedAutomationName) ||		(strings.EqualFold(strings.TrimSpace(namespaceName), xaiCodexAppNamespaceName) &&			strings.EqualFold(toolName, xaiAutomationUpdateToolName))) {		return true	} 	parameters := tool.Get("parameters")	for _, unionName := range []string{"anyOf", "oneOf"} {		union := parameters.Get(unionName)		if !union.IsArray() {			continue		}		for _, branch := range union.Array() {			if !xaiSchemaTypeIsObjectOnly(branch.Get("type")) {				return true			}		}	}	return false} func sanitizeXAIInputEncryptedContent(body []byte) []byte {	input := gjson.GetBytes(body, "input")	if !input.Exists() || !input.IsArray() {		return body	}	items := make([]json.RawMessage, 0, len(input.Array()))	changed := false	dropCount := 0	firstReason := ""	firstItemType := ""	for _, item := range input.Array() {		itemType := strings.TrimSpace(item.Get("type").String())		if itemType != "reasoning" && itemType != "compaction" {			items = append(items, json.RawMessage(item.Raw))			continue		}		encryptedContent := item.Get("encrypted_content")		if !encryptedContent.Exists() {			items = append(items, json.RawMessage(item.Raw))			continue		}		reason := ""		switch encryptedContent.Type {		case gjson.String:			if _, err := signature.InspectGrokEncryptedContent(encryptedContent.String()); err != nil {				reason = err.Error()			}		case gjson.Null:			reason = "encrypted_content is null"		default:			reason = fmt.Sprintf("encrypted_content must be a string, got %s", encryptedContent.Type.String())		}		if reason == "" {			items = append(items, json.RawMessage(item.Raw))			continue		} 		if itemType == "compaction" {			changed = true			dropCount++			if firstReason == "" {				firstReason = reason				firstItemType = itemType			}			continue		} 		next, err := sjson.DeleteBytes([]byte(item.Raw), "encrypted_content")		if err != nil {			items = append(items, json.RawMessage(item.Raw))			continue		}		items = append(items, json.RawMessage(next))		changed = true		dropCount++		if firstReason == "" {			firstReason = reason			firstItemType = itemType		}	}	if !changed {		return body	}	rawInput, err := json.Marshal(items)	if err != nil {		return body	}	updated, err := sjson.SetRawBytes(body, "input", rawInput)

Lines 1–500 of 881.