package service import ( "encoding/json" "fmt" "strings" "oci-portal/internal/aiwire" ) // respRejectStateful 拒绝有状态特性(网关无状态)。 func respRejectStateful(req aiwire.RespRequest) error { if req.PreviousResponseID != "" { return fmt.Errorf("previous_response_id 不支持:网关不保存历史响应,请在 input 中自带完整上下文(store:false 模式)") } if len(req.Conversation) > 0 && string(req.Conversation) != "null" { return fmt.Errorf("conversation 不支持:网关不保存对话状态") } if req.Background != nil && *req.Background { return fmt.Errorf("background 模式不支持") } return nil } // RespPassthroughValidate 校验直通请求:模型必填,有状态特性不支持,工具类型 // 放行 function、Oracle 文档化的服务端工具(web_search / x_search / // code_interpreter / mcp)与 codex 特有形态(namespace 工具分组、custom 自由 // 格式,上游不识别,转发前拍平/转换)。 func RespPassthroughValidate(req aiwire.RespRequest) error { if strings.TrimSpace(req.Model) == "" { return fmt.Errorf("model 不能为空") } if err := respRejectStateful(req); err != nil { return err } for _, t := range req.Tools { switch t.Type { case "function", "web_search", "x_search", "code_interpreter", "mcp", "namespace", "custom", "tool_search": default: return fmt.Errorf("不支持的工具类型 %q:服务端工具仅支持 web_search / x_search / code_interpreter / mcp", t.Type) } } return nil } // RespNsRef 一条拍平映射:限定名对应的原 namespace 与短名。codex 的回程路由 // 依赖 function_call 项上的 name+namespace 双字段,响应侧按此映射还原。 type RespNsRef struct { Namespace string Name string } // RespCompat 是直通请求 codex 兼容改写的结果:Flattened/Converted/Dropped 供 // 调用方记日志;NsRefs / CustomNames 非空表示响应侧需做工具调用项还原。 type RespCompat struct { Flattened []string Converted []string Dropped []string NsRefs map[string]RespNsRef CustomNames map[string]struct{} } // NeedRestore 报告响应是否需要做工具调用项还原。 func (c RespCompat) NeedRestore() bool { return len(c.NsRefs) > 0 || len(c.CustomNames) > 0 } // RespPassthroughBody 以原始请求体为基构造上游 body:强制 store:false(禁上游 // 存态),stream 原样保留(流式直通);用 json.Number 保真未知字段与数值。 // 同时做 codex 兼容改写(namespace 拍平、多轮历史与 tool_choice 重限定)。 func RespPassthroughBody(raw []byte) ([]byte, RespCompat, error) { dec := json.NewDecoder(strings.NewReader(string(raw))) dec.UseNumber() var body map[string]any if err := dec.Decode(&body); err != nil { return nil, RespCompat{}, fmt.Errorf("解析请求体: %w", err) } body["store"] = false compat := respCompatTools(body) respCompatInputCalls(body) respCompatToolChoice(body) out, err := json.Marshal(body) return out, compat, err } // respCompatTools 对 tools 做 codex 兼容改写(上游实测行为见任务档案): // namespace 工具组上游 422,拆平为限定名 function 工具;custom(自由格式)上游 // 同样 422,apply_patch 丢弃(grok 系未训练该补丁格式,失败编辑不如 shell 回退), // 其余转 function 包装;web_search 的 external_web_access 参数上游 400,false 是 // "仅缓存检索"降权模式,按不越权原则连工具剥离,true 等价默认行为仅删键; // 剔空后删 tools,并连删 tool_choice 与 parallel_tool_calls(上游拒绝无 tools // 带 tool_choice)。 func respCompatTools(body map[string]any) RespCompat { tools, ok := body["tools"].([]any) if !ok { return RespCompat{} } compat := RespCompat{NsRefs: map[string]RespNsRef{}, CustomNames: map[string]struct{}{}} kept := make([]any, 0, len(tools)) for _, item := range tools { tool, _ := item.(map[string]any) switch { case tool != nil && tool["type"] == "namespace": ns, _ := tool["name"].(string) compat.Flattened = append(compat.Flattened, "namespace:"+ns) kept = append(kept, respFlattenNsTool(ns, tool, &compat)...) continue case tool != nil && tool["type"] == "custom": name, _ := tool["name"].(string) if name == "apply_patch" { compat.Dropped = append(compat.Dropped, "custom:apply_patch") continue } respConvertCustomTool(tool) if name != "" { compat.CustomNames[name] = struct{}{} compat.Converted = append(compat.Converted, "custom:"+name) } kept = append(kept, tool) continue } if desc, drop := respToolDrop(tool); drop { compat.Dropped = append(compat.Dropped, desc) continue } kept = append(kept, item) } if len(kept) == 0 { delete(body, "tools") delete(body, "tool_choice") delete(body, "parallel_tool_calls") } else { body["tools"] = kept } return compat } // respConvertCustomTool 把 custom(自由格式)工具改写为 function:补 input 包装 // schema(custom 工具本无 parameters),模型以 {"input": 文本} 形态调用,响应侧 // 按 CustomNames 回转;custom 专有的 format(语法约束)字段一并移除。 func respConvertCustomTool(tool map[string]any) { tool["type"] = "function" delete(tool, "format") if _, ok := tool["parameters"]; !ok { tool["parameters"] = map[string]any{ "type": "object", "properties": map[string]any{"input": map[string]any{"type": "string"}}, "required": []any{"input"}, } } } // respFlattenNsTool 把 namespace 组内子工具上提为顶层工具:限定改名、缺 // parameters 补空 schema(上游必填),映射写入 NsRefs 供响应侧还原;非 // function 子工具(如 custom)上游同样不认,剥离并记录。 func respFlattenNsTool(ns string, tool map[string]any, compat *RespCompat) []any { children, _ := tool["tools"].([]any) out := make([]any, 0, len(children)) for _, c := range children { child, _ := c.(map[string]any) if child == nil { continue } short, _ := child["name"].(string) if t, _ := child["type"].(string); t != "" && t != "function" { compat.Dropped = append(compat.Dropped, ns+"."+short+"(type="+t+")") continue } qualified := respQualifyNsName(ns, short) if qualified == "" { continue } child["name"] = qualified if _, ok := child["parameters"]; !ok { child["parameters"] = map[string]any{"type": "object", "properties": map[string]any{}} } compat.NsRefs[qualified] = RespNsRef{Namespace: ns, Name: short} out = append(out, child) } return out } // respQualifyNsName 生成拍平后的限定名,与响应侧还原互逆(对齐 CLIProxyAPI): // mcp__ 开头的子工具名自带全局前缀不再限定;ns 以 __ 结尾直接拼接;已带前缀 // 不重复添加。 func respQualifyNsName(ns, name string) string { ns, name = strings.TrimSpace(ns), strings.TrimSpace(name) if ns == "" || name == "" || strings.HasPrefix(name, "mcp__") { return name } prefix := ns if !strings.HasSuffix(prefix, "__") { prefix += "__" } if strings.HasPrefix(name, prefix) { return name } return prefix + name } // respCompatInputCalls 改写多轮历史 input:function_call 的 namespace 字段重限定 // (上游不认识该字段);custom_tool_call / custom_tool_call_output 转换为上游 // 认识的 function_call(_output) 形态(上游 item 变体表不含 custom 系)。 func respCompatInputCalls(body map[string]any) { input, _ := body["input"].([]any) for _, it := range input { item, _ := it.(map[string]any) if item == nil { continue } switch item["type"] { case "function_call": respQualifyCallField(item) case "custom_tool_call": item["type"] = "function_call" item["arguments"] = respCustomCallArguments(item["input"]) delete(item, "input") case "custom_tool_call_output": item["type"] = "function_call_output" item["output"] = respCustomCallOutput(item["output"]) } } } // respCustomCallArguments 把 custom_tool_call 的 input 包装为 function_call 的 // arguments JSON 串(对齐 CLIProxyAPI):input 串本身是 JSON 对象则直用,普通 // 文本包一层 {"input": 文本};对象取序列化;缺失为 {}。与响应侧 // respUnwrapCustomInput 互逆,保证多轮往返无损。 func respCustomCallArguments(input any) string { switch v := input.(type) { case string: trimmed := strings.TrimSpace(v) var probe map[string]any if json.Unmarshal([]byte(trimmed), &probe) == nil && probe != nil { return trimmed } enc, err := json.Marshal(v) if err != nil { return "{}" } return `{"input":` + string(enc) + `}` case nil: return "{}" default: enc, err := json.Marshal(v) if err != nil { return "{}" } return string(enc) } } // respCustomCallOutput 归一 output 为字符串:非字符串时序列化保底。 func respCustomCallOutput(output any) any { if _, ok := output.(string); ok { return output } if output == nil { return "" } enc, err := json.Marshal(output) if err != nil { return "" } return string(enc) } // respCompatToolChoice 限定化对象形态的 tool_choice 及其 allowed_tools 列表 // (上游不认 tool_choice 上的 namespace 字段)。 func respCompatToolChoice(body map[string]any) { tc, _ := body["tool_choice"].(map[string]any) if tc == nil { return } if tc["type"] == "function" { respQualifyCallField(tc) } list, _ := tc["tools"].([]any) for _, it := range list { if sub, _ := it.(map[string]any); sub != nil && sub["type"] == "function" { respQualifyCallField(sub) } } } // respQualifyCallField 把一个带 name/namespace 的对象改写为限定名形态。 func respQualifyCallField(m map[string]any) { ns, _ := m["namespace"].(string) if strings.TrimSpace(ns) == "" { return } name, _ := m["name"].(string) if q := respQualifyNsName(ns, name); q != "" { m["name"] = q } delete(m, "namespace") } // respToolDrop 判定单个工具是否剥离。web_search 的 external_web_access 参数 // 上游 400:false 表示 OpenAI 的"仅缓存检索"降权模式,上游无对应能力,按不 // 越权原则整个工具剥离;true 等价上游默认行为,仅删键放行。tool_search(codex // 的工具目录搜索)上游不识别,且 namespace 已全量拍平上送、搜索语义冗余,剥离。 func respToolDrop(tool map[string]any) (string, bool) { if tool == nil { return "", false } switch tool["type"] { case "tool_search": return "tool_search", true case "web_search": access, has := tool["external_web_access"] if !has { return "", false } delete(tool, "external_web_access") if access == false { return "web_search(external_web_access=false)", true } } return "", false } // RespGuardBytes 返回请求体中 instructions 与 tools 两字段的原始字节数之和, // 流式保险丝据此判定。上游对二者合计 >≈64.5KB 的流式请求会在推理阶段静默断流 // (纯 EOF,input 正文不计入;2026-07-16 实测仍存在),阈值由设置页 AI Tab 配置。 // 解析失败返回 0(放行,交由上游正常报错)。 func RespGuardBytes(body []byte) int { var probe struct { Instructions json.RawMessage `json:"instructions"` Tools json.RawMessage `json:"tools"` } if err := json.Unmarshal(body, &probe); err != nil { return 0 } return len(probe.Instructions) + len(probe.Tools) } // RespInjectGrokTools 为 xai. 前缀模型默认注入服务端搜索工具:开关开启且请求 // tools 中不存在同名工具时追加 {"type":"web_search"} / {"type":"x_search"}; // 已存在(含任意参数形态)不覆盖。返回改写后 body 与注入清单(观测日志用); // 模型不匹配、两开关全关或解析失败时原样返回。 func RespInjectGrokTools(body []byte, model string, web, x bool) ([]byte, []string) { if !strings.HasPrefix(model, "xai.") || (!web && !x) { return body, nil } dec := json.NewDecoder(strings.NewReader(string(body))) dec.UseNumber() var m map[string]any if err := dec.Decode(&m); err != nil { return body, nil } tools, _ := m["tools"].([]any) missing := map[string]bool{"web_search": web, "x_search": x} for _, t := range tools { tool, ok := t.(map[string]any) if !ok { continue } if typ, _ := tool["type"].(string); missing[typ] { missing[typ] = false } } var injected []string for _, typ := range []string{"web_search", "x_search"} { if missing[typ] { tools = append(tools, map[string]any{"type": typ}) injected = append(injected, typ) } } if len(injected) == 0 { return body, nil } m["tools"] = tools out, err := json.Marshal(m) if err != nil { return body, nil } return out, injected } // RespDisableStream 把请求体的 stream 改为 false(流式升级回退用),其余字段 // 原样保留。 func RespDisableStream(body []byte) ([]byte, error) { dec := json.NewDecoder(strings.NewReader(string(body))) dec.UseNumber() var m map[string]any if err := dec.Decode(&m); err != nil { return nil, fmt.Errorf("解析请求体: %w", err) } m["stream"] = false return json.Marshal(m) } // RespSynthSSEEvents 把一份非流式响应合成为最小 SSE 事件序列:created → // 每个输出项一条 output_item.done → completed。流式升级回退用,客户端拿到 // 完整事件语义但无增量;输出项与 usage 原样承载。 func RespSynthSSEEvents(payload []byte) ([][]byte, error) { dec := json.NewDecoder(strings.NewReader(string(payload))) dec.UseNumber() var resp map[string]any if err := dec.Decode(&resp); err != nil { return nil, fmt.Errorf("解析上游响应: %w", err) } output, _ := resp["output"].([]any) status, hadStatus := resp["status"] resp["status"], resp["output"] = "in_progress", []any{} created, err := json.Marshal(map[string]any{"type": "response.created", "response": resp}) if err != nil { return nil, err } if hadStatus { resp["status"] = status } else { delete(resp, "status") } resp["output"] = output events := [][]byte{created} for i, item := range output { ev, err := json.Marshal(map[string]any{"type": "response.output_item.done", "output_index": i, "item": item}) if err != nil { return nil, err } events = append(events, ev) } completed, err := json.Marshal(map[string]any{"type": "response.completed", "response": resp}) if err != nil { return nil, err } return append(events, completed), nil } // RespRestoreToolCalls 非流式响应还原:output 数组中命中 NsRefs 的 function_call // 把限定名还原为短名并补回 namespace 字段(codex 以该双字段路由);命中 // CustomNames 的回转 custom_tool_call(codex 期望 input 字段形态)。无需还原或 // 解析失败时返回原字节,不破坏直通。 func RespRestoreToolCalls(payload []byte, compat RespCompat) []byte { if !compat.NeedRestore() { return payload } dec := json.NewDecoder(strings.NewReader(string(payload))) dec.UseNumber() var body map[string]any if dec.Decode(&body) != nil { return payload } if !respRestoreOutput(body["output"], compat) { return payload } out, err := json.Marshal(body) if err != nil { return payload } return out } // RespRestoreToolCallsEvent 流式单事件还原:处理事件顶层 item(output_item.*) // 与 response.output(created/completed 等快照)。返回还原后数据与是否改动; // 未改动时调用方转发原始行,保持字节级直通。 func RespRestoreToolCallsEvent(data []byte, compat RespCompat) ([]byte, bool) { if !compat.NeedRestore() { return data, false } dec := json.NewDecoder(strings.NewReader(string(data))) dec.UseNumber() var ev map[string]any if dec.Decode(&ev) != nil { return data, false } changed := respRestoreCallItem(ev["item"], compat) if resp, _ := ev["response"].(map[string]any); resp != nil { changed = respRestoreOutput(resp["output"], compat) || changed } if !changed { return data, false } out, err := json.Marshal(ev) if err != nil { return data, false } return out, true } // respRestoreOutput 还原一个 output 数组,返回是否有改动。 func respRestoreOutput(v any, compat RespCompat) bool { items, _ := v.([]any) changed := false for _, it := range items { if respRestoreCallItem(it, compat) { changed = true } } return changed } // respRestoreCallItem 还原单个 function_call 项,返回是否改动。namespace 还原与 // custom 回转互斥:custom 工具不参与 namespace 拍平。 func respRestoreCallItem(v any, compat RespCompat) bool { item, _ := v.(map[string]any) if item == nil || item["type"] != "function_call" { return false } name, _ := item["name"].(string) if ref, ok := compat.NsRefs[name]; ok { item["name"] = ref.Name item["namespace"] = ref.Namespace return true } if _, ok := compat.CustomNames[name]; ok { item["type"] = "custom_tool_call" item["input"] = respUnwrapCustomInput(item["arguments"]) delete(item, "arguments") return true } return false } // respUnwrapCustomInput 解包 arguments:形如 {"input": 串} 取其 input(非串取该 // 值原文),其余情况整串返回。与请求侧 respCustomCallArguments 互逆。 func respUnwrapCustomInput(arguments any) string { s, _ := arguments.(string) var probe map[string]json.RawMessage if json.Unmarshal([]byte(s), &probe) == nil { if raw, ok := probe["input"]; ok { var str string if json.Unmarshal(raw, &str) == nil { return str } return string(raw) } } return s } // RespPassthroughUsage 从直通响应提取用量;缺失时返回 nil(日志记零)。 func RespPassthroughUsage(payload []byte) *aiwire.Usage { var root struct { Usage *aiwire.RespUsage `json:"usage"` } if json.Unmarshal(payload, &root) != nil { return nil } return usageFromResp(root.Usage) } // usageFromResp 换算 Responses usage 为 OpenAI 口径(缓存命中透传);nil 原样返回。 func usageFromResp(u *aiwire.RespUsage) *aiwire.Usage { if u == nil { return nil } usage := &aiwire.Usage{PromptTokens: u.InputTokens, CompletionTokens: u.OutputTokens, TotalTokens: u.TotalTokens} if cached := u.InputTokensDetails.CachedTokens; cached > 0 { usage.PromptTokensDetails = &aiwire.PromptTokensDetails{CachedTokens: cached} } return usage } // RespStreamCompletedUsage 从一行 SSE data JSON 中提取 response.completed 事件的 // usage;非 completed 事件或解析失败返回 nil。流式直通逐行喂入,最后一次非 nil 生效。 func RespStreamCompletedUsage(data []byte) *aiwire.Usage { var ev struct { Type string `json:"type"` Response json.RawMessage `json:"response"` } if json.Unmarshal(data, &ev) != nil || ev.Type != "response.completed" || len(ev.Response) == 0 { return nil } return RespPassthroughUsage(ev.Response) } // RespStreamErrorMsg 从一行 SSE data JSON 中提取 error / response.failed 事件的 // 错误消息;非错误事件返回空串。流式直通据此把上游错误写入调用日志。 func RespStreamErrorMsg(data []byte) string { var ev respStreamEvent if json.Unmarshal(data, &ev) != nil { return "" } switch ev.Type { case "error": if ev.Message != "" { return ev.Message } return "上游返回错误事件 error" case "response.failed": if m := respErrorMsg(ev.Response); m != "" { return m } return "上游返回错误事件 response.failed" } return "" }