2026-01-09 18:35:58 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-01-13 11:14:32 +08:00
|
|
|
|
_ "embed"
|
2026-01-09 18:35:58 +08:00
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
2026-01-10 20:53:16 +08:00
|
|
|
|
opencodeCodexHeaderURL = "https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/opencode/src/session/prompt/codex_header.txt"
|
|
|
|
|
|
codexCacheTTL = 15 * time.Minute
|
2026-01-09 18:35:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-13 11:14:32 +08:00
|
|
|
|
//go:embed prompts/codex_cli_instructions.md
|
|
|
|
|
|
var codexCLIInstructions string
|
|
|
|
|
|
|
2026-01-09 18:35:58 +08:00
|
|
|
|
var codexModelMap = map[string]string{
|
2026-02-06 07:14:46 +08:00
|
|
|
|
"gpt-5.3": "gpt-5.3",
|
|
|
|
|
|
"gpt-5.3-none": "gpt-5.3",
|
|
|
|
|
|
"gpt-5.3-low": "gpt-5.3",
|
|
|
|
|
|
"gpt-5.3-medium": "gpt-5.3",
|
|
|
|
|
|
"gpt-5.3-high": "gpt-5.3",
|
|
|
|
|
|
"gpt-5.3-xhigh": "gpt-5.3",
|
|
|
|
|
|
"gpt-5.3-codex": "gpt-5.3-codex",
|
|
|
|
|
|
"gpt-5.3-codex-low": "gpt-5.3-codex",
|
|
|
|
|
|
"gpt-5.3-codex-medium": "gpt-5.3-codex",
|
|
|
|
|
|
"gpt-5.3-codex-high": "gpt-5.3-codex",
|
|
|
|
|
|
"gpt-5.3-codex-xhigh": "gpt-5.3-codex",
|
2026-01-09 18:35:58 +08:00
|
|
|
|
"gpt-5.1-codex": "gpt-5.1-codex",
|
|
|
|
|
|
"gpt-5.1-codex-low": "gpt-5.1-codex",
|
|
|
|
|
|
"gpt-5.1-codex-medium": "gpt-5.1-codex",
|
|
|
|
|
|
"gpt-5.1-codex-high": "gpt-5.1-codex",
|
|
|
|
|
|
"gpt-5.1-codex-max": "gpt-5.1-codex-max",
|
|
|
|
|
|
"gpt-5.1-codex-max-low": "gpt-5.1-codex-max",
|
|
|
|
|
|
"gpt-5.1-codex-max-medium": "gpt-5.1-codex-max",
|
|
|
|
|
|
"gpt-5.1-codex-max-high": "gpt-5.1-codex-max",
|
|
|
|
|
|
"gpt-5.1-codex-max-xhigh": "gpt-5.1-codex-max",
|
|
|
|
|
|
"gpt-5.2": "gpt-5.2",
|
|
|
|
|
|
"gpt-5.2-none": "gpt-5.2",
|
|
|
|
|
|
"gpt-5.2-low": "gpt-5.2",
|
|
|
|
|
|
"gpt-5.2-medium": "gpt-5.2",
|
|
|
|
|
|
"gpt-5.2-high": "gpt-5.2",
|
|
|
|
|
|
"gpt-5.2-xhigh": "gpt-5.2",
|
|
|
|
|
|
"gpt-5.2-codex": "gpt-5.2-codex",
|
|
|
|
|
|
"gpt-5.2-codex-low": "gpt-5.2-codex",
|
|
|
|
|
|
"gpt-5.2-codex-medium": "gpt-5.2-codex",
|
|
|
|
|
|
"gpt-5.2-codex-high": "gpt-5.2-codex",
|
|
|
|
|
|
"gpt-5.2-codex-xhigh": "gpt-5.2-codex",
|
|
|
|
|
|
"gpt-5.1-codex-mini": "gpt-5.1-codex-mini",
|
|
|
|
|
|
"gpt-5.1-codex-mini-medium": "gpt-5.1-codex-mini",
|
|
|
|
|
|
"gpt-5.1-codex-mini-high": "gpt-5.1-codex-mini",
|
|
|
|
|
|
"gpt-5.1": "gpt-5.1",
|
|
|
|
|
|
"gpt-5.1-none": "gpt-5.1",
|
|
|
|
|
|
"gpt-5.1-low": "gpt-5.1",
|
|
|
|
|
|
"gpt-5.1-medium": "gpt-5.1",
|
|
|
|
|
|
"gpt-5.1-high": "gpt-5.1",
|
|
|
|
|
|
"gpt-5.1-chat-latest": "gpt-5.1",
|
|
|
|
|
|
"gpt-5-codex": "gpt-5.1-codex",
|
|
|
|
|
|
"codex-mini-latest": "gpt-5.1-codex-mini",
|
|
|
|
|
|
"gpt-5-codex-mini": "gpt-5.1-codex-mini",
|
|
|
|
|
|
"gpt-5-codex-mini-medium": "gpt-5.1-codex-mini",
|
|
|
|
|
|
"gpt-5-codex-mini-high": "gpt-5.1-codex-mini",
|
|
|
|
|
|
"gpt-5": "gpt-5.1",
|
|
|
|
|
|
"gpt-5-mini": "gpt-5.1",
|
|
|
|
|
|
"gpt-5-nano": "gpt-5.1",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type codexTransformResult struct {
|
|
|
|
|
|
Modified bool
|
|
|
|
|
|
NormalizedModel string
|
|
|
|
|
|
PromptCacheKey string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type opencodeCacheMetadata struct {
|
|
|
|
|
|
ETag string `json:"etag"`
|
|
|
|
|
|
LastFetch string `json:"lastFetch,omitempty"`
|
|
|
|
|
|
LastChecked int64 `json:"lastChecked"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 21:22:33 +08:00
|
|
|
|
func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool) codexTransformResult {
|
2026-01-09 18:35:58 +08:00
|
|
|
|
result := codexTransformResult{}
|
2026-01-13 16:47:35 +08:00
|
|
|
|
// 工具续链需求会影响存储策略与 input 过滤逻辑。
|
|
|
|
|
|
needsToolContinuation := NeedsToolContinuation(reqBody)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
|
|
|
|
|
|
model := ""
|
|
|
|
|
|
if v, ok := reqBody["model"].(string); ok {
|
|
|
|
|
|
model = v
|
|
|
|
|
|
}
|
|
|
|
|
|
normalizedModel := normalizeCodexModel(model)
|
|
|
|
|
|
if normalizedModel != "" {
|
|
|
|
|
|
if model != normalizedModel {
|
|
|
|
|
|
reqBody["model"] = normalizedModel
|
|
|
|
|
|
result.Modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
result.NormalizedModel = normalizedModel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 09:17:58 +08:00
|
|
|
|
// OAuth 走 ChatGPT internal API 时,store 必须为 false;显式 true 也会强制覆盖。
|
|
|
|
|
|
// 避免上游返回 "Store must be set to false"。
|
2026-01-14 09:46:10 +08:00
|
|
|
|
if v, ok := reqBody["store"].(bool); !ok || v {
|
2026-01-14 09:17:58 +08:00
|
|
|
|
reqBody["store"] = false
|
|
|
|
|
|
result.Modified = true
|
2026-01-10 03:12:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
if v, ok := reqBody["stream"].(bool); !ok || !v {
|
|
|
|
|
|
reqBody["stream"] = true
|
|
|
|
|
|
result.Modified = true
|
2026-01-09 18:35:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if _, ok := reqBody["max_output_tokens"]; ok {
|
|
|
|
|
|
delete(reqBody, "max_output_tokens")
|
|
|
|
|
|
result.Modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := reqBody["max_completion_tokens"]; ok {
|
|
|
|
|
|
delete(reqBody, "max_completion_tokens")
|
|
|
|
|
|
result.Modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if normalizeCodexTools(reqBody) {
|
|
|
|
|
|
result.Modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if v, ok := reqBody["prompt_cache_key"].(string); ok {
|
|
|
|
|
|
result.PromptCacheKey = strings.TrimSpace(v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 21:22:33 +08:00
|
|
|
|
// instructions 处理逻辑:根据是否是 Codex CLI 分别调用不同方法
|
|
|
|
|
|
if applyInstructions(reqBody, isCodexCLI) {
|
|
|
|
|
|
result.Modified = true
|
2026-01-09 18:35:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 16:47:35 +08:00
|
|
|
|
// 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。
|
2026-01-10 03:12:56 +08:00
|
|
|
|
if input, ok := reqBody["input"].([]any); ok {
|
2026-01-13 16:47:35 +08:00
|
|
|
|
input = filterCodexInput(input, needsToolContinuation)
|
2026-01-10 03:12:56 +08:00
|
|
|
|
reqBody["input"] = input
|
2026-01-09 18:35:58 +08:00
|
|
|
|
result.Modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func normalizeCodexModel(model string) string {
|
|
|
|
|
|
if model == "" {
|
|
|
|
|
|
return "gpt-5.1"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modelID := model
|
|
|
|
|
|
if strings.Contains(modelID, "/") {
|
|
|
|
|
|
parts := strings.Split(modelID, "/")
|
|
|
|
|
|
modelID = parts[len(parts)-1]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if mapped := getNormalizedCodexModel(modelID); mapped != "" {
|
|
|
|
|
|
return mapped
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
normalized := strings.ToLower(modelID)
|
|
|
|
|
|
|
|
|
|
|
|
if strings.Contains(normalized, "gpt-5.2-codex") || strings.Contains(normalized, "gpt 5.2 codex") {
|
|
|
|
|
|
return "gpt-5.2-codex"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "gpt-5.2") || strings.Contains(normalized, "gpt 5.2") {
|
|
|
|
|
|
return "gpt-5.2"
|
|
|
|
|
|
}
|
2026-02-06 07:14:46 +08:00
|
|
|
|
if strings.Contains(normalized, "gpt-5.3-codex") || strings.Contains(normalized, "gpt 5.3 codex") {
|
|
|
|
|
|
return "gpt-5.3-codex"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "gpt-5.3") || strings.Contains(normalized, "gpt 5.3") {
|
|
|
|
|
|
return "gpt-5.3"
|
|
|
|
|
|
}
|
2026-01-09 18:35:58 +08:00
|
|
|
|
if strings.Contains(normalized, "gpt-5.1-codex-max") || strings.Contains(normalized, "gpt 5.1 codex max") {
|
|
|
|
|
|
return "gpt-5.1-codex-max"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "gpt-5.1-codex-mini") || strings.Contains(normalized, "gpt 5.1 codex mini") {
|
|
|
|
|
|
return "gpt-5.1-codex-mini"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "codex-mini-latest") ||
|
|
|
|
|
|
strings.Contains(normalized, "gpt-5-codex-mini") ||
|
|
|
|
|
|
strings.Contains(normalized, "gpt 5 codex mini") {
|
|
|
|
|
|
return "codex-mini-latest"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "gpt-5.1-codex") || strings.Contains(normalized, "gpt 5.1 codex") {
|
|
|
|
|
|
return "gpt-5.1-codex"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "gpt-5.1") || strings.Contains(normalized, "gpt 5.1") {
|
|
|
|
|
|
return "gpt-5.1"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "codex") {
|
|
|
|
|
|
return "gpt-5.1-codex"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(normalized, "gpt-5") || strings.Contains(normalized, "gpt 5") {
|
|
|
|
|
|
return "gpt-5.1"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return "gpt-5.1"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getNormalizedCodexModel(modelID string) string {
|
|
|
|
|
|
if modelID == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
if mapped, ok := codexModelMap[modelID]; ok {
|
|
|
|
|
|
return mapped
|
|
|
|
|
|
}
|
|
|
|
|
|
lower := strings.ToLower(modelID)
|
|
|
|
|
|
for key, value := range codexModelMap {
|
|
|
|
|
|
if strings.ToLower(key) == lower {
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 20:53:16 +08:00
|
|
|
|
func getOpenCodeCachedPrompt(url, cacheFileName, metaFileName string) string {
|
2026-01-09 18:35:58 +08:00
|
|
|
|
cacheDir := codexCachePath("")
|
|
|
|
|
|
if cacheDir == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
2026-01-10 20:53:16 +08:00
|
|
|
|
cacheFile := filepath.Join(cacheDir, cacheFileName)
|
|
|
|
|
|
metaFile := filepath.Join(cacheDir, metaFileName)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
|
|
|
|
|
|
var cachedContent string
|
|
|
|
|
|
if content, ok := readFile(cacheFile); ok {
|
|
|
|
|
|
cachedContent = content
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var meta opencodeCacheMetadata
|
|
|
|
|
|
if loadJSON(metaFile, &meta) && meta.LastChecked > 0 && cachedContent != "" {
|
|
|
|
|
|
if time.Since(time.UnixMilli(meta.LastChecked)) < codexCacheTTL {
|
|
|
|
|
|
return cachedContent
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 20:53:16 +08:00
|
|
|
|
content, etag, status, err := fetchWithETag(url, meta.ETag)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
if err == nil && status == http.StatusNotModified && cachedContent != "" {
|
|
|
|
|
|
return cachedContent
|
|
|
|
|
|
}
|
|
|
|
|
|
if err == nil && status >= 200 && status < 300 && content != "" {
|
|
|
|
|
|
_ = writeFile(cacheFile, content)
|
|
|
|
|
|
meta = opencodeCacheMetadata{
|
|
|
|
|
|
ETag: etag,
|
|
|
|
|
|
LastFetch: time.Now().UTC().Format(time.RFC3339),
|
|
|
|
|
|
LastChecked: time.Now().UnixMilli(),
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = writeJSON(metaFile, meta)
|
|
|
|
|
|
return content
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cachedContent
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 20:53:16 +08:00
|
|
|
|
func getOpenCodeCodexHeader() string {
|
2026-01-13 17:01:21 +08:00
|
|
|
|
// 优先从 opencode 仓库缓存获取指令。
|
2026-01-13 11:14:32 +08:00
|
|
|
|
opencodeInstructions := getOpenCodeCachedPrompt(opencodeCodexHeaderURL, "opencode-codex-header.txt", "opencode-codex-header-meta.json")
|
|
|
|
|
|
|
2026-01-13 17:01:21 +08:00
|
|
|
|
// 若 opencode 指令可用,直接返回。
|
2026-01-13 11:14:32 +08:00
|
|
|
|
if opencodeInstructions != "" {
|
|
|
|
|
|
return opencodeInstructions
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 17:01:21 +08:00
|
|
|
|
// 否则回退使用本地 Codex CLI 指令。
|
2026-01-13 11:14:32 +08:00
|
|
|
|
return getCodexCLIInstructions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getCodexCLIInstructions() string {
|
|
|
|
|
|
return codexCLIInstructions
|
2026-01-10 20:53:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 21:57:57 +08:00
|
|
|
|
func GetOpenCodeInstructions() string {
|
|
|
|
|
|
return getOpenCodeCodexHeader()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 17:01:21 +08:00
|
|
|
|
// GetCodexCLIInstructions 返回内置的 Codex CLI 指令内容。
|
2026-01-13 11:14:32 +08:00
|
|
|
|
func GetCodexCLIInstructions() string {
|
|
|
|
|
|
return getCodexCLIInstructions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 21:22:33 +08:00
|
|
|
|
// applyInstructions 处理 instructions 字段
|
|
|
|
|
|
// isCodexCLI=true: 仅补充缺失的 instructions(使用 opencode 指令)
|
|
|
|
|
|
// isCodexCLI=false: 优先使用 opencode 指令覆盖
|
|
|
|
|
|
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
|
|
|
|
|
if isCodexCLI {
|
|
|
|
|
|
return applyCodexCLIInstructions(reqBody)
|
|
|
|
|
|
}
|
|
|
|
|
|
return applyOpenCodeInstructions(reqBody)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// applyCodexCLIInstructions 为 Codex CLI 请求补充缺失的 instructions
|
|
|
|
|
|
// 仅在 instructions 为空时添加 opencode 指令
|
|
|
|
|
|
func applyCodexCLIInstructions(reqBody map[string]any) bool {
|
|
|
|
|
|
if !isInstructionsEmpty(reqBody) {
|
|
|
|
|
|
return false // 已有有效 instructions,不修改
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
|
|
|
|
|
if instructions != "" {
|
|
|
|
|
|
reqBody["instructions"] = instructions
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// applyOpenCodeInstructions 为非 Codex CLI 请求应用 opencode 指令
|
|
|
|
|
|
// 优先使用 opencode 指令覆盖
|
|
|
|
|
|
func applyOpenCodeInstructions(reqBody map[string]any) bool {
|
|
|
|
|
|
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
|
|
|
|
|
existingInstructions, _ := reqBody["instructions"].(string)
|
|
|
|
|
|
existingInstructions = strings.TrimSpace(existingInstructions)
|
|
|
|
|
|
|
|
|
|
|
|
if instructions != "" {
|
|
|
|
|
|
if existingInstructions != instructions {
|
|
|
|
|
|
reqBody["instructions"] = instructions
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if existingInstructions == "" {
|
|
|
|
|
|
codexInstructions := strings.TrimSpace(getCodexCLIInstructions())
|
|
|
|
|
|
if codexInstructions != "" {
|
|
|
|
|
|
reqBody["instructions"] = codexInstructions
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// isInstructionsEmpty 检查 instructions 字段是否为空
|
|
|
|
|
|
// 处理以下情况:字段不存在、nil、空字符串、纯空白字符串
|
|
|
|
|
|
func isInstructionsEmpty(reqBody map[string]any) bool {
|
|
|
|
|
|
val, exists := reqBody["instructions"]
|
|
|
|
|
|
if !exists {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if val == nil {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
str, ok := val.(string)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.TrimSpace(str) == ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 16:47:35 +08:00
|
|
|
|
// filterCodexInput 按需过滤 item_reference 与 id。
|
|
|
|
|
|
// preserveReferences 为 true 时保持引用与 id,以满足续链请求对上下文的依赖。
|
|
|
|
|
|
func filterCodexInput(input []any, preserveReferences bool) []any {
|
2026-01-09 18:35:58 +08:00
|
|
|
|
filtered := make([]any, 0, len(input))
|
|
|
|
|
|
for _, item := range input {
|
|
|
|
|
|
m, ok := item.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
filtered = append(filtered, item)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-01-12 20:18:53 -08:00
|
|
|
|
typ, _ := m["type"].(string)
|
|
|
|
|
|
if typ == "item_reference" {
|
2026-01-13 16:47:35 +08:00
|
|
|
|
if !preserveReferences {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-01-13 17:01:21 +08:00
|
|
|
|
newItem := make(map[string]any, len(m))
|
|
|
|
|
|
for key, value := range m {
|
|
|
|
|
|
newItem[key] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
filtered = append(filtered, newItem)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
continue
|
2026-01-13 16:47:35 +08:00
|
|
|
|
}
|
2026-01-13 17:01:21 +08:00
|
|
|
|
|
2026-01-13 16:47:35 +08:00
|
|
|
|
newItem := m
|
2026-01-13 17:01:21 +08:00
|
|
|
|
copied := false
|
|
|
|
|
|
// 仅在需要修改字段时创建副本,避免直接改写原始输入。
|
|
|
|
|
|
ensureCopy := func() {
|
|
|
|
|
|
if copied {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-01-13 16:47:35 +08:00
|
|
|
|
newItem = make(map[string]any, len(m))
|
|
|
|
|
|
for key, value := range m {
|
|
|
|
|
|
newItem[key] = value
|
|
|
|
|
|
}
|
2026-01-13 17:01:21 +08:00
|
|
|
|
copied = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 20:18:53 -08:00
|
|
|
|
if isCodexToolCallItemType(typ) {
|
2026-01-13 17:01:21 +08:00
|
|
|
|
if callID, ok := m["call_id"].(string); !ok || strings.TrimSpace(callID) == "" {
|
2026-01-12 20:18:53 -08:00
|
|
|
|
if id, ok := m["id"].(string); ok && strings.TrimSpace(id) != "" {
|
2026-01-13 17:01:21 +08:00
|
|
|
|
ensureCopy()
|
|
|
|
|
|
newItem["call_id"] = id
|
2026-01-12 20:18:53 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-13 17:01:21 +08:00
|
|
|
|
|
|
|
|
|
|
if !preserveReferences {
|
|
|
|
|
|
ensureCopy()
|
2026-01-13 16:47:35 +08:00
|
|
|
|
delete(newItem, "id")
|
2026-01-13 17:01:21 +08:00
|
|
|
|
if !isCodexToolCallItemType(typ) {
|
|
|
|
|
|
delete(newItem, "call_id")
|
|
|
|
|
|
}
|
2026-01-09 18:35:58 +08:00
|
|
|
|
}
|
2026-01-13 17:01:21 +08:00
|
|
|
|
|
2026-01-13 16:47:35 +08:00
|
|
|
|
filtered = append(filtered, newItem)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
return filtered
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 20:18:53 -08:00
|
|
|
|
func isCodexToolCallItemType(typ string) bool {
|
|
|
|
|
|
if typ == "" {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.HasSuffix(typ, "_call") || strings.HasSuffix(typ, "_call_output")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 18:35:58 +08:00
|
|
|
|
func normalizeCodexTools(reqBody map[string]any) bool {
|
|
|
|
|
|
rawTools, ok := reqBody["tools"]
|
|
|
|
|
|
if !ok || rawTools == nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
tools, ok := rawTools.([]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modified := false
|
2026-01-17 11:00:07 +08:00
|
|
|
|
validTools := make([]any, 0, len(tools))
|
|
|
|
|
|
|
|
|
|
|
|
for _, tool := range tools {
|
2026-01-09 18:35:58 +08:00
|
|
|
|
toolMap, ok := tool.(map[string]any)
|
|
|
|
|
|
if !ok {
|
2026-01-17 11:00:07 +08:00
|
|
|
|
// Keep unknown structure as-is to avoid breaking upstream behavior.
|
|
|
|
|
|
validTools = append(validTools, tool)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toolType, _ := toolMap["type"].(string)
|
2026-01-17 11:00:07 +08:00
|
|
|
|
toolType = strings.TrimSpace(toolType)
|
|
|
|
|
|
if toolType != "function" {
|
|
|
|
|
|
validTools = append(validTools, toolMap)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 11:00:07 +08:00
|
|
|
|
// OpenAI Responses-style tools use top-level name/parameters.
|
|
|
|
|
|
if name, ok := toolMap["name"].(string); ok && strings.TrimSpace(name) != "" {
|
|
|
|
|
|
validTools = append(validTools, toolMap)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ChatCompletions-style tools use {type:"function", function:{...}}.
|
|
|
|
|
|
functionValue, hasFunction := toolMap["function"]
|
|
|
|
|
|
function, ok := functionValue.(map[string]any)
|
|
|
|
|
|
if !hasFunction || functionValue == nil || !ok || function == nil {
|
|
|
|
|
|
// Drop invalid function tools.
|
|
|
|
|
|
modified = true
|
2026-01-09 18:35:58 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if _, ok := toolMap["name"]; !ok {
|
|
|
|
|
|
if name, ok := function["name"].(string); ok && strings.TrimSpace(name) != "" {
|
|
|
|
|
|
toolMap["name"] = name
|
|
|
|
|
|
modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := toolMap["description"]; !ok {
|
|
|
|
|
|
if desc, ok := function["description"].(string); ok && strings.TrimSpace(desc) != "" {
|
|
|
|
|
|
toolMap["description"] = desc
|
|
|
|
|
|
modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := toolMap["parameters"]; !ok {
|
|
|
|
|
|
if params, ok := function["parameters"]; ok {
|
|
|
|
|
|
toolMap["parameters"] = params
|
|
|
|
|
|
modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := toolMap["strict"]; !ok {
|
|
|
|
|
|
if strict, ok := function["strict"]; ok {
|
|
|
|
|
|
toolMap["strict"] = strict
|
|
|
|
|
|
modified = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 11:00:07 +08:00
|
|
|
|
validTools = append(validTools, toolMap)
|
2026-01-09 18:35:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if modified {
|
2026-01-17 11:00:07 +08:00
|
|
|
|
reqBody["tools"] = validTools
|
2026-01-09 18:35:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return modified
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func codexCachePath(filename string) string {
|
|
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
cacheDir := filepath.Join(home, ".opencode", "cache")
|
|
|
|
|
|
if filename == "" {
|
|
|
|
|
|
return cacheDir
|
|
|
|
|
|
}
|
|
|
|
|
|
return filepath.Join(cacheDir, filename)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func readFile(path string) (string, bool) {
|
|
|
|
|
|
if path == "" {
|
|
|
|
|
|
return "", false
|
|
|
|
|
|
}
|
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", false
|
|
|
|
|
|
}
|
|
|
|
|
|
return string(data), true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func writeFile(path, content string) error {
|
|
|
|
|
|
if path == "" {
|
|
|
|
|
|
return fmt.Errorf("empty cache path")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return os.WriteFile(path, []byte(content), 0o644)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func loadJSON(path string, target any) bool {
|
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := json.Unmarshal(data, target); err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func writeJSON(path string, value any) error {
|
|
|
|
|
|
if path == "" {
|
|
|
|
|
|
return fmt.Errorf("empty json path")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
data, err := json.Marshal(value)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return os.WriteFile(path, data, 0o644)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func fetchWithETag(url, etag string) (string, string, int, error) {
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
req.Header.Set("User-Agent", "sub2api-codex")
|
|
|
|
|
|
if etag != "" {
|
|
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
|
|
|
|
|
}
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", 0, err
|
|
|
|
|
|
}
|
2026-01-10 22:45:29 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
|
|
}()
|
2026-01-09 18:35:58 +08:00
|
|
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", resp.StatusCode, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return string(body), resp.Header.Get("etag"), resp.StatusCode, nil
|
|
|
|
|
|
}
|