Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
188 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "6kmfi6hp/opencode2api", "files": {"main.go": {"9e4bd061efd2e0f272585317dc6e508e26e64b4c": {"t": 1786068606.056282, "n": 169896, "h": "f54a88df456f810f", "c": "package main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n)\n\nvar httpClient = &http.Client{\n\tTimeout: 300 * time.Second,\n\tTransport: &http.Transport{\n\t\tMaxIdleConns: 100,\n\t\tMaxIdleConnsPerHost: 20,\n\t\tIdleConnTimeout: 90 * time.Second,\n\t},\n}\n\nvar (\n\tversion = \"v0.3.7\"\n\tcommit = \"none\"\n\tdate = \"unknown\"\n)\n\nfunc versionString() string {\n\treturn fmt.Sprintf(\"opencode2api %s (commit=%s, date=%s)\", version, commit, date)\n}\n\n// ======================== SOCKS5 代理 ========================\n\ntype Socks5Proxy struct {\n\tAddr string `json:\"addr\"`\n\tUsername string `json:\"username,omitempty\"`\n\tPassword string `json:\"password,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n}\n\nfunc socks5Dial(proxy Socks5Proxy) func(ctx context.Context, network, addr string) (net.Conn, error) {\n\treturn func(ctx context.Context, network, target string) (net.Conn, error) {\n\t\tconn, err := net.DialTimeout(\"tcp\", proxy.Addr, 10*time.Second)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"socks5 connect to %s: %w\", proxy.Addr, err)\n\t\t}\n\t\tdeadline := time.Now().Add(15 * time.Second)\n\t\tconn.SetDeadline(deadline)\n\n\t\t// 认证方法协商\n\t\tauth := byte(0x00) // no auth\n\t\tif proxy.Username != \"\" {\n\t\t\tauth = 0x02 // username/password\n\t\t}\n\t\tif _, err := conn.Write([]byte{0x05, 0x01, auth}); err != nil {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5 handshake write: %w\", err)\n\t\t}\n\t\tbuf := make([]byte, 2)\n\t\tif _, err := io.ReadFull(conn, buf); err != nil {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5 handshake read: %w\", err)\n\t\t}\n\t\tif buf[0] != 0x05 {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5: not socks5 protocol\")\n\t\t}\n\n\t\t// 用户名/密码认证\n\t\tif buf[1] == 0x02 {\n\t\t\tif proxy.Username == \"\" {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5: server requires auth but no credentials\")\n\t\t\t}\n\t\t\tulen := len(proxy.Username)\n\t\t\tplen := len(proxy.Password)\n\t\t\tauthBuf := make([]byte, 3+ulen+plen)\n\t\t\tauthBuf[0] = 0x01\n\t\t\tauthBuf[1] = byte(ulen)\n\t\t\tcopy(authBuf[2:], proxy.Username)\n\t\t\tauthBuf[2+ulen] = byte(plen)\n\t\t\tcopy(authBuf[3+ulen:], proxy.Password)\n\t\t\tif _, err := conn.Write(authBuf); err != nil {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5 auth write: %w\", err)\n\t\t\t}\n\t\t\tauthResp := make([]byte, 2)\n\t\t\tif _, err := io.ReadFull(conn, authResp); err != nil {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5 auth read: %w\", err)\n\t\t\t}\n\t\t\tif authResp[1] != 0x00 {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5: auth failed\")\n\t\t\t}\n\t\t} else if buf[1] != 0x00 {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5: unsupported auth method 0x%02x\", buf[1])\n\t\t}\n\n\t\t// CONNECT 请求\n\t\thost, portStr, err := net.SplitHostPort(target)\n\t\tif err != nil {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5: invalid target %s: %w\", target, err)\n\t\t}\n\t\tport := 0\n\t\tfmt.Sscanf(portStr, \"%d\", &port)\n\n\t\treq := []byte{0x05, 0x01, 0x00} // VER, CMD=CONNECT, RSV\n\t\tip := net.ParseIP(host)\n\t\tif ip != nil {\n\t\t\tif ip4 := ip.To4(); ip4 != nil {\n\t\t\t\treq = append(req, 0x01) // IPv4\n\t\t\t\treq = append(req, ip4...)\n\t\t\t} else {\n\t\t\t\treq = append(req, 0x04) // IPv6\n\t\t\t\treq = append(req, ip.To16()...)\n\t\t\t}\n\t\t} else {\n\t\t\tif len(host) > 255 {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5: hostname too long\")\n\t\t\t}\n\t\t\treq = append(req, 0x03) // Domain\n\t\t\treq = append(req, byte(len(host)))\n\t\t\treq = append(req, []byte(host)...)\n\t\t}\n\t\treq = append(req, byte(port>>8), byte(port))\n\n\t\tif _, err := conn.Write(req); err != nil {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5 connect write: %w\", err)\n\t\t}\n\n\t\t// 读取响应\n\t\tresp := make([]byte, 4)\n\t\tif _, err := io.ReadFull(conn, resp); err != nil {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5 connect read: %w\", err)\n\t\t}\n\t\tif resp[1] != 0x00 {\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5: connect failed, status 0x%02x\", resp[1])\n\t\t}\n\n\t\t// 读取绑定地址\n\t\tswitch resp[3] {\n\t\tcase 0x01: // IPv4\n\t\t\tif _, err := io.ReadFull(conn, make([]byte, 4+2)); err != nil {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5: read bind ipv4: %w\", err)\n\t\t\t}\n\t\tcase 0x03: // Domain\n\t\t\tdlen := make([]byte, 1)\n\t\t\tif _, err := io.ReadFull(conn, dlen); err != nil {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5: read bind domain len: %w\", err)\n\t\t\t}\n\t\t\tif _, err := io.ReadFull(conn, make([]byte, int(dlen[0])+2)); err != nil {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5: read bind domain: %w\", err)\n\t\t\t}\n\t\tcase 0x04: // IPv6\n\t\t\tif _, err := io.ReadFull(conn, make([]byte, 16+2)); err != nil {\n\t\t\t\tconn.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"socks5: read bind ipv6: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\tconn.Close()\n\t\t\treturn nil, fmt.Errorf(\"socks5: unknown address type 0x%02x\", resp[3])\n\t\t}\n\n\t\tconn.SetDeadline(time.Time{})\n\t\treturn conn, nil\n\t}\n}\n\nvar (\n\tsocks5Proxies []Socks5Proxy\n\tactiveSocks5 string // 启用的代理 Addr,空表示直连,__round_robin__ 表示轮询\n\tsocks5PaidDirect bool // true=带 key/付费直连;false/缺省=全部走代理\n\tsocks5Mu sync.RWMutex\n)\n\nconst socks5RR = \"__round_robin__\"\n\nvar socks5RRIndex uint32\n\nvar (\n\tsocks5Client *http.Client // 缓存的 SOCKS5 客户端\n\tsocks5ClientAddr string // 缓存对应的代理地址\n)\n\nfunc getHTTPClient() *http.Client {\n\tsocks5Mu.RLock()\n\tdefer socks5Mu.RUnlock()\n\n\tif activeSocks5 == \"\" {\n\t\treturn httpClient\n\t}\n\n\tvar proxy Socks5Proxy\n\tvar useRR bool\n\n\tif activeSocks5 == socks5RR {\n\t\tif len(socks5Proxies) == 0 {\n\t\t\treturn httpClient\n\t\t}\n\t\tidx := atomic.AddUint32(&socks5RRIndex, 1) % uint32(len(socks5Proxies))\n\t\tproxy = socks5Proxies[idx]\n\t\tuseRR = true\n\t} else {\n\t\tif socks5Client != nil && socks5ClientAddr == activeSocks5 {\n\t\t\treturn socks5Client\n\t\t}\n\n\t\tvar found bool\n\t\tfor i := range socks5Proxies {\n\t\t\tif socks5Proxies[i].Addr == activeSocks5 {\n\t\t\t\tproxy = socks5Proxies[i]\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn httpClient\n\t\t}\n\t}\n\n\tdial := socks5Dial(proxy)\n\tclient := &http.Client{\n\t\tTimeout: 300 * time.Second,\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: dial,\n\t\t\tMaxIdleConns: 100,\n\t\t\tMaxIdleConnsPerHost: 20,\n\t\t\tIdleConnTimeout: 90 * time.Second,\n\t\t},\n\t}\n\n\tif !useRR {\n\t\tsocks5Client = client\n\t\tsocks5ClientAddr = activeSocks5\n\t}\n\treturn client\n}\n\n// getHTTPClientForTier 按认证层级选择 HTTP 客户端。\n// 默认(socks5_paid_direct 未填或 false):只要配置了 active_socks5,付费/带 key 与 public 都走代理。\n// socks5_paid_direct=true 时恢复旧行为:付费层直连,仅免费层走代理。\nfunc getHTTPClientForTier(tier TierType) *http.Client {\n\tif tier == TierPaid && getSocks5PaidDirect() {\n\t\treturn httpClient\n\t}\n\treturn getHTTPClient()\n}\n\nfunc getSocks5PaidDirect() bool {\n\tsocks5Mu.RLock()\n\tdefer socks5Mu.RUnlock()\n\treturn socks5PaidDirect\n}\n\n// ======================== 随机 ID ========================\n\nfunc randomString(n int) string {\n\tconst letters = \"abcdefghijklmnopqrstuvwxyz0123456789\"\n\tb := make([]byte, n)\n\trand.Read(b)\n\tfor i := range b {\n\t\tb[i] = letters[b[i]%byte(len(letters))]\n\t}\n\treturn string(b)\n}\n\nfunc randomHex(n int) string {\n\tconst hex = \"0123456789abcdef\"\n\tb := make([]byte, n)\n\trand.Read(b)\n\tfor i := range b {\n\t\tb[i] = hex[b[i]%byte(len(hex))]\n\t}\n\treturn string(b)\n}\n\n// ======================== OpenCode 会话 ========================\n\nvar (\n\tocSessionID string\n\tocProjectID string\n\tocClientVer string\n\tocOnce sync.Once\n\trequestCount atomic.Int64\n)\n\nfunc fetchOCVersion() string {\n\treq, _ := http.NewRequest(\"GET\", \"https://registry.npmjs.org/opencode-ai/latest\", nil)\n\treq.Header.Set(\"Accept\", \"application/json\")\n\tresp, err := getHTTPClient().Do(req)\n\tif err != nil {\n\t\treturn \"1.15.3\"\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ := io.ReadAll(resp.Body)\n\tvar info struct {\n\t\tVersion string `json:\"version\"`\n\t}\n\tif json.Unmarshal(body, &info) == nil && info.Version != \"\" {\n\t\treturn info.Version\n\t}\n\treturn \"1.15.3\"\n}\n\nfunc initOCSession() {\n\tocOnce.Do(func() {\n\t\tocClientVer = fetchOCVersion()\n\t\tocSessionID = \"ses_\" + randomString(24)\n\t\tocProjectID = randomHex(40)\n\t\tslog.Info(\"opencode version\", \"version\", ocClientVer)\n\t\tslog.Info(\"session initialized\", \"session_id\", ocSessionID)\n\t\tslog.Info(\"project initialized\", \"project_id\", ocProjectID)\n\t})\n}\n\nfunc refreshOCSession() {\n\tocClientVer = fetchOCVersion()\n\tocSessionID = \"ses_\" + randomString(24)\n\tocProjectID = randomHex(40)\n\tslog.Info(\"session refreshed\", \"version\", ocClientVer, \"session_id\", ocSessionID)\n\t// 重置 Once 以便后续 initOCSession 调用直接通过\n\tocOnce = sync.Once{}\n}\n\n// ======================== 模型 ========================\n\ntype ModelInfo struct {\n\tID string `json:\"id\"`\n\tObject string `json:\"object\"`\n\tCreated int64 `json:\"created\"`\n\tOwnedBy string `json:\"owned_by\"`\n}\n\nvar (\n\tmodelsCache []ModelInfo\n\tgoModelsCache []ModelInfo\n\tmodelMu sync.RWMutex\n\tmodelsLoaded bool\n)\n\nfunc fetchModels() ([]ModelInfo, error) {\n\treq, _ := http.NewRequest(\"GET\", \"https://opencode.ai/zen/v1/models\", nil)\n\treq.Header.Set(\"Authorization\", \"Bearer public\")\n\treq.Header.Set(\"x-opencode-session\", ocSessionID)\n\tresp, err := getHTTPClient().Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ := io.ReadAll(resp.Body)\n\tvar result struct {\n\t\tData []struct {\n\t\t\tID string `json:\"id\"`\n\t\t} `json:\"data\"`\n\t}\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn nil, err\n\t}\n\tvar models []ModelInfo\n\tnow := time.Now().Unix()\n\tfor _, m := range result.Data {\n\t\tmodels = append(models, ModelInfo{ID: m.ID, Object: \"model\", Created: now, OwnedBy: \"opencode\"})\n\t}\n\treturn models, nil\n}\n\nfunc fetchGoModels() ([]ModelInfo, error) {\n\treq, _ := http.NewRequest(\"GET\", \"https://opencode.ai/zen/go/v1/models\", nil)\n\treq.Header.Set(\"Authorization\", \"Bearer public\")\n\treq.Header.Set(\"x-opencode-session\", ocSessionID)\n\tresp, err := getHTTPClient().Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ := io.ReadAll(resp.Body)\n\tvar result struct {\n\t\tData []struct {\n\t\t\tID string `json:\"id\"`\n\t\t} `json:\"data\"`\n\t}\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn nil, err\n\t}\n\tvar models []ModelInfo\n\tnow := time.Now().Unix()\n\tfor _, m := range result.Data {\n\t\tmodels = append(models, ModelInfo{ID: m.ID, Object: \"model\", Created: now, OwnedBy: \"opencode\"})\n\t}\n\treturn models, nil\n}\n\nfunc containsModelWithID(models []ModelInfo, modelID string) bool {\n\tfor _, model := range models {\n\t\tif model.ID == modelID {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isModelInGoCatalog(modelID string) bool {\n\tmodelMu.RLock()\n\tdefer modelMu.RUnlock()\n\treturn containsModelWithID(goModelsCache, modelID)\n}\n\nfunc isGoCatalogOnlyModel(modelID string) bool {\n\tmodelMu.RLock()\n\tdefer modelMu.RUnlock()\n\treturn containsModelWithID(goModelsCache, modelID) && !containsModelWithID(modelsCache, modelID)\n}\n\nfunc getModelIDs() []string {\n\tmodelMu.RLock()\n\tdefer modelMu.RUnlock()\n\tids := make([]string, len(modelsCache))\n\tfor i, m := range modelsCache {\n\t\tids[i] = m.ID\n\t}\n\treturn ids\n}\n\nfunc getGoModelIDs() []string {\n\tmodelMu.RLock()\n\tdefer modelMu.RUnlock()\n\tids := make([]string, len(goModelsCache))\n\tfor i, m := range goModelsCache {\n\t\tids[i] = m.ID\n\t}\n\treturn ids\n}\n\n// isNonRetryableUpstreamError reports billing/credits failures that must not\n// trigger retries.\nfunc isNonRetryableUpstreamError(status int, body []byte) bool {\n\tif status != http.StatusUnauthorized && status != http.StatusPaymentRequired && status != http.StatusForbidden {\n\t\treturn false\n\t}\n\tvar payload struct {\n\t\tType string `json:\"type\"`\n\t\tError struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tMessage string `json:\"message\"`\n\t\t} `json:\"error\"`\n\t}\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\treturn false\n\t}\n\terrType := strings.ToLower(strings.TrimSpace(payload.Error.Type))\n\tif errType == \"\" {\n\t\terrType = strings.ToLower(strings.TrimSpace(payload.Type))\n\t}\n\tif errType == \"creditserror\" || errType == \"insufficient_quota\" || errType == \"billing_error\" {\n\t\treturn true\n\t}\n\tmsg := strings.ToLower(payload.Error.Message)\n\treturn strings.Contains(msg, \"insufficient balance\") || strings.Contains(msg, \"insufficient credits\")\n}\n\n// startModelRefresh 定时刷新模型列表(每 10 分钟)\nfunc startModelRefresh() {\n\tgo func() {\n\t\tticker := time.NewTicker(10 * time.Minute)\n\t\tdefer ticker.Stop()\n\t\tfor range ticker.C {\n\t\t\tfetched, err := fetchModels()\n\t\t\tif err == nil && len(fetched) > 0 {\n\t\t\t\tmodelMu.Lock()\n\t\t\t\tmodelsCache = fetched\n\t\t\t\tmodelsLoaded = true\n\t\t\t\tmodelMu.Unlock()\n\t\t\t\tslog.Info(\"models auto-refreshed\", \"count\", len(fetched))\n\t\t\t} else if err != nil {\n\t\t\t\tslog.Error(\"free models refresh failed\", \"error\", err)\n\t\t\t}\n\n\t\t\tgoFetched, goErr := fetchGoModels()\n\t\t\tif goErr == nil && len(goFetched) > 0 {\n\t\t\t\tmodelMu.Lock()\n\t\t\t\tgoModelsCache = goFetched\n\t\t\t\tmodelMu.Unlock()\n\t\t\t\tslog.Info(\"go catalog auto-refreshed\", \"count\", len(goFetched))\n\t\t\t} else if goErr != nil {\n\t\t\t\tslog.Error(\"go catalog refresh failed\", \"error\", goErr)\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// ======================== 结构化日志 ========================\n\ntype contextKey string\n\nconst reqIDKey contextKey = \"request_id\"\n\nfunc getReqID(ctx context.Context) string {\n\tif id, ok := ctx.Value(reqIDKey).(string); ok {\n\t\treturn id\n\t}\n\treturn \"\"\n}\n\n// ======================== 配置 ========================\n\nvar (\n\tport string\n\tconfigPath = \"config.json\"\n\tmodelAlias = map[string]string{}\n\treasoningEffortMap = map[string]string{}\n\tforceDisableThinking bool\n\tdebugMode bool\n\tconfigMu sync.RWMutex\n\tstoredResponses = map[string]StoredResponseState{}\n\tstoredResponsesMu sync.RWMutex\n)\n\n// ======================== 管理面板认证 ========================\n\nvar (\n\tadminPassword string\n\tsessions = map[string]struct{}{}\n\tsessionsMu sync.Mutex\n)\n\nfunc requireAuth(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif adminPassword == \"\" {\n\t\t\tnext(w, r)\n\t\t\treturn\n\t\t}\n\t\tcookie, err := r.Cookie(\"session\")\n\t\tif err != nil || cookie.Value == \"\" {\n\t\t\thttp.Redirect(w, r, \"/login\", http.StatusFound)\n\t\t\treturn\n\t\t}\n\t\tsessionsMu.Lock()\n\t\t_, ok := sessions[cookie.Value]\n\t\tsessionsMu.Unlock()\n\t\tif !ok {\n\t\t\thttp.Redirect(w, r, \"/login\", http.StatusFound)\n\t\t\treturn\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\nfunc generateToken() (string, error) {\n\tb := make([]byte, 32)\n\tif _, err := rand.Read(b); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(b), nil\n}\n\nfunc loginHandler(w http.ResponseWriter, r *http.Request) {\n\tif adminPassword == \"\" {\n\t\thttp.Redirect(w, r, \"/\", http.StatusFound)\n\t\treturn\n\t}\n\tif r.Method == http.MethodPost {\n\t\tif err := r.ParseForm(); err != nil {\n\t\t\trenderLoginPage(w, \"表单解析失败\")\n\t\t\treturn\n\t\t}\n\t\tif r.FormValue(\"password\") != adminPassword {\n\t\t\trenderLoginPage(w, \"密码错误\")\n\t\t\treturn\n\t\t}\n\t\ttoken, err := generateToken()\n\t\tif err != nil {\n\t\t\trenderLoginPage(w, \"创建会话失败\")\n\t\t\treturn\n\t\t}\n\t\tsessionsMu.Lock()\n\t\tsessions[token] = struct{}{}\n\t\tsessionsMu.Unlock()\n\t\thttp.SetCookie(w, &http.Cookie{Name: \"session\", Value: token, Path: \"/\", HttpOnly: true})\n\t\thttp.Redirect(w, r, \"/\", http.StatusFound)\n\t\treturn\n\t}\n\trenderLoginPage(w, \"\")\n}\n\nfunc logoutHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Redirect(w, r, \"/login\", http.StatusFound)\n\t\treturn\n\t}\n\tcookie, err := r.Cookie(\"session\")\n\tif err == nil && cookie.Value != \"\" {\n\t\tsessionsMu.Lock()\n\t\tdelete(sessions, cookie.Value)\n\t\tsessionsMu.Unlock()\n\t}\n\thttp.SetCookie(w, &http.Cookie{Name: \"session\", Value: \"\", Path: \"/\", HttpOnly: true, MaxAge: -1})\n\thttp.Redirect(w, r, \"/login\", http.StatusFound)\n}\n\n// ======================== Token 统计 ========================\n\ntype ModelStats struct {\n\tRequestCount int64 `json:\"request_count\"`\n\tPromptTokens int64 `json:\"prompt_tokens\"`\n\tCompletionTokens int64 `json:\"completion_tokens\"`\n\tTotalTokens int64 `json:\"total_tokens\"`\n}\n\ntype TokenStatsData struct {\n\tTotalRequests int64 `json:\"total_requests\"`\n\tModels map[string]*ModelStats `json:\"models\"`\n}\n\nvar (\n\ttokenStats = &TokenStatsData{Models: map[string]*ModelStats{}}\n\ttokenStatsMu sync.Mutex\n\ttokenStatsPath = \"stats.json\"\n)\n\n// ======================== 数据模型 ========================\n\ntype OpenAIRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []Message `json:\"messages\"`\n\tStream bool `json:\"stream\"`\n\tTemperature *float64 `json:\"temperature,omitempty\"`\n\tMaxTokens *int `json:\"max_tokens,omitempty\"`\n\tTopP *float64 `json:\"top_p,omitempty\"`\n\tThinking any `json:\"thinking,omitempty\"`\n\tReasoningEffort string `json:\"reasoning_effort,omitempty\"`\n\tExtraBody map[string]any `json:\"extra_body,omitempty\"`\n\tTools []Tool `json:\"tools,omitempty\"`\n\tToolChoice any `json:\"tool_choice,omitempty\"`\n}\n\ntype Message struct {\n\tRole string `json:\"role,omitempty\"`\n\tContent any `json:\"content,omitempty\"`\n\tToolCalls []ToolCall `json:\"tool_calls,omitempty\"`\n\tToolCallID string `json:\"tool_call_id,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n\tReasoningContent *string `json:\"reasoning_content,omitempty\"`\n}\n\ntype ToolCall struct {\n\tID string `json:\"id\"`\n\tType string `json:\"type\"`\n\tFunction FunctionCall `json:\"function\"`\n}\n\ntype FunctionCall struct {\n\tName string `json:\"name\"`\n\tArguments string `json:\"arguments\"`\n}\n\ntype Tool struct {\n\tType string `json:\"type\"`\n\tFunction ToolFunction `json:\"function\"`\n}\n\ntype ToolFunction struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tParameters map[string]any `json:\"parameters\"`\n}\n\ntype AppConfig struct {\n\tModelAlias map[string]string `json:\"model_alias\"`\n\tReasoningEffortMap map[string]string `json:\"reasoning_effort_map\"`\n\tForceDisableThinking bool `json:\"force_disable_thinking\"`\n\tSocks5Proxies []Socks5Proxy `json:\"socks5_proxies,omitempty\"`\n\tActiveSocks5 string `json:\"active_socks5,omitempty\"`\n\t// Socks5PaidDirect controls whether keyed/paid upstream calls bypass SOCKS5.\n\t// Omitted or false (default): all traffic uses the active proxy.\n\t// true: paid/keyed traffic goes direct; only public/free uses SOCKS5.\n\tSocks5PaidDirect bool `json:\"socks5_paid_direct,omitempty\"`\n}\n\n// ======================== Claude Messages API 类型 ========================\n\ntype ClaudeRequest struct {\n\tModel string `json:\"model\"`\n\tMessages []ClaudeMessage `json:\"messages\"`\n\tSystem any `json:\"system,omitempty\"`\n\tMaxTokens *int `json:\"max_tokens,omitempty\"`\n\tTemperature *float64 `json:\"temperature,omitempty\"`\n\tTopP *float64 `json:\"top_p,omitempty\"`\n\tTopK *int `json:\"top_k,omitempty\"`\n\tStream bool `json:\"stream,omitempty\"`\n\tTools []ClaudeTool `json:\"tools,omitempty\"`\n\tToolChoice any `json:\"tool_choice,omitempty\"`\n\tStopSequences []string `json:\"stop_sequences,omitempty\"`\n\tMetadata any `json:\"metadata,omitempty\"`\n\tThinking any `json:\"thinking,omitempty\"`\n\tOutputConfig any `json:\"output_config,omitempty\"`\n\tContextManagement any `json:\"context_management,omitempty\"`\n}\n\ntype ClaudeMessage struct {\n\tRole string `json:\"role\"`\n\tContent any `json:\"content\"`\n}\n\ntype ClaudeContent struct {\n\tType string `json:\"type\"`\n\tText string `json:\"text,omitempty\"`\n\tThinking string `json:\"thinking,omitempty\"`\n\tSignature string `json:\"signature,omitempty\"`\n\tID string `json:\"id,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n\tInput any `json:\"input,omitempty\"`\n\tToolUseID string `json:\"tool_use_id,omitempty\"`\n\tContent any `json:\"content,omitempty\"`\n}\n\ntype ClaudeTool struct {\n\tName string `json:\"name\"`\n\tDescription string `json:\"description,omitempty\"`\n\tInputSchema any `json:\"input_schema\"`\n\tType string `json:\"type,omitempty\"`\n}\n\ntype ClaudeResponse struct {\n\tID string `json:\"id\"`\n\tType string `json:\"type\"`\n\tRole string `json:\"role\"`\n\tContent []ClaudeContent `json:\"content\"`\n\tModel string `json:\"model\"`\n\tStopReason string `json:\"stop_reason\"`\n\tUsage ClaudeUsage `json:\"usage,omitempty\"`\n}\n\ntype ClaudeUsage map[string]any\n\n// ======================== Responses API 类型 ========================\n\ntype ResponsesAPIRequest struct {\n\tModel string `json:\"model\"`\n\tInput any `json:\"input\"`\n\tMessages []Message `json:\"messages,omitempty\"`\n\tInstructions string `json:\"instructions,omitempty\"`\n\tPreviousResponseID string `json:\"previous_response_id,omitempty\"`\n\tStream bool `json:\"stream,omitempty\"`\n\tTemperature *float64 `json:\"temperature,omitempty\"`\n\tMaxTokens *int `json:\"max_output_tokens,omitempty\"`\n\tTopP *float64 `json:\"top_p,omitempty\"`\n\tFrequencyPenalty *float64 `json:\"frequency_penalty,omitempty\"`\n\tPresencePenalty *float64 `json:\"presence_penalty,omitempty\"`\n\tReasoning ReasonEffort `json:\"reasoning,omitempty\"`\n\tInclude []string `json:\"include,omitempty\"`\n\tStore *bool `json:\"store,omitempty\"`\n\tTools []ResponsesTool `json:\"tools,omitempty\"`\n\tToolChoice any `json:\"tool_choice,omitempty\"`\n\tParallelToolCalls *bool `json:\"parallel_tool_calls,omitempty\"`\n\tStop any `json:\"stop,omitempty\"`\n\tUser string `json:\"user,omitempty\"`\n\tStreamOptions any `json:\"stream_options,omitempty\"`\n\tMetadata any `json:\"metadata,omitempty\"`\n}\n\ntype ResponsesTool struct {\n\tType string `json:\"type\"`\n\tName string `json:\"name,omitempty\"`\n\tDescription string `json:\"description,omitempty\"`\n\tParameters map[string]any `json:\"parameters,omitempty\"`\n\tFunction *ToolFunction `json:\"function,omitempty\"`\n\tServerLabel string `json:\"server_label,omitempty\"`\n\tServerURL string `json:\"server_url,omitempty\"`\n\tConnectorID string `json:\"connector_id,omitempty\"`\n\tAuthorization string `json:\"authorization,omitempty\"`\n\tAllowedTools []string `json:\"allowed_tools,omitempty\"`\n\tRequireApproval any `json:\"require_approval,omitempty\"`\n}\n\ntype ReasonEffort struct {\n\tEffort string `json:\"effort,omitempty\"`\n}\n\ntype StoredResponseState struct {\n\tModel string `json:\"model\"`\n\tInstructions string `json:\"instructions,omitempty\"`\n\tTools []ResponsesTool `json:\"tools,omitempty\"`\n\tToolChoice any `json:\"tool_choice,omitempty\"`\n\tOutput []any `json:\"output,omitempty\"`\n}\n\n// ======================== 配置管理 ========================\n\nfunc loadConfig(path string) AppConfig {\n\tvar cfg AppConfig\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn cfg\n\t}\n\tif err := json.Unmarshal(data, &cfg); err != nil {\n\t\tslog.Warn(\"config parse failed\", \"error\", err)\n\t}\n\treturn cfg\n}\n\nfunc saveConfig(path string, cfg AppConfig) error {\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, data, 0644)\n}\n\nfunc applyConfig(cfg AppConfig) {\n\tconfigMu.Lock()\n\tdefer configMu.Unlock()\n\tif cfg.ModelAlias != nil {\n\t\tmodelAlias = cfg.ModelAlias\n\t}\n\tif cfg.ReasoningEffortMap != nil {\n\t\treasoningEffortMap = cfg.ReasoningEffortMap\n\t}\n\tforceDisableThinking = cfg.ForceDisableThinking\n\n\tsocks5Mu.Lock()\n\tif cfg.Socks5Proxies != nil {\n\t\tsocks5Proxies = cfg.Socks5Proxies\n\t}\n\tif activeSocks5 != cfg.ActiveSocks5 {\n\t\tactiveSocks5 = cfg.ActiveSocks5\n\t\tsocks5Client = nil\n\t\tsocks5ClientAddr = \"\"\n\t\tatomic.StoreUint32(&socks5RRIndex, 0)\n\t}\n\tsocks5PaidDirect = cfg.Socks5PaidDirect\n\tsocks5Mu.Unlock()\n\n}\n\nfunc resolveModel(model string) string {\n\tm := strings.TrimSpace(model)\n\tconfigMu.RLock()\n\talias, ok := modelAlias[m]\n\tconfigMu.RUnlock()\n\tif ok {\n\t\treturn alias\n\t}\n\t// Clients see free models without the \"-free\" suffix from /v1/models.\n\t// Map the display name back to the upstream free ID when that is the only match.\n\tif m != \"\" && !isFreeModel(m) {\n\t\tfreeID := m + \"-free\"\n\t\tif !modelExistsInCaches(m) && modelExistsInCaches(freeID) {\n\t\t\treturn freeID\n\t\t}\n\t}\n\treturn m\n}\n\nfunc getForceDisableThinking() bool {\n\tconfigMu.RLock()\n\tdefer configMu.RUnlock()\n\treturn forceDisableThinking\n}\n\nfunc getReasoningEffortMap() map[string]string {\n\tconfigMu.RLock()\n\tdefer configMu.RUnlock()\n\tcp := make(map[string]string, len(reasoningEffortMap))\n\tfor k, v := range reasoningEffortMap {\n\t\tcp[k] = v\n\t}\n\treturn cp\n}\n\n// ======================== Token 统计 ========================\n\nfunc loadTokenStats() {\n\tdata, err := os.ReadFile(tokenStatsPath)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar st TokenStatsData\n\tif err := json.Unmarshal(data, &st); err != nil {\n\t\treturn\n\t}\n\ttokenStatsMu.Lock()\n\tif st.Models == nil {\n\t\tst.Models = map[string]*ModelStats{}\n\t}\n\ttokenStats = &st\n\ttokenStatsMu.Unlock()\n}\n\nfunc saveTokenStats() {\n\ttokenStatsMu.Lock()\n\tdata, err := json.MarshalIndent(tokenStats, \"\", \" \")\n\ttokenStatsMu.Unlock()\n\tif err != nil {\n\t\treturn\n\t}\n\tos.WriteFile(tokenStatsPath, data, 0644)\n}\n\nfunc recordTokenUsage(model string, promptTokens, completionTokens, totalTokens int64) {\n\ttokenStatsMu.Lock()\n\ttokenStats.TotalRequests++\n\tms, ok := tokenStats.Models[model]\n\tif !ok {\n\t\tms = &ModelStats{}\n\t\ttokenStats.Models[model] = ms\n\t}\n\tms.RequestCount++\n\tms.PromptTokens += promptTokens\n\tms.CompletionTokens += completionTokens\n\tms.TotalTokens += totalTokens\n\ttokenStatsMu.Unlock()\n\tgo saveTokenStats()\n}\n\n// ======================== Thinking/Reasoning 判断 ========================\n\nfunc isThinkingEnabled(value any) bool {\n\tswitch v := value.(type) {\n\tcase map[string]any:\n\t\tt, _ := v[\"type\"].(string)\n\t\t// Claude Code sends adaptive thinking with --effort / CLAUDE_CODE_EFFORT_LEVEL.\n\t\treturn t == \"enabled\" || t == \"adaptive\"\n\tcase bool:\n\t\treturn v\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// effortFromOutputConfig reads Claude Code's output_config.effort\n// (set by --effort / CLAUDE_CODE_EFFORT_LEVEL).\nfunc effortFromOutputConfig(value any) string {\n\tm, ok := value.(map[string]any)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\teffort, _ := m[\"effort\"].(string)\n\treturn strings.TrimSpace(effort)\n}\n\nfunc isThinkingDisabled(value any) bool {\n\tswitch v := value.(type) {\n\tcase map[string]any:\n\t\tt, _ := v[\"type\"].(string)\n\t\treturn t == \"disabled\"\n\tcase bool:\n\t\treturn !v\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// buildUpstreamThinking preserves budget_tokens / effort fields when present.\nfunc buildUpstreamThinking(value any) map[string]any {\n\tout := map[string]any{\"type\": \"enabled\"}\n\tm, ok := value.(map[string]any)\n\tif !ok {\n\t\treturn out\n\t}\n\tfor _, key := range []string{\"budget_tokens\", \"effort\"} {\n\t\tif v, exists := m[key]; exists && v != nil {\n\t\t\tout[key] = v\n\t\t}\n\t}\n\treturn out\n}\n\n// reasoningEffortFromThinking maps Anthropic-style budget_tokens onto an\n// OpenAI-compatible reasoning_effort when the client did not set one explicitly.\nfunc reasoningEffortFromThinking(value any) string {\n\tm, ok := value.(map[string]any)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tif effort, ok := m[\"effort\"].(string); ok && effort != \"\" {\n\t\treturn effort\n\t}\n\tvar budget float64\n\tswitch v := m[\"budget_tokens\"].(type) {\n\tcase float64:\n\t\tbudget = v\n\tcase int:\n\t\tbudget = float64(v)\n\tcase int64:\n\t\tbudget = float64(v)\n\tcase json.Number:\n\t\tf, err := v.Float64()\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tbudget = f\n\tdefault:\n\t\treturn \"\"\n\t}\n\tswitch {\n\tcase budget <= 0:\n\t\treturn \"\"\n\tcase budget < 2048:\n\t\treturn \"low\"\n\tcase budget < 8192:\n\t\treturn \"medium\"\n\tcase budget < 16384:\n\t\treturn \"high\"\n\tdefault:\n\t\treturn \"xhigh\"\n\t}\n}\n\nfunc wantsReasoning(req *OpenAIRequest) bool {\n\tif getForceDisableThinking() {\n\t\treturn false\n\t}\n\tif isThinkingDisabled(req.Thinking) {\n\t\treturn false\n\t}\n\tif isThinkingEnabled(req.Thinking) {\n\t\treturn true\n\t}\n\tif req.ExtraBody != nil {\n\t\tif isThinkingDisabled(req.ExtraBody[\"thinking\"]) {\n\t\t\treturn false\n\t\t}\n\t\tif isThinkingEnabled(req.ExtraBody[\"thinking\"]) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn true\n}\n\n// ======================== 消息处理 ========================\n// normalizeContent 是 dumb pipe 透传:保留 string 与 []any 两种入参形状\n// (其它非常规类型走 json.Marshal 兜底),不解析或过滤任何 multimodal part。\n// 能力协商由 opencode 客户端 + 上游负责;这里既不\"硬降级\"也不\"补全\"。\nfunc normalizeContent(content any) any {\n\tif content == nil {\n\t\treturn nil\n\t}\n\tif s, ok := content.(string); ok {\n\t\treturn s\n\t}\n\tif arr, ok := content.([]any); ok {\n\t\treturn arr\n\t}\n\tb, err := json.Marshal(content)\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn string(b)\n}\n\nfunc fixToolCallGaps(messages []Message) []Message {\n\ttoolResponses := map[string]*Message{}\n\tfor i := range messages {\n\t\tif messages[i].Role == \"tool\" && messages[i].ToolCallID != \"\" {\n\t\t\ttoolResponses[messages[i].ToolCallID] = &messages[i]\n\t\t}\n\t}\n\tfixed := make([]Message, 0, len(messages)+len(messages)/4)\n\temitted := map[string]bool{}\n\tfor _, msg := range messages {\n\t\tif msg.Role == \"tool\" && msg.ToolCallID != \"\" {\n\t\t\tif emitted[msg.ToolCallID] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tfixed = append(fixed, msg)\n\t\tif msg.Role == \"assistant\" && len(msg.ToolCalls) > 0 {\n\t\t\tfor _, tc := range msg.ToolCalls {\n\t\t\t\tif resp, found := toolResponses[tc.ID]; found {\n\t\t\t\t\tfixed = append(fixed, *resp)\n\t\t\t\t} else {\n\t\t\t\t\tfixed = append(fixed, Message{Role: \"tool\", ToolCallID: tc.ID, Content: \"Tool call result not available\"})\n\t\t\t\t}\n\t\t\t\temitted[tc.ID] = true\n\t\t\t}\n\t\t}\n\t}\n\treturn fixed\n}\n\nfunc ensureReasoningContent(messages []Message, thinking bool) []Message {\n\tif !thinking {\n\t\treturn messages\n\t}\n\tfor i := range messages {\n\t\tif messages[i].Role == \"assistant\" && messages[i].ReasoningContent == nil {\n\t\t\tempty := \"\"\n\t\t\tmessages[i].ReasoningContent = &empty\n\t\t}\n\t}\n\treturn messages\n}\n\nfunc convertMessagesForUpstream(messages []Message) []map[string]any {\n\tconverted := make([]map[string]any, 0, len(messages))\n\tfor _, msg := range messages {\n\t\tclean := map[string]any{}\n\t\tif msg.Role != \"\" {\n\t\t\tclean[\"role\"] = msg.Role\n\t\t}\n\t\tcontent := normalizeContent(msg.Content)\n\t\treasoningContent := msg.ReasoningContent\n\t\tif content != nil {\n\t\t\tclean[\"content\"] = content\n\t\t}\n\t\tif reasoningContent != nil {\n\t\t\tclean[\"reasoning_content\"] = *reasoningContent\n\t\t}\n\t\tif len(msg.ToolCalls) > 0 {\n\t\t\tclean[\"tool_calls\"] = msg.ToolCalls\n\t\t}\n\t\tif msg.ToolCallID != \"\" {\n\t\t\tclean[\"tool_call_id\"] = msg.ToolCallID\n\t\t}\n\t\tif msg.Name != \"\" {\n\t\t\tclean[\"name\"] = msg.Name\n\t\t}\n\t\tconverted = append(converted, clean)\n\t}\n\treturn converted\n}\n\n// ======================== 完整请求转换(含 thinking/reasoning_effort/ExtraBody) ========================\n\nfunc convertRequest(req *OpenAIRequest) map[string]any {\n\tconverted := map[string]any{\n\t\t\"model\": req.Model,\n\t\t\"messages\": convertMessagesForUpstream(req.Messages),\n\t\t\"stream\": req.Stream,\n\t}\n\tif req.Temperature != nil {\n\t\tconverted[\"temperature\"] = *req.Temperature\n\t}\n\tif req.MaxTokens != nil {\n\t\tconverted[\"max_tokens\"] = *req.MaxTokens\n\t}\n\tif req.TopP != nil {\n\t\tconverted[\"top_p\"] = *req.TopP\n\t}\n\tif len(req.Tools) > 0 {\n\t\tconverted[\"tools\"] = req.Tools\n\t}\n\tif req.ToolChoice != nil {\n\t\tconverted[\"tool_choice\"] = req.ToolChoice\n\t}\n\t// 处理思维模式 — 仅当用户显式指定时才发送,避免 MiniMax 等模型报错\n\tif getForceDisableThinking() || isThinkingDisabled(req.Thinking) {\n\t\tconverted[\"thinking\"] = map[string]string{\"type\": \"disabled\"}\n\t} else if req.Thinking != nil && isThinkingEnabled(req.Thinking) {\n\t\tconverted[\"thinking\"] = buildUpstreamThinking(req.Thinking)\n\t} else if req.ExtraBody != nil {\n\t\tif isThinkingDisabled(req.ExtraBody[\"thinking\"]) {\n\t\t\tconverted[\"thinking\"] = map[string]string{\"type\": \"disabled\"}\n\t\t} else if isThinkingEnabled(req.ExtraBody[\"thinking\"]) {\n\t\t\tconverted[\"thinking\"] = buildUpstreamThinking(req.ExtraBody[\"thinking\"])\n\t\t}\n\t}\n\t// 处理 reasoning_effort(含从 thinking.budget_tokens 推导)\n\teffort := req.ReasoningEffort\n\tif effort == \"\" && !isThinkingDisabled(req.Thinking) {\n\t\teffort = reasoningEffortFromThinking(req.Thinking)\n\t}\n\tif !getForceDisableThinking() && effort != \"\" {\n\t\teffortMap := getReasoningEffortMap()\n\t\tif mapped, ok := effortMap[effort]; ok {\n\t\t\tconverted[\"reasoning_effort\"] = mapped\n\t\t} else {\n\t\t\tconverted[\"reasoning_effort\"] = effort\n\t\t}\n\t}\n\t// 合并 ExtraBody\n\tif req.ExtraBody != nil {\n\t\tfor k, v := range req.ExtraBody {\n\t\t\tif _, exists := converted[k]; !exists {\n\t\t\t\tconverted[k] = v\n\t\t\t}\n\t\t}\n\t}\n\treturn converted\n}\n\nfunc buildUpstreamBody(req *OpenAIRequest) []byte {\n\tconverted := convertRequest(req)\n\tb, err := json.Marshal(converted)\n\tif err != nil {\n\t\tslog.Error(\"marshal upstream body failed\", \"error\", err)\n\t}\n\treturn b\n}\n\n// ======================== Anthropic 格式兼容 ========================\n\nfunc isAnthropicFormat(body []byte) bool {\n\tvar obj map[string]any\n\tif json.Unmarshal(body, &obj) == nil {\n\t\tif typ, _ := obj[\"type\"].(string); typ == \"message\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tlines := bytes.Split(body, []byte(\"\\n\"))\n\tfor _, line := range lines {\n\t\tline = bytes.TrimSpace(line)\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tvar event map[string]any\n\t\tif err := json.Unmarshal(line, &event); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\ttyp, _ := event[\"type\"].(string)\n\t\tswitch typ {\n\t\tcase \"message_start\", \"content_block_start\", \"content_block_delta\",\n\t\t\t\"content_block_stop\", \"message_delta\", \"message_stop\", \"ping\":\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\treturn false\n}\n\nfunc parseAnthropicSSE(body []byte) (map[string]any, string, []map[string]any) {\n\tlines := bytes.Split(body, []byte(\"\\n\"))\n\tvar anthropicMsg map[string]any\n\tvar textBuilder, currentToolInputBuilder strings.Builder\n\tvar currentToolUse map[string]any\n\tvar toolUseBlocks []map[string]any\n\tfor _, line := range lines {\n\t\tline = bytes.TrimSpace(line)\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tvar event map[string]any\n\t\tif err := json.Unmarshal(line, &event); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\ttyp, _ := event[\"type\"].(string)\n\t\tswitch typ {\n\t\tcase \"message_start\":\n\t\t\tif m, ok := event[\"message\"].(map[string]any); ok {\n\t\t\t\tanthropicMsg = m\n\t\t\t}\n\t\tcase \"content_block_start\":\n\t\t\tif cb, ok := event[\"content_block\"].(map[string]any); ok {\n\t\t\t\tif cbType, _ := cb[\"type\"].(string); cbType == \"tool_use\" {\n\t\t\t\t\tcurrentToolUse = cb\n\t\t\t\t\tcurrentToolInputBuilder.Reset()\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"content_block_delta\":\n\t\t\tif delta, ok := event[\"delta\"].(map[string]any); ok {\n\t\t\t\tif t, ok := delta[\"text\"].(string); ok {\n\t\t\t\t\ttextBuilder.WriteString(t)\n\t\t\t\t}\n\t\t\t\tif dt, _ := delta[\"type\"].(string); dt == \"input_json_delta\" {\n\t\t\t\t\tif partial, ok := delta[\"partial_json\"].(string); ok {\n\t\t\t\t\t\tcurrentToolInputBuilder.WriteString(partial)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"content_block_stop\":\n\t\t\tif currentToolUse != nil {\n\t\t\t\tinputStr := currentToolInputBuilder.String()\n\t\t\t\tvar input any = inputStr\n\t\t\t\tvar parsed any\n\t\t\t\tif json.Unmarshal([]byte(inputStr), &parsed) == nil {\n\t\t\t\t\tinput = parsed\n\t\t\t\t}\n\t\t\t\tcurrentToolUse[\"input\"] = input\n\t\t\t\ttoolUseBlocks = append(toolUseBlocks, currentToolUse)\n\t\t\t\tcurrentToolUse = nil\n\t\t\t}\n\t\tcase \"message_delta\":\n\t\t\tif delta, ok := event[\"delta\"].(map[string]any); ok {\n\t\t\t\tif anthropicMsg == nil {\n\t\t\t\t\tanthropicMsg = map[string]any{}\n\t\t\t\t}\n\t\t\t\tif stop, ok := delta[\"stop_reason\"].(string); ok {\n\t\t\t\t\tanthropicMsg[\"stop_reason\"] = stop\n\t\t\t\t}\n\t\t\t\tif usage, ok := delta[\"usage\"].(map[string]any); ok {\n\t\t\t\t\tanthropicMsg[\"usage\"] = usage\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"message_stop\":\n\t\tcase \"error\":\n\t\t\treturn nil, \"\", nil\n\t\t}\n\t}\n\treturn anthropicMsg, textBuilder.String(), toolUseBlocks\n}\n\nfunc buildOpenAIResponse(anthropicMsg map[string]any, text string, toolUseBlocks []map[string]any, modelID string) []byte {\n\tif anthropicMsg == nil {\n\t\treturn nil\n\t}\n\tnow := time.Now().Unix()\n\trole, _ := anthropicMsg[\"role\"].(string)\n\tif role == \"\" {\n\t\trole = \"assistant\"\n\t}\n\tfinishReason, _ := anthropicMsg[\"stop_reason\"].(string)\n\tfinishReason = normalizeFinishReason(finishReason)\n\tchoice := map[string]any{\n\t\t\"index\": 0,\n\t\t\"message\": map[string]any{\"role\": role, \"content\": text},\n\t\t\"finish_reason\": finishReason,\n\t}\n\tif len(toolUseBlocks) > 0 {\n\t\tvar toolCalls []map[string]any\n\t\tfor _, tb := range toolUseBlocks {\n\t\t\ttoolInput := tb[\"input\"]\n\t\t\targsJSON, _ := json.Marshal(toolInput)\n\t\t\ttoolCalls = append(toolCalls, map[string]any{\n\t\t\t\t\"id\": tb[\"id\"],\n\t\t\t\t\"type\": \"function\",\n\t\t\t\t\"function\": map[string]any{\n\t\t\t\t\t\"name\": tb[\"name\"],\n\t\t\t\t\t\"arguments\": string(argsJSON),\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t\tchoice[\"message\"].(map[string]any)[\"tool_calls\"] = toolCalls\n\t\tif text == \"\" {\n\t\t\tchoice[\"message\"].(map[string]any)[\"content\"] = nil\n\t\t}\n\t}\n\tresp := map[string]any{\n\t\t\"id\": anthropicMsg[\"id\"],\n\t\t\"object\": \"chat.completion\",\n\t\t\"created\": now,\n\t\t\"model\": modelID,\n\t\t\"choices\": []map[string]any{choice},\n\t}\n\tif usage, ok := anthropicMsg[\"usage\"].(map[string]any); ok {\n\t\tresp[\"usage\"] = anthropicUsageToChat(usage)\n\t}\n\tresult, _ := json.Marshal(resp)\n\treturn result\n}\n\nfunc convertAnthropicMessageToOpenAI(msg map[string]any, modelID string) []byte {\n\tif msg[\"model\"] == nil {\n\t\tmsg[\"model\"] = modelID\n\t}\n\tvar textBuilder strings.Builder\n\tvar toolUses []map[string]any\n\tif content, ok := msg[\"content\"].([]any); ok {\n\t\tfor _, c := range content {\n\t\t\tif block, ok := c.(map[string]any); ok {\n\t\t\t\tswitch block[\"type\"] {\n\t\t\t\tcase \"text\":\n\t\t\t\t\tif t, ok := block[\"text\"].(string); ok {\n\t\t\t\t\t\ttextBuilder.WriteString(t)\n\t\t\t\t\t}\n\t\t\t\tcase \"tool_use\":\n\t\t\t\t\ttoolUses = append(toolUses, block)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn buildOpenAIResponse(msg, textBuilder.String(), toolUses, modelID)\n}\n\nfunc convertAnthropicToOpenAI(body []byte, modelID string) []byte {\n\tvar singleMsg map[string]any\n\tif json.Unmarshal(body, &singleMsg) == nil {\n\t\tif typ, _ := singleMsg[\"type\"].(string); typ == \"message\" {\n\t\t\treturn convertAnthropicMessageToOpenAI(singleMsg, modelID)\n\t\t}\n\t}\n\tmsg, text, toolUses := parseAnthropicSSE(body)\n\tif msg == nil {\n\t\treturn body\n\t}\n\tif msg[\"model\"] == nil {\n\t\tmsg[\"model\"] = modelID\n\t}\n\treturn buildOpenAIResponse(msg, text, toolUses, modelID)\n}\n\n// ======================== 响应清理 ========================\n\nfunc cleanNulls(m map[string]any) {\n\tfor k, v := range m {\n\t\tif v == nil {\n\t\t\tdelete(m, k)\n\t\t\tcontinue\n\t\t}\n\t\tif s, ok := v.(string); ok && s == \"\" {\n\t\t\tdelete(m, k)\n\t\t}\n\t}\n}\n\n// promoteMisplacedReasoning moves reasoning_content into content when upstream\n// put the visible answer in reasoning_content (opencode-go #37635). Only runs\n// when content is empty and the chunk has no tool_calls, so genuine CoT that\n// precedes tool calls is left alone when keepReasoning is true.\nfunc promoteMisplacedReasoning(fields map[string]any, keepReasoning bool) bool {\n\trc, _ := fields[\"reasoning_content\"].(string)\n\tif rc == \"\" {\n\t\treturn false\n\t}\n\tif raw, ok := fields[\"tool_calls\"]; ok && raw != nil {\n\t\tif arr, ok := raw.([]any); ok && len(arr) > 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\tcontent, _ := fields[\"content\"].(string)\n\tif content != \"\" {\n\t\treturn false\n\t}\n\tif keepReasoning {\n\t\t// Preserve CoT for thinking blocks / clients that read reasoning_content.\n\t\treturn false\n\t}\n\tfields[\"content\"] = rc\n\tdelete(fields, \"reasoning_content\")\n\treturn true\n}\n\nfunc cleanStreamDelta(delta map[string]any, keepReasoning bool) {\n\t_ = promoteMisplacedReasoning(delta, keepReasoning)\n\tif v, ok := delta[\"content\"]; ok && v == nil {\n\t\tdelete(delta, \"content\")\n\t}\n\tif s, ok := delta[\"content\"].(string); ok && s == \"\" {\n\t\tdelete(delta, \"content\")\n\t}\n\tif !keepReasoning {\n\t\tdelete(delta, \"reasoning_content\")\n\t} else {\n\t\tif v, ok := delta[\"reasoning_content\"]; ok && v == nil {\n\t\t\tdelete(delta, \"reasoning_content\")\n\t\t}\n\t\tif s, ok := delta[\"reasoning_content\"].(string); ok && s == \"\" {\n\t\t\tdelete(delta, \"reasoning_content\")\n\t\t}\n\t}\n\tif s, ok := delta[\"role\"].(string); ok && s == \"\" {\n\t\tdelete(delta, \"role\")\n\t}\n}\n\n// convertStreamChunkWithUsage 转换流式 chunk 并同时提取 usage,避免二次解析\nfunc convertStreamChunkWithUsage(line string, keepReasoning bool) (string, map[string]any) {\n\ttrimmed := strings.TrimSpace(line)\n\tif trimmed == \"data: [DONE]\" || trimmed == \"[DONE]\" {\n\t\treturn line, nil\n\t}\n\tif !strings.HasPrefix(line, \"data: \") {\n\t\treturn line, nil\n\t}\n\tdata := line[6:]\n\tvar raw map[string]any\n\tif err := json.Unmarshal([]byte(data), &raw); err != nil {\n\t\treturn line, nil\n\t}\n\n\t// 提取 usage\n\tvar usage map[string]any\n\tif u, ok := raw[\"usage\"].(map[string]any); ok {\n\t\tusage = u\n\t}\n\n\tchoices, ok := raw[\"choices\"].([]any)\n\tif !ok || len(choices) == 0 {\n\t\t// Chat Completions deliberately uses an empty choices array for the\n\t\t// terminal usage chunk. It is part of the client-visible stream.\n\t\tdelete(raw, \"cost\")\n\t\tconverted, err := json.Marshal(raw)\n\t\tif err != nil {\n\t\t\treturn line, usage\n\t\t}\n\t\treturn \"data: \" + string(converted), usage\n\t}\n\tfor i, c := range choices {\n\t\tchoice, ok := c.(map[string]any)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif delta, ok := choice[\"delta\"].(map[string]any); ok {\n\t\t\tcleanStreamDelta(delta, keepReasoning)\n\t\t\tchoice[\"delta\"] = delta\n\t\t}\n\t\tif msg, ok := choice[\"message\"].(map[string]any); ok {\n\t\t\tcleanNulls(msg)\n\t\t\tpromoteMisplacedReasoning(msg, keepReasoning)\n\t\t\tif !keepReasoning {\n\t\t\t\tdelete(msg, \"reasoning_content\")\n\t\t\t}\n\t\t\tchoice[\"message\"] = msg\n\t\t}\n\t\tif v, ok := choice[\"logprobs\"]; ok && v == nil {\n\t\t\tdelete(choice, \"logprobs\")\n\t\t}\n\t\tif v, ok := choice[\"finish_reason\"]; ok && v == nil {\n\t\t\tdelete(choice, \"finish_reason\")\n\t\t}\n\t\tif s, ok := choice[\"finish_reason\"].(string); ok && s == \"\" {\n\t\t\tdelete(choice, \"finish_reason\")\n\t\t}\n\t\tchoices[i] = choice\n\t}\n\traw[\"choices\"] = choices\n\tif v, ok := raw[\"usage\"]; ok && v == nil {\n\t\tdelete(raw, \"usage\")\n\t}\n\tdelete(raw, \"cost\")\n\tconverted, err := json.Marshal(raw)\n\tif err != nil {\n\t\treturn line, usage\n\t}\n\treturn \"data: \" + string(converted), usage\n}\n\nfunc convertResponse(data []byte, keepReasoning bool) ([]byte, error) {\n\tvar raw map[string]any\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\tslog.Warn(\"convertResponse unmarshal failed\", \"error\", err)\n\t\treturn data, nil\n\t}\n\tif choices, ok := raw[\"choices\"].([]any); ok {\n\t\tfor i, c := range choices {\n\t\t\tif choice, ok := c.(map[string]any); ok {\n\t\t\t\tif msg, ok := choice[\"message\"].(map[string]any); ok {\n\t\t\t\t\tcleanNulls(msg)\n\t\t\t\t\tpromoteMisplacedReasoning(msg, keepReasoning)\n\t\t\t\t\tif !keepReasoning {\n\t\t\t\t\t\tdelete(msg, \"reasoning_content\")\n\t\t\t\t\t}\n\t\t\t\t\tchoice[\"message\"] = msg\n\t\t\t\t}\n\t\t\t\tif v, ok := choice[\"logprobs\"]; ok && v == nil {\n\t\t\t\t\tdelete(choice, \"logprobs\")\n\t\t\t\t}\n\t\t\t\tchoices[i] = choice\n\t\t\t}\n\t\t}\n\t\traw[\"choices\"] = choices\n\t}\n\tdelete(raw, \"cost\")\n\treturn json.Marshal(raw)\n}\n\n// ======================== 认证层级 ========================\n\ntype TierType int\n\nconst (\n\tTierFree TierType = iota\n\tTierPaid\n)\n\ntype AuthRouteMode int\n\nconst (\n\tAuthRoutePublic AuthRouteMode = iota\n\tAuthRouteAuto\n\tAuthRouteZen\n\tAuthRouteGo\n)\n\ntype UpstreamAuth struct {\n\tToken string\n\tMode AuthRouteMode\n\tSource string // authorization | x-api-key | none\n}\n\nfunc extractUpstreamAuth(r *http.Request) UpstreamAuth {\n\ttoken := \"\"\n\tsource := \"none\"\n\tauth := r.Header.Get(\"Authorization\")\n\tif strings.HasPrefix(auth, \"Bearer \") {\n\t\ttoken = strings.TrimSpace(strings.TrimPrefix(auth, \"Bearer \"))\n\t\tsource = \"authorization\"\n\t}\n\tif token == \"\" {\n\t\tif key := strings.TrimSpace(r.Header.Get(\"x-api-key\")); key != \"\" {\n\t\t\ttoken = key\n\t\t\tsource = \"x-api-key\"\n\t\t}\n\t}\n\tif token == \"\" || token == \"public\" {\n\t\tsrc := source\n\t\tif token == \"\" {\n\t\t\tsrc = \"none\"\n\t\t}\n\t\treturn UpstreamAuth{Mode: AuthRoutePublic, Source: src}\n\t}\n\t// go:/zen: 前缀路由:去掉前缀后剩余部分仍需是有效 key(sk- 开头)\n\tif rest, ok := strings.CutPrefix(token, \"go:\"); ok && isValidOpenCodeKey(rest) {\n\t\treturn UpstreamAuth{Token: rest, Mode: AuthRouteGo, Source: source}\n\t}\n\tif rest, ok := strings.CutPrefix(token, \"zen:\"); ok && isValidOpenCodeKey(rest) {\n\t\treturn UpstreamAuth{Token: rest, Mode: AuthRouteZen, Source: source}\n\t}\n\t// 只有 sk- 开头的才是有效 key,其余(no-key-required 等占位符)一律走 public\n\tif isValidOpenCodeKey(token) {\n\t\treturn UpstreamAuth{Token: token, Mode: AuthRouteAuto, Source: source}\n\t}\n\treturn UpstreamAuth{Mode: AuthRoutePublic, Source: source}\n}\n\n// 只认 sk- 开头的 opencode key;Anthropic sk-ant-* 不能转发上游。\nfunc isValidOpenCodeKey(token string) bool {\n\tif strings.HasPrefix(token, \"sk-ant-\") {\n\t\treturn false\n\t}\n\treturn strings.HasPrefix(token, \"sk-\") && len(token) > 15\n}\n\nfunc (auth UpstreamAuth) tier() TierType {\n\tif auth.Mode == AuthRoutePublic {\n\t\treturn TierFree\n\t}\n\treturn TierPaid\n}\n\nfunc (auth UpstreamAuth) authorizationHeader() string {\n\tif auth.Mode == AuthRoutePublic {\n\t\treturn \"Bearer public\"\n\t}\n\treturn \"Bearer \" + auth.Token\n}\n\nfunc (auth UpstreamAuth) shouldUseGoCatalog() bool {\n\treturn auth.Mode == AuthRouteGo\n}\n\nfunc (auth UpstreamAuth) shouldUseGoEndpoint(modelID string) bool {\n\tswitch auth.Mode {\n\tcase AuthRouteGo:\n\t\treturn isModelInGoCatalog(modelID)\n\tcase AuthRouteAuto:\n\t\treturn isGoCatalogOnlyModel(modelID)\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// isFreeModel 判断模型是否属于免费模型(以 -free 结尾)\nfunc isFreeModel(modelID string) bool {\n\treturn strings.HasSuffix(modelID, \"-free\")\n}\n\n// publicFacingModelID strips the upstream \"-free\" suffix for client-visible catalogs.\nfunc publicFacingModelID(modelID string) string {\n\tif isFreeModel(modelID) {\n\t\treturn strings.TrimSuffix(modelID, \"-free\")\n\t}\n\treturn modelID\n}\n\nfunc modelExistsInCaches(modelID string) bool {\n\tmodelMu.RLock()\n\tdefer modelMu.RUnlock()\n\treturn containsModelWithID(modelsCache, modelID) || containsModelWithID(goModelsCache, modelID)\n}\n\nfunc buildOCRequest(modelID string, bodyMap map[string]any, auth UpstreamAuth) (*http.Request, error) {\n\treturn buildOCRequestWithEndpoint(modelID, bodyMap, auth, auth.shouldUseGoEndpoint(modelID))\n}\n\nfunc buildOCRequestWithEndpoint(modelID string, bodyMap map[string]any, auth UpstreamAuth, useGoEndpoint bool) (*http.Request, error) {\n\tbodyMap[\"model\"] = modelID\n\ttryBody, err := json.Marshal(bodyMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar upstreamURL string\n\tif useGoEndpoint {\n\t\tupstreamURL = \"https://opencode.ai/zen/go/v1/chat/completions\"\n\t} else {\n\t\tupstreamURL = \"https://opencode.ai/zen/v1/chat/completions\"\n\t}\n\treq, err := http.NewRequest(\"POST\", upstreamURL, bytes.NewReader(tryBody))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", auth.authorizationHeader())\n\treq.Header.Set(\"User-Agent\", fmt.Sprintf(\"opencode/%s\", ocClientVer))\n\treq.Header.Set(\"x-opencode-client\", \"cli\")\n\treq.Header.Set(\"x-opencode-project\", ocProjectID)\n\treq.Header.Set(\"x-opencode-session\", ocSessionID)\n\treq.Header.Set(\"x-opencode-request\", \"req_\"+randomString(24))\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treturn req, nil\n}\n\nfunc shouldRetryUpstreamStatus(status int) bool {\n\t// 仅重试可恢复的临时性错误(始终同模型重试,不换模型)\n\tswitch status {\n\tcase http.StatusUnauthorized, // 401 认证过期或 token 未同步\n\t\thttp.StatusTooManyRequests, // 429 限流\n\t\thttp.StatusBadGateway, // 502\n\t\thttp.StatusServiceUnavailable, // 503\n\t\thttp.StatusGatewayTimeout: // 504\n\t\treturn true\n\t}\n\t// 其他 5xx 也重试,但 4xx 中只有 401 和 429 重试\n\treturn status >= 500 && status < 600\n}\n\nconst (\n\tmaxUpstreamRetries = 3\n\tmax401Retries = 3\n)\n\nfunc maxAttemptsForUpstreamStatus(status int) int {\n\tif status == http.StatusUnauthorized {\n\t\treturn max401Retries\n\t}\n\treturn maxUpstreamRetries\n}\n\nfunc callOpenCodeAPI(ctx context.Context, upstreamBody []byte, modelID string, auth UpstreamAuth) ([]byte, int, http.Header, error) {\n\tinitOCSession()\n\n\tvar bodyMap map[string]any\n\tif err := json.Unmarshal(upstreamBody, &bodyMap); err != nil {\n\t\treturn nil, 500, nil, fmt.Errorf(\"invalid request body\")\n\t}\n\tuseGoEndpoint := auth.shouldUseGoEndpoint(modelID)\n\tsurface := \"zen\"\n\tif useGoEndpoint {\n\t\tsurface = \"go\"\n\t}\n\tlog := reqLogger(ctx)\n\n\tvar lastErr error\n\tvar retryCount int\n\tvar lastBody []byte\n\tvar lastStatus int\n\tvar lastHeader http.Header\n\tmaxAttempts := maxUpstreamRetries\n\tif max401Retries > maxAttempts {\n\t\tmaxAttempts = max401Retries\n\t}\n\n\tfor attempt := 0; attempt < maxAttempts; attempt++ {\n\t\tup, err := buildOCRequestWithEndpoint(modelID, bodyMap, auth, useGoEndpoint)\n\t\tif err != nil {\n\t\t\treturn nil, 500, nil, err\n\t\t}\n\t\tclient := getHTTPClientForTier(auth.tier())\n\t\tattemptStart := time.Now()\n\t\tresp, err := client.Do(up)\n\t\tdurationMs := time.Since(attemptStart).Milliseconds()\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tlastStatus = 0\n\t\t\tretryReason := \"transport_error\"\n\t\t\tcanRetry := attempt+1 < maxUpstreamRetries\n\t\t\tif !canRetry {\n\t\t\t\tretryReason = \"\"\n\t\t\t}\n\t\t\tlog.Info(\"upstream_attempt\",\n\t\t\t\t\"try_model\", modelID,\n\t\t\t\t\"surface\", surface,\n\t\t\t\t\"status\", 0,\n\t\t\t\t\"duration_ms\", durationMs,\n\t\t\t\t\"attempt_index\", attempt,\n\t\t\t\t\"retry_reason\", retryReason,\n\t\t\t\t\"error\", err.Error(),\n\t\t\t)\n\t\t\tif canRetry {\n\t\t\t\tclient.CloseIdleConnections()\n\t\t\t\tretryCount++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\t\tb, readErr := io.ReadAll(resp.Body)\n\t\t\tresp.Body.Close()\n\t\t\tif readErr != nil {\n\t\t\t\treturn nil, 0, nil, readErr\n\t\t\t}\n\t\t\tif isAnthropicFormat(b) {\n\t\t\t\tb = convertAnthropicToOpenAI(b, modelID)\n\t\t\t}\n\t\t\tlog.Info(\"upstream_attempt\",\n\t\t\t\t\"try_model\", modelID,\n\t\t\t\t\"surface\", surface,\n\t\t\t\t\"status\", resp.StatusCode,\n\t\t\t\t\"duration_ms\", durationMs,\n\t\t\t\t\"attempt_index\", attempt,\n\t\t\t)\n\t\t\tlog.Info(\"upstream_result\",\n\t\t\t\t\"models_tried\", []string{modelID},\n\t\t\t\t\"retries\", retryCount,\n\t\t\t\t\"final_status\", resp.StatusCode,\n\t\t\t\t\"fallback_used\", false,\n\t\t\t)\n\t\t\treturn b, resp.StatusCode, resp.Header, nil\n\t\t}\n\t\terrBody, _ := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tlogUpstreamError(ctx, modelID, resp.StatusCode, errBody)\n\t\tnonRetryable := isNonRetryableUpstreamError(resp.StatusCode, errBody)\n\t\tcanRetry := !nonRetryable && shouldRetryUpstreamStatus(resp.StatusCode) && attempt+1 < maxAttemptsForUpstreamStatus(resp.StatusCode)\n\t\tretryReason := \"\"\n\t\tif canRetry {\n\t\t\tretryReason = fmt.Sprintf(\"status_%d\", resp.StatusCode)\n\t\t}\n\t\tif nonRetryable {\n\t\t\tretryReason = \"non_retryable_upstream\"\n\t\t}\n\t\tlog.Info(\"upstream_attempt\",\n\t\t\t\"try_model\", modelID,\n\t\t\t\"surface\", surface,\n\t\t\t\"status\", resp.StatusCode,\n\t\t\t\"duration_ms\", durationMs,\n\t\t\t\"attempt_index\", attempt,\n\t\t\t\"retry_reason\", retryReason,\n\t\t)\n\t\tlastBody = errBody\n\t\tlastStatus = resp.StatusCode\n\t\tlastHeader = resp.Header\n\t\tlastErr = fmt.Errorf(\"upstream error\")\n\t\tif !canRetry {\n\t\t\tbreak\n\t\t}\n\t\tclient.CloseIdleConnections()\n\t\tretryCount++\n\t}\n\tlog.Info(\"upstream_result\",\n\t\t\"models_tried\", []string{modelID},\n\t\t\"retries\", retryCount,\n\t\t\"final_status\", lastStatus,\n\t\t\"fallback_used\", false,\n\t)\n\treturn lastBody, lastStatus, lastHeader, lastErr\n}\n\nfunc callOpenCodeAPIStream(ctx context.Context, upstreamBody []byte, modelID string, auth UpstreamAuth) (io.ReadCloser, int, http.Header, error) {\n\tinitOCSession()\n\n\tvar bodyMap map[string]any\n\tif err := json.Unmarshal(upstreamBody, &bodyMap); err != nil {\n\t\treturn nil, 500, nil, fmt.Errorf(\"invalid request body\")\n\t}\n\tuseGoEndpoint := auth.shouldUseGoEndpoint(modelID)\n\tsurface := \"zen\"\n\tif useGoEndpoint {\n\t\tsurface = \"go\"\n\t}\n\tlog := reqLogger(ctx)\n\n\tvar lastBody []byte\n\tvar lastStatus int\n\tvar lastHeader http.Header\n\tvar retryCount int\n\tmaxAttempts := maxUpstreamRetries\n\tif max401Retries > maxAttempts {\n\t\tmaxAttempts = max401Retries\n\t}\n\n\tfor attempt := 0; attempt < maxAttempts; attempt++ {\n\t\tup, err := buildOCRequestWithEndpoint(modelID, bodyMap, auth, useGoEndpoint)\n\t\tif err != nil {\n\t\t\treturn nil, 500, nil, err\n\t\t}\n\t\tclient := getHTTPClientForTier(auth.tier())\n\t\tattemptStart := time.Now()\n\t\tresp, err := client.Do(up)\n\t\tdurationMs := time.Since(attemptStart).Milliseconds()\n\t\tif err != nil {\n\t\t\tretryReason := \"transport_error\"\n\t\t\tcanRetry := attempt+1 < maxUpstreamRetries\n\t\t\tif !canRetry {\n\t\t\t\tretryReason = \"\"\n\t\t\t}\n\t\t\tlog.Info(\"upstream_attempt\",\n\t\t\t\t\"try_model\", modelID,\n\t\t\t\t\"surface\", surface,\n\t\t\t\t\"status\", 0,\n\t\t\t\t\"duration_ms\", durationMs,\n\t\t\t\t\"attempt_index\", attempt,\n\t\t\t\t\"retry_reason\", retryReason,\n\t\t\t\t\"error\", err.Error(),\n\t\t\t)\n\t\t\tif canRetry {\n\t\t\t\tclient.CloseIdleConnections()\n\t\t\t\tretryCount++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\t\tlog.Info(\"upstream_attempt\",\n\t\t\t\t\"try_model\", modelID,\n\t\t\t\t\"surface\", surface,\n\t\t\t\t\"status\", resp.StatusCode,\n\t\t\t\t\"duration_ms\", durationMs,\n\t\t\t\t\"attempt_index\", attempt,\n\t\t\t)\n\t\t\tlog.Info(\"upstream_result\",\n\t\t\t\t\"models_tried\", []string{modelID},\n\t\t\t\t\"retries\", retryCount,\n\t\t\t\t\"final_status\", resp.StatusCode,\n\t\t\t\t\"fallback_used\", false,\n\t\t\t)\n\t\t\treturn resp.Body, resp.StatusCode, resp.Header, nil\n\t\t}\n\t\terrBody, _ := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tlogUpstreamError(ctx, modelID, resp.StatusCode, errBody)\n\t\tnonRetryable := isNonRetryableUpstreamError(resp.StatusCode, errBody)\n\t\tcanRetry := !nonRetryable && shouldRetryUpstreamStatus(resp.StatusCode) && attempt+1 < maxAttemptsForUpstreamStatus(resp.StatusCode)\n\t\tretryReason := \"\"\n\t\tif canRetry {\n\t\t\tretryReason = fmt.Sprintf(\"status_%d\", resp.StatusCode)\n\t\t}\n\t\tif nonRetryable {\n\t\t\tretryReason = \"non_retryable_upstream\"\n\t\t}\n\t\tlog.Info(\"upstream_attempt\",\n\t\t\t\"try_model\", modelID,\n\t\t\t\"surface\", surface,\n\t\t\t\"status\", resp.StatusCode,\n\t\t\t\"duration_ms\", durationMs,\n\t\t\t\"attempt_index\", attempt,\n\t\t\t\"retry_reason\", retryReason,\n\t\t)\n\t\tlastBody = errBody\n\t\tlastStatus = resp.StatusCode\n\t\tlastHeader = resp.Header\n\t\tif !canRetry {\n\t\t\tbreak\n\t\t}\n\t\tclient.CloseIdleConnections()\n\t\tretryCount++\n\t}\n\tlog.Info(\"upstream_result\",\n\t\t\"models_tried\", []string{modelID},\n\t\t\"retries\", retryCount,\n\t\t\"final_status\", lastStatus,\n\t\t\"fallback_used\", false,\n\t)\n\tif lastStatus != 0 {\n\t\treturn io.NopCloser(bytes.NewReader(lastBody)), lastStatus, lastHeader, nil\n\t}\n\treturn nil, 500, nil, fmt.Errorf(\"upstream request failed\")\n}\n\n// ======================== 安全响应头过滤 ========================\n\nvar safeResponseHeaders = map[string]bool{\n\t\"Content-Type\": true,\n\t\"X-RateLimit-Limit\": true,\n\t\"X-RateLimit-Remaining\": true,\n\t\"X-RateLimit-Reset\": true,\n}\n\nfunc filterResponseHeaders(h http.Header) http.Header {\n\tfiltered := make(http.Header)\n\tfor k, v := range h {\n\t\tif safeResponseHeaders[k] {\n\t\t\tfiltered[k] = v\n\t\t}\n\t}\n\treturn filtered\n}\n\n// ======================== Chat Completions Handler ========================\n\nfunc chatCompletionsHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\tauth := extractUpstreamAuth(r)\n\tbody, err := io.ReadAll(io.LimitReader(r.Body, 10*1024*1024))\n\tif err != nil {\n\t\thttp.Error(w, \"Failed to read request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcnt := requestCount.Add(1)\n\tmaybeLogBodySummary(r.Context(), \"chat completion request body\", body)\n\t_ = cnt\n\n\tvar req OpenAIRequest\n\tif err := json.Unmarshal(body, &req); err != nil {\n\t\thttp.Error(w, \"Invalid JSON\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tmodelIn := req.Model\n\treq.Model = resolveModel(req.Model)\n\tif req.Model == \"\" {\n\t\tmodelIDs := getModelIDs()\n\t\tif len(modelIDs) > 0 {\n\t\t\treq.Model = modelIDs[0]\n\t\t} else {\n\t\t\treq.Model = \"deepseek-v4-flash-free\"\n\t\t}\n\t}\n\n\t// 多模态路由:检测到图片时转发到配置的上游\n\n\treq.Messages = fixToolCallGaps(req.Messages)\n\tkeepReasoning := wantsReasoning(&req)\n\treq.Messages = ensureReasoningContent(req.Messages, keepReasoning)\n\tif req.Stream {\n\t\tif req.ExtraBody == nil {\n\t\t\treq.ExtraBody = map[string]any{}\n\t\t}\n\t\treq.ExtraBody[\"stream_options\"] = map[string]any{\"include_usage\": true}\n\t}\n\teffortIn := req.ReasoningEffort\n\tif effortIn == \"\" && !isThinkingDisabled(req.Thinking) {\n\t\teffortIn = reasoningEffortFromThinking(req.Thinking)\n\t}\n\tupstreamSurface := \"zen\"\n\tif auth.shouldUseGoEndpoint(req.Model) {\n\t\tupstreamSurface = \"go\"\n\t}\n\tlogRequestPlan(r.Context(), map[string]any{\n\t\t\"protocol\": \"chat\",\n\t\t\"model_in\": modelIn,\n\t\t\"model_resolved\": req.Model,\n\t\t\"auth_mode\": authModeString(auth.Mode),\n\t\t\"auth_source\": auth.Source,\n\t\t\"has_key\": auth.Token != \"\",\n\t\t\"upstream_surface\": upstreamSurface,\n\t\t\"stream\": req.Stream,\n\t\t\"keep_reasoning\": keepReasoning,\n\t\t\"thinking\": thinkingState(req.Thinking),\n\t\t\"reasoning_effort_in\": effortIn,\n\t\t\"reasoning_effort_out\": mappedReasoningEffort(effortIn),\n\t\t\"tools_count\": len(req.Tools),\n\t\t\"messages_count\": len(req.Messages),\n\t})\n\tupstreamBody := buildUpstreamBody(&req)\n\n\tif req.Stream {\n\t\tupResp, status, _, err := callOpenCodeAPIStream(r.Context(), upstreamBody, req.Model, auth)\n\t\tif err != nil || status < 200 || status >= 300 {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.WriteHeader(status)\n\t\t\tif upResp != nil {\n\t\t\t\terrBody, _ := io.ReadAll(upResp)\n\t\t\t\tif len(errBody) > 0 {\n\t\t\t\t\tw.Write(errBody)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson.NewEncoder(w).Encode(map[string]any{\"error\": map[string]any{\"message\": \"upstream error\", \"type\": \"upstream_error\"}})\n\t\t\treturn\n\t\t}\n\t\tdefer upResp.Close()\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\t\tw.Header().Set(\"Connection\", \"keep-alive\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\treader := bufio.NewReader(upResp)\n\t\tstats := &streamResultStats{start: time.Now()}\n\t\tdoneSeen := false\n\t\tfor {\n\t\t\tline, err := reader.ReadString('\\n')\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treqLogger(r.Context()).Error(\"stream read error\", \"error\", err)\n\t\t\t\t// 发送错误事件通知客户端\n\t\t\t\tw.Write([]byte(\"data: {\\\"error\\\":\\\"stream read error\\\"}\\n\\n\"))\n\t\t\t\tif f, ok := w.(http.Flusher); ok {\n\t\t\t\t\tf.Flush()\n\t\t\t\t}\n\t\t\t\tstats.log(r.Context(), \"chat\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif doneSeen {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttrimmed := strings.TrimSpace(line)\n\t\t\tif trimmed == \"data: [DONE]\" {\n\t\t\t\tdoneSeen = true\n\t\t\t\tstats.doneSeen = true\n\t\t\t\tw.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t\t\t\tif f, ok := w.(http.Flusher); ok {\n\t\t\t\t\tf.Flush()\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(line, \"data: \") {\n\t\t\t\tvar raw map[string]any\n\t\t\t\tif json.Unmarshal([]byte(line[6:]), &raw) == nil {\n\t\t\t\t\tif choices, ok := raw[\"choices\"].([]any); ok && len(choices) > 0 {\n\t\t\t\t\t\tif choice, ok := choices[0].(map[string]any); ok {\n\t\t\t\t\t\t\tif delta, ok := choice[\"delta\"].(map[string]any); ok {\n\t\t\t\t\t\t\t\tstats.observeDelta(delta, keepReasoning)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif fr, ok := choice[\"finish_reason\"].(string); ok && fr != \"\" {\n\t\t\t\t\t\t\t\tstats.finishReason = fr\n\t\t\t\t\t\t\t\tstats.sawFinish = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tout, usage := convertStreamChunkWithUsage(line, keepReasoning)\n\t\t\tif out == \"\" {\n\t\t\t\t// 空choices chunk,但可能有 usage\n\t\t\t\tif usage != nil {\n\t\t\t\t\tpt, _ := usage[\"prompt_tokens\"].(float64)\n\t\t\t\t\tct, _ := usage[\"completion_tokens\"].(float64)\n\t\t\t\t\ttt, _ := usage[\"total_tokens\"].(float64)\n\t\t\t\t\tif tt > 0 {\n\t\t\t\t\t\trecordTokenUsage(req.Model, int64(pt), int64(ct), int64(tt))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 提取 usage(已在 convertStreamChunkWithUsage 中解析)\n\t\t\tif usage != nil && !doneSeen {\n\t\t\t\tpt, _ := usage[\"prompt_tokens\"].(float64)\n\t\t\t\tct, _ := usage[\"completion_tokens\"].(float64)\n\t\t\t\ttt, _ := usage[\"total_tokens\"].(float64)\n\t\t\t\tif tt > 0 {\n\t\t\t\t\trecordTokenUsage(req.Model, int64(pt), int64(ct), int64(tt))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw.Write([]byte(out))\n\t\t\tw.Write([]byte(\"\\n\"))\n\t\t\tif f, ok := w.(http.Flusher); ok {\n\t\t\t\tf.Flush()\n\t\t\t}\n\t\t}\n\t\tstats.log(r.Context(), \"chat\")\n\t\treturn\n\t}\n\n\trespBody, status, _, err := callOpenCodeAPI(r.Context(), upstreamBody, req.Model, auth)\n\tif err != nil || status < 200 || status >= 300 {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(status)\n\t\tif len(respBody) > 0 {\n\t\t\tw.Write(respBody)\n\t\t} else {\n\t\t\tjson.NewEncoder(w).Encode(map[string]any{\"error\": map[string]any{\"message\": \"upstream error\", \"type\": \"upstream_error\"}})\n\t\t}\n\t\treturn\n\t}\n\toutBody := respBody\n\tconvertedResp, err := convertResponse(respBody, keepReasoning)\n\tif err == nil {\n\t\toutBody = convertedResp\n\t}\n\tresult := summarizeChatResult(outBody)\n\tif !keepReasoning {\n\t\tvar before map[string]any\n\t\tif json.Unmarshal(respBody, &before) == nil {\n\t\t\tif choices, ok := before[\"choices\"].([]any); ok && len(choices) > 0 {\n\t\t\t\tif choice, ok := choices[0].(map[string]any); ok {\n\t\t\t\t\tif msg, ok := choice[\"message\"].(map[string]any); ok {\n\t\t\t\t\t\tcontent, _ := msg[\"content\"].(string)\n\t\t\t\t\t\trc, _ := msg[\"reasoning_content\"].(string)\n\t\t\t\t\t\tif content == \"\" && rc != \"\" {\n\t\t\t\t\t\t\tresult[\"promoted_reasoning\"] = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tlogRequestResult(r.Context(), result)\n\t// Record token usage\n\tvar usageResp map[string]any\n\tif json.Unmarshal(respBody, &usageResp) == nil {\n\t\tif u, ok := usageResp[\"usage\"].(map[string]any); ok {\n\t\t\tpt, _ := u[\"prompt_tokens\"].(float64)\n\t\t\tct, _ := u[\"completion_tokens\"].(float64)\n\t\t\ttt, _ := u[\"total_tokens\"].(float64)\n\t\t\tif tt > 0 {\n\t\t\t\trecordTokenUsage(req.Model, int64(pt), int64(ct), int64(tt))\n\t\t\t}\n\t\t}\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(status)\n\tw.Write(outBody)\n}\n\n// ======================== Models Handler ========================\n\nfunc listModelsHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tmodelMu.RLock()\n\tloaded, models := modelsLoaded, modelsCache\n\tmodelMu.RUnlock()\n\tif !loaded || len(models) == 0 {\n\t\tfetched, err := fetchModels()\n\t\tif err == nil && len(fetched) > 0 {\n\t\t\tmodelMu.Lock()\n\t\t\tmodelsCache = fetched\n\t\t\tmodelsLoaded = true\n\t\t\tmodels = modelsCache\n\t\t\tmodelMu.Unlock()\n\t\t}\n\t}\n\tif len(models) == 0 {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\tjson.NewEncoder(w).Encode(map[string]string{\n\t\t\t\"error\": \"无法获取模型列表,请检查上游服务是否可用\",\n\t\t})\n\t\treturn\n\t}\n\t// 保存别名快照;目录权限仍按真实上游模型判断,最后再替换为客户端可见名称。\n\tconfigMu.RLock()\n\taliases := make(map[string]string, len(modelAlias))\n\tfor alias, upstream := range modelAlias {\n\t\taliases[alias] = upstream\n\t}\n\tconfigMu.RUnlock()\n\n\tauth := extractUpstreamAuth(r)\n\tvar combinedModels []ModelInfo\n\tswitch {\n\tcase auth.shouldUseGoCatalog():\n\t\tmodelMu.RLock()\n\t\tcombinedModels = make([]ModelInfo, 0, len(models)+len(goModelsCache))\n\t\tfor _, model := range models {\n\t\t\tif isFreeModel(model.ID) {\n\t\t\t\tcombinedModels = append(combinedModels, model)\n\t\t\t}\n\t\t}\n\t\tfor _, goModel := range goModelsCache {\n\t\t\tif !containsModelWithID(combinedModels, goModel.ID) {\n\t\t\t\tcombinedModels = append(combinedModels, goModel)\n\t\t\t}\n\t\t}\n\t\tmodelMu.RUnlock()\n\tcase auth.Mode == AuthRoutePublic:\n\t\tcombinedModels = models\n\t\tfiltered := make([]ModelInfo, 0, len(combinedModels))\n\t\tfor _, m := range combinedModels {\n\t\t\tif isFreeModel(m.ID) {\n\t\t\t\tfiltered = append(filtered, m)\n\t\t\t}\n\t\t}\n\t\tif len(filtered) > 0 {\n\t\t\tcombinedModels = filtered\n\t\t}\n\tdefault:\n\t\tcombinedModels = models\n\t}\n\tallModels := replaceModelIDsWithAliases(combinedModels, aliases)\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(map[string]any{\n\t\t\"object\": \"list\",\n\t\t\"data\": allModels,\n\t})\n}\n\nfunc replaceModelIDsWithAliases(models []ModelInfo, aliases map[string]string) []ModelInfo {\n\taliasesByUpstream := make(map[string][]string, len(aliases))\n\tfor alias, upstream := range aliases {\n\t\talias = strings.TrimSpace(alias)\n\t\tupstream = strings.TrimSpace(upstream)\n\t\tif alias == \"\" || upstream == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\taliasesByUpstream[upstream] = append(aliasesByUpstream[upstream], alias)\n\t}\n\tfor upstream := range aliasesByUpstream {\n\t\tsort.Strings(aliasesByUpstream[upstream])\n\t}\n\n\tresult := make([]ModelInfo, 0, len(models))\n\tseen := make(map[string]struct{}, len(models))\n\tfor _, model := range models {\n\t\tvisibleIDs := aliasesByUpstream[model.ID]\n\t\tif len(visibleIDs) == 0 {\n\t\t\tvisibleIDs = []string{publicFacingModelID(model.ID)}\n\t\t}\n\t\tfor _, visibleID := range visibleIDs {\n\t\t\tif _, exists := seen[visibleID]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvisibleModel := model\n\t\t\tvisibleModel.ID = visibleID\n\t\t\tif visibleID != model.ID {\n\t\t\t\tvisibleModel.OwnedBy = \"alias\"\n\t\t\t}\n\t\t\tresult = append(result, visibleModel)\n\t\t\tseen[visibleID] = struct{}{}\n\t\t}\n\t}\n\treturn result\n}\n\n// ======================== Claude Messages API ========================\n\nfunc extractClaudeSystemText(system any) string {\n\tif system == nil {\n\t\treturn \"\"\n\t}\n\tswitch v := system.(type) {\n\tcase string:\n\t\treturn v\n\tcase []any:\n\t\tvar parts []string\n\t\tfor _, item := range v {\n\t\t\tif block, ok := item.(map[string]any); ok {\n\t\t\t\tif block[\"type\"] == \"text\" {\n\t\t\t\t\tif text, ok := block[\"text\"].(string); ok {\n\t\t\t\t\t\tparts = append(parts, text)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(parts, \"\\n\")\n\tdefault:\n\t\tb, _ := json.Marshal(v)\n\t\treturn string(b)\n\t}\n}\n\nfunc cleanJsonSchema(schema any) any {\n\tm, ok := schema.(map[string]any)\n\tif !ok {\n\t\treturn schema\n\t}\n\tclean := make(map[string]any, len(m))\n\tfor k, v := range m {\n\t\t// Annotation-only keys are omitted for upstream compatibility. Constraint\n\t\t// keys such as additionalProperties and format are preserved.\n\t\tif k == \"$schema\" || k == \"title\" || k == \"examples\" {\n\t\t\tcontinue\n\t\t}\n\t\tswitch child := v.(type) {\n\t\tcase map[string]any:\n\t\t\tclean[k] = cleanJsonSchema(child)\n\t\tcase []any:\n\t\t\tcopyArray := make([]any, len(child))\n\t\t\tfor i, elem := range child {\n\t\t\t\tcopyArray[i] = cleanJsonSchema(elem)\n\t\t\t}\n\t\t\tclean[k] = copyArray\n\t\tdefault:\n\t\t\tclean[k] = v\n\t\t}\n\t}\n\treturn clean\n}\n\nfunc claudeImageBlockToOpenAI(block map[string]any) (map[string]any, bool) {\n\tsource, _ := block[\"source\"].(map[string]any)\n\tif source == nil {\n\t\treturn nil, false\n\t}\n\tsrcType, _ := source[\"type\"].(string)\n\tmediaType, _ := source[\"media_type\"].(string)\n\tdata, _ := source[\"data\"].(string)\n\turl, _ := source[\"url\"].(string)\n\tif srcType == \"url\" && url != \"\" {\n\t\treturn map[string]any{\"type\": \"image_url\", \"image_url\": map[string]string{\"url\": url}}, true\n\t}\n\tif srcType == \"base64\" && data != \"\" {\n\t\tif mediaType == \"\" {\n\t\t\tmediaType = \"image/png\"\n\t\t}\n\t\treturn map[string]any{\n\t\t\t\"type\": \"image_url\",\n\t\t\t\"image_url\": map[string]string{\n\t\t\t\t\"url\": \"data:\" + mediaType + \";base64,\" + data,\n\t\t\t},\n\t\t}, true\n\t}\n\treturn nil, false\n}\n\nfunc extractClaudeContentText(content any) string {\n\tswitch c := content.(type) {\n\tcase string:\n\t\treturn c\n\tcase []any:\n\t\tvar parts []string\n\t\tfor _, item := range c {\n\t\t\tblock, ok := item.(map[string]any)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif block[\"type\"] == \"text\" {\n\t\t\t\tif text, ok := block[\"text\"].(string); ok && text != \"\" {\n\t\t\t\t\tparts = append(parts, text)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(parts, \"\\n\")\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc claudeToOpenAIMessages(claudeMsgs []ClaudeMessage, system any) []Message {\n\tvar systemParts []string\n\tif sysText := extractClaudeSystemText(system); sysText != \"\" {\n\t\tsystemParts = append(systemParts, sysText)\n\t}\n\n\tvar body []Message\n\tfor _, msg := range claudeMsgs {\n\t\tif msg.Role == \"system\" {\n\t\t\tif text := extractClaudeContentText(msg.Content); text != \"\" {\n\t\t\t\tsystemParts = append(systemParts, text)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch content := msg.Content.(type) {\n\t\tcase string:\n\t\t\tbody = append(body, Message{Role: msg.Role, Content: content})\n\t\tcase []any:\n\t\t\tvar orderedContent []any\n\t\t\tvar reasoningParts []string\n\t\t\tvar toolCalls []ToolCall\n\t\t\tvar toolResults []Message\n\t\t\tvar followupImages []any\n\t\t\tfor _, item := range content {\n\t\t\t\tblock, ok := item.(map[string]any)\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tblockType, _ := block[\"type\"].(string)\n\t\t\t\tswitch blockType {\n\t\t\t\tcase \"text\":\n\t\t\t\t\tif text, ok := block[\"text\"].(string); ok && text != \"\" {\n\t\t\t\t\t\torderedContent = append(orderedContent, map[string]any{\"type\": \"text\", \"text\": text})\n\t\t\t\t\t}\n\t\t\t\tcase \"image\":\n\t\t\t\t\tif part, ok := claudeImageBlockToOpenAI(block); ok {\n\t\t\t\t\t\torderedContent = append(orderedContent, part)\n\t\t\t\t\t}\n\t\t\t\tcase \"thinking\":\n\t\t\t\t\tif thinking, ok := block[\"thinking\"].(string); ok && thinking != \"\" {\n\t\t\t\t\t\treasoningParts = append(reasoningParts, thinking)\n\t\t\t\t\t}\n\t\t\t\tcase \"tool_use\":\n\t\t\t\t\tid, _ := block[\"id\"].(string)\n\t\t\t\t\tname, _ := block[\"name\"].(string)\n\t\t\t\t\tvar args string\n\t\t\t\t\tswitch input := block[\"input\"].(type) {\n\t\t\t\t\tcase string:\n\t\t\t\t\t\targs = input\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tif input != nil {\n\t\t\t\t\t\t\tb, _ := json.Marshal(input)\n\t\t\t\t\t\t\targs = string(b)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif args == \"\" {\n\t\t\t\t\t\targs = \"{}\"\n\t\t\t\t\t}\n\t\t\t\t\ttoolCalls = append(toolCalls, ToolCall{\n\t\t\t\t\t\tID: id,\n\t\t\t\t\t\tType: \"function\",\n\t\t\t\t\t\tFunction: FunctionCall{\n\t\t\t\t\t\t\tName: name,\n\t\t\t\t\t\t\tArguments: args,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\tcase \"tool_result\":\n\t\t\t\t\ttoolUseID, _ := block[\"tool_use_id\"].(string)\n\t\t\t\t\tvar resultText string\n\t\t\t\t\tvar imageParts []any\n\t\t\t\t\tswitch c := block[\"content\"].(type) {\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tresultText = c\n\t\t\t\t\tcase []any:\n\t\t\t\t\t\tvar parts []string\n\t\t\t\t\t\tfor _, p := range c {\n\t\t\t\t\t\t\tpb, ok := p.(map[string]any)\n\t\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tswitch pb[\"type\"] {\n\t\t\t\t\t\t\tcase \"text\":\n\t\t\t\t\t\t\t\tif t, ok := pb[\"text\"].(string); ok {\n\t\t\t\t\t\t\t\t\tparts = append(parts, t)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcase \"image\":\n\t\t\t\t\t\t\t\tif part, ok := claudeImageBlockToOpenAI(pb); ok {\n\t\t\t\t\t\t\t\t\timageParts = append(imageParts, part)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresultText = strings.Join(parts, \"\\n\")\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tif c != nil {\n\t\t\t\t\t\t\tb, _ := json.Marshal(c)\n\t\t\t\t\t\t\tresultText = string(b)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif len(imageParts) > 0 {\n\t\t\t\t\t\tif resultText != \"\" {\n\t\t\t\t\t\t\tresultText += \"\\n\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresultText += \"[image attached]\"\n\t\t\t\t\t\tfollowupImages = append(followupImages, imageParts...)\n\t\t\t\t\t}\n\t\t\t\t\tif isError, _ := block[\"is_error\"].(bool); isError {\n\t\t\t\t\t\tresultText = \"Error: \" + resultText\n\t\t\t\t\t}\n\t\t\t\t\ttoolResults = append(toolResults, Message{\n\t\t\t\t\t\tRole: \"tool\",\n\t\t\t\t\t\tToolCallID: toolUseID,\n\t\t\t\t\t\tContent: resultText,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tom := Message{Role: msg.Role}\n\t\t\tif len(orderedContent) > 0 {\n\t\t\t\tom.Content = orderedContent\n\t\t\t} else if len(toolCalls) == 0 {\n\t\t\t\tom.Content = \"\"\n\t\t\t}\n\t\t\tif len(reasoningParts) > 0 {\n\t\t\t\trc := strings.Join(reasoningParts, \"\\n\")\n\t\t\t\tom.ReasoningContent = &rc\n\t\t\t}\n\t\t\tif len(toolCalls) > 0 {\n\t\t\t\tom.ToolCalls = toolCalls\n\t\t\t}\n\t\t\t// Anthropic requires tool_result blocks to precede ordinary user\n\t\t\t// content. Preserve that order when translating them to Chat\n\t\t\t// Completions' separate tool messages.\n\t\t\tif msg.Role == \"user\" {\n\t\t\t\tbody = append(body, toolResults...)\n\t\t\t\tif len(followupImages) > 0 {\n\t\t\t\t\tbody = append(body, Message{Role: \"user\", Content: followupImages})\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(orderedContent) > 0 || len(reasoningParts) > 0 || len(toolCalls) > 0 || len(toolResults) == 0 {\n\t\t\t\tbody = append(body, om)\n\t\t\t}\n\t\t\tif msg.Role != \"user\" {\n\t\t\t\tbody = append(body, toolResults...)\n\t\t\t\tif len(followupImages) > 0 {\n\t\t\t\t\tbody = append(body, Message{Role: \"user\", Content: followupImages})\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\tb, _ := json.Marshal(content)\n\t\t\tbody = append(body, Message{Role: msg.Role, Content: string(b)})\n\t\t}\n\t}\n\n\tvar messages []Message\n\tif len(systemParts) > 0 {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: strings.Join(systemParts, \"\\n\\n\")})\n\t}\n\tmessages = append(messages, body...)\n\treturn messages\n}\n\nfunc claudeToOpenAITools(claudeTools []ClaudeTool) ([]Tool, []string) {\n\ttools := make([]Tool, 0, len(claudeTools))\n\tvar skipped []string\n\tfor _, ct := range claudeTools {\n\t\t// Server tools (web_search_*, etc.) carry a vendor type and no client schema.\n\t\t// Emitting them as empty function tools would invite bogus model calls.\n\t\tif ct.Type != \"\" && ct.InputSchema == nil {\n\t\t\tskipped = append(skipped, ct.Name)\n\t\t\tcontinue\n\t\t}\n\t\tparams := ct.InputSchema\n\t\tif params == nil {\n\t\t\tparams = map[string]any{\"type\": \"object\", \"properties\": map[string]any{}}\n\t\t}\n\t\tparams = cleanJsonSchema(params)\n\t\tparamsMap, ok := params.(map[string]any)\n\t\tif !ok {\n\t\t\tparamsMap = map[string]any{\"type\": \"object\", \"properties\": map[string]any{}}\n\t\t}\n\t\ttools = append(tools, Tool{\n\t\t\tType: \"function\",\n\t\t\tFunction: ToolFunction{\n\t\t\t\tName: ct.Name,\n\t\t\t\tDescription: ct.Description,\n\t\t\t\tParameters: paramsMap,\n\t\t\t},\n\t\t})\n\t}\n\treturn tools, skipped\n}\n\nfunc countClaudeSystemParts(msgs []ClaudeMessage, system any) int {\n\tn := 0\n\tif extractClaudeSystemText(system) != \"\" {\n\t\tn++\n\t}\n\tfor _, msg := range msgs {\n\t\tif msg.Role == \"system\" && extractClaudeContentText(msg.Content) != \"\" {\n\t\t\tn++\n\t\t}\n\t}\n\treturn n\n}\n\nfunc countAnthropicBetas(header string) int {\n\theader = strings.TrimSpace(header)\n\tif header == \"\" {\n\t\treturn 0\n\t}\n\tn := 0\n\tfor _, part := range strings.Split(header, \",\") {\n\t\tif strings.TrimSpace(part) != \"\" {\n\t\t\tn++\n\t\t}\n\t}\n\treturn n\n}\n\nfunc countCacheControlInValue(v any) int {\n\tswitch x := v.(type) {\n\tcase map[string]any:\n\t\tn := 0\n\t\tif _, ok := x[\"cache_control\"]; ok {\n\t\t\tn++\n\t\t}\n\t\tfor _, child := range x {\n\t\t\tn += countCacheControlInValue(child)\n\t\t}\n\t\treturn n\n\tcase []any:\n\t\tn := 0\n\t\tfor _, child := range x {\n\t\t\tn += countCacheControlInValue(child)\n\t\t}\n\t\treturn n\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc countClaudeCacheControlBlocks(req ClaudeRequest) int {\n\tn := countCacheControlInValue(req.System)\n\tfor _, msg := range req.Messages {\n\t\tn += countCacheControlInValue(msg.Content)\n\t}\n\tfor _, tool := range req.Tools {\n\t\tn += countCacheControlInValue(tool.InputSchema)\n\t}\n\treturn n\n}\n\nvar claudeUnsupportedBlockTypes = map[string]struct{}{\n\t\"redacted_thinking\": {},\n\t\"document\": {},\n\t\"search_result\": {},\n\t\"server_tool_use\": {},\n\t\"web_search_tool_result\": {},\n\t\"container_upload\": {},\n}\n\nfunc scanClaudeUnsupportedBlocks(msgs []ClaudeMessage) map[string]int {\n\tcounts := map[string]int{}\n\tvar walk func(any)\n\twalk = func(v any) {\n\t\tswitch x := v.(type) {\n\t\tcase map[string]any:\n\t\t\tif t, _ := x[\"type\"].(string); t != \"\" {\n\t\t\t\tif _, ok := claudeUnsupportedBlockTypes[t]; ok {\n\t\t\t\t\tcounts[t]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, child := range x {\n\t\t\t\twalk(child)\n\t\t\t}\n\t\tcase []any:\n\t\t\tfor _, child := range x {\n\t\t\t\twalk(child)\n\t\t\t}\n\t\t}\n\t}\n\tfor _, msg := range msgs {\n\t\twalk(msg.Content)\n\t}\n\tif len(counts) == 0 {\n\t\treturn nil\n\t}\n\treturn counts\n}\n\nfunc openAIToClaudeResponse(chatBody []byte, model string, wantReasoning bool) []byte {\n\tvar chat struct {\n\t\tID string `json:\"id\"`\n\t\tModel string `json:\"model\"`\n\t\tCreated int64 `json:\"created\"`\n\t\tChoices []struct {\n\t\t\tMessage struct {\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t\tReasoningContent string `json:\"reasoning_content\"`\n\t\t\t\tToolCalls []ToolCall `json:\"tool_calls\"`\n\t\t\t} `json:\"message\"`\n\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t} `json:\"choices\"`\n\t\tUsage map[string]any `json:\"usage\"`\n\t}\n\tif err := json.Unmarshal(chatBody, &chat); err != nil {\n\t\tslog.Warn(\"openAIToClaudeResponse unmarshal failed\", \"error\", err)\n\t}\n\n\tcontent := []ClaudeContent{}\n\tstopReason := \"end_turn\"\n\n\tif len(chat.Choices) > 0 {\n\t\tmsg := chat.Choices[0].Message\n\t\tfr := chat.Choices[0].FinishReason\n\t\tif wantReasoning && msg.ReasoningContent != \"\" {\n\t\t\tcontent = append(content, ClaudeContent{\n\t\t\t\tType: \"thinking\",\n\t\t\t\tThinking: msg.ReasoningContent,\n\t\t\t})\n\t\t}\n\t\ttext := msg.Content\n\t\t// #37635: Go gateway often puts the whole answer in reasoning_content.\n\t\t// Promote to text when content is empty so Claude Code does not see an\n\t\t// empty end_turn and exit the agent loop.\n\t\tif text == \"\" && msg.ReasoningContent != \"\" && len(msg.ToolCalls) == 0 {\n\t\t\ttext = msg.ReasoningContent\n\t\t}\n\t\tif text != \"\" {\n\t\t\tcontent = append(content, ClaudeContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: text,\n\t\t\t})\n\t\t}\n\t\tfor _, tc := range msg.ToolCalls {\n\t\t\tvar input any\n\t\t\tjson.Unmarshal([]byte(tc.Function.Arguments), &input)\n\t\t\tif input == nil {\n\t\t\t\tinput = map[string]any{}\n\t\t\t}\n\t\t\tcontent = append(content, ClaudeContent{\n\t\t\t\tType: \"tool_use\",\n\t\t\t\tID: tc.ID,\n\t\t\t\tName: tc.Function.Name,\n\t\t\t\tInput: input,\n\t\t\t})\n\t\t}\n\t\tswitch fr {\n\t\tcase \"stop\":\n\t\t\tstopReason = \"end_turn\"\n\t\tcase \"length\":\n\t\t\tstopReason = \"max_tokens\"\n\t\tcase \"tool_calls\", \"function_call\":\n\t\t\tstopReason = \"tool_use\"\n\t\tcase \"content_filter\":\n\t\t\tstopReason = \"refusal\"\n\t\t}\n\t}\n\n\tif len(content) == 0 {\n\t\tcontent = append(content, ClaudeContent{Type: \"text\", Text: \"\"})\n\t}\n\n\tresp := ClaudeResponse{\n\t\tID: fmt.Sprintf(\"msg_%s\", randomString(24)),\n\t\tType: \"message\",\n\t\tRole: \"assistant\",\n\t\tContent: content,\n\t\tModel: model,\n\t\tStopReason: stopReason,\n\t}\n\tif chat.Usage != nil {\n\t\tresp.Usage = buildClaudeMessageUsage(chat.Usage)\n\t}\n\tresult, _ := json.Marshal(resp)\n\treturn result\n}\n\nfunc toFloat64(v any) float64 {\n\tswitch n := v.(type) {\n\tcase float64:\n\t\treturn n\n\tcase int:\n\t\treturn float64(n)\n\tcase int64:\n\t\treturn float64(n)\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc usageIntField(fields map[string]any, key string) (int, bool) {\n\tif fields == nil {\n\t\treturn 0, false\n\t}\n\tvalue, ok := fields[key]\n\tif !ok || value == nil {\n\t\treturn 0, false\n\t}\n\treturn int(toFloat64(value)), true\n}\n\nfunc usageMapField(fields map[string]any, key string) (map[string]any, bool) {\n\tif fields == nil {\n\t\treturn nil, false\n\t}\n\tvalue, ok := fields[key]\n\tif !ok || value == nil {\n\t\treturn nil, false\n\t}\n\tmapped, ok := value.(map[string]any)\n\treturn mapped, ok\n}\n\nfunc buildClaudeUsageCore(upstreamUsage map[string]any) ClaudeUsage {\n\tif len(upstreamUsage) == 0 {\n\t\treturn nil\n\t}\n\n\tusage := ClaudeUsage{}\n\tif value, ok := usageIntField(upstreamUsage, \"prompt_tokens\"); ok {\n\t\tusage[\"input_tokens\"] = value\n\t}\n\tif value, ok := usageIntField(upstreamUsage, \"input_tokens\"); ok {\n\t\tif _, exists := usage[\"input_tokens\"]; !exists {\n\t\t\tusage[\"input_tokens\"] = value\n\t\t}\n\t}\n\tif value, ok := usageIntField(upstreamUsage, \"completion_tokens\"); ok {\n\t\tusage[\"output_tokens\"] = value\n\t}\n\tif value, ok := usageIntField(upstreamUsage, \"output_tokens\"); ok {\n\t\tif _, exists := usage[\"output_tokens\"]; !exists {\n\t\t\tusage[\"output_tokens\"] = value\n\t\t}\n\t}\n\tif value, ok := usageIntField(upstreamUsage, \"cache_creation_input_tokens\"); ok {\n\t\tusage[\"cache_creation_input_tokens\"] = value\n\t}\n\tif value, ok := usageIntField(upstreamUsage, \"cache_read_input_tokens\"); ok {\n\t\tusage[\"cache_read_input_tokens\"] = value\n\t} else if promptDetails, ok := usageMapField(upstreamUsage, \"prompt_tokens_details\"); ok {\n\t\tif value, ok := usageIntField(promptDetails, \"cached_tokens\"); ok {\n\t\t\tusage[\"cache_read_input_tokens\"] = value\n\t\t}\n\t}\n\tif outputDetails, ok := usageMapField(upstreamUsage, \"output_tokens_details\"); ok {\n\t\tusage[\"output_tokens_details\"] = outputDetails\n\t} else if outputDetails, ok := usageMapField(upstreamUsage, \"completion_tokens_details\"); ok {\n\t\tusage[\"output_tokens_details\"] = outputDetails\n\t}\n\tif serverToolUse, ok := usageMapField(upstreamUsage, \"server_tool_use\"); ok {\n\t\tusage[\"server_tool_use\"] = serverToolUse\n\t}\n\tif len(usage) == 0 {\n\t\treturn nil\n\t}\n\treturn usage\n}\n\nfunc buildClaudeMessageUsage(upstreamUsage map[string]any) ClaudeUsage {\n\tusage := buildClaudeUsageCore(upstreamUsage)\n\tif usage == nil {\n\t\tusage = ClaudeUsage{}\n\t}\n\tif cacheCreation, ok := usageMapField(upstreamUsage, \"cache_creation\"); ok {\n\t\tusage[\"cache_creation\"] = cacheCreation\n\t}\n\tif serviceTier, ok := upstreamUsage[\"service_tier\"].(string); ok && serviceTier != \"\" {\n\t\tusage[\"service_tier\"] = serviceTier\n\t}\n\tif inferenceGeo, ok := upstreamUsage[\"inference_geo\"].(string); ok && inferenceGeo != \"\" {\n\t\tusage[\"inference_geo\"] = inferenceGeo\n\t}\n\tif _, exists := usage[\"input_tokens\"]; !exists {\n\t\tusage[\"input_tokens\"] = 0\n\t}\n\tif _, exists := usage[\"output_tokens\"]; !exists {\n\t\tusage[\"output_tokens\"] = 0\n\t}\n\treturn usage\n}\n\nfunc buildClaudeDeltaUsage(upstreamUsage map[string]any) ClaudeUsage {\n\tusage := buildClaudeUsageCore(upstreamUsage)\n\tif usage == nil {\n\t\tusage = ClaudeUsage{}\n\t}\n\tif _, exists := usage[\"output_tokens\"]; !exists {\n\t\tusage[\"output_tokens\"] = 0\n\t}\n\treturn usage\n}\n\nfunc claudeMessagesHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\tauth := extractUpstreamAuth(r)\n\tbody, err := io.ReadAll(io.LimitReader(r.Body, 10*1024*1024))\n\tif err != nil {\n\t\thttp.Error(w, \"Failed to read request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcnt := requestCount.Add(1)\n\tmaybeLogBodySummary(r.Context(), \"claude messages request body\", body)\n\t_ = cnt\n\n\tvar claudeReq ClaudeRequest\n\tif err := json.Unmarshal(body, &claudeReq); err != nil {\n\t\thttp.Error(w, `{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"Invalid JSON\"}}`, http.StatusBadRequest)\n\t\treturn\n\t}\n\tmodelIn := claudeReq.Model\n\tclaudeReq.Model = resolveModel(claudeReq.Model)\n\n\t// 多模态路由\n\n\tchatReq, skippedServerTools := convertClaudeRequest(claudeReq)\n\tchatReq.Messages = fixToolCallGaps(chatReq.Messages)\n\tif claudeReq.Stream {\n\t\tif chatReq.ExtraBody == nil {\n\t\t\tchatReq.ExtraBody = map[string]any{}\n\t\t}\n\t\tchatReq.ExtraBody[\"stream_options\"] = map[string]any{\"include_usage\": true}\n\t}\n\n\t// Keep CoT by default so Claude Code still sees thinking blocks. Only drop\n\t// reasoning when force-disabled or the client explicitly disables thinking.\n\t// Empty-reply protection is handled by promoteMisplacedReasoning (!keep)\n\t// and emitEmptyTextFallback (keep + no text/tool_use).\n\twantReasoning := !getForceDisableThinking()\n\tif claudeReq.Thinking != nil && isThinkingDisabled(claudeReq.Thinking) {\n\t\twantReasoning = false\n\t}\n\tkeepReasoning := wantReasoning\n\tchatReq.Messages = ensureReasoningContent(chatReq.Messages, keepReasoning)\n\n\teffortIn := chatReq.ReasoningEffort\n\tif effortIn == \"\" && !isThinkingDisabled(claudeReq.Thinking) {\n\t\teffortIn = reasoningEffortFromThinking(claudeReq.Thinking)\n\t}\n\tupstreamSurface := \"zen\"\n\tif auth.shouldUseGoEndpoint(chatReq.Model) {\n\t\tupstreamSurface = \"go\"\n\t}\n\tsystemMerged := countClaudeSystemParts(claudeReq.Messages, claudeReq.System) > 1\n\tplan := map[string]any{\n\t\t\"protocol\": \"claude\",\n\t\t\"model_in\": modelIn,\n\t\t\"model_resolved\": chatReq.Model,\n\t\t\"auth_mode\": authModeString(auth.Mode),\n\t\t\"auth_source\": auth.Source,\n\t\t\"has_key\": auth.Token != \"\",\n\t\t\"upstream_surface\": upstreamSurface,\n\t\t\"stream\": claudeReq.Stream,\n\t\t\"keep_reasoning\": keepReasoning,\n\t\t\"thinking\": thinkingState(claudeReq.Thinking),\n\t\t\"reasoning_effort_in\": effortIn,\n\t\t\"reasoning_effort_out\": mappedReasoningEffort(effortIn),\n\t\t\"tools_count\": len(chatReq.Tools),\n\t\t\"messages_count\": len(chatReq.Messages),\n\t\t\"system_merged\": systemMerged,\n\t\t\"context_management\": claudeReq.ContextManagement != nil,\n\t\t\"cache_control_blocks\": countClaudeCacheControlBlocks(claudeReq),\n\t\t\"client_beta_count\": countAnthropicBetas(r.Header.Get(\"anthropic-beta\")),\n\t\t\"unsupported_blocks\": scanClaudeUnsupportedBlocks(claudeReq.Messages),\n\t}\n\tif len(skippedServerTools) > 0 {\n\t\tplan[\"skipped_server_tools\"] = skippedServerTools\n\t}\n\tlogRequestPlan(r.Context(), plan)\n\n\tupstreamBody := buildUpstreamBody(&chatReq)\n\n\tif claudeReq.Stream {\n\t\tupResp, status, _, err := callOpenCodeAPIStream(r.Context(), upstreamBody, chatReq.Model, auth)\n\t\tif err != nil || status < 200 || status >= 300 {\n\t\t\terrResp := map[string]any{\n\t\t\t\t\"type\": \"error\",\n\t\t\t\t\"error\": map[string]string{\"type\": \"api_error\", \"message\": \"upstream error\"},\n\t\t\t}\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.WriteHeader(status)\n\t\t\tjson.NewEncoder(w).Encode(errResp)\n\t\t\treturn\n\t\t}\n\t\tdefer upResp.Close()\n\t\tclaudeStreamHandler(r.Context(), w, upResp, claudeReq.Model, keepReasoning)\n\t\treturn\n\t}\n\n\trespBody, status, _, err := callOpenCodeAPI(r.Context(), upstreamBody, chatReq.Model, auth)\n\tif err != nil || status < 200 || status >= 300 {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(status)\n\t\tif len(respBody) > 0 {\n\t\t\tw.Write(respBody)\n\t\t} else {\n\t\t\tjson.NewEncoder(w).Encode(map[string]any{\"type\": \"error\", \"error\": map[string]string{\"type\": \"api_error\", \"message\": \"upstream error\"}})\n\t\t}\n\t\treturn\n\t}\n\n\tclaudeRespBody := openAIToClaudeResponse(respBody, claudeReq.Model, wantReasoning)\n\tresult := summarizeClaudeResult(claudeRespBody)\n\tif !wantReasoning {\n\t\tvar before map[string]any\n\t\tif json.Unmarshal(respBody, &before) == nil {\n\t\t\tif choices, ok := before[\"choices\"].([]any); ok && len(choices) > 0 {\n\t\t\t\tif choice, ok := choices[0].(map[string]any); ok {\n\t\t\t\t\tif msg, ok := choice[\"message\"].(map[string]any); ok {\n\t\t\t\t\t\tcontent, _ := msg[\"content\"].(string)\n\t\t\t\t\t\trc, _ := msg[\"reasoning_content\"].(string)\n\t\t\t\t\t\tif content == \"\" && rc != \"\" {\n\t\t\t\t\t\t\tresult[\"promoted_reasoning\"] = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tlogRequestResult(r.Context(), result)\n\n\t// Record token usage\n\tvar usageResp map[string]any\n\tif json.Unmarshal(respBody, &usageResp) == nil {\n\t\tif u, ok := usageResp[\"usage\"].(map[string]any); ok {\n\t\t\tpt, _ := u[\"prompt_tokens\"].(float64)\n\t\t\tct, _ := u[\"completion_tokens\"].(float64)\n\t\t\ttt, _ := u[\"total_tokens\"].(float64)\n\t\t\tif tt > 0 {\n\t\t\t\trecordTokenUsage(claudeReq.Model, int64(pt), int64(ct), int64(tt))\n\t\t\t}\n\t\t}\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(http.StatusOK)\n\tmaybeLogBodySummary(r.Context(), \"claude response body\", claudeRespBody)\n\tw.Write(claudeRespBody)\n}\n\nfunc claudeStreamHandler(ctx context.Context, w http.ResponseWriter, respBody io.ReadCloser, model string, keepReasoning bool) {\n\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\tw.Header().Set(\"Connection\", \"keep-alive\")\n\tw.WriteHeader(http.StatusOK)\n\n\tflusher, _ := w.(http.Flusher)\n\treader := bufio.NewReader(respBody)\n\tstats := &streamResultStats{start: time.Now()}\n\n\tmsgID := fmt.Sprintf(\"msg_%s\", randomString(24))\n\tblockIndex := 0\n\tthinkingBlockOpen := false\n\ttextBlockOpen := false\n\ttoolCallAccumulator := map[int]map[string]string{}\n\ttoolBlockIndices := map[int]int{}\n\ttoolCallOrder := []int{}\n\tmessageStartSent := false\n\tfinished := false\n\tstopReason := \"end_turn\"\n\tfullUsage := map[string]any{}\n\t// Accumulates reasoning when keepReasoning so we can fall back to a text\n\t// block if the stream never produces content/tool_use (#37635).\n\treasoningFallback := strings.Builder{}\n\tdefer func() {\n\t\tif len(fullUsage) > 0 {\n\t\t\tpt, _ := fullUsage[\"prompt_tokens\"].(float64)\n\t\t\tct, _ := fullUsage[\"completion_tokens\"].(float64)\n\t\t\ttt, _ := fullUsage[\"total_tokens\"].(float64)\n\t\t\tif tt > 0 {\n\t\t\t\trecordTokenUsage(model, int64(pt), int64(ct), int64(tt))\n\t\t\t}\n\t\t}\n\t\tstats.toolCallCount = len(toolCallOrder)\n\t\tstats.log(ctx, \"claude\")\n\t}()\n\n\temitClaudeEvent := func(event string, data any) {\n\t\tjsonData, err := json.Marshal(data)\n\t\tif err != nil {\n\t\t\treqLogger(ctx).Error(\"marshal SSE event failed\", \"error\", err)\n\t\t\treturn\n\t\t}\n\t\tw.Write([]byte(\"event: \" + event + \"\\n\"))\n\t\tw.Write([]byte(\"data: \" + string(jsonData) + \"\\n\\n\"))\n\t\tif flusher != nil {\n\t\t\tflusher.Flush()\n\t\t}\n\t}\n\n\tcloseThinkingBlock := func() {\n\t\tif !thinkingBlockOpen {\n\t\t\treturn\n\t\t}\n\t\temitClaudeEvent(\"content_block_stop\", map[string]any{\n\t\t\t\"type\": \"content_block_stop\",\n\t\t\t\"index\": blockIndex - 1,\n\t\t\t\"content_block\": map[string]any{\"type\": \"thinking\"},\n\t\t})\n\t\tthinkingBlockOpen = false\n\t}\n\n\tcloseTextBlock := func() {\n\t\tif !textBlockOpen {\n\t\t\treturn\n\t\t}\n\t\temitClaudeEvent(\"content_block_stop\", map[string]any{\n\t\t\t\"type\": \"content_block_stop\",\n\t\t\t\"index\": blockIndex - 1,\n\t\t\t\"content_block\": map[string]any{\"type\": \"text\"},\n\t\t})\n\t\ttextBlockOpen = false\n\t}\n\n\tensureMessageStart := func() {\n\t\tif messageStartSent {\n\t\t\treturn\n\t\t}\n\t\tmessageStartSent = true\n\t\temitClaudeEvent(\"message_start\", map[string]any{\n\t\t\t\"type\": \"message_start\",\n\t\t\t\"message\": map[string]any{\n\t\t\t\t\"id\": msgID,\n\t\t\t\t\"type\": \"message\",\n\t\t\t\t\"role\": \"assistant\",\n\t\t\t\t\"content\": []any{},\n\t\t\t\t\"model\": model,\n\t\t\t\t\"stop_reason\": nil,\n\t\t\t\t\"usage\": buildClaudeMessageUsage(fullUsage),\n\t\t\t},\n\t\t})\n\t\temitClaudeEvent(\"ping\", map[string]any{\"type\": \"ping\"})\n\t}\n\n\temitTextDelta := func(contentStr string) {\n\t\tif contentStr == \"\" {\n\t\t\treturn\n\t\t}\n\t\tstats.textChars += len(contentStr)\n\t\tcloseThinkingBlock()\n\t\tif !textBlockOpen {\n\t\t\temitClaudeEvent(\"content_block_start\", map[string]any{\n\t\t\t\t\"type\": \"content_block_start\",\n\t\t\t\t\"index\": blockIndex,\n\t\t\t\t\"content_block\": map[string]any{\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"text\": \"\",\n\t\t\t\t},\n\t\t\t})\n\t\t\ttextBlockOpen = true\n\t\t\tblockIndex++\n\t\t}\n\t\temitClaudeEvent(\"content_block_delta\", map[string]any{\n\t\t\t\"type\": \"content_block_delta\",\n\t\t\t\"index\": blockIndex - 1,\n\t\t\t\"delta\": map[string]any{\n\t\t\t\t\"type\": \"text_delta\",\n\t\t\t\t\"text\": contentStr,\n\t\t\t},\n\t\t})\n\t}\n\n\temitEmptyTextFallback := func() {\n\t\tif textBlockOpen || len(toolCallOrder) > 0 {\n\t\t\treturn\n\t\t}\n\t\tfallback := reasoningFallback.String()\n\t\tif fallback == \"\" {\n\t\t\treturn\n\t\t}\n\t\tstats.promotedReasoning = true\n\t\temitTextDelta(fallback)\n\t}\n\n\tfinalizeContentBlocks := func() {\n\t\temitEmptyTextFallback()\n\t\tcloseThinkingBlock()\n\t\tcloseTextBlock()\n\t\tfor _, idx := range toolCallOrder {\n\t\t\tacc := toolCallAccumulator[idx]\n\t\t\temitClaudeEvent(\"content_block_stop\", map[string]any{\n\t\t\t\t\"type\": \"content_block_stop\",\n\t\t\t\t\"index\": toolBlockIndices[idx],\n\t\t\t\t\"content_block\": map[string]any{\n\t\t\t\t\t\"type\": \"tool_use\",\n\t\t\t\t\t\"id\": acc[\"id\"],\n\t\t\t\t\t\"name\": acc[\"name\"],\n\t\t\t\t\t\"input\": map[string]any{},\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\tfor {\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treqLogger(ctx).Error(\"stream read error\", \"error\", err)\n\t\t\tbreak\n\t\t}\n\n\t\ttrimmed := strings.TrimSpace(line)\n\t\tif trimmed == \"data: [DONE]\" || trimmed == \"[DONE]\" {\n\t\t\tstats.doneSeen = true\n\t\t\tbreak\n\t\t}\n\t\tif !strings.HasPrefix(line, \"data: \") {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar chunk map[string]any\n\t\tif err := json.Unmarshal([]byte(line[6:]), &chunk); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif usage, ok := chunk[\"usage\"].(map[string]any); ok {\n\t\t\tfullUsage = usage\n\t\t}\n\n\t\tchoices, ok := chunk[\"choices\"].([]any)\n\t\tif !ok || len(choices) == 0 {\n\t\t\t// Usage-only trailing chunk (OpenAI stream_options.include_usage).\n\t\t\tcontinue\n\t\t}\n\n\t\tchoice, _ := choices[0].(map[string]any)\n\t\tdelta, _ := choice[\"delta\"].(map[string]any)\n\t\tfinishReason, _ := choice[\"finish_reason\"].(string)\n\t\tstats.noteChunk()\n\n\t\tensureMessageStart()\n\n\t\t// After finish_reason, ignore further content deltas but keep reading\n\t\t// so a later usage-only chunk can populate fullUsage.\n\t\tif finished {\n\t\t\tcontinue\n\t\t}\n\n\t\tif rc, ok := delta[\"reasoning_content\"]; ok {\n\t\t\trcStr, _ := rc.(string)\n\t\t\tif rcStr != \"\" {\n\t\t\t\tstats.reasoningChars += len(rcStr)\n\t\t\t\tif keepReasoning {\n\t\t\t\t\treasoningFallback.WriteString(rcStr)\n\t\t\t\t\tcloseTextBlock()\n\t\t\t\t\tif !thinkingBlockOpen {\n\t\t\t\t\t\temitClaudeEvent(\"content_block_start\", map[string]any{\n\t\t\t\t\t\t\t\"type\": \"content_block_start\",\n\t\t\t\t\t\t\t\"index\": blockIndex,\n\t\t\t\t\t\t\t\"content_block\": map[string]any{\n\t\t\t\t\t\t\t\t\"type\": \"thinking\",\n\t\t\t\t\t\t\t\t\"thinking\": \"\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t\tthinkingBlockOpen = true\n\t\t\t\t\t\tblockIndex++\n\t\t\t\t\t}\n\t\t\t\t\temitClaudeEvent(\"content_block_delta\", map[string]any{\n\t\t\t\t\t\t\"type\": \"content_block_delta\",\n\t\t\t\t\t\t\"index\": blockIndex - 1,\n\t\t\t\t\t\t\"delta\": map[string]any{\n\t\t\t\t\t\t\t\"type\": \"thinking_delta\",\n\t\t\t\t\t\t\t\"thinking\": rcStr,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\t// Thinking not requested: promote misplaced CoT to visible text (#37635).\n\t\t\t\t\tstats.promotedReasoning = true\n\t\t\t\t\temitTextDelta(rcStr)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif c, ok := delta[\"content\"]; ok && c != nil {\n\t\t\tcontentStr, _ := c.(string)\n\t\t\tif contentStr != \"\" {\n\t\t\t\temitTextDelta(contentStr)\n\t\t\t}\n\t\t}\n\n\t\tif rawToolCalls, ok := delta[\"tool_calls\"].([]any); ok {\n\t\t\tfor _, rawTC := range rawToolCalls {\n\t\t\t\ttc, ok := rawTC.(map[string]any)\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tidxFloat, _ := tc[\"index\"].(float64)\n\t\t\t\tupstreamIndex := int(idxFloat)\n\n\t\t\t\tcloseThinkingBlock()\n\t\t\t\tcloseTextBlock()\n\n\t\t\t\tif _, exists := toolCallAccumulator[upstreamIndex]; !exists {\n\t\t\t\t\tcallID, _ := tc[\"id\"].(string)\n\t\t\t\t\tif callID == \"\" {\n\t\t\t\t\t\tcallID = \"toolu_\" + randomString(12)\n\t\t\t\t\t}\n\t\t\t\t\tfn, _ := tc[\"function\"].(map[string]any)\n\t\t\t\t\tname, _ := fn[\"name\"].(string)\n\t\t\t\t\ttoolCallAccumulator[upstreamIndex] = map[string]string{\n\t\t\t\t\t\t\"id\": callID,\n\t\t\t\t\t\t\"name\": name,\n\t\t\t\t\t\t\"args\": \"\",\n\t\t\t\t\t}\n\t\t\t\t\ttoolCallOrder = append(toolCallOrder, upstreamIndex)\n\t\t\t\t\ttoolBlockIndices[upstreamIndex] = blockIndex\n\t\t\t\t\temitClaudeEvent(\"content_block_start\", map[string]any{\n\t\t\t\t\t\t\"type\": \"content_block_start\",\n\t\t\t\t\t\t\"index\": blockIndex,\n\t\t\t\t\t\t\"content_block\": map[string]any{\n\t\t\t\t\t\t\t\"type\": \"tool_use\",\n\t\t\t\t\t\t\t\"id\": callID,\n\t\t\t\t\t\t\t\"name\": name,\n\t\t\t\t\t\t\t\"input\": map[string]any{},\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\tblockIndex++\n\t\t\t\t}\n\n\t\t\t\tfn, _ := tc[\"function\"].(map[string]any)\n\t\t\t\tif argDelta, ok := fn[\"arguments\"].(string); ok && argDelta != \"\" {\n\t\t\t\t\ttoolCallAccumulator[upstreamIndex][\"args\"] += argDelta\n\t\t\t\t\temitClaudeEvent(\"content_block_delta\", map[string]any{\n\t\t\t\t\t\t\"type\": \"content_block_delta\",\n\t\t\t\t\t\t\"index\": toolBlockIndices[upstreamIndex],\n\t\t\t\t\t\t\"delta\": map[string]any{\n\t\t\t\t\t\t\t\"type\": \"input_json_delta\",\n\t\t\t\t\t\t\t\"partial_json\": argDelta,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif finishReason == \"stop\" || finishReason == \"length\" || finishReason == \"tool_calls\" || finishReason == \"function_call\" || finishReason == \"content_filter\" {\n\t\t\tstats.finishReason = finishReason\n\t\t\tstats.sawFinish = true\n\t\t\tfinished = true\n\t\t\tfinalizeContentBlocks()\n\n\t\t\tstopReason = \"end_turn\"\n\t\t\tswitch finishReason {\n\t\t\tcase \"length\":\n\t\t\t\tstopReason = \"max_tokens\"\n\t\t\tcase \"tool_calls\", \"function_call\":\n\t\t\t\tstopReason = \"tool_use\"\n\t\t\tcase \"content_filter\":\n\t\t\t\tstopReason = \"refusal\"\n\t\t\t}\n\t\t\t// Do not emit message_delta/stop yet: OpenAI-compatible upstreams often\n\t\t\t// send the usage-only chunk after finish_reason when include_usage=true.\n\t\t\tcontinue\n\t\t}\n\t}\n\n\tensureMessageStart()\n\tif !finished {\n\t\tfinalizeContentBlocks()\n\t}\n\temitClaudeEvent(\"message_delta\", map[string]any{\n\t\t\"type\": \"message_delta\",\n\t\t\"delta\": map[string]any{\"stop_reason\": stopReason},\n\t\t\"usage\": buildClaudeDeltaUsage(fullUsage),\n\t})\n\temitClaudeEvent(\"message_stop\", map[string]any{\"type\": \"message_stop\"})\n}\n\nfunc indexOfInt(slice []int, val int) int {\n\tfor i, v := range slice {\n\t\tif v == val {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn 0\n}\n\n// ======================== Responses API ========================\n\nfunc responsesInputToMessages(input any, instructions string) []Message {\n\tvar messages []Message\n\tif instructions != \"\" {\n\t\tmessages = append(messages, Message{Role: \"system\", Content: instructions})\n\t}\n\tswitch v := input.(type) {\n\tcase string:\n\t\tmessages = append(messages, Message{Role: \"user\", Content: v})\n\tcase []any:\n\t\tfunctionOutputs := collectFunctionOutputs(v)\n\t\tfor _, item := range v {\n\t\t\tswitch elem := item.(type) {\n\t\t\tcase string:\n\t\t\t\tmessages = append(messages, Message{Role: \"user\", Content: elem})\n\t\t\tcase map[string]any:\n\t\t\t\titemType, _ := elem[\"type\"].(string)\n\t\t\t\tswitch itemType {\n\t\t\t\tcase \"function_call\", \"tool_call\", \"apply_patch_call\", \"shell_call\":\n\t\t\t\t\tcallID, _ := elem[\"call_id\"].(string)\n\t\t\t\t\tif callID == \"\" {\n\t\t\t\t\t\tcallID, _ = elem[\"id\"].(string)\n\t\t\t\t\t}\n\t\t\t\t\tname, _ := elem[\"name\"].(string)\n\t\t\t\t\tif name == \"\" {\n\t\t\t\t\t\tswitch itemType {\n\t\t\t\t\t\tcase \"apply_patch_call\":\n\t\t\t\t\t\t\tname = \"apply_patch\"\n\t\t\t\t\t\tcase \"shell_call\":\n\t\t\t\t\t\t\tname = \"shell\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\targs, _ := elem[\"arguments\"].(string)\n\t\t\t\t\tif name == \"\" {\n\t\t\t\t\t\tif tu, ok := elem[\"tool_use\"].(map[string]any); ok {\n\t\t\t\t\t\t\tname, _ = tu[\"name\"].(string)\n\t\t\t\t\t\t\tcallID, _ = tu[\"id\"].(string)\n\t\t\t\t\t\t\tif a, ok := tu[\"arguments\"].(string); ok {\n\t\t\t\t\t\t\t\targs = a\n\t\t\t\t\t\t\t} else if inp, ok := tu[\"input\"]; ok {\n\t\t\t\t\t\t\t\tb, _ := json.Marshal(inp)\n\t\t\t\t\t\t\t\targs = string(b)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif args == \"\" {\n\t\t\t\t\t\targs = buildBuiltInToolCallArguments(itemType, elem)\n\t\t\t\t\t}\n\t\t\t\t\tif args == \"\" {\n\t\t\t\t\t\targs = \"{}\"\n\t\t\t\t\t}\n\t\t\t\t\tmessages = append(messages, Message{\n\t\t\t\t\t\tRole: \"assistant\",\n\t\t\t\t\t\tContent: \"\",\n\t\t\t\t\t\tToolCalls: []ToolCall{{\n\t\t\t\t\t\t\tID: callID,\n\t\t\t\t\t\t\tType: \"function\",\n\t\t\t\t\t\t\tFunction: FunctionCall{\n\t\t\t\t\t\t\t\tName: name,\n\t\t\t\t\t\t\t\tArguments: args,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t})\n\t\t\t\t\tif callID != \"\" {\n\t\t\t\t\t\toutput := functionOutputs[callID]\n\t\t\t\t\t\tif output == \"\" {\n\t\t\t\t\t\t\toutput = \"[tool output missing]\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmessages = append(messages, Message{Role: \"tool\", ToolCallID: callID, Content: output})\n\t\t\t\t\t}\n\t\t\t\tcase \"function_call_output\", \"tool_result\", \"apply_patch_call_output\", \"shell_call_output\":\n\t\t\t\t\tcallID, _ := elem[\"call_id\"].(string)\n\t\t\t\t\tif callID == \"\" {\n\t\t\t\t\t\tcallID, _ = elem[\"tool_use_id\"].(string)\n\t\t\t\t\t}\n\t\t\t\t\tif callID != \"\" {\n\t\t\t\t\t\toutput := functionOutputs[callID]\n\t\t\t\t\t\tif output == \"\" {\n\t\t\t\t\t\t\tswitch o := elem[\"output\"].(type) {\n\t\t\t\t\t\t\tcase string:\n\t\t\t\t\t\t\t\toutput = o\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tif o != nil {\n\t\t\t\t\t\t\t\t\tb, _ := json.Marshal(o)\n\t\t\t\t\t\t\t\t\toutput = string(b)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif output == \"\" {\n\t\t\t\t\t\t\tb, err := json.Marshal(elem)\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\toutput = string(b)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif output == \"\" {\n\t\t\t\t\t\t\toutput = \"[tool output missing]\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmessages = append(messages, Message{Role: \"tool\", ToolCallID: callID, Content: output})\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"reasoning\":\n\t\t\t\t\tif text := extractTextFromContentParts(elem[\"summary\"]); text != \"\" {\n\t\t\t\t\t\tmessages = append(messages, Message{Role: \"assistant\", Content: \"\", ReasoningContent: &text})\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"message\", \"\":\n\t\t\t\t\trole := \"user\"\n\t\t\t\t\tif r, ok := elem[\"role\"].(string); ok && r != \"\" {\n\t\t\t\t\t\trole = r\n\t\t\t\t\t}\n\t\t\t\t\tif role == \"developer\" {\n\t\t\t\t\t\trole = \"system\"\n\t\t\t\t\t}\n\t\t\t\t\tcontent := responsesContentToMessageContent(elem[\"content\"])\n\t\t\t\t\tmessages = append(messages, Message{Role: role, Content: content})\n\t\t\t\tdefault:\n\t\t\t\t\trole := \"user\"\n\t\t\t\t\tif r, ok := elem[\"role\"].(string); ok && r != \"\" {\n\t\t\t\t\t\trole = r\n\t\t\t\t\t}\n\t\t\t\t\tcontent := responsesContentToMessageContent(elem[\"content\"])\n\t\t\t\t\temptyContent := false\n\t\t\t\t\tswitch v := content.(type) {\n\t\t\t\t\tcase nil:\n\t\t\t\t\t\temptyContent = true\n\t\t\t\t\tcase string:\n\t\t\t\t\t\temptyContent = v == \"\"\n\t\t\t\t\tcase []any:\n\t\t\t\t\t\temptyContent = len(v) == 0\n\t\t\t\t\t}\n\t\t\t\t\tif emptyContent {\n\t\t\t\t\t\tb, err := json.Marshal(elem)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent = string(b)\n\t\t\t\t\t}\n\t\t\t\t\tmessages = append(messages, Message{Role: role, Content: content})\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tb, _ := json.Marshal(elem)\n\t\t\t\tmessages = append(messages, Message{Role: \"user\", Content: string(b)})\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tb, _ := json.Marshal(v)\n\t\tmessages = append(messages, Message{Role: \"user\", Content: string(b)})\n\t}\n\treturn messages\n}\n\nfunc convertResponsesTools(tools []ResponsesTool) []Tool {\n\tconverted := make([]Tool, 0, len(tools))\n\tfor _, tool := range tools {\n\t\tfn, ok := responsesToolFunction(tool)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tconverted = append(converted, Tool{Type: \"function\", Function: fn})\n\t}\n\treturn converted\n}\n\nfunc responsesToolFunction(tool ResponsesTool) (ToolFunction, bool) {\n\tswitch tool.Type {\n\tcase \"function\":\n\t\tfn := ToolFunction{\n\t\t\tName: tool.Name,\n\t\t\tDescription: tool.Description,\n\t\t\tParameters: tool.Parameters,\n\t\t}\n\t\tif tool.Function != nil {\n\t\t\tfn = *tool.Function\n\t\t}\n\t\tif fn.Parameters == nil {\n\t\t\tfn.Parameters = map[string]any{\"type\": \"object\", \"properties\": map[string]any{}}\n\t\t}\n\t\treturn fn, true\n\tcase \"apply_patch\":\n\t\treturn ToolFunction{\n\t\t\tName: \"apply_patch\",\n\t\t\tDescription: \"Create, update, or delete files using a structured patch operation or unified diff.\",\n\t\t\tParameters: map[string]any{\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": map[string]any{\n\t\t\t\t\t\"input\": map[string]any{\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"description\": \"Patch diff or patch instructions to apply.\",\n\t\t\t\t\t},\n\t\t\t\t\t\"operation\": map[string]any{\n\t\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\t\"description\": \"Structured patch operation, including file action and diff payload.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, true\n\tcase \"shell\":\n\t\treturn ToolFunction{\n\t\t\tName: \"shell\",\n\t\t\tDescription: \"Run a shell command in the local workspace and return stdout, stderr, and exit details.\",\n\t\t\tParameters: map[string]any{\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": map[string]any{\n\t\t\t\t\t\"command\": map[string]any{\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"description\": \"Shell command to execute.\",\n\t\t\t\t\t},\n\t\t\t\t\t\"timeout_ms\": map[string]any{\n\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\"description\": \"Optional timeout in milliseconds.\",\n\t\t\t\t\t},\n\t\t\t\t\t\"working_directory\": map[string]any{\n\t\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\t\"description\": \"Optional working directory for the command.\",\n\t\t\t\t\t},\n\t\t\t\t\t\"max_output_tokens\": map[string]any{\n\t\t\t\t\t\t\"type\": \"integer\",\n\t\t\t\t\t\t\"description\": \"Optional output budget hint.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"required\": []string{\"command\"},\n\t\t\t},\n\t\t}, true\n\tdefault:\n\t\treturn ToolFunction{}, false\n\t}\n}\n\nfunc responsesToolName(tool ResponsesTool) string {\n\tswitch tool.Type {\n\tcase \"function\":\n\t\tif tool.Function != nil && tool.Function.Name != \"\" {\n\t\t\treturn tool.Function.Name\n\t\t}\n\t\treturn tool.Name\n\tcase \"apply_patch\":\n\t\treturn \"apply_patch\"\n\tcase \"shell\":\n\t\treturn \"shell\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc responsesToolKindMap(tools []ResponsesTool) map[string]string {\n\tkinds := make(map[string]string, len(tools))\n\tfor _, tool := range tools {\n\t\tname := responsesToolName(tool)\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkinds[name] = tool.Type\n\t}\n\treturn kinds\n}\n\nfunc toolCallOutputType(name string, kinds map[string]string) string {\n\tswitch kinds[name] {\n\tcase \"apply_patch\":\n\t\treturn \"apply_patch_call\"\n\tcase \"shell\":\n\t\treturn \"shell_call\"\n\tdefault:\n\t\treturn \"function_call\"\n\t}\n}\n\nfunc convertResponsesToolChoice(choice any) any {\n\tif choice == nil {\n\t\treturn nil\n\t}\n\tchoiceMap, ok := choice.(map[string]any)\n\tif !ok {\n\t\treturn choice\n\t}\n\tif choiceMap[\"type\"] == \"function\" {\n\t\tif name, ok := choiceMap[\"name\"].(string); ok && name != \"\" {\n\t\t\treturn map[string]any{\n\t\t\t\t\"type\": \"function\",\n\t\t\t\t\"function\": map[string]any{\"name\": name},\n\t\t\t}\n\t\t}\n\t}\n\tif choiceType, ok := choiceMap[\"type\"].(string); ok {\n\t\tswitch choiceType {\n\t\tcase \"apply_patch\", \"shell\":\n\t\t\treturn map[string]any{\n\t\t\t\t\"type\": \"function\",\n\t\t\t\t\"function\": map[string]any{\"name\": choiceType},\n\t\t\t}\n\t\t}\n\t}\n\treturn choice\n}\n\nfunc collectFunctionOutputs(items []any) map[string]string {\n\toutputs := map[string]string{}\n\tfor _, item := range items {\n\t\telem, ok := item.(map[string]any)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\titemType, _ := elem[\"type\"].(string)\n\t\tswitch itemType {\n\t\tcase \"function_call_output\", \"apply_patch_call_output\", \"shell_call_output\":\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tcallID, _ := elem[\"call_id\"].(string)\n\t\tif callID == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tswitch v := elem[\"output\"].(type) {\n\t\tcase string:\n\t\t\toutputs[callID] = v\n\t\tdefault:\n\t\t\tb, _ := json.Marshal(v)\n\t\t\toutputs[callID] = string(b)\n\t\t}\n\t}\n\treturn outputs\n}\n\nfunc parseJSONString(input string) any {\n\tvar parsed any\n\tif input == \"\" {\n\t\treturn nil\n\t}\n\tif err := json.Unmarshal([]byte(input), &parsed); err != nil {\n\t\treturn nil\n\t}\n\treturn parsed\n}\n\nfunc buildBuiltInToolCallArguments(itemType string, elem map[string]any) string {\n\tif arguments, ok := elem[\"arguments\"].(string); ok && arguments != \"\" {\n\t\treturn arguments\n\t}\n\n\tpayload := map[string]any{}\n\tswitch itemType {\n\tcase \"apply_patch_call\":\n\t\tif input, ok := elem[\"input\"].(string); ok && input != \"\" {\n\t\t\tpayload[\"input\"] = input\n\t\t}\n\t\tif operation, ok := elem[\"operation\"]; ok && operation != nil {\n\t\t\tpayload[\"operation\"] = operation\n\t\t}\n\tcase \"shell_call\":\n\t\tfor _, key := range []string{\"command\", \"timeout_ms\", \"working_directory\", \"max_output_tokens\"} {\n\t\t\tif value, ok := elem[key]; ok && value != nil {\n\t\t\t\tpayload[key] = value\n\t\t\t}\n\t\t}\n\t}\n\tif len(payload) == 0 {\n\t\tpayload = elem\n\t}\n\tencoded, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"{}\"\n\t}\n\treturn string(encoded)\n}\n\nfunc buildResponseToolCallItem(tc ToolCall, outputType string) map[string]any {\n\tswitch outputType {\n\tcase \"apply_patch_call\":\n\t\titem := map[string]any{\n\t\t\t\"id\": \"apc_\" + tc.ID,\n\t\t\t\"type\": outputType,\n\t\t\t\"status\": \"completed\",\n\t\t\t\"call_id\": tc.ID,\n\t\t}\n\t\tif parsed, ok := parseJSONString(tc.Function.Arguments).(map[string]any); ok {\n\t\t\tfor key, value := range parsed {\n\t\t\t\titem[key] = value\n\t\t\t}\n\t\t} else if tc.Function.Arguments != \"\" {\n\t\t\titem[\"arguments\"] = tc.Function.Arguments\n\t\t}\n\t\treturn item\n\tcase \"shell_call\":\n\t\titem := map[string]any{\n\t\t\t\"id\": \"shc_\" + tc.ID,\n\t\t\t\"type\": outputType,\n\t\t\t\"status\": \"completed\",\n\t\t\t\"call_id\": tc.ID,\n\t\t}\n\t\tif parsed, ok := parseJSONString(tc.Function.Arguments).(map[string]any); ok {\n\t\t\tfor key, value := range parsed {\n\t\t\t\titem[key] = value\n\t\t\t}\n\t\t} else if tc.Function.Arguments != \"\" {\n\t\t\titem[\"arguments\"] = tc.Function.Arguments\n\t\t}\n\t\treturn item\n\tdefault:\n\t\treturn map[string]any{\n\t\t\t\"id\": \"fc_\" + tc.ID,\n\t\t\t\"type\": \"function_call\",\n\t\t\t\"status\": \"completed\",\n\t\t\t\"arguments\": tc.Function.Arguments,\n\t\t\t\"call_id\": tc.ID,\n\t\t\t\"name\": tc.Function.Name,\n\t\t}\n\t}\n}\n\nfunc cloneJSONValue[T any](value T) T {\n\tencoded, err := json.Marshal(value)\n\tif err != nil {\n\t\treturn value\n\t}\n\tvar cloned T\n\tif err := json.Unmarshal(encoded, &cloned); err != nil {\n\t\treturn value\n\t}\n\treturn cloned\n}\n\nfunc storeResponseState(response map[string]any, req ResponsesAPIRequest) {\n\tif req.Store != nil && !*req.Store {\n\t\treturn\n\t}\n\tresponseID, _ := response[\"id\"].(string)\n\tif responseID == \"\" {\n\t\treturn\n\t}\n\toutput, _ := response[\"output\"].([]any)\n\tstoredResponsesMu.Lock()\n\tstoredResponses[responseID] = StoredResponseState{\n\t\tModel: req.Model,\n\t\tInstructions: req.Instructions,\n\t\tTools: cloneJSONValue(req.Tools),\n\t\tToolChoice: cloneJSONValue(req.ToolChoice),\n\t\tOutput: cloneJSONValue(output),\n\t}\n\tstoredResponsesMu.Unlock()\n}\n\nfunc loadResponseState(responseID string) (StoredResponseState, bool) {\n\tstoredResponsesMu.RLock()\n\tdefer storedResponsesMu.RUnlock()\n\tstate, ok := storedResponses[responseID]\n\tif !ok {\n\t\treturn StoredResponseState{}, false\n\t}\n\treturn cloneJSONValue(state), true\n}\n\nfunc extractTextFromContentParts(content any) string {\n\tparts, ok := content.([]any)\n\tif !ok {\n\t\tif s, ok := content.(string); ok {\n\t\t\treturn s\n\t\t}\n\t\treturn \"\"\n\t}\n\tvar texts []string\n\tfor _, p := range parts {\n\t\tif part, ok := p.(map[string]any); ok {\n\t\t\tif part[\"type\"] == \"input_text\" || part[\"type\"] == \"output_text\" {\n\t\t\t\tif t, ok := part[\"text\"].(string); ok {\n\t\t\t\t\ttexts = append(texts, t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn strings.Join(texts, \"\\n\")\n}\n\nfunc convertResponsesContentPart(part map[string]any) (map[string]any, bool) {\n\tpartType, _ := part[\"type\"].(string)\n\tswitch partType {\n\tcase \"input_text\", \"output_text\", \"text\":\n\t\ttext, _ := part[\"text\"].(string)\n\t\tif text == \"\" {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn map[string]any{\n\t\t\t\"type\": \"text\",\n\t\t\t\"text\": text,\n\t\t}, true\n\tcase \"input_image\":\n\t\timageURL, _ := part[\"image_url\"].(string)\n\t\tif imageURL == \"\" {\n\t\t\treturn nil, false\n\t\t}\n\t\timageURLValue := map[string]any{\n\t\t\t\"url\": imageURL,\n\t\t}\n\t\tif detail, ok := part[\"detail\"].(string); ok && detail != \"\" {\n\t\t\timageURLValue[\"detail\"] = detail\n\t\t}\n\t\treturn map[string]any{\n\t\t\t\"type\": \"image_url\",\n\t\t\t\"image_url\": imageURLValue,\n\t\t}, true\n\tdefault:\n\t\treturn nil, false\n\t}\n}\n\nfunc responsesContentToMessageContent(content any) any {\n\tif content == nil {\n\t\treturn nil\n\t}\n\tif s, ok := content.(string); ok {\n\t\treturn s\n\t}\n\n\tparts, ok := content.([]any)\n\tif !ok {\n\t\tb, err := json.Marshal(content)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn string(b)\n\t}\n\n\tconvertedParts := make([]any, 0, len(parts))\n\ttexts := make([]string, 0, len(parts))\n\tonlyTextParts := true\n\n\tfor _, rawPart := range parts {\n\t\tpart, ok := rawPart.(map[string]any)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tconvertedPart, ok := convertResponsesContentPart(part)\n\t\tif !ok {\n\t\t\ttext := extractTextFromContentParts([]any{part})\n\t\t\tif text == \"\" {\n\t\t\t\tb, err := json.Marshal(part)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\ttext = string(b)\n\t\t\t}\n\t\t\tconvertedParts = append(convertedParts, map[string]any{\n\t\t\t\t\"type\": \"text\",\n\t\t\t\t\"text\": text,\n\t\t\t})\n\t\t\ttexts = append(texts, text)\n\t\t\tcontinue\n\t\t}\n\n\t\tif convertedPart[\"type\"] != \"text\" {\n\t\t\tonlyTextParts = false\n\t\t}\n\t\tif text, ok := convertedPart[\"text\"].(string); ok && text != \"\" {\n\t\t\ttexts = append(texts, text)\n\t\t}\n\t\tconvertedParts = append(convertedParts, convertedPart)\n\t}\n\n\tif len(convertedParts) == 0 {\n\t\treturn \"\"\n\t}\n\tif onlyTextParts {\n\t\treturn strings.Join(texts, \"\\n\")\n\t}\n\treturn convertedParts\n}\n\nfunc chatContentToResponsesContent(content any) ([]any, string) {\n\tswitch v := content.(type) {\n\tcase nil:\n\t\treturn nil, \"\"\n\tcase string:\n\t\tif v == \"\" {\n\t\t\treturn nil, \"\"\n\t\t}\n\t\treturn []any{map[string]any{\n\t\t\t\"type\": \"output_text\",\n\t\t\t\"text\": v,\n\t\t\t\"annotations\": []any{},\n\t\t\t\"logprobs\": []any{},\n\t\t}}, v\n\tcase []any:\n\t\tparts := make([]any, 0, len(v))\n\t\ttexts := make([]string, 0, len(v))\n\t\tfor _, rawPart := range v {\n\t\t\tpart, ok := rawPart.(map[string]any)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpartType, _ := part[\"type\"].(string)\n\t\t\tswitch partType {\n\t\t\tcase \"text\", \"input_text\", \"output_text\":\n\t\t\t\ttext, _ := part[\"text\"].(string)\n\t\t\t\tif text == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tannotations, ok := part[\"annotations\"]\n\t\t\t\tif !ok {\n\t\t\t\t\tannotations = []any{}\n\t\t\t\t}\n\t\t\t\tlogprobs, ok := part[\"logprobs\"]\n\t\t\t\tif !ok {\n\t\t\t\t\tlogprobs = []any{}\n\t\t\t\t}\n\t\t\t\ttexts = append(texts, text)\n\t\t\t\tparts = append(parts, map[string]any{\n\t\t\t\t\t\"type\": \"output_text\",\n\t\t\t\t\t\"text\": text,\n\t\t\t\t\t\"annotations\": annotations,\n\t\t\t\t\t\"logprobs\": logprobs,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn parts, strings.Join(texts, \"\\n\")\n\tdefault:\n\t\tb, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn nil, \"\"\n\t\t}\n\t\ttext := string(b)\n\t\treturn []any{map[string]any{\n\t\t\t\"type\": \"output_text\",\n\t\t\t\"text\": text,\n\t\t\t\"annotations\": []any{},\n\t\t\t\"logprobs\": []any{},\n\t\t}}, text\n\t}\n}\n\nfunc responsesHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\tauth := extractUpstreamAuth(r)\n\tbody, err := io.ReadAll(io.LimitReader(r.Body, 10*1024*1024))\n\tif err != nil {\n\t\thttp.Error(w, \"Failed to read request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcnt := requestCount.Add(1)\n\tmaybeLogBodySummary(r.Context(), \"responses request body\", body)\n\t_ = cnt\n\n\tvar respReq ResponsesAPIRequest\n\tif err := json.Unmarshal(body, &respReq); err != nil {\n\t\thttp.Error(w, \"Invalid JSON\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tmodelIn := respReq.Model\n\trespReq.Model = resolveModel(respReq.Model)\n\tpreviousState, hasPreviousState := StoredResponseState{}, false\n\tif respReq.PreviousResponseID != \"\" {\n\t\tpreviousState, hasPreviousState = loadResponseState(respReq.PreviousResponseID)\n\t\tif respReq.Model == \"\" && previousState.Model != \"\" {\n\t\t\trespReq.Model = previousState.Model\n\t\t}\n\t\tif len(respReq.Tools) == 0 && len(previousState.Tools) > 0 {\n\t\t\trespReq.Tools = previousState.Tools\n\t\t}\n\t\tif respReq.ToolChoice == nil && previousState.ToolChoice != nil {\n\t\t\trespReq.ToolChoice = previousState.ToolChoice\n\t\t}\n\t}\n\tif respReq.Model == \"\" {\n\t\tmodelIDs := getModelIDs()\n\t\tif len(modelIDs) > 0 {\n\t\t\trespReq.Model = modelIDs[0]\n\t\t} else {\n\t\t\trespReq.Model = \"deepseek-v4-flash-free\"\n\t\t}\n\t}\n\n\t// 多模态路由\n\n\tmessages := respReq.Messages\n\tif len(messages) == 0 {\n\t\tif hasPreviousState && len(previousState.Output) > 0 {\n\t\t\tmessages = append(messages, responsesInputToMessages(previousState.Output, \"\")...)\n\t\t}\n\t\tmessages = append(messages, responsesInputToMessages(respReq.Input, respReq.Instructions)...)\n\t} else if respReq.Instructions != \"\" {\n\t\tmessages = append([]Message{{Role: \"system\", Content: respReq.Instructions}}, messages...)\n\t}\n\n\tchatReq := OpenAIRequest{\n\t\tModel: respReq.Model,\n\t\tMessages: messages,\n\t\tStream: respReq.Stream,\n\t}\n\tif respReq.Stream {\n\t\tchatReq.ExtraBody = map[string]any{\n\t\t\t\"stream_options\": map[string]any{\"include_usage\": true},\n\t\t}\n\t}\n\tif respReq.Temperature != nil {\n\t\tchatReq.Temperature = respReq.Temperature\n\t}\n\tif respReq.MaxTokens != nil {\n\t\tchatReq.MaxTokens = respReq.MaxTokens\n\t}\n\tif respReq.TopP != nil {\n\t\tchatReq.TopP = respReq.TopP\n\t}\n\tif len(respReq.Tools) > 0 {\n\t\tchatReq.Tools = convertResponsesTools(respReq.Tools)\n\t}\n\tif respReq.ToolChoice != nil {\n\t\tchatReq.ToolChoice = convertResponsesToolChoice(respReq.ToolChoice)\n\t}\n\tif respReq.ParallelToolCalls != nil {\n\t\tif chatReq.ExtraBody == nil {\n\t\t\tchatReq.ExtraBody = map[string]any{}\n\t\t}\n\t\tchatReq.ExtraBody[\"parallel_tool_calls\"] = *respReq.ParallelToolCalls\n\t}\n\tif respReq.Stop != nil {\n\t\tif chatReq.ExtraBody == nil {\n\t\t\tchatReq.ExtraBody = map[string]any{}\n\t\t}\n\t\tchatReq.ExtraBody[\"stop\"] = respReq.Stop\n\t}\n\tif respReq.FrequencyPenalty != nil {\n\t\tif chatReq.ExtraBody == nil {\n\t\t\tchatReq.ExtraBody = map[string]any{}\n\t\t}\n\t\tchatReq.ExtraBody[\"frequency_penalty\"] = *respReq.FrequencyPenalty\n\t}\n\tif respReq.PresencePenalty != nil {\n\t\tif chatReq.ExtraBody == nil {\n\t\t\tchatReq.ExtraBody = map[string]any{}\n\t\t}\n\t\tchatReq.ExtraBody[\"presence_penalty\"] = *respReq.PresencePenalty\n\t}\n\tif respReq.User != \"\" {\n\t\tif chatReq.ExtraBody == nil {\n\t\t\tchatReq.ExtraBody = map[string]any{}\n\t\t}\n\t\tchatReq.ExtraBody[\"user\"] = respReq.User\n\t}\n\tif respReq.StreamOptions != nil {\n\t\tif chatReq.ExtraBody == nil {\n\t\t\tchatReq.ExtraBody = map[string]any{}\n\t\t}\n\t\tstreamOptions, ok := respReq.StreamOptions.(map[string]any)\n\t\tif !ok {\n\t\t\tstreamOptions = map[string]any{}\n\t\t}\n\t\tif _, exists := streamOptions[\"include_usage\"]; !exists && respReq.Stream {\n\t\t\tstreamOptions[\"include_usage\"] = true\n\t\t}\n\t\tchatReq.ExtraBody[\"stream_options\"] = streamOptions\n\t}\n\t// 将 Responses API reasoning.effort 映射到 Chat Completions\n\tif !getForceDisableThinking() && respReq.Reasoning.Effort != \"\" {\n\t\tif respReq.Reasoning.Effort != \"none\" {\n\t\t\tchatReq.ReasoningEffort = respReq.Reasoning.Effort\n\t\t}\n\t}\n\n\twantReasoning := !getForceDisableThinking()\n\tchatReq.Messages = fixToolCallGaps(chatReq.Messages)\n\tkeepReasoning := wantsReasoning(&chatReq)\n\tchatReq.Messages = ensureReasoningContent(chatReq.Messages, keepReasoning)\n\n\teffortIn := chatReq.ReasoningEffort\n\tif effortIn == \"\" {\n\t\teffortIn = respReq.Reasoning.Effort\n\t}\n\tupstreamSurface := \"zen\"\n\tif auth.shouldUseGoEndpoint(chatReq.Model) {\n\t\tupstreamSurface = \"go\"\n\t}\n\tlogRequestPlan(r.Context(), map[string]any{\n\t\t\"protocol\": \"responses\",\n\t\t\"model_in\": modelIn,\n\t\t\"model_resolved\": chatReq.Model,\n\t\t\"auth_mode\": authModeString(auth.Mode),\n\t\t\"auth_source\": auth.Source,\n\t\t\"has_key\": auth.Token != \"\",\n\t\t\"upstream_surface\": upstreamSurface,\n\t\t\"stream\": respReq.Stream,\n\t\t\"keep_reasoning\": keepReasoning,\n\t\t\"thinking\": thinkingState(nil),\n\t\t\"reasoning_effort_in\": effortIn,\n\t\t\"reasoning_effort_out\": mappedReasoningEffort(effortIn),\n\t\t\"tools_count\": len(respReq.Tools),\n\t\t\"messages_count\": len(chatReq.Messages),\n\t})\n\n\tupstreamBody := buildUpstreamBody(&chatReq)\n\n\tif respReq.Stream {\n\t\tupResp, status, _, err := callOpenCodeAPIStream(r.Context(), upstreamBody, chatReq.Model, auth)\n\t\tif err != nil || status < 200 || status >= 300 {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.WriteHeader(status)\n\t\t\tif upResp != nil {\n\t\t\t\terrBody, _ := io.ReadAll(upResp)\n\t\t\t\tif len(errBody) > 0 {\n\t\t\t\t\tw.Write(errBody)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson.NewEncoder(w).Encode(map[string]any{\"error\": map[string]any{\"message\": \"upstream error\"}})\n\t\t\treturn\n\t\t}\n\t\tdefer upResp.Close()\n\n\t\tresp := &http.Response{\n\t\t\tStatusCode: status,\n\t\t\tBody: upResp,\n\t\t\tHeader: make(http.Header),\n\t\t}\n\t\tresponsesStreamHandler(w, r, resp, chatReq.Model, chatReq.Model, wantReasoning, respReq.Tools, respReq.ToolChoice, respReq)\n\t\treturn\n\t}\n\n\trespBody, status, _, err := callOpenCodeAPI(r.Context(), upstreamBody, chatReq.Model, auth)\n\tif err != nil || status < 200 || status >= 300 {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(status)\n\t\tif len(respBody) > 0 {\n\t\t\tw.Write(respBody)\n\t\t} else {\n\t\t\tjson.NewEncoder(w).Encode(map[string]any{\"error\": map[string]any{\"message\": \"upstream error\"}})\n\t\t}\n\t\treturn\n\t}\n\n\tresponsesBody := convertChatToResponses(respBody, chatReq.Model, wantReasoning, respReq.Tools, respReq.ToolChoice)\n\tvar responseMap map[string]any\n\tif json.Unmarshal(responsesBody, &responseMap) == nil {\n\t\tapplyResponsesRequestEcho(responseMap, respReq)\n\t\tif enriched, marshalErr := json.Marshal(responseMap); marshalErr == nil {\n\t\t\tresponsesBody = enriched\n\t\t}\n\t\tstoreResponseState(responseMap, respReq)\n\t}\n\n\tresult := summarizeChatResult(respBody)\n\tlogRequestResult(r.Context(), result)\n\n\tvar usageResp map[string]any\n\tif json.Unmarshal(respBody, &usageResp) == nil {\n\t\tif u, ok := usageResp[\"usage\"].(map[string]any); ok {\n\t\t\tpt, _ := u[\"prompt_tokens\"].(float64)\n\t\t\tct, _ := u[\"completion_tokens\"].(float64)\n\t\t\ttt, _ := u[\"total_tokens\"].(float64)\n\t\t\tif tt > 0 {\n\t\t\t\trecordTokenUsage(chatReq.Model, int64(pt), int64(ct), int64(tt))\n\t\t\t}\n\t\t}\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(http.StatusOK)\n\tmaybeLogBodySummary(r.Context(), \"responses response body\", responsesBody)\n\tw.Write(responsesBody)\n}\n\n// ======================== Responses Stream Handler ========================\n\nfunc responsesStreamHandler(w http.ResponseWriter, r *http.Request, resp *http.Response, model string, _ string, wantReasoning bool, tools []ResponsesTool, toolChoice any, originalReq ResponsesAPIRequest) {\n\tctx := context.Background()\n\tif r != nil {\n\t\tctx = r.Context()\n\t}\n\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\tw.Header().Set(\"Connection\", \"keep-alive\")\n\tw.WriteHeader(http.StatusOK)\n\n\tflusher, _ := w.(http.Flusher)\n\treader := bufio.NewReader(resp.Body)\n\tstats := &streamResultStats{start: time.Now()}\n\n\tresponseID := \"resp_\" + time.Now().Format(\"20060102150405\") + \"_\" + randomString(8)\n\treasoningID := \"rs_\" + responseID\n\tmsgID := \"msg_\" + responseID + \"_0\"\n\tcreatedAt := time.Now().Unix()\n\tseq := 0\n\n\treasoningStarted := false\n\treasoningDone := false\n\tmessageStarted := false\n\tmessageDone := false\n\tfullReasoning := \"\"\n\tfullText := \"\"\n\ttotalUsage := map[string]any{}\n\tcreatedSent := false\n\tterminalStatus := \"completed\"\n\tterminalEvent := \"response.completed\"\n\titemStatus := \"completed\"\n\ttoolCalls := map[int]map[string]any{}\n\ttoolOrder := []int{}\n\ttoolKinds := responsesToolKindMap(tools)\n\tindexAllocator := outputIndexAllocator{}\n\treasoningOutputIndex := -1\n\tmessageIndex := -1\n\n\tdefer func() {\n\t\tstats.textChars = len(fullText)\n\t\tstats.reasoningChars = len(fullReasoning)\n\t\tstats.toolCallCount = len(toolOrder)\n\t\tstats.log(ctx, \"responses\")\n\t}()\n\n\tmessageOutputIndex := func() int {\n\t\tif messageIndex < 0 {\n\t\t\tmessageIndex = indexAllocator.Allocate()\n\t\t}\n\t\treturn messageIndex\n\t}\n\n\treasoningItem := func(status string) map[string]any {\n\t\titem := map[string]any{\n\t\t\t\"id\": reasoningID,\n\t\t\t\"type\": \"reasoning\",\n\t\t\t\"summary\": []any{},\n\t\t}\n\t\tif status != \"\" {\n\t\t\titem[\"status\"] = status\n\t\t}\n\t\tif status == \"completed\" {\n\t\t\titem[\"encrypted_content\"] = \"\"\n\t\t}\n\t\tif fullReasoning != \"\" {\n\t\t\titem[\"summary\"] = []any{map[string]any{\"type\": \"summary_text\", \"text\": fullReasoning}}\n\t\t}\n\t\treturn item\n\t}\n\n\tmessageItem := func(status string) map[string]any {\n\t\tcontent := []any{map[string]any{\n\t\t\t\"type\": \"output_text\",\n\t\t\t\"annotations\": []any{},\n\t\t\t\"logprobs\": []any{},\n\t\t\t\"text\": fullText,\n\t\t}}\n\t\treturn map[string]any{\n\t\t\t\"id\": msgID,\n\t\t\t\"type\": \"message\",\n\t\t\t\"status\": status,\n\t\t\t\"content\": content,\n\t\t\t\"role\": \"assistant\",\n\t\t}\n\t}\n\n\temitReasoningDone := func() {\n\t\tif !reasoningStarted || reasoningDone {\n\t\t\treturn\n\t\t}\n\t\tseq++\n\t\temitSSEEvent(w, flusher, \"response.reasoning_summary_text.done\", map[string]any{\n\t\t\t\"type\": \"response.reasoning_summary_text.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"item_id\": reasoningID,\n\t\t\t\"output_index\": reasoningOutputIndex,\n\t\t\t\"summary_index\": 0,\n\t\t\t\"text\": fullReasoning,\n\t\t})\n\t\tseq++\n\t\temitSSEEvent(w, flusher, \"response.reasoning_summary_part.done\", map[string]any{\n\t\t\t\"type\": \"response.reasoning_summary_part.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"item_id\": reasoningID,\n\t\t\t\"output_index\": reasoningOutputIndex,\n\t\t\t\"summary_index\": 0,\n\t\t\t\"part\": map[string]any{\"type\": \"summary_text\", \"text\": fullReasoning},\n\t\t})\n\t\tseq++\n\t\temitSSEEvent(w, flusher, \"response.output_item.done\", map[string]any{\n\t\t\t\"type\": \"response.output_item.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"output_index\": reasoningOutputIndex,\n\t\t\t\"item\": reasoningItem(itemStatus),\n\t\t})\n\t\treasoningDone = true\n\t}\n\n\temitMessageDone := func() {\n\t\tif !messageStarted || messageDone {\n\t\t\treturn\n\t\t}\n\t\tidx := messageOutputIndex()\n\t\tseq++\n\t\temitSSEEvent(w, flusher, \"response.output_text.done\", map[string]any{\n\t\t\t\"type\": \"response.output_text.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"item_id\": msgID,\n\t\t\t\"output_index\": idx,\n\t\t\t\"content_index\": 0,\n\t\t\t\"text\": fullText,\n\t\t\t\"logprobs\": []any{},\n\t\t})\n\t\tseq++\n\t\temitSSEEvent(w, flusher, \"response.content_part.done\", map[string]any{\n\t\t\t\"type\": \"response.content_part.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"item_id\": msgID,\n\t\t\t\"output_index\": idx,\n\t\t\t\"content_index\": 0,\n\t\t\t\"part\": map[string]any{\"type\": \"output_text\", \"annotations\": []any{}, \"logprobs\": []any{}, \"text\": fullText},\n\t\t})\n\t\tseq++\n\t\temitSSEEvent(w, flusher, \"response.output_item.done\", map[string]any{\n\t\t\t\"type\": \"response.output_item.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"output_index\": idx,\n\t\t\t\"item\": messageItem(itemStatus),\n\t\t})\n\t\tmessageDone = true\n\t}\n\n\temitToolCallDone := func(idx int, call map[string]any) {\n\t\tif done, _ := call[\"done\"].(bool); done {\n\t\t\treturn\n\t\t}\n\t\tcall[\"done\"] = true\n\t\titemID, _ := call[\"item_id\"].(string)\n\t\tcallID, _ := call[\"call_id\"].(string)\n\t\tname, _ := call[\"name\"].(string)\n\t\targs, _ := call[\"arguments\"].(string)\n\t\tseq++\n\t\temitSSEEvent(w, flusher, \"response.function_call_arguments.done\", map[string]any{\n\t\t\t\"type\": \"response.function_call_arguments.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"item_id\": itemID,\n\t\t\t\"output_index\": idx,\n\t\t\t\"name\": name,\n\t\t\t\"arguments\": args,\n\t\t})\n\t\tseq++\n\t\titemType, _ := call[\"item_type\"].(string)\n\t\tif itemType == \"\" {\n\t\t\titemType = \"function_call\"\n\t\t}\n\t\titem := buildResponseToolCallItem(ToolCall{ID: callID, Function: FunctionCall{Name: name, Arguments: args}}, itemType)\n\t\titem[\"status\"] = itemStatus\n\t\temitSSEEvent(w, flusher, \"response.output_item.done\", map[string]any{\n\t\t\t\"type\": \"response.output_item.done\",\n\t\t\t\"sequence_number\": seq,\n\t\t\t\"output_index\": idx,\n\t\t\t\"item\": item,\n\t\t})\n\t}\n\n\tfor {\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treqLogger(ctx).Error(\"stream read error\", \"error\", err)\n\t\t\treturn\n\t\t}\n\n\t\ttrimmed := strings.TrimSpace(line)\n\t\tif trimmed == \"data: [DONE]\" || trimmed == \"[DONE]\" {\n\t\t\tstats.doneSeen = true\n\t\t\tbreak\n\t\t}\n\t\tif !strings.HasPrefix(line, \"data: \") {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar chunk map[string]any\n\t\tif err := json.Unmarshal([]byte(line[6:]), &chunk); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tstats.noteChunk()\n\t\tif !createdSent {\n\t\t\tif id, ok := chunk[\"id\"].(string); ok && id != \"\" {\n\t\t\t\tresponseID = id\n\t\t\t\treasoningID = \"rs_\" + responseID + \"_0\"\n\t\t\t\tmsgID = \"msg_\" + responseID + \"_0\"\n\t\t\t}\n\t\t\tif created, ok := chunk[\"created\"].(float64); ok {\n\t\t\t\tcreatedAt = int64(created)\n\t\t\t}\n\t\t\tseq++\n\t\t\temitSSEEvent(w, flusher, \"response.created\", map[string]any{\n\t\t\t\t\"type\": \"response.created\",\n\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\"response\": map[string]any{\"id\": responseID, \"object\": \"response\", \"created_at\": createdAt, \"status\": \"in_progress\", \"background\": false, \"error\": nil, \"output\": []any{}},\n\t\t\t})\n\t\t\tseq++\n\t\t\temitSSEEvent(w, flusher, \"response.in_progress\", map[string]any{\n\t\t\t\t\"type\": \"response.in_progress\",\n\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\"response\": map[string]any{\"id\": responseID, \"object\": \"response\", \"created_at\": createdAt, \"status\": \"in_progress\"},\n\t\t\t})\n\t\t\tcreatedSent = true\n\t\t}\n\t\tchoices, ok := chunk[\"choices\"].([]any)\n\t\tif !ok || len(choices) == 0 {\n\t\t\tif usage, ok := chunk[\"usage\"].(map[string]any); ok {\n\t\t\t\ttotalUsage = usage\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tchoice, _ := choices[0].(map[string]any)\n\t\tdelta, _ := choice[\"delta\"].(map[string]any)\n\t\tfinishReason, _ := choice[\"finish_reason\"].(string)\n\t\tif finishReason != \"\" {\n\t\t\tstats.finishReason = finishReason\n\t\t\tstats.sawFinish = true\n\t\t}\n\n\t\tif rc, ok := delta[\"reasoning_content\"]; ok && wantReasoning {\n\t\t\trcStr, _ := rc.(string)\n\t\t\tif rcStr != \"\" {\n\t\t\t\tif !reasoningStarted {\n\t\t\t\t\treasoningOutputIndex = indexAllocator.Allocate()\n\t\t\t\t\tseq++\n\t\t\t\t\temitSSEEvent(w, flusher, \"response.output_item.added\", map[string]any{\n\t\t\t\t\t\t\"type\": \"response.output_item.added\",\n\t\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\t\"output_index\": reasoningOutputIndex,\n\t\t\t\t\t\t\"item\": reasoningItem(\"in_progress\"),\n\t\t\t\t\t})\n\t\t\t\t\tseq++\n\t\t\t\t\temitSSEEvent(w, flusher, \"response.reasoning_summary_part.added\", map[string]any{\n\t\t\t\t\t\t\"type\": \"response.reasoning_summary_part.added\",\n\t\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\t\"item_id\": reasoningID,\n\t\t\t\t\t\t\"output_index\": reasoningOutputIndex,\n\t\t\t\t\t\t\"summary_index\": 0,\n\t\t\t\t\t\t\"part\": map[string]any{\"type\": \"summary_text\", \"text\": \"\"},\n\t\t\t\t\t})\n\t\t\t\t\treasoningStarted = true\n\t\t\t\t}\n\t\t\t\tfullReasoning += rcStr\n\t\t\t\tseq++\n\t\t\t\temitSSEEvent(w, flusher, \"response.reasoning_summary_text.delta\", map[string]any{\n\t\t\t\t\t\"type\": \"response.reasoning_summary_text.delta\",\n\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\"item_id\": reasoningID,\n\t\t\t\t\t\"output_index\": reasoningOutputIndex,\n\t\t\t\t\t\"summary_index\": 0,\n\t\t\t\t\t\"delta\": rcStr,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tcontentStr := \"\"\n\t\tif c, ok := delta[\"content\"]; ok && c != nil {\n\t\t\tcontentStr, _ = c.(string)\n\t\t}\n\t\t// #37635: when thinking is not kept, promote misplaced reasoning to visible text.\n\t\tif contentStr == \"\" && !wantReasoning {\n\t\t\tif rc, ok := delta[\"reasoning_content\"].(string); ok {\n\t\t\t\tif rc != \"\" {\n\t\t\t\t\tstats.promotedReasoning = true\n\t\t\t\t}\n\t\t\t\tcontentStr = rc\n\t\t\t}\n\t\t}\n\t\tif contentStr != \"\" {\n\t\t\t// The terminal finish reason determines the item's final status. Keep the\n\t\t\t// reasoning item open until that reason is known so a truncation cannot\n\t\t\t// first announce it as completed.\n\t\t\tif !messageStarted {\n\t\t\t\tidx := messageOutputIndex()\n\t\t\t\tseq++\n\t\t\t\temitSSEEvent(w, flusher, \"response.output_item.added\", map[string]any{\n\t\t\t\t\t\"type\": \"response.output_item.added\",\n\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\"output_index\": idx,\n\t\t\t\t\t\"item\": map[string]any{\"id\": msgID, \"type\": \"message\", \"status\": \"in_progress\", \"content\": []any{}, \"role\": \"assistant\"},\n\t\t\t\t})\n\t\t\t\tseq++\n\t\t\t\temitSSEEvent(w, flusher, \"response.content_part.added\", map[string]any{\n\t\t\t\t\t\"type\": \"response.content_part.added\",\n\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\"item_id\": msgID,\n\t\t\t\t\t\"output_index\": idx,\n\t\t\t\t\t\"content_index\": 0,\n\t\t\t\t\t\"part\": map[string]any{\"type\": \"output_text\", \"annotations\": []any{}, \"logprobs\": []any{}, \"text\": \"\"},\n\t\t\t\t})\n\t\t\t\tmessageStarted = true\n\t\t\t}\n\t\t\tfullText += contentStr\n\t\t\tseq++\n\t\t\temitSSEEvent(w, flusher, \"response.output_text.delta\", map[string]any{\n\t\t\t\t\"type\": \"response.output_text.delta\",\n\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\"item_id\": msgID,\n\t\t\t\t\"output_index\": messageOutputIndex(),\n\t\t\t\t\"content_index\": 0,\n\t\t\t\t\"delta\": contentStr,\n\t\t\t\t\"logprobs\": []any{},\n\t\t\t})\n\t\t}\n\n\t\trawToolCalls, _ := delta[\"tool_calls\"].([]any)\n\t\tfor _, rawToolCall := range rawToolCalls {\n\t\t\ttc, ok := rawToolCall.(map[string]any)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tidxFloat, _ := tc[\"index\"].(float64)\n\t\t\tupstreamIndex := int(idxFloat)\n\t\t\tcall, exists := toolCalls[upstreamIndex]\n\t\t\tif !exists {\n\t\t\t\toutputIndex := indexAllocator.Allocate()\n\t\t\t\tcallID, _ := tc[\"id\"].(string)\n\t\t\t\tif callID == \"\" {\n\t\t\t\t\tcallID = \"call_\" + randomString(12)\n\t\t\t\t}\n\t\t\t\tfn, _ := tc[\"function\"].(map[string]any)\n\t\t\t\tname, _ := fn[\"name\"].(string)\n\t\t\t\titemType := toolCallOutputType(name, toolKinds)\n\t\t\t\tcall = map[string]any{\n\t\t\t\t\t\"output_index\": outputIndex,\n\t\t\t\t\t\"item_id\": \"fc_\" + callID,\n\t\t\t\t\t\"call_id\": callID,\n\t\t\t\t\t\"name\": name,\n\t\t\t\t\t\"arguments\": \"\",\n\t\t\t\t\t\"done\": false,\n\t\t\t\t\t\"item_type\": itemType,\n\t\t\t\t}\n\t\t\t\ttoolCalls[upstreamIndex] = call\n\t\t\t\ttoolOrder = append(toolOrder, upstreamIndex)\n\t\t\t\tseq++\n\t\t\t\temitSSEEvent(w, flusher, \"response.output_item.added\", map[string]any{\n\t\t\t\t\t\"type\": \"response.output_item.added\",\n\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\"output_index\": outputIndex,\n\t\t\t\t\t\"item\": map[string]any{\n\t\t\t\t\t\t\"id\": call[\"item_id\"],\n\t\t\t\t\t\t\"type\": itemType,\n\t\t\t\t\t\t\"status\": \"in_progress\",\n\t\t\t\t\t\t\"arguments\": \"\",\n\t\t\t\t\t\t\"call_id\": callID,\n\t\t\t\t\t\t\"name\": name,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\tfn, _ := tc[\"function\"].(map[string]any)\n\t\t\tif name, _ := fn[\"name\"].(string); name != \"\" {\n\t\t\t\tcall[\"name\"] = name\n\t\t\t\tif call[\"item_type\"] == \"function_call\" {\n\t\t\t\t\tcall[\"item_type\"] = toolCallOutputType(name, toolKinds)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif argDelta, _ := fn[\"arguments\"].(string); argDelta != \"\" {\n\t\t\t\tcall[\"arguments\"] = call[\"arguments\"].(string) + argDelta\n\t\t\t\tseq++\n\t\t\t\temitSSEEvent(w, flusher, \"response.function_call_arguments.delta\", map[string]any{\n\t\t\t\t\t\"type\": \"response.function_call_arguments.delta\",\n\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\"item_id\": call[\"item_id\"],\n\t\t\t\t\t\"output_index\": call[\"output_index\"],\n\t\t\t\t\t\"delta\": argDelta,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif usage, ok := chunk[\"usage\"].(map[string]any); ok {\n\t\t\ttotalUsage = usage\n\t\t}\n\t\tif finishReason == \"stop\" || finishReason == \"length\" || finishReason == \"content_filter\" {\n\t\t\tif finishReason == \"length\" {\n\t\t\t\tterminalStatus = \"incomplete\"\n\t\t\t\tterminalEvent = \"response.incomplete\"\n\t\t\t\titemStatus = \"incomplete\"\n\t\t\t}\n\t\t\temitReasoningDone()\n\t\t\tif !messageStarted && len(toolCalls) == 0 {\n\t\t\t\tidx := messageOutputIndex()\n\t\t\t\tseq++\n\t\t\t\temitSSEEvent(w, flusher, \"response.output_item.added\", map[string]any{\n\t\t\t\t\t\"type\": \"response.output_item.added\",\n\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\"output_index\": idx,\n\t\t\t\t\t\"item\": map[string]any{\"id\": msgID, \"type\": \"message\", \"status\": \"in_progress\", \"content\": []any{}, \"role\": \"assistant\"},\n\t\t\t\t})\n\t\t\t\tseq++\n\t\t\t\temitSSEEvent(w, flusher, \"response.content_part.added\", map[string]any{\n\t\t\t\t\t\"type\": \"response.content_part.added\",\n\t\t\t\t\t\"sequence_number\": seq,\n\t\t\t\t\t\"item_id\": msgID,\n\t\t\t\t\t\"output_index\": idx,\n\t\t\t\t\t\"content_index\": 0,\n\t\t\t\t\t\"part\": map[string]any{\"type\": \"output_text\", \"annotations\": []any{}, \"logprobs\": []any{}, \"text\": \"\"},\n\t\t\t\t})\n\t\t\t\tmessageStarted = true\n\t\t\t}\n\t\t\temitMessageDone()\n\t\t\tfor _, idx := range toolOrder {\n\t\t\t\temitToolCallDone(toolCalls[idx][\"output_index\"].(int), toolCalls[idx])\n\t\t\t}\n\t\t}\n\t}\n\n\temitReasoningDone()\n\temitMessageDone()\n\tfor _, idx := range toolOrder {\n\t\temitToolCallDone(toolCalls[idx][\"output_index\"].(int), toolCalls[idx])\n\t}\n\n\toutput := make([]any, indexAllocator.Len())\n\tif reasoningStarted {\n\t\toutput[reasoningOutputIndex] = reasoningItem(itemStatus)\n\t}\n\tif messageStarted {\n\t\toutput[messageIndex] = messageItem(itemStatus)\n\t}\n\tfor _, idx := range toolOrder {\n\t\tcall := toolCalls[idx]\n\t\titemType, _ := call[\"item_type\"].(string)\n\t\tif itemType == \"\" {\n\t\t\titemType = \"function_call\"\n\t\t}\n\t\titem := buildResponseToolCallItem(ToolCall{\n\t\t\tID: call[\"call_id\"].(string),\n\t\t\tFunction: FunctionCall{\n\t\t\t\tName: call[\"name\"].(string),\n\t\t\t\tArguments: call[\"arguments\"].(string),\n\t\t\t},\n\t\t}, itemType)\n\t\titem[\"status\"] = itemStatus\n\t\toutput[call[\"output_index\"].(int)] = item\n\t}\n\n\tcompletedResponse := map[string]any{\n\t\t\"id\": responseID,\n\t\t\"object\": \"response\",\n\t\t\"created_at\": createdAt,\n\t\t\"status\": terminalStatus,\n\t\t\"background\": false,\n\t\t\"error\": nil,\n\t\t\"incomplete_details\": nil,\n\t\t\"model\": model,\n\t\t\"output\": output,\n\t}\n\tif terminalStatus == \"incomplete\" {\n\t\tcompletedResponse[\"incomplete_details\"] = map[string]any{\"reason\": \"max_output_tokens\"}\n\t}\n\tapplyResponsesRequestEcho(completedResponse, originalReq)\n\tif len(tools) > 0 {\n\t\tcompletedResponse[\"tools\"] = tools\n\t}\n\tif toolChoice != nil {\n\t\tcompletedResponse[\"tool_choice\"] = toolChoice\n\t}\n\n\tif len(totalUsage) > 0 {\n\t\tusage := map[string]any{}\n\t\tif v, ok := totalUsage[\"prompt_tokens\"]; ok {\n\t\t\tusage[\"input_tokens\"] = v\n\t\t}\n\t\tif v, ok := totalUsage[\"prompt_tokens_details\"]; ok {\n\t\t\tusage[\"input_tokens_details\"] = v\n\t\t} else {\n\t\t\tusage[\"input_tokens_details\"] = map[string]any{\"cached_tokens\": 0}\n\t\t}\n\t\tif v, ok := totalUsage[\"completion_tokens\"]; ok {\n\t\t\tusage[\"output_tokens\"] = v\n\t\t}\n\t\tif v, ok := totalUsage[\"completion_tokens_details\"]; ok {\n\t\t\tusage[\"output_tokens_details\"] = v\n\t\t}\n\t\tif v, ok := totalUsage[\"total_tokens\"]; ok {\n\t\t\tusage[\"total_tokens\"] = v\n\t\t}\n\t\tif v, ok := totalUsage[\"input_tokens\"]; ok && usage[\"input_tokens\"] == nil {\n\t\t\tusage[\"input_tokens\"] = v\n\t\t}\n\t\tif v, ok := totalUsage[\"output_tokens\"]; ok && usage[\"output_tokens\"] == nil {\n\t\t\tusage[\"output_tokens\"] = v\n\t\t}\n\t\tcompletedResponse[\"usage\"] = usage\n\t}\n\n\tif totalUsage != nil {\n\t\tpt, _ := totalUsage[\"prompt_tokens\"].(float64)\n\t\tct, _ := totalUsage[\"completion_tokens\"].(float64)\n\t\ttt, _ := totalUsage[\"total_tokens\"].(float64)\n\t\tif tt > 0 {\n\t\t\trecordTokenUsage(model, int64(pt), int64(ct), int64(tt))\n\t\t}\n\t}\n\n\tseq++\n\temitSSEEvent(w, flusher, terminalEvent, map[string]any{\n\t\t\"type\": terminalEvent,\n\t\t\"sequence_number\": seq,\n\t\t\"response\": completedResponse,\n\t})\n\n\tif flusher != nil {\n\t\tflusher.Flush()\n\t}\n\tstoreResponseState(completedResponse, originalReq)\n}\n\nfunc convertChatToResponses(chatBody []byte, model string, wantReasoning bool, tools []ResponsesTool, toolChoice any) []byte {\n\tvar chat struct {\n\t\tID string `json:\"id\"`\n\t\tCreated int64 `json:\"created\"`\n\t\tChoices []struct {\n\t\t\tFinishReason string `json:\"finish_reason\"`\n\t\t\tMessage struct {\n\t\t\t\tContent any `json:\"content\"`\n\t\t\t\tRefusal string `json:\"refusal\"`\n\t\t\t\tReasoningContent string `json:\"reasoning_content\"`\n\t\t\t\tToolCalls []ToolCall `json:\"tool_calls\"`\n\t\t\t} `json:\"message\"`\n\t\t} `json:\"choices\"`\n\t\tUsage map[string]any `json:\"usage\"`\n\t}\n\tif err := json.Unmarshal(chatBody, &chat); err != nil {\n\t\tslog.Warn(\"convertChatToResponses unmarshal failed\", \"error\", err)\n\t}\n\n\treasoning := \"\"\n\tfinishReason := \"\"\n\tvar toolCalls []ToolCall\n\tmessageContent := []any(nil)\n\ttoolKinds := responsesToolKindMap(tools)\n\tif len(chat.Choices) > 0 {\n\t\tmessageContent, _ = chatContentToResponsesContent(chat.Choices[0].Message.Content)\n\t\tif refusal := chat.Choices[0].Message.Refusal; refusal != \"\" {\n\t\t\tmessageContent = []any{map[string]any{\"type\": \"refusal\", \"refusal\": refusal}}\n\t\t}\n\t\trc := chat.Choices[0].Message.ReasoningContent\n\t\tif wantReasoning {\n\t\t\treasoning = rc\n\t\t}\n\t\ttoolCalls = chat.Choices[0].Message.ToolCalls\n\t\tfinishReason = chat.Choices[0].FinishReason\n\t\tif len(messageContent) == 0 && rc != \"\" && len(toolCalls) == 0 {\n\t\t\tmessageContent, _ = chatContentToResponsesContent(rc)\n\t\t}\n\t}\n\n\toutcome := responsesOutcome(finishReason)\n\tstatus := outcome.Status\n\tresponses := map[string]any{\n\t\t\"id\": chat.ID,\n\t\t\"object\": \"response\",\n\t\t\"status\": status,\n\t\t\"background\": false,\n\t\t\"error\": nil,\n\t\t\"incomplete_details\": outcome.IncompleteDetails,\n\t\t\"model\": model,\n\t\t\"created_at\": chat.Created,\n\t}\n\tif len(tools) > 0 {\n\t\tresponses[\"tools\"] = tools\n\t}\n\tif toolChoice != nil {\n\t\tresponses[\"tool_choice\"] = toolChoice\n\t}\n\toutputID := \"msg_\" + chat.ID + \"_0\"\n\toutput := []any{}\n\tif reasoning != \"\" {\n\t\toutput = append(output, map[string]any{\n\t\t\t\"id\": \"rs_\" + chat.ID,\n\t\t\t\"type\": \"reasoning\",\n\t\t\t\"encrypted_content\": \"\",\n\t\t\t\"summary\": []any{map[string]any{\"type\": \"summary_text\", \"text\": reasoning}},\n\t\t})\n\t}\n\tif len(messageContent) > 0 {\n\t\toutput = append(output, map[string]any{\n\t\t\t\"id\": outputID,\n\t\t\t\"type\": \"message\",\n\t\t\t\"status\": status,\n\t\t\t\"role\": \"assistant\",\n\t\t\t\"content\": messageContent,\n\t\t})\n\t}\n\tfor _, tc := range toolCalls {\n\t\titem := buildResponseToolCallItem(tc, toolCallOutputType(tc.Function.Name, toolKinds))\n\t\titem[\"status\"] = status\n\t\toutput = append(output, item)\n\t}\n\tresponses[\"output\"] = output\n\tif chat.Usage != nil {\n\t\tusage := map[string]any{}\n\t\tif v, ok := chat.Usage[\"prompt_tokens\"]; ok {\n\t\t\tusage[\"input_tokens\"] = v\n\t\t}\n\t\tif v, ok := chat.Usage[\"prompt_tokens_details\"]; ok {\n\t\t\tusage[\"input_tokens_details\"] = v\n\t\t} else {\n\t\t\tusage[\"input_tokens_details\"] = map[string]any{\"cached_tokens\": 0}\n\t\t}\n\t\tif v, ok := chat.Usage[\"completion_tokens\"]; ok {\n\t\t\tusage[\"output_tokens\"] = v\n\t\t}\n\t\tif v, ok := chat.Usage[\"completion_tokens_details\"]; ok {\n\t\t\tusage[\"output_tokens_details\"] = v\n\t\t}\n\t\tif v, ok := chat.Usage[\"total_tokens\"]; ok {\n\t\t\tusage[\"total_tokens\"] = v\n\t\t}\n\t\tif v, ok := chat.Usage[\"input_tokens\"]; ok && usage[\"input_tokens\"] == nil {\n\t\t\tusage[\"input_tokens\"] = v\n\t\t}\n\t\tif v, ok := chat.Usage[\"output_tokens\"]; ok && usage[\"output_tokens\"] == nil {\n\t\t\tusage[\"output_tokens\"] = v\n\t\t}\n\t\tresponses[\"usage\"] = usage\n\t}\n\n\tresult, _ := json.Marshal(responses)\n\treturn result\n}\n\nfunc emitSSEEvent(w http.ResponseWriter, flusher http.Flusher, event string, data map[string]any) {\n\tjsonData, err := json.Marshal(data)\n\tif err != nil {\n\t\tslog.Error(\"marshal SSE event failed\", \"error\", err)\n\t\treturn\n\t}\n\tw.Write([]byte(\"event: \" + event + \"\\n\"))\n\tw.Write([]byte(\"data: \" + string(jsonData) + \"\\n\\n\"))\n\tif flusher != nil {\n\t\tflusher.Flush()\n\t}\n}\n\n// ======================== Admin 管理页面 ========================\n\nfunc reloadHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\trefreshOCSession()\n\tfetched, err := fetchModels()\n\tif err == nil && len(fetched) > 0 {\n\t\tmodelMu.Lock()\n\t\tmodelsCache = fetched\n\t\tmodelsLoaded = true\n\t\tmodelMu.Unlock()\n\t\tslog.Info(\"free models refreshed\", \"count\", len(fetched))\n\t}\n\tgoFetched, goErr := fetchGoModels()\n\tif goErr == nil && len(goFetched) > 0 {\n\t\tmodelMu.Lock()\n\t\tgoModelsCache = goFetched\n\t\tmodelMu.Unlock()\n\t\tslog.Info(\"go catalog refreshed\", \"count\", len(goFetched))\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(map[string]any{\n\t\t\"status\": \"ok\",\n\t\t\"session\": ocSessionID,\n\t\t\"free\": len(modelsCache),\n\t\t\"go\": len(goModelsCache),\n\t})\n\n}\nfunc adminConfigHandler(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase http.MethodGet:\n\t\tconfigMu.RLock()\n\t\tcfg := AppConfig{ModelAlias: modelAlias, ReasoningEffortMap: reasoningEffortMap, ForceDisableThinking: forceDisableThinking}\n\t\tconfigMu.RUnlock()\n\t\tsocks5Mu.RLock()\n\t\tcfg.Socks5Proxies = socks5Proxies\n\t\tcfg.ActiveSocks5 = activeSocks5\n\t\tcfg.Socks5PaidDirect = socks5PaidDirect\n\t\tsocks5Mu.RUnlock()\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(w).Encode(map[string]any{\n\t\t\t\"model_alias\": cfg.ModelAlias,\n\t\t\t\"reasoning_effort_map\": cfg.ReasoningEffortMap,\n\t\t\t\"force_disable_thinking\": cfg.ForceDisableThinking,\n\t\t\t\"socks5_proxies\": cfg.Socks5Proxies,\n\t\t\t\"active_socks5\": cfg.ActiveSocks5,\n\t\t\t\"socks5_paid_direct\": cfg.Socks5PaidDirect,\n\t\t\t\"log_level\": getLogLevelString(),\n\t\t\t\"log_bodies\": getLogBodies(),\n\t\t})\n\tcase http.MethodPost:\n\t\tvar payload struct {\n\t\t\tAppConfig\n\t\t\tLogLevel *string `json:\"log_level,omitempty\"`\n\t\t\tLogBodies *bool `json:\"log_bodies,omitempty\"`\n\t\t}\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\thttp.Error(w, `{\"error\":\"Invalid JSON\"}`, http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tif err := saveConfig(configPath, payload.AppConfig); err != nil {\n\t\t\thttp.Error(w, `{\"error\":\"Failed to save config\"}`, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tapplyConfig(payload.AppConfig)\n\t\tif payload.LogLevel != nil {\n\t\t\tsetLogLevelString(*payload.LogLevel)\n\t\t}\n\t\tif payload.LogBodies != nil {\n\t\t\tsetLogBodies(*payload.LogBodies)\n\t\t}\n\t\tif debugMode {\n\t\t\tslog.Info(\"config updated\",\n\t\t\t\t\"aliases\", len(payload.ModelAlias),\n\t\t\t\t\"effort_map\", len(payload.ReasoningEffortMap),\n\t\t\t\t\"force_disable\", payload.ForceDisableThinking,\n\t\t\t\t\"log_level\", getLogLevelString(),\n\t\t\t\t\"log_bodies\", getLogBodies(),\n\t\t\t)\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(w).Encode(map[string]string{\"status\": \"ok\"})\n\tdefault:\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t}\n}\n\nfunc adminStatsHandler(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase http.MethodGet:\n\t\ttokenStatsMu.Lock()\n\t\tdata, err := json.Marshal(tokenStats)\n\t\ttokenStatsMu.Unlock()\n\t\tif err != nil {\n\t\t\thttp.Error(w, `{\"error\":\"marshal error\"}`, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write(data)\n\tcase http.MethodDelete:\n\t\ttokenStatsMu.Lock()\n\t\ttokenStats = &TokenStatsData{Models: map[string]*ModelStats{}}\n\t\ttokenStatsMu.Unlock()\n\t\tsaveTokenStats()\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(w).Encode(map[string]string{\"status\": \"ok\"})\n\tdefault:\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t}\n}\n\nfunc adminPageHandler(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")\n\tw.Write([]byte(adminHTML))\n}\n\nfunc renderLoginPage(w http.ResponseWriter, msg string) {\n\tw.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")\n\tw.Write([]byte(adminLoginHTML))\n\tif msg != \"\" {\n\t\tw.Write([]byte(\"<script>document.addEventListener('DOMContentLoaded',function(){var m=document.getElementById('login-msg');if(m){m.textContent='\" + msg + \"';m.style.display='block'}})</script>\"))\n\t}\n}\n\nconst adminLoginHTML = `<!DOCTYPE html>\n<html lang=\"zh\" data-theme=\"light\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>登录 — OPENCODE TO API</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link href=\"https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n<style>\n:root{--bg:#f4f6fa;--surface:#fff;--border:#e2e6ed;--text:#1a1d26;--text-sec:#6a7180;--accent:#6c8aff;--accent-hover:#5a78f0;--radius:12px;--radius-sm:8px;--font:'Noto Sans SC',system-ui,-apple-system,sans-serif;--mono:'JetBrains Mono',Consolas,monospace}\n[data-theme=\"dark\"]{--bg:#0c0e14;--surface:#14161e;--border:#252835;--text:#e8eaf0;--text-sec:#8b90a5;--accent:#6c8aff;--accent-hover:#5a78f0}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14px;line-height:1.6;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}\nbody::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(ellipse at 30% 20%,rgba(108,138,255,.04) 0%,transparent 50%),radial-gradient(ellipse at 70% 80%,rgba(61,214,140,.03) 0%,transparent 50%);pointer-events:none;z-index:0}\n.container{max-width:400px;width:100%;position:relative;z-index:1}\n.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:36px 32px 32px}\n.logo{display:flex;align-items:center;gap:10px;margin-bottom:6px}\n.logo-mark{width:36px;height:36px;background:linear-gradient(135deg,var(--accent),#8b6cff);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#fff;flex-shrink:0}\n.logo-text{font-size:20px;font-weight:700;letter-spacing:-.5px;background:linear-gradient(135deg,var(--text),var(--text-sec));-webkit-background-clip:text;-webkit-text-fill-color:transparent}\n.logo-sub{font-size:12px;color:var(--text-sec);margin-top:2px}\n.subtitle{font-size:13px;color:var(--text-sec);margin-bottom:28px;margin-top:4px}\n.field{margin-bottom:16px}\n.field label{display:block;font-size:12px;font-weight:500;color:var(--text-sec);margin-bottom:6px;letter-spacing:.3px}\n.field input{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:14px;font-family:var(--mono);background:var(--surface);color:var(--text);transition:border-color .15s,box-shadow .15s}\n.field input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(108,138,255,.1)}\n.msg{display:none;background:rgba(240,96,96,.1);color:#d64545;padding:10px 14px;border-radius:var(--radius-sm);margin-bottom:16px;font-size:13px;text-align:center;border:1px solid rgba(240,96,96,.2)}\n[data-theme=\"dark\"] .msg{color:#f06060}\n.btn{width:100%;padding:10px;border:none;border-radius:var(--radius-sm);font-size:14px;font-weight:600;cursor:pointer;font-family:var(--font);background:var(--accent);color:#fff;transition:background .15s}\n.btn:hover{background:var(--accent-hover)}\n.theme-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}\n.theme-toggle{background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 12px;cursor:pointer;font-size:13px;color:var(--text-sec);font-family:var(--font);transition:all .15s}\n.theme-toggle:hover{border-color:var(--accent);color:var(--accent)}\n@media(max-width:500px){.card{padding:24px 20px}}\n</style>\n</head>\n<body>\n<div class=\"container\">\n<div class=\"card\">\n<div class=\"theme-bar\">\n<div class=\"logo\">\n<div class=\"logo-mark\">⌨</div>\n<div>\n<div class=\"logo-text\">OPENCODE TO API</div>\n<div class=\"logo-sub\">管理面板</div>\n</div>\n</div>\n<button class=\"theme-toggle\" onclick=\"toggleTheme()\">☀</button>\n</div>\n<div class=\"subtitle\">请输入管理密码以继续</div>\n<div class=\"msg\" id=\"login-msg\"></div>\n<form method=\"post\" action=\"/login\">\n<div class=\"field\">\n<label for=\"pwd\">密码</label>\n<input id=\"pwd\" name=\"password\" type=\"password\" placeholder=\"输入管理密码\" autocomplete=\"current-password\" required>\n</div>\n<button class=\"btn\" type=\"submit\">登录</button>\n</form>\n</div>\n</div>\n<script>\n(function(){var t=localStorage.getItem('theme');if(t==='dark'){document.documentElement.setAttribute('data-theme','dark')}})();\nfunction toggleTheme(){var d=document.documentElement;var n=d.getAttribute('data-theme')==='dark'?'light':'dark';if(n==='dark')d.setAttribute('data-theme','dark');else d.removeAttribute('data-theme');localStorage.setItem('theme',n);document.querySelector('.theme-toggle').textContent=n==='dark'?'🌙':'☀'}\n</script>\n</body>\n</html>`\n\nconst adminHTML = `<!DOCTYPE html>\n<html lang=\"zh\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>OPENCODE TO API 管理面板</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link href=\"https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n<style>\n:root {\n --bg: #f4f6fa;\n --surface: #ffffff;\n --surface-2: #f0f2f7;\n --border: #e2e6ed;\n --border-light: #d0d4df;\n --text: #1a1d26;\n --text-sec: #6a7180;\n --text-ter: #9ca3b0;\n --accent: #6c8aff;\n --accent-dim: rgba(108,138,255,.08);\n --accent-hover: #5a78f0;\n --green: #22a85a;\n --green-dim: rgba(34,168,90,.08);\n --green-hover: #1d9850;\n --orange: #d9600a;\n --orange-dim: rgba(217,96,10,.08);\n --orange-hover: #c45507;\n --red: #dc2626;\n --red-dim: rgba(220,38,38,.08);\n --radius: 12px;\n --radius-sm: 8px;\n --font: 'Noto Sans SC', system-ui, -apple-system, sans-serif;\n --mono: 'JetBrains Mono', Consolas, monospace;\n --glow-a: rgba(108,138,255,.03);\n --glow-b: rgba(61,214,140,.02);\n --stats-total-bg: #f0f2f7;\n}\n[data-theme=\"dark\"] {\n --bg: #0c0e14;\n --surface: #14161e;\n --surface-2: #1a1d27;\n --border: #252835;\n --border-light: #2e3142;\n --text: #e8eaf0;\n --text-sec: #8b90a5;\n --text-ter: #5c6080;\n --accent: #6c8aff;\n --accent-dim: rgba(108,138,255,.12);\n --accent-hover: #5a78f0;\n --green: #3dd68c;\n --green-dim: rgba(61,214,140,.12);\n --green-hover: #30c47a;\n --orange: #f0a050;\n --orange-dim: rgba(240,160,80,.12);\n --orange-hover: #e09040;\n --red: #f06060;\n --red-dim: rgba(240,96,96,.12);\n --glow-a: rgba(108,138,255,.04);\n --glow-b: rgba(61,214,140,.03);\n --stats-total-bg: var(--surface-2);\n}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14px;line-height:1.6;min-height:100vh}\nbody::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(ellipse at 30% 20%,var(--glow-a) 0%,transparent 50%),radial-gradient(ellipse at 70% 80%,var(--glow-b) 0%,transparent 50%);pointer-events:none;z-index:0}\n.container{max-width:1020px;margin:0 auto;padding:32px 24px;position:relative;z-index:1}\nheader{display:flex;align-items:flex-end;gap:16px;margin-bottom:28px;padding-bottom:20px;border-bottom:1px solid var(--border);justify-content:space-between}\n.logo{display:flex;align-items:center;gap:10px}\n.logo-mark{width:36px;height:36px;background:linear-gradient(135deg,var(--accent),#8b6cff);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#fff;flex-shrink:0}\n.logo-text{font-size:22px;font-weight:700;letter-spacing:-.5px;background:linear-gradient(135deg,var(--text),var(--text-sec));-webkit-background-clip:text;-webkit-text-fill-color:transparent}\n.logo-sub{font-size:12.5px;color:var(--text-ter);margin-bottom:2px}\n.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px 24px;transition:border-color .2s}\n.card:hover{border-color:var(--border-light)}\n.card h2{font-size:13px;font-weight:600;margin-bottom:16px;letter-spacing:.2px;display:flex;align-items:center;gap:8px;color:var(--text-sec);text-transform:uppercase}\n.card h2 .dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}\n.config-grid{display:grid;grid-template-columns:2fr 3fr;gap:16px;margin-top:16px}\n.config-grid .card{margin-bottom:0}\n.full-row{grid-column:1/-1}\n.form-group{margin-bottom:14px}\n.form-group:last-child{margin-bottom:0}\n.form-group label{display:block;font-size:11.5px;font-weight:500;color:var(--text-ter);margin-bottom:5px;letter-spacing:.4px;text-transform:uppercase}\n.form-group input[type=\"text\"],.form-group input[type=\"url\"],.form-group input[type=\"password\"],.form-group textarea,.form-group select,.m-select{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:13px;font-family:var(--mono);background:var(--surface-2);color:var(--text);transition:border-color .15s,box-shadow .15s}\n.form-group input:focus,.form-group textarea:focus,.form-group select:focus,.m-select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)}\n.form-group .hint{font-size:11px;color:var(--text-ter);margin-top:4px;line-height:1.4}\n.actions{display:flex;gap:8px;margin-top:14px;flex-wrap:wrap}\n.btn{padding:8px 16px;border-radius:var(--radius-sm);font-size:12.5px;font-weight:500;cursor:pointer;border:none;transition:all .15s;font-family:var(--font);white-space:nowrap}\n.btn-primary{background:var(--accent-dim);color:var(--accent)}\n.btn-primary:hover{background:var(--accent);color:#fff}\n.btn-default{background:var(--surface-2);color:var(--text-sec);border:1px solid var(--border)}\n.btn-default:hover{border-color:var(--border-light);color:var(--text)}\n.btn-success{background:var(--green-dim);color:var(--green)}\n.btn-success:hover{background:var(--green);color:#fff}\n.btn-warning{background:var(--orange-dim);color:var(--orange)}\n.btn-warning:hover{background:var(--orange);color:#fff}\n.btn-danger{background:var(--red-dim);color:var(--red)}\n.btn-danger:hover{background:var(--red);color:#fff}\n.tbl{width:100%;border-collapse:collapse;font-size:12.5px}\n.tbl th{text-align:left;font-weight:500;color:var(--text-ter);padding:8px 10px;border-bottom:1px solid var(--border);font-size:11px;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap}\n.tbl td{padding:7px 10px;border-bottom:1px solid var(--border)}\n.tbl tr:last-child td{border-bottom:none}\n.tbl input{width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-size:12.5px;font-family:var(--mono);background:var(--surface-2);color:var(--text);transition:border-color .15s,box-shadow .15s}\n.tbl input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-dim)}\n.tbl .m-select{padding:6px 10px;font-size:12.5px}\n.tbl th:last-child{width:52px}\n.tbl td:last-child{white-space:nowrap;text-align:center}\n#statsTable th:last-child{width:auto}\n#statsTable td:last-child{text-align:left;white-space:nowrap}\n.tbl .btn{padding:4px 10px;font-size:11px;white-space:nowrap}\n#statsTable td:first-child{font-weight:500;color:var(--text)}\n#statsTable td:not(:first-child){font-family:var(--mono);color:var(--text-sec);text-align:left}\n#statsTable tbody tr:hover{background:var(--surface-2)}\n#statsTable thead+tbody tr:last-child td{font-weight:600;color:var(--text);background:var(--stats-total-bg);border-top:1px solid var(--border-light)}\n.stats-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px}\n.stats-header .btns{display:flex;gap:6px;align-items:center}\n#toast{position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:var(--radius-sm);font-size:13px;font-weight:500;color:#fff;opacity:0;transition:opacity .25s,transform .25s;z-index:999;transform:translateY(-8px);pointer-events:none;backdrop-filter:blur(8px)}\n#toast.success{background:rgba(61,214,140,.85)}\n#toast.error{background:rgba(240,96,96,.85)}\n#toast.show{opacity:1;transform:translateY(0)}\n.empty-hint{color:var(--text-ter);font-size:13px;padding:28px;text-align:center}\n.think-row{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:12px;transition:border-color .15s}\n.think-row:hover{border-color:var(--border-light)}\n.think-row input[type=\"checkbox\"]{width:16px;height:16px;accent-color:var(--accent);cursor:pointer}\n.think-row label{font-size:13px;font-weight:500;cursor:pointer;margin:0;color:var(--text)}\n.think-row .hint{font-size:11px;color:var(--text-ter);margin:0 0 0 auto;white-space:nowrap}\n@media(max-width:700px){.config-grid{grid-template-columns:1fr}.container{padding:16px 12px}header{flex-direction:column;align-items:flex-start;gap:8px}}\n.theme-toggle{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 12px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;transition:all .15s;color:var(--text-sec);flex-shrink:0;line-height:1}\n.theme-toggle:hover{border-color:var(--border-light);color:var(--text)}\n</style>\n</head>\n<body>\n<div class=\"container\">\n<header>\n<div class=\"logo\">\n<div class=\"logo-mark\">⌨</div>\n<div>\n<div class=\"logo-text\">OPENCODE TO API</div>\n<div class=\"logo-sub\">OpenCode 免费 API → 兼容格式代理</div>\n</div>\n</div>\n<div style=\"display:flex;align-items:center;gap:8px\">\n<button class=\"theme-toggle\" onclick=\"toggleTheme()\" title=\"切换主题\">☀</button>\n<form method=\"post\" action=\"/logout\" style=\"margin:0\"><button class=\"theme-toggle\" type=\"submit\" title=\"退出登录\" style=\"font-size:14px\">退出</button></form>\n</div>\n</header>\n\n<div class=\"card\">\n<div class=\"stats-header\">\n<h2><span class=\"dot\" style=\"background:var(--green)\"></span>Token 统计</h2>\n<div class=\"btns\">\n<button class=\"btn btn-success\" onclick=\"reloadConfig()\">刷新</button>\n<button class=\"btn btn-danger\" onclick=\"resetStats()\">清空统计</button>\n<span id=\"resetStatus\" style=\"font-size:11px;color:var(--text-ter)\"></span>\n</div>\n</div>\n<div id=\"statsContent\" style=\"font-size:12.5px\">\n<div class=\"empty-hint\">加载中...</div>\n</div>\n</div>\n\n<div class=\"config-grid\">\n<div class=\"card\">\n<h2><span class=\"dot\" style=\"background:var(--orange)\"></span>推理力度映射</h2>\n<div style=\"margin-bottom:12px\">\n<table class=\"tbl\" id=\"effortTable\">\n<thead><tr><th style=\"width:35%\">请求值</th><th style=\"width:42%\">映射值</th><th style=\"width:23%\"></th></tr></thead>\n<tbody></tbody>\n</table>\n</div>\n<div class=\"think-row\">\n<input type=\"checkbox\" id=\"force_disable_thinking\">\n<label for=\"force_disable_thinking\">强制禁用思考模式</label>\n<span class=\"hint\">移除所有推理内容</span>\n</div>\n<div class=\"actions\">\n<button class=\"btn btn-primary\" onclick=\"addEffortRow()\">添加映射</button>\n<button class=\"btn btn-success\" onclick=\"saveConfig()\">保存全部</button>\n</div>\n</div>\n\n<div class=\"card\">\n<h2><span class=\"dot\" style=\"background:var(--accent)\"></span>模型映射</h2>\n<div style=\"margin-bottom:12px\">\n<table class=\"tbl\" id=\"aliasTable\">\n<thead><tr><th style=\"width:35%\">别名(请求名)</th><th style=\"width:42%\">实际模型(上游名)</th><th style=\"width:23%\"></th></tr></thead>\n<tbody></tbody>\n</table>\n</div>\n<div class=\"actions\">\n<button class=\"btn btn-primary\" onclick=\"addAliasRow()\">添加别名</button>\n<button class=\"btn btn-success\" onclick=\"saveConfig()\">保存全部</button>\n</div>\n</div>\n\n<div class=\"card full-row\">\n<h2><span class=\"dot\" style=\"background:var(--accent)\"></span>SOCKS5 代理</h2>\n<div style=\"margin-bottom:12px\">\n<table class=\"tbl\" id=\"socks5Table\">\n<thead><tr><th style=\"width:25%\">名称</th><th style=\"width:28%\">地址</th><th style=\"width:17%\">用户名</th><th style=\"width:17%\">密码</th><th style=\"width:13%\"></th></tr></thead>\n<tbody></tbody>\n</table>\n</div>\n<div class=\"form-group\">\n<label>启用代理</label>\n<select id=\"activeSocks5\" class=\"m-select\">\n<option value=\"\">直连(不使用代理)</option>\n</select>\n</div>\n<label class=\"check\"><input type=\"checkbox\" id=\"socks5_paid_direct\"> 带 key / 付费请求直连(不走 SOCKS5)</label>\n<p style=\"margin:6px 0 12px;color:var(--muted);font-size:13px\">默认关闭:只要启用了代理,public 与带 key 请求都走 SOCKS5。</p>\n<div class=\"actions\">\n<button class=\"btn btn-primary\" onclick=\"addSocks5Row()\">添加代理</button>\n<button class=\"btn btn-success\" onclick=\"saveConfig()\">保存全部</button>\n</div>\n</div>\n</div>\n</div>\n<div id=\"toast\"></div>\n<script>\nlet aliasData={},effortData={},modelList=[],socks5Data=[];\nfunction toggleTheme(){const d=document.documentElement;const cur=d.getAttribute('data-theme');const next=cur==='dark'?null:'dark';if(next)d.setAttribute('data-theme',next);else d.removeAttribute('data-theme');localStorage.setItem('theme',next||'light');document.querySelector('.theme-toggle').textContent=next==='dark'?'🌙':'☀'}\n(function(){const t=localStorage.getItem('theme');if(t==='dark'){document.documentElement.setAttribute('data-theme','dark');document.addEventListener('DOMContentLoaded',()=>{const b=document.querySelector('.theme-toggle');if(b)b.textContent='🌙'})}})();\nfunction reloadConfig(){const sy=window.scrollY;fetch('/api/reload',{method:'POST'}).then(r=>r.json()).then(d=>{showToast('会话已刷新,模型 '+d.models+' 个','success')}).catch(()=>{}).finally(()=>{loadConfig();loadStats();setTimeout(()=>window.scrollTo(0,sy),100)})}\nasync function loadConfig(){const sy=window.scrollY;try{const r=await fetch('/api/config');const cfg=await r.json();document.getElementById('force_disable_thinking').checked=cfg.force_disable_thinking||false;document.getElementById('socks5_paid_direct').checked=!!cfg.socks5_paid_direct;aliasData=cfg.model_alias||{};effortData=cfg.reasoning_effort_map||{};socks5Data=cfg.socks5_proxies||[];const mr=await fetch('/v1/models');const md=await mr.json();modelList=(md.data||[]).map(m=>m.id).sort();renderAliasTable();renderEffortTable();renderSocks5Table();document.getElementById('activeSocks5').value=cfg.active_socks5||'';setTimeout(()=>window.scrollTo(0,sy),0)}catch(e){showToast('失败: '+e.message,'error')}}\nfunction renderAliasTable(){const tb=document.querySelector('#aliasTable tbody');const ks=Object.keys(aliasData);if(!ks.length){tb.innerHTML='<tr><td colspan=\"3\" class=\"empty-hint\">暂无别名配置</td></tr>';return}tb.innerHTML=ks.map(k=>'<tr><td><input value=\"'+esc(k)+'\" data-field=\"key\"></td><td>'+modelSelectHtml(aliasData[k])+'</td><td><button class=\"btn btn-danger\" onclick=\"delAlias(this)\">删除</button></td></tr>').join('')}\nfunction modelSelectHtml(selected){let h='<select data-field=\"val\" class=\"m-select\">';h+='<option value=\"\">-- 选择模型 --</option>';for(const m of modelList){h+='<option value=\"'+esc(m)+'\"'+(selected===m?' selected':'')+'>'+esc(m)+'</option>'}h+='</select>';return h}\nfunction addAliasRow(){const tb=document.querySelector('#aliasTable tbody');if(tb.querySelector('.empty-hint'))tb.innerHTML='';tb.insertAdjacentHTML('beforeend','<tr><td><input value=\"\" placeholder=\"例如: gpt-5.5\" data-field=\"key\"></td><td>'+modelSelectHtml('')+'</td><td><button class=\"btn btn-danger\" onclick=\"delAlias(this)\">删除</button></td></tr>')}\nfunction delAlias(btn){const row=btn.closest('tr');const ki=row.querySelector('[data-field=\"key\"]');if(ki&&ki.value&&aliasData[ki.value])delete aliasData[ki.value];row.remove();if(!Object.keys(aliasData).length)document.querySelector('#aliasTable tbody').innerHTML='<tr><td colspan=\"3\" class=\"empty-hint\">暂无别名配置</td></tr>'}\nfunction collectAliases(){const r={};document.querySelectorAll('#aliasTable tbody tr').forEach(tr=>{const k=tr.querySelector('[data-field=\"key\"]'),v=tr.querySelector('[data-field=\"val\"]');if(k&&k.value.trim())r[k.value.trim()]=v?v.value.trim():''});aliasData=r;return r}\nfunction renderEffortTable(){const tb=document.querySelector('#effortTable tbody');const ks=Object.keys(effortData);if(!ks.length){tb.innerHTML='<tr><td colspan=\"3\" class=\"empty-hint\">暂无映射配置</td></tr>';return}tb.innerHTML=ks.map(k=>'<tr><td><input value=\"'+esc(k)+'\" data-field=\"key\"></td><td><input value=\"'+esc(effortData[k])+'\" data-field=\"val\"></td><td><button class=\"btn btn-danger\" onclick=\"delEffort(this)\">删除</button></td></tr>').join('')}\nfunction addEffortRow(){const tb=document.querySelector('#effortTable tbody');if(tb.querySelector('.empty-hint'))tb.innerHTML='';tb.insertAdjacentHTML('beforeend','<tr><td><input value=\"\" placeholder=\"例如: low\" data-field=\"key\"></td><td><input value=\"\" placeholder=\"例如: high\" data-field=\"val\"></td><td><button class=\"btn btn-danger\" onclick=\"delEffort(this)\">删除</button></td></tr>')}\nfunction delEffort(btn){const row=btn.closest('tr');const ki=row.querySelector('[data-field=\"key\"]');if(ki&&ki.value&&effortData[ki.value])delete effortData[ki.value];row.remove();if(!Object.keys(effortData).length)document.querySelector('#effortTable tbody').innerHTML='<tr><td colspan=\"3\" class=\"empty-hint\">暂无映射配置</td></tr>'}\nfunction collectEfforts(){const r={};document.querySelectorAll('#effortTable tbody tr').forEach(tr=>{const k=tr.querySelector('[data-field=\"key\"]'),v=tr.querySelector('[data-field=\"val\"]');if(k&&k.value.trim())r[k.value.trim()]=v?v.value.trim():''});effortData=r;return r}\nfunction renderSocks5Table(){const tb=document.querySelector('#socks5Table tbody');if(!socks5Data.length){tb.innerHTML='<tr><td colspan=\"5\" class=\"empty-hint\">暂无代理配置</td></tr>';return}tb.innerHTML=socks5Data.map((p,i)=>'<tr><td><input value=\"'+esc(p.name||'')+'\" data-field=\"name\"></td><td><input value=\"'+esc(p.addr)+'\" data-field=\"addr\" placeholder=\"例如: 127.0.0.1:1080\"></td><td><input value=\"'+esc(p.username||'')+'\" data-field=\"username\"></td><td><input value=\"'+esc(p.password||'')+'\" data-field=\"password\" type=\"password\"></td><td><button class=\"btn btn-danger\" onclick=\"delSocks5('+i+')\">删除</button></td></tr>').join('');renderSocks5Select()}\nfunction addSocks5Row(){const tb=document.querySelector('#socks5Table tbody');if(tb.querySelector('.empty-hint'))tb.innerHTML='';socks5Data.push({addr:'',name:''});renderSocks5Table()}\nfunction delSocks5(i){socks5Data.splice(i,1);renderSocks5Table()}\nfunction collectSocks5(){const r=[];document.querySelectorAll('#socks5Table tbody tr').forEach(tr=>{const a=tr.querySelector('[data-field=\"addr\"]');if(a&&a.value.trim())r.push({addr:a.value.trim(),name:(tr.querySelector('[data-field=\"name\"]')||{}).value?.trim()||'',username:(tr.querySelector('[data-field=\"username\"]')||{}).value?.trim()||'',password:(tr.querySelector('[data-field=\"password\"]')||{}).value?.trim()||''})});socks5Data=r;return r}\nfunction renderSocks5Select(){const sel=document.getElementById('activeSocks5');const cur=sel.value;sel.innerHTML='<option value=\"\">直连(不使用代理)</option>';socks5Data.forEach(p=>{if(p.addr){const label=p.name?p.name+' ('+p.addr+')':p.addr;const opt=document.createElement('option');opt.value=p.addr;opt.textContent=label;sel.appendChild(opt)}});if(socks5Data.length>=2){const opt=document.createElement('option');opt.value='__round_robin__';opt.textContent='轮询(自动切换)';sel.appendChild(opt)}sel.value=cur;if(!sel.value)sel.value='';}\nasync function saveConfig(){collectAliases();collectEfforts();collectSocks5();const cfg={model_alias:aliasData,reasoning_effort_map:effortData,force_disable_thinking:document.getElementById('force_disable_thinking').checked,socks5_proxies:socks5Data,active_socks5:document.getElementById('activeSocks5').value,socks5_paid_direct:document.getElementById('socks5_paid_direct').checked};try{const r=await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});if(!r.ok)throw new Error(await r.text());showToast('配置已保存','success');loadConfig()}catch(e){showToast('保存失败: '+e.message,'error')}}\nfunction esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}\nfunction showToast(msg,t){const e=document.getElementById('toast');e.textContent=msg;e.className=t+' show';clearTimeout(e._tid);e._tid=setTimeout(()=>e.classList.remove('show'),2500)}\nasync function resetStats(){if(!confirm('确认清空所有 Token 统计?\\n此操作不可撤销。'))return;const s=document.getElementById('resetStatus');s.textContent='清空中...';try{const r=await fetch('/api/stats',{method:'DELETE'});if(!r.ok)throw new Error(await r.text());document.getElementById('statsContent').innerHTML='<div class=\"empty-hint\">暂无数据</div>';s.textContent='已清空';setTimeout(()=>s.textContent='',2000)}catch(e){s.textContent='失败: '+e.message}}\nasync function loadStats(){try{const r=await fetch('/api/stats');const d=await r.json();const ms=d.models||{};const ks=Object.keys(ms);let h='<table class=\"tbl\" id=\"statsTable\"><thead><tr><th>模型</th><th>请求数</th><th>输入 Token</th><th>输出 Token</th><th>总计 Token</th></tr></thead><tbody>';if(!ks.length){h+='<tr><td colspan=\"5\" class=\"empty-hint\">暂无数据</td></tr>'}else{let tr=0,pt=0,ct=0,tt=0;for(const k of ks){const m=ms[k];h+='<tr><td>'+esc(k)+'</td><td>'+fmt(m.request_count)+'</td><td>'+fmt(m.prompt_tokens)+'</td><td>'+fmt(m.completion_tokens)+'</td><td>'+fmt(m.total_tokens)+'</td></tr>';tr+=m.request_count;pt+=m.prompt_tokens;ct+=m.completion_tokens;tt+=m.total_tokens}h+='<tr><td>总计</td><td>'+fmt(tr)+'</td><td>'+fmt(pt)+'</td><td>'+fmt(ct)+'</td><td>'+fmt(tt)+'</td></tr>'}h+='</tbody></table>';document.getElementById('statsContent').innerHTML=h}catch(e){document.getElementById('statsContent').innerHTML='<div class=\"empty-hint\">加载失败</div>'}}\nfunction fmt(n){return n.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g,',')}window.onload=function(){loadConfig();loadStats()};setInterval(loadStats,5000);document.addEventListener('visibilitychange',function(){if(!document.hidden)loadStats()});\n</script>\n</body>\n</html>`\n\n// ======================== Main ========================\n\nfunc main() {\n\tvar showVersion bool\n\tflag.StringVar(&port, \"port\", \"8000\", \"服务端口\")\n\tflag.StringVar(&configPath, \"config\", \"config.json\", \"配置文件路径\")\n\tflag.StringVar(&adminPassword, \"password\", \"123456\", \"管理面板密码(留空则不启用登录验证)\")\n\tflag.BoolVar(&debugMode, \"debug\", false, \"启用调试日志\")\n\tflag.StringVar(&logLevel, \"log-level\", \"info\", \"日志级别: debug/info/warn/error\")\n\tflag.StringVar(&logFile, \"log-file\", \"opencode2api.log\", \"日志文件路径\")\n\tflag.BoolVar(&logStdout, \"log-stdout\", true, \"是否同时写 stdout\")\n\tflag.IntVar(&logMaxSize, \"log-max-size\", 100, \"单日志文件最大 MB,超过即轮换\")\n\tflag.IntVar(&logMaxBackups, \"log-max-backups\", 7, \"保留的旧日志文件个数\")\n\tflag.IntVar(&logMaxAge, \"log-max-age\", 14, \"旧日志保留天数\")\n\tflag.BoolVar(&logCompress, \"log-compress\", true, \"轮换后 gzip 压缩\")\n\tflag.BoolVar(&logBodies, \"log-bodies\", false, \"Debug 下记录截断的 body 摘要\")\n\tflag.BoolVar(&showVersion, \"version\", false, \"显示版本信息\")\n\tflag.Parse()\n\n\tinitLogger()\n\tdefer closeLogRotator()\n\n\tif showVersion {\n\t\tfmt.Println(versionString())\n\t\treturn\n\t}\n\n\tcfg := loadConfig(configPath)\n\tapplyConfig(cfg)\n\tif err := saveConfig(configPath, cfg); err != nil {\n\t\tslog.Warn(\"failed to save config\", \"path\", configPath, \"error\", err)\n\t}\n\n\tloadTokenStats()\n\tslog.Info(\"config loaded\", \"path\", configPath)\n\tinitOCSession()\n\tmodels, err := fetchModels()\n\tif err != nil {\n\t\tslog.Warn(\"failed to fetch models on startup\", \"error\", err)\n\t} else {\n\t\tmodelMu.Lock()\n\t\tmodelsCache = models\n\t\tmodelsLoaded = true\n\t\tmodelMu.Unlock()\n\t\tslog.Info(\"models loaded\", \"count\", len(models))\n\t}\n\n\tgoModels, goErr := fetchGoModels()\n\tif goErr != nil {\n\t\tslog.Warn(\"failed to fetch go catalog on startup\", \"error\", goErr)\n\t} else {\n\t\tmodelMu.Lock()\n\t\tgoModelsCache = goModels\n\t\tmodelMu.Unlock()\n\t\tslog.Info(\"go catalog loaded\", \"count\", len(goModels))\n\t}\n\tstartModelRefresh()\n\tslog.Info(\"server starting\",\n\t\t\"port\", port,\n\t\t\"log_level\", getLogLevelString(),\n\t\t\"models\", len(getModelIDs()),\n\t\t\"aliases\", len(modelAlias),\n\t)\n\tif adminPassword != \"\" {\n\t\tslog.Info(\"admin panel enabled\", \"url\", fmt.Sprintf(\"http://localhost:%s/\", port))\n\t} else {\n\t\tslog.Info(\"admin panel disabled (no password)\")\n\t}\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/v1/chat/completions\", loggingMiddleware(chatCompletionsHandler))\n\tmux.HandleFunc(\"/v1/responses\", loggingMiddleware(responsesHandler))\n\tmux.HandleFunc(\"/v1/messages\", loggingMiddleware(claudeMessagesHandler))\n\tmux.HandleFunc(\"/v1/models\", loggingMiddleware(listModelsHandler))\n\tmux.HandleFunc(\"/login\", loggingMiddleware(loginHandler))\n\tmux.HandleFunc(\"/logout\", loggingMiddleware(logoutHandler))\n\tmux.HandleFunc(\"/api/config\", loggingMiddleware(requireAuth(adminConfigHandler)))\n\tmux.HandleFunc(\"/api/stats\", loggingMiddleware(requireAuth(adminStatsHandler)))\n\tmux.HandleFunc(\"/api/reload\", loggingMiddleware(requireAuth(reloadHandler)))\n\tmux.HandleFunc(\"/health\", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(\"OK\"))\n\t}))\n\tmux.HandleFunc(\"/\", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path == \"/\" {\n\t\t\trequireAuth(adminPageHandler)(w, r)\n\t\t\treturn\n\t\t}\n\t\thttp.NotFound(w, r)\n\t}))\n\n\taddr := \":\" + port\n\tserver := &http.Server{Addr: addr, Handler: mux}\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tgo func() {\n\t\tslog.Info(\"listening\", \"addr\", addr)\n\t\tif err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n\t\t\tslog.Error(\"server terminated\", \"error\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\t<-ctx.Done()\n\tslog.Info(\"shutting down\")\n\tshutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\tif err := server.Shutdown(shutdownCtx); err != nil {\n\t\tslog.Error(\"graceful shutdown failed\", \"error\", err)\n\t}\n}\n"}}}}