| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- package service
- import (
- "encoding/json"
- "fmt"
- "strings"
- "github.com/gin-gonic/gin"
- "github.com/tidwall/gjson"
- "github.com/tidwall/sjson"
- )
- const (
- codexReservedPythonToolName = "python"
- codexPythonToolAlias = "python__sub2api"
- codexToolNameReverseKey = "openai_codex_tool_name_reverse"
- codexToolNameSessionKey = "openai_codex_tool_name_session_reverse"
- )
- type codexToolNameField struct {
- object map[string]any
- key string
- name string
- }
- // aliasOpenAIOAuthReservedToolNames avoids names reserved by the ChatGPT
- // Codex backend. It validates every declaration/reference before mutating so
- // collisions cannot leave a partially rewritten request.
- func aliasOpenAIOAuthReservedToolNames(reqBody map[string]any) (map[string]string, bool, error) {
- if reqBody == nil {
- return nil, false, nil
- }
- fields := collectOpenAIResponsesToolNameFields(reqBody)
- owners := make(map[string]string)
- reverse := make(map[string]string)
- for _, field := range fields {
- normalized := aliasOpenAIOAuthReservedToolName(field.name)
- original := field.name
- if normalized != field.name {
- original = strings.TrimSpace(field.name)
- }
- if previous, exists := owners[normalized]; exists && previous != original {
- return nil, false, fmt.Errorf("tool names %q and %q both normalize to %q", previous, original, normalized)
- }
- owners[normalized] = original
- if normalized != field.name {
- reverse[normalized] = original
- }
- }
- if len(reverse) == 0 {
- return nil, false, nil
- }
- for _, field := range fields {
- if aliased := aliasOpenAIOAuthReservedToolName(field.name); aliased != field.name {
- field.object[field.key] = aliased
- }
- }
- return reverse, true, nil
- }
- func aliasOpenAIOAuthReservedToolName(name string) string {
- trimmed := strings.TrimSpace(name)
- if strings.EqualFold(trimmed, codexReservedPythonToolName) {
- return codexPythonToolAlias
- }
- return name
- }
- func collectOpenAIResponsesToolNameFields(reqBody map[string]any) []codexToolNameField {
- fields := make([]codexToolNameField, 0, 8)
- appendName := func(object map[string]any, key string) {
- if object == nil {
- return
- }
- name, ok := object[key].(string)
- if !ok || strings.TrimSpace(name) == "" {
- return
- }
- fields = append(fields, codexToolNameField{object: object, key: key, name: name})
- }
- var collectTools func(any)
- collectTools = func(rawTools any) {
- tools, ok := rawTools.([]any)
- if !ok {
- return
- }
- for _, raw := range tools {
- tool, ok := raw.(map[string]any)
- if !ok {
- continue
- }
- toolType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(tool["type"])))
- if toolType == "function" {
- appendName(tool, "name")
- if function, ok := tool["function"].(map[string]any); ok {
- appendName(function, "name")
- }
- }
- if toolType == "namespace" {
- collectTools(tool["tools"])
- }
- }
- }
- collectTools(reqBody["tools"])
- if functions, ok := reqBody["functions"].([]any); ok {
- for _, raw := range functions {
- function, _ := raw.(map[string]any)
- appendName(function, "name")
- }
- }
- if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(reqBody["type"])), "session.update") {
- if session, ok := reqBody["session"].(map[string]any); ok {
- collectTools(session["tools"])
- }
- }
- if choice, ok := reqBody["tool_choice"].(map[string]any); ok {
- if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(choice["type"])), "allowed_tools") {
- collectTools(choice["tools"])
- }
- if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(choice["type"])), "function") {
- appendName(choice, "name")
- if function, ok := choice["function"].(map[string]any); ok {
- appendName(function, "name")
- }
- }
- }
- if input, ok := reqBody["input"].([]any); ok {
- for _, raw := range input {
- item, ok := raw.(map[string]any)
- if !ok {
- continue
- }
- typ := strings.ToLower(strings.TrimSpace(firstNonEmptyString(item["type"])))
- if typ == "additional_tools" {
- collectTools(item["tools"])
- }
- if typ == "function_call" {
- appendName(item, "name")
- if function, ok := item["function"].(map[string]any); ok {
- appendName(function, "name")
- }
- }
- }
- }
- return fields
- }
- func aliasOpenAIOAuthReservedToolNamesBody(body []byte) ([]byte, map[string]string, bool, error) {
- if len(body) == 0 || !containsASCIIFold(body, []byte(codexReservedPythonToolName)) {
- return body, nil, false, nil
- }
- var reqBody map[string]any
- if err := decodeOpenAIJSONUseNumber(body, &reqBody); err != nil {
- return body, nil, false, fmt.Errorf("decode OAuth reserved tool names: %w", err)
- }
- reverse, changed, err := aliasOpenAIOAuthReservedToolNames(reqBody)
- if err != nil || !changed {
- return body, reverse, false, err
- }
- normalized, err := json.Marshal(reqBody)
- if err != nil {
- return body, nil, false, fmt.Errorf("encode OAuth reserved tool names: %w", err)
- }
- return normalized, reverse, true, nil
- }
- func containsASCIIFold(haystack, needle []byte) bool {
- if len(needle) == 0 || len(haystack) < len(needle) {
- return false
- }
- for i := 0; i <= len(haystack)-len(needle); i++ {
- matched := true
- for j := range needle {
- a, b := haystack[i+j], needle[j]
- if a >= 'A' && a <= 'Z' {
- a += 'a' - 'A'
- }
- if b >= 'A' && b <= 'Z' {
- b += 'a' - 'A'
- }
- if a != b {
- matched = false
- break
- }
- }
- if matched {
- return true
- }
- }
- return false
- }
- func setCodexToolNameReverse(c *gin.Context, reverse map[string]string) {
- if c == nil {
- return
- }
- storeCodexToolNameReverse(c, codexToolNameReverseKey, reverse)
- storeCodexToolNameReverse(c, codexToolNameSessionKey, nil)
- }
- func storeCodexToolNameReverse(c *gin.Context, key string, reverse map[string]string) {
- if c == nil {
- return
- }
- copyMap := make(map[string]string, len(reverse))
- for aliased, original := range reverse {
- copyMap[aliased] = original
- }
- c.Set(key, copyMap)
- }
- func mergeCodexToolNameReverse(c *gin.Context, reverse map[string]string) {
- if c == nil || len(reverse) == 0 {
- return
- }
- merged := make(map[string]string, len(reverse)+len(codexToolNameReverseFromContext(c)))
- for aliased, original := range codexToolNameReverseFromContext(c) {
- merged[aliased] = original
- }
- for aliased, original := range reverse {
- merged[aliased] = original
- }
- storeCodexToolNameReverse(c, codexToolNameReverseKey, merged)
- }
- func codexToolNameReverseFromContext(c *gin.Context) map[string]string {
- return codexToolNameReverseForKey(c, codexToolNameReverseKey)
- }
- func codexToolNameReverseForKey(c *gin.Context, key string) map[string]string {
- if c == nil {
- return nil
- }
- raw, ok := c.Get(key)
- if !ok {
- return nil
- }
- reverse, _ := raw.(map[string]string)
- return reverse
- }
- // updateCodexToolNameReverseForWSFrame keeps the active turn isolated from
- // session updates that may arrive while that turn is still streaming.
- func updateCodexToolNameReverseForWSFrame(c *gin.Context, frame []byte, reverse map[string]string) {
- if c == nil {
- return
- }
- eventType := strings.TrimSpace(gjson.GetBytes(frame, "type").String())
- switch eventType {
- case "session.update":
- if gjson.GetBytes(frame, "session.tools").Exists() {
- storeCodexToolNameReverse(c, codexToolNameSessionKey, reverse)
- }
- case "response.create", "":
- active := reverse
- if !openAIWSFrameHasExplicitToolDeclarations(frame) {
- active = mergeCodexToolNameReverseMaps(
- codexToolNameReverseForKey(c, codexToolNameSessionKey),
- reverse,
- )
- }
- storeCodexToolNameReverse(c, codexToolNameReverseKey, active)
- }
- }
- func openAIWSFrameHasExplicitToolDeclarations(frame []byte) bool {
- if gjson.GetBytes(frame, "tools").Exists() {
- return true
- }
- for _, item := range gjson.GetBytes(frame, "input").Array() {
- if strings.EqualFold(strings.TrimSpace(item.Get("type").String()), "additional_tools") && item.Get("tools").Exists() {
- return true
- }
- }
- return false
- }
- func mergeCodexToolNameReverseMaps(base, overlay map[string]string) map[string]string {
- merged := make(map[string]string, len(base)+len(overlay))
- for aliased, original := range base {
- merged[aliased] = original
- }
- for aliased, original := range overlay {
- merged[aliased] = original
- }
- return merged
- }
- func restoreCodexToolNamesInJSON(data []byte, reverse map[string]string) []byte {
- if len(data) == 0 || len(reverse) == 0 || !json.Valid(data) {
- return data
- }
- var decoded any
- if err := decodeOpenAIJSONUseNumber(data, &decoded); err != nil {
- return data
- }
- if !restoreCodexToolNameFields(decoded, reverse) {
- return data
- }
- restored, err := json.Marshal(decoded)
- if err != nil {
- return data
- }
- return restored
- }
- func restoreCodexToolNamesFromContext(c *gin.Context, data []byte) []byte {
- reverse := codexToolNameReverseFromContext(c)
- switch strings.TrimSpace(gjson.GetBytes(data, "type").String()) {
- case "session.created", "session.updated":
- reverse = codexToolNameReverseForKey(c, codexToolNameSessionKey)
- }
- return restoreCodexToolNamesInJSON(data, reverse)
- }
- func restoreCodexToolNamesFromSSEContext(c *gin.Context, data []byte, eventType string) []byte {
- if strings.TrimSpace(gjson.GetBytes(data, "type").String()) != "" || strings.TrimSpace(eventType) == "" {
- return restoreCodexToolNamesFromContext(c, data)
- }
- compat := []byte(openAICompatPayloadWithEventType(string(data), eventType))
- restored := restoreCodexToolNamesFromContext(c, compat)
- if string(restored) == string(compat) {
- return data
- }
- withoutSyntheticType, err := sjson.DeleteBytes(restored, "type")
- if err != nil {
- return restored
- }
- return withoutSyntheticType
- }
- func restoreCodexToolNameFields(value any, reverse map[string]string) bool {
- root, ok := value.(map[string]any)
- if !ok {
- return false
- }
- changed := false
- restoreItem := func(raw any) {
- item, ok := raw.(map[string]any)
- if !ok || !strings.EqualFold(strings.TrimSpace(firstNonEmptyString(item["type"])), "function_call") {
- return
- }
- name, _ := item["name"].(string)
- if original, exists := reverse[name]; exists {
- item["name"] = original
- changed = true
- }
- }
- restoreOutput := func(raw any) {
- output, _ := raw.([]any)
- for _, item := range output {
- restoreItem(item)
- }
- }
- restoreResponse := func(raw any) {
- response, ok := raw.(map[string]any)
- if ok {
- restoreOutput(response["output"])
- }
- }
- restoreFunction := func(raw any) {
- function, ok := raw.(map[string]any)
- if !ok {
- return
- }
- name, _ := function["name"].(string)
- if original, exists := reverse[name]; exists {
- function["name"] = original
- changed = true
- }
- }
- restoreChatToolCalls := func(raw any) {
- toolCalls, _ := raw.([]any)
- for _, rawCall := range toolCalls {
- call, ok := rawCall.(map[string]any)
- if !ok {
- continue
- }
- callType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(call["type"])))
- if callType == "" || callType == "function" {
- restoreFunction(call["function"])
- }
- }
- }
- restoreMessageContent := func(raw any) {
- content, _ := raw.([]any)
- for _, rawBlock := range content {
- block, ok := rawBlock.(map[string]any)
- if !ok || !strings.EqualFold(strings.TrimSpace(firstNonEmptyString(block["type"])), "tool_use") {
- continue
- }
- name, _ := block["name"].(string)
- if original, exists := reverse[name]; exists {
- block["name"] = original
- changed = true
- }
- }
- }
- var restoreTools func(any)
- restoreTools = func(raw any) {
- tools, _ := raw.([]any)
- for _, rawTool := range tools {
- tool, ok := rawTool.(map[string]any)
- if !ok {
- continue
- }
- toolType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(tool["type"])))
- if toolType == "function" {
- name, _ := tool["name"].(string)
- if original, exists := reverse[name]; exists {
- tool["name"] = original
- changed = true
- }
- }
- if toolType == "namespace" {
- restoreTools(tool["tools"])
- }
- }
- }
- eventType := strings.ToLower(strings.TrimSpace(firstNonEmptyString(root["type"])))
- switch eventType {
- case "response.output_item.added", "response.output_item.done":
- restoreItem(root["item"])
- case "response.created", "response.in_progress", "response.completed", "response.done", "response.failed", "response.incomplete", "response.cancelled", "response.canceled":
- restoreResponse(root["response"])
- case "session.created", "session.updated":
- if session, ok := root["session"].(map[string]any); ok {
- restoreTools(session["tools"])
- }
- }
- if _, hasOutput := root["output"]; hasOutput {
- restoreOutput(root["output"])
- }
- if choices, ok := root["choices"].([]any); ok {
- for _, rawChoice := range choices {
- choice, ok := rawChoice.(map[string]any)
- if !ok {
- continue
- }
- for _, key := range []string{"message", "delta"} {
- if message, ok := choice[key].(map[string]any); ok {
- restoreChatToolCalls(message["tool_calls"])
- }
- }
- }
- }
- restoreMessageContent(root["content"])
- if block, ok := root["content_block"].(map[string]any); ok && strings.EqualFold(strings.TrimSpace(firstNonEmptyString(block["type"])), "tool_use") {
- name, _ := block["name"].(string)
- if original, exists := reverse[name]; exists {
- block["name"] = original
- changed = true
- }
- }
- return changed
- }
|