Full Code of BenedictKing/claude-proxy for AI

main 330011372f6c cached
152 files
1.2 MB
393.6k tokens
954 symbols
1 requests
Download .txt
Showing preview only (1,432K chars total). Download the full file or copy to clipboard to get everything.
Repository: BenedictKing/claude-proxy
Branch: main
Commit: 330011372f6c
Files: 152
Total size: 1.2 MB

Directory structure:
gitextract_1jsidgpo/

├── .claudeignore
├── .dockerignore
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── docker-build.yml
│       ├── release-linux.yml
│       ├── release-macos.yml
│       └── release-windows.yml
├── .gitignore
├── .prettierrc
├── AGENTS.md
├── ARCHITECTURE.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── Dockerfile
├── Dockerfile_China
├── ENVIRONMENT.md
├── LICENSE
├── Makefile
├── README.md
├── RELEASE.md
├── VERSION
├── backend-go/
│   ├── .air.toml
│   ├── .env.example
│   ├── .gitignore
│   ├── CLAUDE.md
│   ├── DEV_GUIDE.md
│   ├── Makefile
│   ├── README.md
│   ├── build.sh
│   ├── docs/
│   │   └── MALFORMED_TOOLCALL_MEMO.md
│   ├── go.mod
│   ├── go.sum
│   ├── internal/
│   │   ├── config/
│   │   │   ├── config.go
│   │   │   ├── config_baseurl_test.go
│   │   │   ├── config_gemini.go
│   │   │   ├── config_loader.go
│   │   │   ├── config_messages.go
│   │   │   ├── config_responses.go
│   │   │   ├── config_utils.go
│   │   │   └── env.go
│   │   ├── converters/
│   │   │   ├── chat_to_responses.go
│   │   │   ├── chat_to_responses_test.go
│   │   │   ├── claude_converter.go
│   │   │   ├── converter.go
│   │   │   ├── converter_test.go
│   │   │   ├── factory.go
│   │   │   ├── gemini_converter.go
│   │   │   ├── gemini_converter_test.go
│   │   │   ├── openai_converter.go
│   │   │   ├── responses_converter.go
│   │   │   ├── responses_passthrough.go
│   │   │   └── responses_to_chat.go
│   │   ├── handlers/
│   │   │   ├── channel_metrics_handler.go
│   │   │   ├── common/
│   │   │   │   ├── client_error_test.go
│   │   │   │   ├── failover.go
│   │   │   │   ├── failover_test.go
│   │   │   │   ├── multi_channel_failover.go
│   │   │   │   ├── request.go
│   │   │   │   ├── stream.go
│   │   │   │   ├── stream_test.go
│   │   │   │   └── upstream_failover.go
│   │   │   ├── frontend.go
│   │   │   ├── gemini/
│   │   │   │   ├── channels.go
│   │   │   │   ├── dashboard.go
│   │   │   │   ├── dashboard_test.go
│   │   │   │   ├── handler.go
│   │   │   │   ├── handler_test.go
│   │   │   │   └── stream.go
│   │   │   ├── global_stats_handler.go
│   │   │   ├── health.go
│   │   │   ├── messages/
│   │   │   │   ├── channels.go
│   │   │   │   ├── handler.go
│   │   │   │   └── models.go
│   │   │   ├── responses/
│   │   │   │   ├── channels.go
│   │   │   │   ├── compact.go
│   │   │   │   └── handler.go
│   │   │   └── settings.go
│   │   ├── httpclient/
│   │   │   └── client.go
│   │   ├── logger/
│   │   │   └── logger.go
│   │   ├── metrics/
│   │   │   ├── channel_metrics.go
│   │   │   ├── channel_metrics_activity_test.go
│   │   │   ├── channel_metrics_cache_stats_test.go
│   │   │   ├── persistence.go
│   │   │   └── sqlite_store.go
│   │   ├── middleware/
│   │   │   ├── auth.go
│   │   │   ├── auth_test.go
│   │   │   ├── cors.go
│   │   │   └── logger.go
│   │   ├── providers/
│   │   │   ├── claude.go
│   │   │   ├── gemini.go
│   │   │   ├── openai.go
│   │   │   ├── provider.go
│   │   │   ├── request_context_test.go
│   │   │   ├── responses.go
│   │   │   └── url_builder_test.go
│   │   ├── scheduler/
│   │   │   ├── channel_scheduler.go
│   │   │   └── channel_scheduler_test.go
│   │   ├── session/
│   │   │   ├── manager.go
│   │   │   └── trace_affinity.go
│   │   ├── types/
│   │   │   ├── gemini.go
│   │   │   ├── gemini_test.go
│   │   │   ├── responses.go
│   │   │   └── types.go
│   │   ├── utils/
│   │   │   ├── compression.go
│   │   │   ├── headers.go
│   │   │   ├── headers_test.go
│   │   │   ├── json.go
│   │   │   ├── json_compact_test.go
│   │   │   ├── json_test.go
│   │   │   ├── stream_synthesizer.go
│   │   │   ├── token_counter.go
│   │   │   └── token_counter_test.go
│   │   └── warmup/
│   │       └── url_manager.go
│   ├── main.go
│   └── version.go
├── docker-compose.yml
└── frontend/
    ├── .env.example
    ├── CLAUDE.md
    ├── ESLINT.md
    ├── eslint.config.js
    ├── index.html
    ├── package.json
    ├── src/
    │   ├── App.vue
    │   ├── assets/
    │   │   └── style.css
    │   ├── components/
    │   │   ├── AddChannelModal.vue
    │   │   ├── ChannelCard.vue
    │   │   ├── ChannelMetricsChart.vue
    │   │   ├── ChannelOrchestration.vue
    │   │   ├── ChannelStatusBadge.vue
    │   │   ├── GlobalStatsChart.vue
    │   │   └── KeyTrendChart.vue
    │   ├── composables/
    │   │   └── useTheme.ts
    │   ├── env.d.ts
    │   ├── main.ts
    │   ├── plugins/
    │   │   └── vuetify.ts
    │   ├── router/
    │   │   └── index.ts
    │   ├── services/
    │   │   ├── api.ts
    │   │   └── version.ts
    │   ├── stores/
    │   │   ├── auth.ts
    │   │   ├── channel.ts
    │   │   ├── dialog.ts
    │   │   ├── index.ts
    │   │   ├── preferences.ts
    │   │   └── system.ts
    │   ├── styles/
    │   │   └── settings.scss
    │   ├── utils/
    │   │   ├── quickInputParser.test.ts
    │   │   └── quickInputParser.ts
    │   └── views/
    │       └── ChannelsView.vue
    ├── tsconfig.json
    └── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .claudeignore
================================================

# 忽略常规依赖
**/node_modules
**/frontend/node_modules
**/dist
**/.git
**/.next
**/.vercel

# 忽略构建产物和二进制
**/target
**/bin
**/obj
*.mmdb
*.pack
*.bin
*.rlib
*.exe

# 忽略日志
**/logs
*.log
*.log.gz

# 忽略外部引用
refs/

================================================
FILE: .dockerignore
================================================
# Git 相关
.git
.gitignore
.github

# Node 模块
node_modules
frontend/node_modules
backend/node_modules

# 构建产物(会在 Docker 中重新构建)
frontend/dist
backend/dist
backend-go/dist
backend-go/frontend/dist
dist

# 日志和缓存
*.log
logs
.cache
.vite
frontend/node_modules/.vite

# 开发工具
.vscode
.idea
*.swp
*.swo
*~

# 环境配置
.env
.env.local
.env.*.local
backend/.env
backend-go/.env

# 配置文件(运行时生成)
.config
backend-go/.config

# 测试和覆盖率
coverage
*.coverprofile
coverage.out
coverage.html

# 临时文件
tmp
*.tmp
.DS_Store
Thumbs.db

# Aider 历史文件
.aider*

# Lock 文件保留
!bun.lock
!package-lock.json
!go.sum


================================================
FILE: .gitattributes
================================================
# Bun lockfile - 配置 Git 以显示可读的差异
# bun.lockb (二进制格式) - 需要 textconv 转换
*.lockb binary diff=lockb
# bun.lock (文本格式) - 直接作为文本处理
bun.lock text diff

# 确保跨平台行尾符一致性
* text=auto eol=lf

# 明确标记文本文件
*.go text diff=golang
*.js text
*.ts text
*.vue text
*.json text
*.md text
*.yaml text
*.yml text
*.toml text
*.sh text eol=lf

# 明确标记二进制文件
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary


================================================
FILE: .github/workflows/docker-build.yml
================================================
name: Build Docker Image

on:
  push:
    tags:
      - 'v*'

env:
  REGISTRY: crpi-i19l8zl0ugidq97v.cn-hangzhou.personal.cr.aliyuncs.com
  IMAGE_NAME: bene/claude-proxy

jobs:
  build_docker_image:
    name: Build Docker Image
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Aliyun Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.ALIYUN_CR_USERNAME }}
          password: ${{ secrets.ALIYUN_CR_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          flavor: |
            latest=false
          tags: |
            type=ref,event=tag
            type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            VERSION=${{ github.ref_name }}


================================================
FILE: .github/workflows/release-linux.yml
================================================
name: Release Linux Build
permissions:
  contents: write

on:
  push:
    tags:
      - "v*"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      - name: Build Frontend
        run: bun install && bun run build
        working-directory: ./frontend

      - name: Copy Frontend to Backend
        run: |
          rm -rf backend-go/frontend/dist
          mkdir -p backend-go/frontend/dist
          cp -r frontend/dist/* backend-go/frontend/dist/

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: "1.22.x"

      - name: Build Backend for amd64
        run: |
          go mod tidy
          go mod download
          CGO_ENABLED=0 go build -ldflags "-s -w -X main.Version=${{ github.ref_name }} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S') -X main.GitCommit=${{ github.sha }}" -o claude-proxy-linux-amd64
        working-directory: ./backend-go

      - name: Build Backend for arm64
        run: |
          CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X main.Version=${{ github.ref_name }} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S') -X main.GitCommit=${{ github.sha }}" -o claude-proxy-linux-arm64
        working-directory: ./backend-go

      - name: Release
        uses: softprops/action-gh-release@v2.0.5
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: |
            backend-go/claude-proxy-linux-amd64
            backend-go/claude-proxy-linux-arm64
          draft: true
          generate_release_notes: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release-macos.yml
================================================
name: Release MacOS Build
permissions:
  contents: write

on:
  push:
    tags:
      - "v*"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  release:
    runs-on: macos-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      - name: Build Frontend
        run: bun install && bun run build
        working-directory: ./frontend

      - name: Copy Frontend to Backend
        run: |
          rm -rf backend-go/frontend/dist
          mkdir -p backend-go/frontend/dist
          cp -r frontend/dist/* backend-go/frontend/dist/

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: "1.22.x"

      - name: Build Backend arm64
        run: |
          go mod tidy
          go mod download
          CGO_ENABLED=0 go build -ldflags "-s -w -X main.Version=${{ github.ref_name }} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S') -X main.GitCommit=${{ github.sha }}" -o claude-proxy-darwin-arm64
        working-directory: ./backend-go

      - name: Build Backend amd64
        run: |
          CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.Version=${{ github.ref_name }} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S') -X main.GitCommit=${{ github.sha }}" -o claude-proxy-darwin-amd64
        working-directory: ./backend-go

      - name: Release
        uses: softprops/action-gh-release@v2.0.5
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: |
            backend-go/claude-proxy-darwin-arm64
            backend-go/claude-proxy-darwin-amd64
          draft: true
          generate_release_notes: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release-windows.yml
================================================
name: Release Windows Build
permissions:
  contents: write

on:
  push:
    tags:
      - "v*"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  release:
    runs-on: windows-latest
    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      - name: Build Frontend
        run: bun install && bun run build
        working-directory: ./frontend

      - name: Copy Frontend to Backend
        run: |
          rm -rf backend-go/frontend/dist
          mkdir -p backend-go/frontend/dist
          cp -r frontend/dist/* backend-go/frontend/dist/

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: "1.22.x"

      - name: Build Backend amd64
        run: |
          go mod tidy
          go mod download
          CGO_ENABLED=0 go build -ldflags "-s -w -X main.Version=${{ github.ref_name }} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S') -X main.GitCommit=${{ github.sha }}" -o claude-proxy-windows-amd64.exe
        working-directory: ./backend-go

      - name: Build Backend arm64
        run: |
          CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w -X main.Version=${{ github.ref_name }} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S') -X main.GitCommit=${{ github.sha }}" -o claude-proxy-windows-arm64.exe
        working-directory: ./backend-go

      - name: Release
        uses: softprops/action-gh-release@v2.0.5
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: |
            backend-go/claude-proxy-windows-amd64.exe
            backend-go/claude-proxy-windows-arm64.exe
          draft: true
          generate_release_notes: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
# Logs

logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)

report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data

pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover

lib-cov

# Coverage directory used by tools like istanbul

coverage
*.lcov

# nyc test coverage

.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)

.grunt

# Bower dependency directory (https://bower.io/)

bower_components

# node-waf configuration

.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)

build/Release

# Dependency directories

node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)

web_modules/

# TypeScript cache

*.tsbuildinfo

# Optional npm cache directory

.npm

# Optional eslint cache

.eslintcache

# Optional stylelint cache

.stylelintcache

# Microbundle cache

.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history

.node_repl_history

# Output of 'npm pack'

*.tgz

# Yarn Integrity file

.yarn-integrity

# dotenv environment variable files

.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)

.cache
.parcel-cache

# Next.js build output

.next
out

# Nuxt.js build / generate output

.nuxt
dist

# Gatsby files

.cache/

# Comment in the public line in if your project uses Gatsby and not Next.js

# https://nextjs.org/blog/next-9-1#public-directory-support

# public

# vuepress build output

.vuepress/dist

# vuepress v2.x temp and cache directory

.temp
.cache

# Docusaurus cache and generated files

.docusaurus

# Serverless directories

.serverless/

# FuseBox cache

.fusebox/

# DynamoDB Local files

.dynamodb/

# TernJS port file

.tern-port

# Stores VSCode versions used for testing VSCode extensions

.vscode-test

# yarn v2

.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# Cursor rules

.cursorrules


# VSCode

.vscode/

node_modules

# Output
.output
.vercel
.netlify
.svelte-kit
build

# OS
.DS_Store
Thumbs.db

# Env
.env
.env.*
!.env.example
!.env.test

# Vite
vite.config.js.timestamp-*  
vite.config.ts.timestamp-*

# Custom
.claude/*
!.claude/skills/
.claude/skills/*
!.claude/skills/version-bump/
!.claude/skills/codex-review/
!.claude/skills/github-release/

.aider*
*.log
# 不要忽略包管理器的 lock 文件(package-lock.json, bun.lock, yarn.lock, pnpm-lock.yaml 等)
# 这些文件对于确保依赖版本一致性至关重要
# *.lock
# *-lock.json
# *-lock.yaml
*.patch

*.tsbuildinfo
**/.config/

# 测试和脚本文件
test-*.sh
test-*.js
test-*.ts
.gocache/
.gomodcache/

.snow/
refs/

# CCB/Codex 会话配置(包含本机路径等敏感信息)
.ccb_config/

# 临时调查文档
backend-go/cmd/
!backend-go/cmd/stream_verify/
docs/claude-code-investigation-*.md


================================================
FILE: .prettierrc
================================================
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "printWidth": 120,
  "tabWidth": 2,
  "arrowParens": "avoid",
  "useTabs": false,
  "bracketSpacing": true,
  "endOfLine": "lf"
}


================================================
FILE: AGENTS.md
================================================
# 仓库协作指南

## 重要约定
- **始终使用简体中文回复**。
- 遵循 SOLID / KISS / DRY / YAGNI;优先修复根因,避免无关重构。

## 项目结构与模块
- `backend-go/`:主 Go 服务(Gin),构建后内嵌前端静态资源;Go 代码位于 `backend-go/internal/`。
- `frontend/`:Vue 3 + Vite + Vuetify 管理界面;构建产物复制到 `backend-go/frontend/dist/` 并由后端 embed。
- `dist/`:发布构建产物(Go 二进制/打包后的 UI),不要手动编辑。
- `.config/`:运行时配置目录(`config.json` 及 `backups/`),随容器/本地持久化。
- `refs/`:外部参考项目存档,仅供对照,默认只读。
- 文档入口:`README.md`、`ARCHITECTURE.md`、`DEVELOPMENT.md`、`ENVIRONMENT.md`、`RELEASE.md`。

## 构建/测试/开发命令
- 全栈开发(推荐):根目录 `make dev`(前端 `bun run dev` + 后端 `air` 热重载)。
- 仅后端:`cd backend-go && make dev`。
- 构建运行:
  - 根目录 `make run` / `make build`(先构建前端再编译后端)。
  - 后端本地构建:`cd backend-go && make build-local`。
- 测试:`cd backend-go && make test`(或 `make test-cover` 生成覆盖率)。
- 前端:`cd frontend && bun install` 后 `bun run dev|build|type-check`。
- Docker:`docker-compose up -d` 默认拉取镜像;本地构建请按 `docker-compose.yml` 注释说明启用 `build`(可选 `Dockerfile_China`)。

## 代码风格
- Go:保持包职责单一、接口清晰;修改后运行 `go fmt ./...`。
- 前端:遵循现有 Vuetify/Tailwind/Prettier 风格;TypeScript 保持 strict。
- 配置/密钥:`.env`/`.json` 只提交示例文件(`*.example`),禁止提交真实密钥。

## 测试规范
- 新增/修改后端逻辑尽量补 `_test.go`,优先表驱动 + `httptest`。
- 前端目前无测试框架;如增加复杂逻辑再引入轻量单测。

## 安全与配置提示
- 部署前必须设置强 `PROXY_ACCESS_KEY`;生产环境建议关闭详细请求/响应日志。
- 代理端点统一鉴权(Header `x-api-key` / `Authorization: Bearer`);生产环境不建议使用 query `key`。


================================================
FILE: ARCHITECTURE.md
================================================
# 项目架构与设计

本文档详细介绍 Claude / Codex / Gemini API Proxy 的架构设计、技术选型和实现细节。

## 项目结构

项目采用一体化架构,Go 后端嵌入前端构建产物,实现单二进制部署:

```
claude-proxy/
├── backend-go/              # Go 后端服务(主程序)
│   ├── main.go             # 程序入口
│   ├── internal/           # 内部实现
│   │   ├── config/        # 配置管理
│   │   ├── handlers/      # HTTP 处理器
│   │   ├── middleware/    # 中间件
│   │   ├── providers/     # 上游服务适配器
│   │   ├── converters/    # Responses API 协议转换器
│   │   ├── scheduler/     # 多渠道调度器
│   │   ├── session/       # 会话管理
│   │   └── metrics/       # 渠道指标监控
│   ├── .config/           # 运行时配置
│   │   ├── config.json    # 主配置文件
│   │   └── backups/       # 配置备份 (保留最近10个)
│   └── .env               # 环境变量
├── frontend/               # Vue 3 + Vuetify 前端
│   ├── src/
│   │   ├── components/    # Vue 组件
│   │   ├── services/      # API 服务
│   │   └── styles/        # 样式文件
│   ├── public/            # 静态资源
│   └── dist/              # 构建产物(嵌入到 Go 二进制)
├── Makefile               # 构建和开发命令
├── docker-compose.yml     # Docker 部署配置
└── Dockerfile             # 容器镜像定义
```

## 核心技术栈

### 后端 (backend-go/)

- **运行时**: Go 1.22+
- **框架**: Gin Web Framework
- **配置管理**: fsnotify (热重载) + godotenv
- **前端嵌入**: Go `embed.FS`
- **并发模型**: 原生 Goroutine
- **性能优势**:
  - 启动时间: < 100ms (vs Node.js 2-3s)
  - 内存占用: ~20MB (vs Node.js 50-100MB)
  - 部署包大小: ~15MB (vs Node.js 200MB+)

### 前端 (frontend/)

- **框架**: Vue 3 (Composition API)
- **UI 组件库**: Vuetify 3
- **UI 主题**: 复古像素 (Neo-Brutalism)
- **构建工具**: Vite
- **状态管理**: Vue Composition API
- **HTTP 客户端**: Fetch API

### 构建系统

- **包管理器**: Bun (推荐) / npm / pnpm
- **构建工具**: Makefile + Shell Scripts
- **跨平台编译**: 支持 Linux/macOS/Windows, amd64/arm64

## 模块索引

| 模块           | 路径                              | 职责                        |
| -------------- | --------------------------------- | --------------------------- |
| **后端核心**   | `backend-go/`                     | API 代理、协议转换、配置管理 |
| **前端界面**   | `frontend/`                       | Web 管理界面、渠道配置       |
| **提供商适配** | `backend-go/internal/providers/`  | 上游服务协议转换            |
| **配置系统**   | `backend-go/internal/config/`     | 配置文件管理和热重载        |
| **HTTP 处理**  | `backend-go/internal/handlers/`   | REST API 路由和业务逻辑     |
| **中间件**     | `backend-go/internal/middleware/` | 认证、日志、CORS            |
| **会话管理**   | `backend-go/internal/session/`    | Responses API 会话跟踪      |
| **调度器**     | `backend-go/internal/scheduler/`  | 多渠道智能调度              |
| **指标管理**   | `backend-go/internal/metrics/`    | 渠道健康度和性能指标        |

## 设计模式

### 1. 提供商模式 (Provider Pattern)

所有上游 AI 服务都实现统一的 `Provider` 接口,实现协议转换:

```go
type Provider interface {
    // 将 Claude 格式请求转换为上游格式
    ConvertRequest(claudeRequest *ClaudeRequest) (*UpstreamRequest, error)

    // 将上游响应转换为 Claude 格式
    ConvertResponse(upstreamResponse *UpstreamResponse) (*ClaudeResponse, error)

    // 处理流式响应
    StreamResponse(upstream io.Reader, downstream io.Writer) error
}
```

**已实现的提供商**:
- `OpenAI`: 支持 OpenAI API 和兼容 API
- `Gemini`: Google Gemini API
- `Claude`: Anthropic Claude API (直接透传)
- `Responses`: Codex Responses API (支持会话管理)
- `OpenAI Old`: 旧版 OpenAI API 兼容

### 2. 配置管理器模式

`ConfigManager` 负责配置的生命周期管理:

```go
type ConfigManager struct {
    config     *Config
    configPath string
    watcher    *fsnotify.Watcher
    mu         sync.RWMutex
}

// 核心功能
func (cm *ConfigManager) Load() error
func (cm *ConfigManager) Save() error
func (cm *ConfigManager) Watch() error
func (cm *ConfigManager) GetNextAPIKey(channelID string) (string, error)
```

**特性**:
- 配置文件热重载 (无需重启服务)
- 自动备份机制 (保留最近 10 个版本)
- 线程安全的读写操作
- API 密钥轮询策略

### 3. 会话管理模式 (Session Manager)

为 Responses API 提供有状态的多轮对话支持:

```go
type SessionManager struct {
    sessions       map[string]*Session
    responseMap    map[string]string  // responseID -> sessionID
    mu             sync.RWMutex
    expiration     time.Duration
    maxMessages    int
    maxTokens      int
}

// 核心功能
func (sm *SessionManager) GetOrCreateSession(previousResponseID string) (*Session, error)
func (sm *SessionManager) AppendMessage(sessionID string, item ResponsesItem, tokens int)
func (sm *SessionManager) UpdateLastResponseID(sessionID, responseID string)
func (sm *SessionManager) RecordResponseMapping(responseID, sessionID string)
```

**特性**:
- 自动会话创建和关联
- 基于 `previous_response_id` 的会话追踪
- 限制消息数量(默认 100 条)
- 限制 Token 总数(默认 100k)
- 自动过期清理(默认 24 小时)
- 线程安全的并发访问

**会话流程**:
1. 首次请求:创建新会话,返回 `response_id`
2. 后续请求:通过 `previous_response_id` 查找会话
3. 自动追加用户输入和模型输出
4. 响应中包含 `previous_id` 链接历史

### 4. 转换器模式 (Converter Pattern) 🆕

**v2.0.5 新增**:为 Responses API 提供统一的协议转换架构。

#### 转换器接口

```go
type ResponsesConverter interface {
    // 将 Responses 请求转换为上游服务格式
    ToProviderRequest(sess *session.Session, req *types.ResponsesRequest) (interface{}, error)

    // 将上游响应转换为 Responses 格式
    FromProviderResponse(resp map[string]interface{}, sessionID string) (*types.ResponsesResponse, error)

    // 获取上游服务名称
    GetProviderName() string
}
```

#### 已实现的转换器

| 转换器 | 文件 | 转换方向 |
|--------|------|----------|
| `OpenAIChatConverter` | `openai_converter.go` | Responses ↔ OpenAI Chat Completions |
| `OpenAICompletionsConverter` | `openai_converter.go` | Responses ↔ OpenAI Completions |
| `ClaudeConverter` | `claude_converter.go` | Responses ↔ Claude Messages API |
| `ResponsesPassthroughConverter` | `responses_passthrough.go` | Responses ↔ Responses (透传) |

#### 工厂模式

```go
func NewConverter(serviceType string) ResponsesConverter {
    switch serviceType {
    case "openai":
        return &OpenAIChatConverter{}
    case "claude":
        return &ClaudeConverter{}
    case "responses":
        return &ResponsesPassthroughConverter{}
    default:
        return &OpenAIChatConverter{}
    }
}
```

#### 核心转换逻辑

**1. Instructions 字段处理**

```go
// OpenAI: instructions → messages[0] (role: system)
if req.Instructions != "" {
    messages = append(messages, map[string]interface{}{
        "role": "system",
        "content": req.Instructions,
    })
}

// Claude: instructions → system 参数(独立字段)
if req.Instructions != "" {
    claudeReq["system"] = req.Instructions
}
```

**2. 嵌套 Content 数组提取**

```go
func extractTextFromContent(content interface{}) string {
    // 1. 如果是 string,直接返回
    if str, ok := content.(string); ok {
        return str
    }

    // 2. 如果是 []ContentBlock,提取 input_text/output_text
    if arr, ok := content.([]interface{}); ok {
        texts := []string{}
        for _, c := range arr {
            if block["type"] == "input_text" || block["type"] == "output_text" {
                texts = append(texts, block["text"])
            }
        }
        return strings.Join(texts, "\n")
    }

    return ""
}
```

**3. Message Type 区分**

```go
switch item.Type {
case "message":
    // 新格式:嵌套结构(type=message, role=user/assistant, content=[]ContentBlock)
    role := item.Role  // 直接从 item.role 获取
    contentText := extractTextFromContent(item.Content)

case "text":
    // 旧格式:简单 string(向后兼容)
    contentStr := extractTextFromContent(item.Content)
    role := item.Role  // 使用 role 字段,不再依赖 [ASSISTANT] 前缀
}
```

#### 架构优势

- **易于扩展** - 新增上游只需实现 `ResponsesConverter` 接口
- **职责清晰** - 转换逻辑与 Provider 解耦
- **可测试性** - 每个转换器可独立测试
- **代码复用** - 公共逻辑提取到 `extractTextFromContent` 等基础函数
- **统一流程** - 所有上游使用相同的转换流程

#### 使用示例

```go
// 在 ResponsesProvider 中使用
converter := converters.NewConverter(upstream.ServiceType)
providerReq, err := converter.ToProviderRequest(sess, &responsesReq)
```

#### 支持的 Responses API 格式

```json
{
  "model": "gpt-4",
  "instructions": "You are a helpful assistant.",  // ✅ 新增
  "input": [
    {
      "type": "message",  // ✅ 新增
      "role": "user",     // ✅ 新增
      "content": [
        {
          "type": "input_text",  // ✅ 新增
          "text": "Hello!"
        }
      ]
    }
  ],
  "previous_response_id": "resp_xxxxx",
  "max_tokens": 1000
}
```

**对比旧格式**:

```json
{
  "model": "gpt-4",
  "input": [
    {
      "type": "text",
      "content": "Hello!"  // 简单 string
    },
    {
      "type": "text",
      "content": "[ASSISTANT]Hi there!"  // ❌ 使用前缀 hack
    }
  ]
}
```

### 5. 多渠道调度模式 (Channel Scheduler) 🆕

**v2.0.11 新增**:智能多渠道调度系统,支持优先级排序、健康检查和故障转移。

#### 核心组件

```go
// ChannelScheduler 多渠道调度器
type ChannelScheduler struct {
    configManager           *config.ConfigManager
    messagesMetricsManager  *metrics.MetricsManager
    responsesMetricsManager *metrics.MetricsManager
    traceAffinity           *session.TraceAffinityManager
}

// SelectChannel 选择最佳渠道
func (s *ChannelScheduler) SelectChannel(
    ctx context.Context,
    userID string,
    failedChannels map[int]bool,
    isResponses bool,
) (*SelectionResult, error)
```

#### 调度优先级

调度器按以下优先级选择渠道:

```
1. Trace 亲和性检查
   └─ 同一用户优先使用之前成功的渠道
        ↓ (无亲和记录或渠道不健康)
2. 健康检查
   └─ 跳过失败率 >= 50% 的渠道
        ↓ (渠道健康)
3. 优先级排序
   └─ 按 priority 字段排序(数字越小优先级越高)
        ↓ (所有健康渠道都失败)
4. 降级选择
   └─ 选择失败率最低的渠道
```

#### 渠道状态

```go
type UpstreamConfig struct {
    // ... 其他字段
    Priority int    `json:"priority"` // 优先级(数字越小越高)
    Status   string `json:"status"`   // active | suspended | disabled
}
```

| 状态 | 参与调度 | 故障转移 | 说明 |
|------|----------|----------|------|
| `active` | ✅ | ✅ | 正常运行 |
| `suspended` | ✅ | ✅ | 暂停但保留在序列中(被健康检查跳过) |
| `disabled` | ❌ | ❌ | 备用池,完全不参与 |

> ⚠️ **状态 vs 熔断的区别**:
> - `status: suspended` 是**配置层面**的状态,需要手动改为 `active` 才能恢复
> - 运行时熔断(`CircuitBrokenAt`)是**指标层面**的临时保护,15 分钟后自动恢复
> - 启动时就是 `suspended` 的渠道不会触发自动恢复逻辑

#### 指标管理

```go
// MetricsManager 渠道指标管理
type MetricsManager struct {
    metrics             map[int]*ChannelMetrics
    windowSize          int           // 滑动窗口大小(默认 10)
    failureThreshold    float64       // 失败率阈值(默认 0.5)
    circuitRecoveryTime time.Duration // 熔断恢复时间(默认 15 分钟)
}

// 核心方法
func (m *MetricsManager) RecordSuccess(channelIndex int)
func (m *MetricsManager) RecordFailure(channelIndex int)
func (m *MetricsManager) CalculateFailureRate(channelIndex int) float64
func (m *MetricsManager) IsChannelHealthy(channelIndex int) bool
```

**滑动窗口算法**:
- 基于最近 N 次请求(默认 10 次)计算失败率
- 失败率 >= 50% 触发熔断,记录 `CircuitBrokenAt` 时间戳
- 熔断恢复方式:
  - **自动恢复**:15 分钟后后台任务自动重置滑动窗口
  - **成功请求恢复**:任意成功请求立即清除熔断状态
  - **手动恢复**:通过 API 调用 `Reset()` 重置

#### Trace 亲和性

```go
// TraceAffinityManager 用户会话亲和性
type TraceAffinityManager struct {
    affinity map[string]*TraceAffinity // key: user_id
    ttl      time.Duration             // 默认 30 分钟
}

// 核心方法
func (m *TraceAffinityManager) GetPreferredChannel(userID string) (int, bool)
func (m *TraceAffinityManager) SetPreferredChannel(userID string, channelIndex int)
```

**工作原理**:
1. 用户首次请求 → 调度器选择渠道 → 记录亲和关系
2. 用户后续请求 → 优先使用之前成功的渠道
3. 渠道不健康或 30 分钟无活动 → 清除亲和记录

#### 调度流程图

```mermaid
graph TD
    A[请求到达] --> B{检查 Trace 亲和}
    B -->|有亲和| C{渠道健康?}
    C -->|是| D[使用亲和渠道]
    C -->|否| E[遍历活跃渠道]
    B -->|无亲和| E
    E --> F{渠道健康?}
    F -->|是| G[选择该渠道]
    F -->|否| H[跳过,继续遍历]
    H --> I{还有渠道?}
    I -->|是| E
    I -->|否| J[降级选择最佳渠道]
    D --> K[执行请求]
    G --> K
    J --> K
    K --> L{请求成功?}
    L -->|是| M[记录成功 + 更新亲和]
    L -->|否| N[记录失败 + 尝试下一渠道]
```

### 6. 中间件模式

Express/Gin 使用中间件架构处理横切关注点:

```go
// 认证中间件
func AuthMiddleware() gin.HandlerFunc

// 日志记录中间件
func LoggerMiddleware() gin.HandlerFunc

// 错误处理中间件
func ErrorHandler() gin.HandlerFunc

// CORS 中间件
func CORSMiddleware() gin.HandlerFunc
```

## 数据流图

```mermaid
graph TD
    A[Client Request] --> B[Gin Router]
    B --> C[Auth Middleware]
    C --> D[Logger Middleware]
    D --> E[Route Handler]
    E --> F[Channel Scheduler]
    F --> G[Trace Affinity Check]
    G --> H[Health Check]
    H --> I[Provider Factory]
    I --> J[Request Converter]
    J --> K[Upstream API]
    K --> L[Response Converter]
    L --> M[Metrics Recorder]
    M --> N[Client Response]

    O[Config Manager] --> F
    P[Metrics Manager] --> H
    Q[File Watcher] --> O
```

**Messages API 流程说明**:
1. 客户端请求到达 Gin 路由器
2. 通过认证和日志中间件
3. 路由处理器调用 Channel Scheduler
4. 调度器检查 Trace 亲和性(优先使用历史成功渠道)
5. 健康检查过滤不健康渠道
6. Provider 工厂创建对应的协议转换器
7. 转换请求格式并发送到上游 API
8. 接收上游响应并转换回 Claude 格式
9. 记录指标(成功/失败)并返回给客户端

**Responses API 特殊流程**:
```mermaid
graph TD
    A[Client Request] --> B[Responses Handler]
    B --> C[Session Manager]
    C --> D{检查 previous_response_id}
    D -->|存在| E[获取现有会话]
    D -->|不存在| F[创建新会话]
    E --> G[Responses Provider]
    F --> G
    G --> H[上游 API]
    H --> I[响应转换]
    I --> J[更新会话历史]
    J --> K[记录 Response Mapping]
    K --> L[返回带 response_id 的响应]
```

**Responses API 会话管理**:
1. 检查请求中的 `previous_response_id`
2. 如存在,通过 `responseMap` 查找对应的会话
3. 如不存在,创建新的会话 ID
4. 将用户输入追加到会话历史
5. 发送请求到上游 Responses API
6. 将模型输出追加到会话历史
7. 更新会话的 `last_response_id`
8. 记录 `response_id` → `session_id` 映射
9. 返回响应,包含 `id` (当前) 和 `previous_id` (上一轮)

## 技术选型决策

### 前端资源嵌入方案

#### 实现对比

**当前方案**:
```go
//go:embed frontend/dist/*
var frontendFS embed.FS

func ServeStaticFiles(r *gin.Engine) {
    // API 路由优先处理
    r.NoRoute(func(c *gin.Context) {
        path := c.Request.URL.Path

        // 检测 API 路径
        if isAPIPath(path) {
            c.JSON(404, gin.H{"error": "API endpoint not found"})
            return
        }

        // 尝试读取静态文件
        fileContent, err := fs.ReadFile(distFS, path[1:])
        if err == nil {
            contentType := getContentType(path)
            c.Data(200, contentType, fileContent)
            return
        }

        // SPA 回退到 index.html
        indexContent, _ := fs.ReadFile(distFS, "index.html")
        c.Data(200, "text/html; charset=utf-8", indexContent)
    })
}
```

**关键优势**:
1. ✅ **单次嵌入**: 只嵌入一次整个目录,避免重复
2. ✅ **智能文件检测**: 先尝试读取实际文件
3. ✅ **动态 Content-Type**: 根据扩展名返回正确类型
4. ✅ **API 路由优先**: API 404 返回 JSON 而非 HTML
5. ✅ **简洁代码**: 无需自定义 FileSystem 适配器

#### 缓存策略

**已实施**:
- API 路由返回 JSON 格式 404 错误
- 静态文件正确的 MIME 类型检测

**待优化**:
- HTML 文件: `Cache-Control: no-cache, no-store, must-revalidate`
- 静态资源 (.css, .js, 字体): `Cache-Control: public, max-age=31536000, immutable`

### Go vs TypeScript 重写

v2.0.0 将后端完全重写为 Go 语言:

| 指标            | TypeScript/Bun | Go         | 提升      |
| --------------- | -------------- | ---------- | --------- |
| **启动时间**    | 2-3s           | < 100ms    | **20x**   |
| **内存占用**    | 50-100MB       | ~20MB      | **70%↓**  |
| **部署包大小**  | 200MB+         | ~15MB      | **90%↓**  |
| **并发处理**    | 事件循环       | Goroutine  | 原生并发  |
| **部署依赖**    | Node.js 运行时 | 单二进制   | 零依赖    |

**选择 Go 的原因**:
- 高性能和低资源占用
- 单二进制部署,无需运行时
- 原生并发支持,适合高并发场景
- 强类型系统和出色的工具链

## 性能优化

### 智能构建缓存

Makefile 实现了智能缓存机制:

```makefile
.build-marker: $(shell find frontend/src -type f)
	@echo "检测到前端文件变更,重新构建..."
	cd frontend && npm run build
	@touch .build-marker

ensure-frontend-built: .build-marker
```

**性能对比**:

| 场景               | 之前   | 现在      | 提升       |
| ------------------ | ------ | --------- | ---------- |
| 首次构建           | ~10秒  | ~10秒     | 无变化     |
| **无变更重启**     | ~10秒  | **0.07秒** | **142x** 🚀 |
| 有变更重新构建     | ~10秒  | ~8.5秒    | 15%提升    |

### 请求头优化

针对不同上游使用不同的请求头策略:

- **Claude 渠道**: 保留原始请求头 (支持 `anthropic-version` 等)
- **OpenAI/Gemini 渠道**: 最小化请求头 (仅 `Host` 和 `Content-Type`)

这避免了转发无关头部导致上游 API 拒绝请求的问题。

## 安全设计

### 统一认证架构

所有访问入口受 `PROXY_ACCESS_KEY` 保护:

```go
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 健康检查无需认证
        if c.Request.URL.Path == "/health" {
            c.Next()
            return
        }

        // 验证访问密钥
        apiKey := c.GetHeader("x-api-key")
        if apiKey != expectedKey {
            c.JSON(401, gin.H{"error": "Unauthorized"})
            c.Abort()
            return
        }

        c.Next()
    }
}
```

**受保护的入口**:
1. 前端管理界面 (`/`)
2. 管理 API (`/api/*`)
3. Messages API (`/v1/messages`)
4. Responses API (`/v1/responses`)

**公开入口**:
- 健康检查 (`/health`)

### 敏感信息保护

- API 密钥掩码显示 (仅显示前 8 位和后 4 位)
- 日志中自动隐藏 `Authorization` 头
- 配置文件权限控制 (`.config/` 目录)

## 扩展性

### 添加新的上游服务

1. 在 `internal/providers/` 创建新的 provider 文件
2. 实现 `Provider` 接口
3. 在 `ProviderFactory` 注册新 provider
4. 更新配置文件模式

示例:
```go
// internal/providers/myapi.go
type MyAPIProvider struct{}

func (p *MyAPIProvider) ConvertRequest(req *ClaudeRequest) (*UpstreamRequest, error) {
    // 实现协议转换逻辑
}

// 在 factory 中注册
func GetProvider(providerType string) Provider {
    switch providerType {
    case "myapi":
        return &MyAPIProvider{}
    // ...
    }
}
```

## 文档资源

- **快速开始**: 参见 [README.md](README.md)
- **环境配置**: 参见 [ENVIRONMENT.md](ENVIRONMENT.md)
- **贡献指南**: 参见 [CONTRIBUTING.md](CONTRIBUTING.md)
- **版本历史**: 参见 [CHANGELOG.md](CHANGELOG.md)


================================================
FILE: CHANGELOG.md
================================================
# 版本历史

> **注意**: v2.0.0 开始为 Go 语言重写版本,v1.x 为 TypeScript 版本

---

## [v2.5.13] - 2026-01-31

### 修复

- **Gemini functionDeclaration parameters 类型修复** - 修复 Gemini API 返回 400 错误的问题
  - 问题:当 Claude 工具的 `InputSchema` 为 nil、缺少 `type` 字段或缺少 `properties` 字段时,Gemini API 拒绝请求
  - 新增 `normalizeGeminiParameters()` 辅助函数,确保 parameters schema 符合 Gemini 要求:
    - `parameters` 必须有 `type: "object"` 字段
    - `parameters` 必须有 `properties` 字段(即使为空对象)
  - 涉及文件:`backend-go/internal/providers/gemini.go`

---

## [v2.5.12] - 2026-01-30

### 新增

- **渠道置顶/置底功能** - 在渠道编排菜单中新增一键调整渠道位置的操作
  - 在渠道右侧弹出菜单中添加"置顶"和"置底"选项
  - 第一个渠道不显示"置顶",最后一个渠道不显示"置底"
  - 操作后立即保存到后端,复用现有 `saveOrder()` 函数
  - 解决渠道数量较多时拖拽排序不便的问题
  - 涉及文件:
    - `frontend/src/components/ChannelOrchestration.vue` - 添加菜单项和处理函数
    - `frontend/src/plugins/vuetify.ts` - 添加 `arrow-collapse-up/down` 图标

- **隐式缓存读取推断** - 当上游未明确返回 `cache_read_input_tokens` 但存在显著 token 差异时,自动推断缓存命中
  - 检测 `message_start` 与 `message_delta` 事件中 `input_tokens` 的差异
  - 触发条件:差额 > 10% 或差额 > 10000 tokens
  - 将差额自动填充到 `CacheReadInputTokens` 字段,使 token 统计更准确
  - **下游转发支持**:推断的 `cache_read_input_tokens` 会写入 `message_delta` 事件并转发给下游客户端
  - 新增 `StreamContext.MessageStartInputTokens` 字段记录初始 token 数
  - 新增 `inferImplicitCacheRead()` 函数在流结束时执行推断
  - 新增 `PatchTokensInEventWithCache()` 函数在修补 token 的同时写入推断的缓存值
  - **关键修复**:
    - `message_start` 的 `input_tokens` 不再累积到 `CollectedUsage.InputTokens`,确保差额计算正确
    - 使用 `originalUsageData` 传递给 `PatchMessageStartInputTokensIfNeeded`,避免误判
    - Token 修补逻辑增加隐式缓存信号检测,避免覆盖缓存命中场景下的正确低值
    - 隐式缓存推断在转发前执行,确保下游客户端能收到推断值
    - 仅当上游事件中不存在 `cache_read_input_tokens` 字段时才写入推断值,避免覆盖上游显式返回的 0 值
  - 涉及文件:
    - `backend-go/internal/handlers/common/stream.go` - 核心逻辑实现
    - `backend-go/internal/handlers/common/stream_test.go` - 单元测试(15 个边界场景)

---

## [v2.5.10] - 2026-01-26

### 新增

- **删除渠道时自动清理指标数据** - 修复删除渠道后内存和 SQLite 指标数据残留问题
  - 扩展 `PersistenceStore` 接口,新增按 `metrics_key` 和 `api_type` 批量删除记录的方法
  - 新增 `MetricsManager.DeleteChannelMetrics()` 方法,支持同时清理内存和持久化数据
  - 新增 `ChannelScheduler.DeleteChannelMetrics()` 统一删除入口
  - 修改 `DeleteUpstream` Handler(Messages/Responses/Gemini),删除后自动调用指标清理
  - SQLite 清理不依赖内存状态,确保即使内存中无数据也能正确清理持久化记录
  - 删除渠道时同时清理历史 Key 的指标数据
  - **按 `api_type` 过滤删除**:避免误删其他接口类型(messages/responses/gemini)的指标数据
  - **分批删除**:每批 500 条,避免触发 SQLite 变量上限(999)导致删除失败
  - **并发安全**:`flushMu` 互斥锁串行化 flush 与 delete;`asyncFlushWg` 确保 Close 前所有异步 flush 完成
  - 涉及文件:
    - `backend-go/internal/metrics/persistence.go` - 接口扩展(新增 apiType 参数)
    - `backend-go/internal/metrics/sqlite_store.go` - 实现 SQLite 删除逻辑(分批 + api_type 过滤)
    - `backend-go/internal/metrics/channel_metrics.go` - 新增删除方法,导出 `GenerateMetricsKey()`
    - `backend-go/internal/scheduler/channel_scheduler.go` - 新增统一删除入口
    - `backend-go/internal/handlers/*/channels.go` - 删除 Handler 改造
    - `backend-go/main.go` - 路由注册更新

- **换 Key 后历史数据累计统计** - 修复更换 API Key 后旧 Key 的历史统计数据丢失问题
  - 新增 `UpstreamConfig.HistoricalAPIKeys` 字段,存储历史 API Key 列表
  - 更新渠道时自动维护历史 Key 列表:被移除的 Key 进入历史列表,恢复的 Key 从历史列表移除
  - `Add*APIKey` / `Remove*APIKey` 接口同样维护历史 Key 列表
  - `ToResponseMultiURL()` 支持聚合历史 Key 指标(只计入总数,不影响实时失败率和熔断判断)
  - 前端查看渠道统计时,总数包含历史 Key 数据,Key 详情列表只显示当前活跃 Key
  - 涉及文件:
    - `backend-go/internal/config/config.go` - 新增 `HistoricalAPIKeys` 字段
    - `backend-go/internal/config/config_utils.go` - `Clone()` 方法深拷贝历史 Key
    - `backend-go/internal/config/config_*.go` - 更新渠道时维护历史 Key 列表
    - `backend-go/internal/metrics/channel_metrics.go` - 聚合逻辑支持历史 Key
    - `backend-go/internal/handlers/channel_metrics_handler.go` - 传入历史 Key 参数
    - `backend-go/internal/handlers/gemini/dashboard.go` - 传入历史 Key 参数

---

## [v2.5.9] - 2026-01-24

### 新增

- **前端模型映射智能选择功能** - 优化模型重定向配置体验,支持自动获取上游模型列表
  - 前端直连上游 `/v1/models` 接口,无需后端代理
  - 目标模型输入框改为 `v-combobox`,点击时自动获取模型列表
  - 为每个 API Key 并行检测 models 接口状态,提高效率
  - 在 API 密钥列表中实时显示状态标签:
    - 成功:绿色标签显示 `models 200 (N 个)`
    - 失败:红色标签显示 `models 错误码`,鼠标悬停显示详细错误消息
    - 加载中:蓝色标签显示 `检测中...`
  - 智能错误解析,支持上游标准错误格式 `{ "error": { "message": "...", "code": "..." } }`
  - 合并所有成功的模型列表并去重,提供完整的模型选项
  - 涉及文件:
    - `frontend/src/services/api.ts` - 新增 `fetchUpstreamModels` 函数和 `buildModelsURL` 工具函数
    - `frontend/src/components/AddChannelModal.vue` - 优化交互体验和状态管理

---

## [v2.5.8] - 2026-01-21

### 修复

- **客户端取消请求误计入失败** - 修复用户主动取消请求被错误计入渠道失败指标的问题
  - 新增 `isClientSideError` 函数,使用 `errors.Is` 正确识别被包装的 `context.Canceled` 错误
  - 仅识别明确的客户端取消(`context.Canceled`),连接故障(`broken pipe`、`connection reset`)继续 failover
  - 统一口径:`SendRequest` 和 `handleSuccess` 路径均应用客户端取消判断
  - 新增 `RecordRequestFinalizeClientCancel` 方法,客户端取消时仅计入总请求数,不计入失败数和失败率
  - 客户端取消不重置 `ConsecutiveFailures`,保留真实的连续失败计数
  - 涉及文件:
    - `backend-go/internal/handlers/common/upstream_failover.go` - 错误类型判断与分流
    - `backend-go/internal/metrics/channel_metrics.go` - 新增客户端取消记录方法
    - `backend-go/internal/handlers/common/client_error_test.go` - 单元测试

- **指标二次计数 Bug** - 修复 `RecordRequestFinalize*` fallback 路径导致的请求计数重复问题
  - 将 `RequestCount++` 从 `RecordRequestConnected` 移至 `RecordRequestFinalize*` 阶段
  - 采用延迟计数策略:连接时预写历史记录,完成时统一计数
  - 确保 fallback 路径(requestID 丢失/索引越界)不会触发二次计数
  - 涉及文件:`backend-go/internal/metrics/channel_metrics.go`

### 重构

- **指标记录架构优化** - 将指标记录职责从 handler 层下沉到 failover 层,实现"连接即计数"的实时统计
  - 新增 `RecordRequestConnected` / `RecordRequestFinalizeSuccess` / `RecordRequestFinalizeFailure` 三阶段记录机制
  - TCP 建连时即计入活跃请求数,响应完成后回写成功/失败与 token 数据
  - 移除 handler 层的 `RecordSuccessWithUsage` / `RecordFailure` 调用,统一由 `upstream_failover.go` 管理
  - 修改 `HandleSuccessFunc` 签名:返回 `(*types.Usage, error)` 而非 `*types.Usage`,支持流式响应错误处理
  - 修改 `ProcessStreamEvents` / `HandleStreamResponse` 返回 usage,避免在 stream 层直接记录指标
  - 新增 `pendingHistoryIdx` 映射表,支持请求 ID 到历史记录索引的快速查找
  - 新增 `cleanupHistoryLocked` 函数,清理过期历史记录时同步修正索引
  - 涉及文件:
    - `backend-go/internal/handlers/common/stream.go` - 移除指标记录,返回 usage
    - `backend-go/internal/handlers/common/upstream_failover.go` - 三阶段指标记录
    - `backend-go/internal/handlers/messages/handler.go` - 移除指标记录调用
    - `backend-go/internal/handlers/responses/handler.go` - 移除指标记录调用
    - `backend-go/internal/handlers/gemini/handler.go` - 移除指标记录调用
    - `backend-go/internal/metrics/channel_metrics.go` - 新增三阶段记录 API

## [v2.5.6] - 2026-01-20

### 修复

- **Gemini CLI 工具调用签名兼容** - 修复多轮工具调用中签名字段位置/命名不一致导致上游返回 400 的问题(启用 `injectDummyThoughtSignature` 时会为缺失签名的 `functionCall` 注入 dummy)。
- **Gemini CLI tools schema 兼容** - 支持 `parametersJsonSchema` 并在转发前清洗不兼容字段(`$schema` / `additionalProperties` / `const`),避免上游 400。
- **Gemini Dashboard stripThoughtSignature 字段缺失** - Dashboard API 补齐 `stripThoughtSignature` 字段,避免配置在刷新后丢失。

- **Gemini 渠道 stripThoughtSignature 字段无法保存** - 修复前端无法正确显示和保存"移除 Thought Signature"配置的问题
  - 修复 `GetUpstreams` 函数返回数据中缺失 `stripThoughtSignature` 字段
  - 修复前端图标显示问题(将 `mdi-signature-freehand` 改为 `mdi-close-circle`)
  - 统一图标和开关颜色为 `error` 红色,与"移除"操作语义一致
  - 涉及文件:
    - `backend-go/internal/handlers/gemini/channels.go` - 添加缺失字段
    - `frontend/src/components/AddChannelModal.vue` - 修复图标和颜色

### 新增

- **Gemini API thought_signature 兼容性方案** - 新增 `stripThoughtSignature` 配置项,支持兼容旧版 Gemini API
  - 新增 `StripThoughtSignature` 配置字段(布尔值),用于移除 `thought_signature` 字段
  - 实现 `stripThoughtSignatures()` 函数,移除所有 functionCall 的 thought_signature 字段
  - 配置优先级:`StripThoughtSignature` > `InjectDummyThoughtSignature`
  - 保持深拷贝机制,避免多渠道 failover 时污染后续请求
  - 前端添加"移除 Thought Signature"开关(仅 Gemini 渠道显示)
  - 涉及文件:
    - `backend-go/internal/config/config.go` - 配置结构定义
    - `backend-go/internal/config/config_gemini.go` - 配置更新逻辑
    - `backend-go/internal/handlers/gemini/handler.go` - 请求处理逻辑
    - `backend-go/internal/handlers/gemini/handler_test.go` - 单元测试
    - `frontend/src/components/AddChannelModal.vue` - 前端开关
    - `frontend/src/services/api.ts` - 类型定义

## [v2.5.5] - 2026-01-19

## [v2.5.4] - 2026-01-19

### 重构

- **Failover 逻辑模块化** - 将多渠道和单上游 failover 逻辑提取到公共模块,大幅减少代码重复
  - 新增 `backend-go/internal/handlers/common/multi_channel_failover.go` - 多渠道 failover 外壳逻辑
  - 新增 `backend-go/internal/handlers/common/upstream_failover.go` - 单上游 Key/BaseURL 轮转逻辑
  - 重构 Messages、Responses、Gemini 三个 handler,使用统一的 failover 函数
  - 代码行数减少:-1253 行,+475 行(净减少 778 行)
  - 涉及文件:
    - `backend-go/internal/handlers/messages/handler.go`
    - `backend-go/internal/handlers/responses/handler.go`
    - `backend-go/internal/handlers/gemini/handler.go`
    - `backend-go/internal/scheduler/channel_scheduler.go`

## [v2.5.3] - 2026-01-19

### 修复

- **Models API 日志标签修正** - 修正 Models API 相关日志标签,确保正确区分 Messages 和 Responses 渠道
  - 修正 `models.go` 中 `tryModelsRequest` 和 `fetchModelsFromChannel` 函数的日志标签
  - 使用动态 `channelType` 变量替代硬编码的 `"Messages"` 字符串
  - 日志标签格式统一为 `[Messages-Models]` 或 `[Responses-Models]`
  - 涉及文件:`backend-go/internal/handlers/messages/models.go`
- **多渠道 failover 客户端取消检测** - 在 failover 循环中添加客户端断开检测,避免客户端已取消请求后继续尝试其他渠道
  - 在每次渠道选择前检查 `c.Request.Context().Done()`
  - 客户端断开时立即返回,不再进行无效的渠道 failover
  - 涉及文件:
    - `backend-go/internal/handlers/gemini/handler.go` - Gemini API 处理器
    - `backend-go/internal/handlers/messages/handler.go` - Messages API 处理器
    - `backend-go/internal/handlers/responses/handler.go` - Responses API 处理器

### 新增

- **响应 model 字段改写可配置化** - 新增环境变量 `REWRITE_RESPONSE_MODEL` 控制是否改写响应中的 model 字段
  - 默认值:`false`(保持上游返回的原始 model)
  - 启用后:当上游返回的 model 与请求的 model 不一致时,自动改写为请求的 model
  - 适用范围:仅影响 Messages API 的流式响应,不影响 Responses API 和 Gemini API
  - 涉及文件:
    - `backend-go/.env.example` - 添加配置说明和默认值
    - `backend-go/internal/config/env.go` - 添加 `RewriteResponseModel` 配置字段
    - `backend-go/internal/handlers/common/stream.go` - 修改 `PatchMessageStartEvent` 函数,仅在配置启用时改写 model 字段

## [v2.5.2] - 2026-01-19

### 新增

- **Gemini thought_signature 可配置化** - 新增渠道级配置开关 `injectDummyThoughtSignature`
  - 新增 `ensureThoughtSignatures` 函数:为所有缺失 `thought_signature` 的 `functionCall` 注入 dummy 值
  - 使用官方推荐的 `skip_thought_signature_validator` 跳过验证
  - **默认关闭**:保持原样,符合官方 Gemini API 标准
  - **用户可开启**:为需要该字段的第三方 API 注入 dummy signature
  - 前端 UI:在 Gemini 渠道编辑界面添加"注入 Dummy Thought Signature"开关
  - 涉及文件:
    - `backend-go/internal/config/config.go` - 添加 `InjectDummyThoughtSignature` 配置字段
    - `backend-go/internal/config/config_gemini.go` - 更新方法支持新字段
    - `backend-go/internal/config/config_messages.go` - 更新方法支持新字段
    - `backend-go/internal/handlers/gemini/handler.go` - 根据配置决定是否调用 `ensureThoughtSignatures`
    - `backend-go/internal/types/gemini.go` - 新增共享常量 `DummyThoughtSignature`
    - `backend-go/internal/converters/gemini_converter.go` - 使用共享常量
    - `frontend/src/services/api.ts` - 添加类型定义
    - `frontend/src/components/AddChannelModal.vue` - 添加配置开关 UI
    - `frontend/src/plugins/vuetify.ts` - 添加 `mdi-signature` 图标映射
  - 配置优化:将 `.ccb_config/` 目录加入 `.gitignore`,避免泄露本机路径等敏感信息

- **codex-review 技能 v2.1.0** - 新增自动暂存新增文件功能,避免 codex 审核时报 P1 错误
  - 新增步骤 2:在审核前自动暂存所有新增文件
  - 使用安全的 `git ls-files -z | while read` 命令,正确处理特殊文件名(空格、换行、以 `-` 开头)
  - 修复空列表问题:当没有新增文件时安全跳过,不会报错
  - 优化元数据:添加 `user-invocable: true` 和 `context: fork` 字段
  - 优化描述:添加触发关键词,移除 `(user)` 后缀
  - 更新完整审核协议:增加 `[PREPARE] Stage Untracked Files` 步骤
  - 创建 Plugin Marketplace 配置:`.claude-plugin/marketplace.json`
  - 创建详细文档:`.claude/skills/codex-review/README.md`
  - 涉及文件:`.claude/skills/codex-review/SKILL.md`, `.claude-plugin/marketplace.json`, `.claude/skills/codex-review/README.md`

### 优化

- **渠道活跃度图表颜色优化** - 状态条柱状图颜色改为显示每个 6 秒段的独立成功率
  - 修改 SVG 渐变定义:为每个柱子单独定义渐变色(`gradient-${channelIndex}-${i}`)
  - 重构 `getActivityBars` 函数:为每个 6 秒时间段计算独立的成功率并分配颜色
  - 颜色规则(7 档分级):
    - 深红色(0-5%):极端故障
    - 红色(5-20%):严重失败
    - 深橙色(20-40%):高失败率
    - 橙色(40-60%):中等失败率
    - 黄色(60-80%):轻微失败
    - 黄绿色(80-95%):良好
    - 绿色(95-100%):优秀
  - 效果:用户可以更清晰地看到每个时间段的健康状况,颜色变化更细腻
  - 性能优化:新增 `activityBarsCache` 计算属性缓存柱状图数据,避免重复计算
  - 代码清理:删除未使用的 `activityColorCache` 和 `getActivityColor` 函数
  - 涉及文件:`frontend/src/components/ChannelOrchestration.vue`

- **修复 Dashboard 切换 Tab 时数据闪烁问题** - 将 Dashboard 数据改为按 API 类型独立缓存
  - 重构 `channelStore`:将单一全局 `dashboardMetrics`/`dashboardStats`/`dashboardRecentActivity` 改为按 Tab(messages/responses/gemini)独立缓存的 `dashboardCache` 结构
  - 新增 `currentDashboardMetrics`、`currentDashboardStats`、`currentDashboardRecentActivity` 计算属性,根据当前 Tab 返回对应缓存数据
  - 切换 Tab 时直接显示该 Tab 的缓存数据,避免显示其他 Tab 的旧数据导致闪烁
  - 涉及文件:`frontend/src/stores/channel.ts`、`frontend/src/views/ChannelsView.vue`

### 重构

- **前端系统状态管理重构** - 将 App.vue 中的系统级状态迁移到 SystemStore
  - 新增 `src/stores/system.ts` 系统状态 Store,统一管理系统运行状态、版本信息、Fuzzy 模式加载状态
  - 重构 `src/App.vue`,移除本地系统状态变量(systemStatus、versionInfo、isCheckingVersion、fuzzyModeLoading、fuzzyModeLoadError),改用 SystemStore 统一管理
  - 更新 `src/stores/index.ts`,导出 SystemStore
  - 新增 2 个计算属性:systemStatusText、systemStatusDesc
  - 新增 8 个状态管理方法:setSystemStatus、setVersionInfo、setCurrentVersion、setCheckingVersion、setFuzzyModeLoading、setFuzzyModeLoadError、resetSystemState
  - 优势:
    - 状态集中:所有系统级状态统一管理,避免分散在组件中
    - 代码简化:App.vue 系统状态逻辑更清晰,减少本地状态管理
    - 可复用性:其他组件可直接使用 SystemStore 的系统状态
    - 易维护:系统状态变更集中在 Store 中,便于调试和扩展
  - 涉及文件:`frontend/src/stores/system.ts`、`frontend/src/stores/index.ts`、`frontend/src/App.vue`

- **前端对话框状态管理重构** - 将 App.vue 中的对话框状态迁移到 DialogStore
  - 新增 `src/stores/dialog.ts` 对话框状态 Store,统一管理添加/编辑渠道对话框和添加 API 密钥对话框
  - 重构 `src/App.vue`,移除本地对话框状态变量(showAddChannelModal、showAddKeyModalRef、editingChannel、selectedChannelForKey、newApiKey),改用 DialogStore 统一管理
  - 更新 `src/stores/index.ts`,导出 DialogStore
  - 新增 6 个状态管理方法:openAddChannelModal、openEditChannelModal、closeAddChannelModal、openAddKeyModal、closeAddKeyModal、resetDialogState
  - 优势:
    - 状态集中:所有对话框相关状态统一管理,避免分散在组件中
    - 代码简化:App.vue 对话框逻辑更清晰,减少本地状态管理
    - 可复用性:其他组件可直接使用 DialogStore 的对话框状态
    - 易维护:对话框状态变更集中在 Store 中,便于调试和扩展
  - 涉及文件:`frontend/src/stores/dialog.ts`、`frontend/src/stores/index.ts`、`frontend/src/App.vue`

- **前端偏好设置管理重构** - 将 App.vue 中的用户偏好设置迁移到 PreferencesStore
  - 新增 `src/stores/preferences.ts` 偏好设置 Store,统一管理暗色模式、Fuzzy 模式、全局统计面板状态
  - 重构 `src/App.vue`,移除本地偏好设置变量(darkModePreference、fuzzyModeEnabled、showGlobalStats),改用 PreferencesStore 统一管理
  - 更新 `src/stores/index.ts`,导出 PreferencesStore
  - 支持自动持久化到 localStorage(使用 pinia-plugin-persistedstate)
  - 优势:
    - 状态集中:所有用户偏好设置统一管理,避免分散在组件中
    - 自动持久化:用户设置自动保存到本地存储,刷新页面后保持
    - 代码简化:App.vue 偏好设置逻辑更清晰,减少本地状态管理
    - 可复用性:其他组件可直接使用 PreferencesStore 的偏好设置
  - 涉及文件:`frontend/src/stores/preferences.ts`、`frontend/src/stores/index.ts`、`frontend/src/App.vue`

- **前端认证状态管理重构** - 将 App.vue 中的认证相关状态迁移到 AuthStore
  - 扩展 `src/stores/auth.ts`,新增认证 UI 状态管理(authError、authAttempts、authLockoutTime、isAutoAuthenticating、isInitialized、authLoading、authKeyInput)
  - 重构 `src/App.vue`,移除本地认证状态变量,改用 AuthStore 统一管理
  - 新增 `isAuthLocked` 计算属性,自动判断认证锁定状态
  - 新增 8 个状态管理方法:setAuthError、incrementAuthAttempts、resetAuthAttempts、setAuthLockout、setAutoAuthenticating、setInitialized、setAuthLoading、setAuthKeyInput
  - 优势:
    - 状态集中:所有认证相关状态统一管理,避免分散在组件中
    - 代码简化:App.vue 认证逻辑更清晰,减少本地状态管理
    - 可复用性:其他组件可直接使用 AuthStore 的认证状态
    - 安全性增强:认证失败次数和锁定时间集中管理,便于扩展
  - 涉及文件:`frontend/src/stores/auth.ts`、`frontend/src/App.vue`

- **前端渠道管理逻辑重构** - 将 App.vue 中的渠道管理逻辑提取到 Pinia Store
  - 新增 `src/stores/channel.ts` 渠道状态 Store,统一管理三种 API 类型(Messages/Responses/Gemini)的渠道数据
  - 重构 `src/App.vue`,移除 300+ 行本地状态和业务逻辑,改用 ChannelStore 统一管理
  - 更新 `src/stores/index.ts`,导出 ChannelStore
  - 优势:
    - 代码解耦:App.vue 从 1000+ 行减少到 700+ 行,职责更清晰
    - 状态集中:渠道数据、指标、自动刷新定时器统一管理
    - 可复用性:其他组件可直接使用 ChannelStore,无需通过 props 传递
    - 可测试性:业务逻辑独立于组件,便于单元测试
  - 涉及文件:`frontend/src/stores/channel.ts`、`frontend/src/stores/index.ts`、`frontend/src/App.vue`

- **前端状态管理架构升级** - 引入 Pinia 状态管理库,替代原有的本地状态管理
  - 新增 `pinia` 和 `pinia-plugin-persistedstate` 依赖,实现响应式状态管理和自动持久化
  - 新增 `src/stores/auth.ts` 认证状态 Store,统一管理 API Key 和认证状态
  - 重构 `src/services/api.ts`,从 AuthStore 获取 API Key,移除本地状态管理逻辑
  - 重构 `src/App.vue`,使用 AuthStore 替代 `isAuthenticated` 本地状态,简化认证流程
  - 更新 `src/main.ts`,初始化 Pinia 和持久化插件
  - 配置 `tsconfig.json` 路径别名 `@/*`,支持模块化导入
  - 优势:响应式状态管理、自动持久化、更好的类型推断、代码解耦
  - 涉及文件:`frontend/package.json`、`frontend/src/stores/auth.ts`、`frontend/src/services/api.ts`、`frontend/src/App.vue`、`frontend/src/main.ts`、`frontend/tsconfig.json`

---

## [v2.4.34] - 2026-01-17

### 新增

- **会话管理增强** - 支持 Gemini API 的 `X-Gemini-Api-Privileged-User-Id` 请求头
  - 在 `ExtractConversationID()` 函数中新增对该请求头的支持,用于会话亲和性管理
  - 优先级顺序:Conversation_id > Session_id > X-Gemini-Api-Privileged-User-Id > prompt_cache_key > metadata.user_id
  - 涉及文件:`backend-go/internal/handlers/common/request.go`

### 优化

- **Gemini Dashboard API 性能优化** - 将前端 3 个独立请求合并为 1 个后端统一接口
  - 新增 `/api/gemini/channels/dashboard` 端点,一次性返回 channels、metrics、stats、recentActivity 数据
  - 后端新增 `internal/handlers/gemini/dashboard.go` 处理器,减少网络往返次数
  - 涉及文件:`backend-go/main.go`、`backend-go/internal/handlers/gemini/dashboard.go`

### 重构

- **前端 UI 框架统一** - 移除 Tailwind CSS 和 DaisyUI,完全使用 Vuetify
  - 从 package.json 移除 tailwindcss、daisyui、autoprefixer、postcss 依赖
  - 删除 tailwind.config.js 和 postcss.config.js 配置文件
  - 更新 src/assets/style.css,移除 @tailwind 指令,保留自定义样式
  - 优势:消除多框架样式冲突、减少打包体积、统一设计语言(Material Design)
  - 涉及文件:`frontend/package.json`、`frontend/src/assets/style.css`、`frontend/src/main.ts`

---

## [v2.4.33] - 2026-01-17

### 新增

- **渠道实时活跃度可视化** - 在渠道列表中显示最近 15 分钟的活跃度数据
  - 后端新增 `GetRecentActivityMultiURL()` 方法,按 **6 秒粒度**分段统计请求量、成功/失败数、Token 消耗(共 150 段)
  - **支持多 URL 和多 Key 聚合**:自动聚合渠道所有故障转移 URL 和所有活跃 API Key 的数据,提供完整的渠道活跃度视图
  - Dashboard API 返回 `recentActivity` 字段,包含每个渠道的 150 段活跃度数据
  - 前端渠道行显示 RPM/TPM 指标,**背景波形柱状图**实时反映活跃度变化(整体颜色根据全局失败率着色:绿色=成功率≥80%,橙色=成功率≥50%,红色=成功率<50%)
  - 柱状图每 2 秒自动更新,用户调用 API 后立即看到柱子"跳动",提供直观的脉冲式活跃度展示
  - 涉及文件:`backend-go/internal/metrics/channel_metrics.go`、`backend-go/internal/handlers/channel_metrics_handler.go`、`frontend/src/components/ChannelOrchestration.vue`、`frontend/src/services/api.ts`、`frontend/src/App.vue`

---

## [v2.4.32] - 2026-01-14

### ✨ 新增

- **Gemini 渠道支持 thinking 模式函数调用签名传递** - `GeminiFunctionCall` 结构体新增 `ThoughtSignature` 字段
  - 用于 thinking 模式下的签名,需原样传回上游
  - 涉及文件:`backend-go/internal/types/gemini.go`

### 🔧 优化

- **Gemini 渠道添加模态框增强** - 扩展服务类型和模型选项
  - 服务类型新增 OpenAI 和 Claude 选项,支持更多上游协议
  - 更新 Gemini 模型列表:新增 gemini-2、gemini-2.5-flash-lite、gemini-2.5-flash-image、TTS 预览模型、gemini-3 系列预览模型
  - 涉及文件:`frontend/src/components/AddChannelModal.vue`

### 🐛 修复

- **修复快速输入解析器冒号分隔导致 URL 被截断的问题** - 增强 `extractTokens()` 函数支持冒号作为分隔符,同时保护 URL 完整性
  - 新增 URL 占位符机制:先提取完整 URL 并替换为占位符,分割后再恢复
  - 支持中文标点分隔符:逗号(,)、分号(;)、冒号(:)
  - 涉及文件:`frontend/src/utils/quickInputParser.ts`

---

## [v2.4.31] - 2026-01-12

### 🐛 修复

- **修复流式工具调用输出稳定性和合并逻辑** - 增强 `stream_synthesizer.go` 的工具调用处理
  - 工具调用输出按 index 排序,避免 map 遍历顺序不稳定导致日志顺序随机
  - 修复 ID 生成错误:`string(rune(index))` 改为 `strconv.Itoa(index)`,避免非 ASCII 字符
  - 合并逻辑增强:仅合并连续 index 的工具调用,防止误合并不相关调用
  - 新增 ID 匹配检查:合并时验证两个 block 的 ID 一致(或其中一个为空)
  - 支持 ID 补全:合并时若 curr 无 ID 但 next 有,自动补全
  - 涉及文件:`backend-go/internal/utils/stream_synthesizer.go`

---

## [v2.4.30] - 2026-01-10

### 🐛 修复

- **修复流式响应工具调用分裂问题** - 当上游返回的工具调用被意外分成两个 content_block 时自动合并
  - 问题场景:第一个 block 有 name 和 id 但参数为空 "{}",第二个 block 没有 name 但有完整参数
  - 新增 `mergeSplitToolCalls()` 方法检测并合并分裂的工具调用
  - 在 `GetSynthesizedContent()` 中调用,确保日志输出正确的工具调用信息
  - 涉及文件:`backend-go/internal/utils/stream_synthesizer.go`

---

## [v2.4.29] - 2026-01-10

### 🐛 修复

- **修复空 signature 字段导致 Claude API 400 错误** - 客户端可能发送带空 `signature` 字段(空字符串或 null)的请求,Claude API 会拒绝并返回 400 错误
  - 新增 `RemoveEmptySignatures()` 函数,定向移除 `messages[*].content[*].signature` 路径下的空值
  - 使用 `json.Decoder` 保留数字精度,`SetEscapeHTML(false)` 保持原始格式
  - **注意**:当请求体被修改时,JSON 字段顺序可能发生变化(不影响 API 语义)
  - 在 Messages Handler 入口处调用预处理,确保请求发送前清理无效字段
  - 涉及文件:`backend-go/internal/handlers/common/request.go`、`backend-go/internal/handlers/messages/handler.go`

### ✨ 改进

- **增强 Trace 亲和性日志记录** - 在关键操作点添加详细日志,方便排查亲和性相关问题
  - `[Affinity-Set]` 记录新建/变更用户亲和
  - `[Affinity-Remove]` 记录手动移除用户亲和
  - `[Affinity-RemoveByChannel]` 记录渠道移除时批量清理
  - `[Affinity-Cleanup]` 记录定时清理过期记录
  - 日志在锁外执行,避免高负载下的尾延迟
  - 用户 ID 分级脱敏:短 ID 也保留部分字符便于关联
  - 涉及文件:`backend-go/internal/session/trace_affinity.go`

## [v2.4.28] - 2026-01-07

### 🐛 修复

- **修复内容审核错误导致无限重试问题** - 当上游返回 `sensitive_words_detected` 等内容审核错误时,单渠道场景下会无限重试
  - 根因:`classifyByStatusCode(500)` 触发 failover,但未检查 `error.code` 字段中的不可重试错误码
  - 新增 `isNonRetryableErrorCode()` 函数,检测内容审核和无效请求错误码
  - 新增 `isNonRetryableError()` 函数,从响应体提取并检测不可重试错误
  - 在 `shouldRetryWithNextKeyNormal()` 和 `shouldRetryWithNextKeyFuzzy()` 入口处优先检测
  - 不可重试错误码:`sensitive_words_detected`、`content_policy_violation`、`content_filter`、`content_blocked`、`moderation_blocked`、`invalid_request`、`invalid_request_error`、`bad_request`
  - 涉及文件:`backend-go/internal/handlers/common/failover.go`

### 🧪 测试

- **新增不可重试错误码测试** - 覆盖 `sensitive_words_detected` 等错误码在 Normal/Fuzzy 模式下的行为
  - 涉及文件:`backend-go/internal/handlers/common/failover_test.go`

## [v2.4.27] - 2026-01-05

### 🐛 修复

- **修复多端点 failover 渠道统计丢失问题** - 当渠道配置多个 `baseUrls` 时,请求路由到非主 URL 后指标无法正确聚合到渠道统计
  - 根因:指标存储使用 `hash(baseURL + apiKey)` 作为键,但查询方法只使用主 BaseURL
  - 新增 4 个多 URL 聚合方法:`GetHistoricalStatsMultiURL`、`GetChannelKeyUsageInfoMultiURL`、`GetKeyHistoricalStatsMultiURL`、`calculateAggregatedTimeWindowsMultiURL`
  - `ToResponseMultiURL` 按 API Key 去重聚合,避免同一 Key 在多 URL 场景下产生重复条目
  - Handler 层全部改用 `upstream.GetAllBaseURLs()` 获取所有 URL 进行聚合
  - 涉及文件:`backend-go/internal/metrics/channel_metrics.go`、`backend-go/internal/handlers/channel_metrics_handler.go`

## [v2.4.26] - 2026-01-05

### 🐛 修复

- **修复 Key 趋势图切换时间范围后不刷新问题** - 持久化 view/duration 选择到 localStorage,使用 requestId 防止自动刷新旧响应覆盖新选择
  - 涉及文件:`frontend/src/components/KeyTrendChart.vue`

- **修复 KeyTrendChart SSR 兼容性和健壮性问题**
  - 添加 `isLocalStorageAvailable()` 检查,防止 SSR 环境下访问 localStorage 崩溃
  - 为 localStorage 读写操作添加 try/catch 异常捕获(配额超限、隐私模式等场景)
  - 添加 `channelType` prop 变化监听,切换渠道类型时自动重载偏好设置并刷新数据
  - 优化 channelType watcher 逻辑,避免与 duration watcher 重复触发刷新
  - 涉及文件:`frontend/src/components/KeyTrendChart.vue`

- **修复缓存创建统计缺失问题** - 当上游仅返回 TTL 细分字段(5m/1h)时,兜底汇总为 cacheCreationTokens
  - 涉及文件:`backend-go/internal/metrics/channel_metrics.go`

- **透传缓存 TTL 细分字段到指标层** - Responses 非流式/流式 usage 现在包含 CacheCreation5m/1h + CacheTTL
  - 涉及文件:`backend-go/internal/handlers/responses/handler.go`

### 🧪 测试

- **新增 TTL 细分字段兜底测试** - 覆盖 cache_creation_input_tokens 为 0 时的汇总场景
  - 涉及文件:`backend-go/internal/metrics/channel_metrics_cache_stats_test.go`

## [v2.4.25] - 2026-01-04

### 🧪 测试

- **新增 baseUrl/baseUrls 一致性测试套件** - 覆盖 URL 配置的完整场景,防止编辑渠道时数据不一致问题回归
  - `TestUpdateUpstream_BaseURLConsistency`: 验证 Messages 渠道更新时 baseUrl/baseUrls 的一致性(4 场景)
  - `TestUpdateResponsesUpstream_BaseURLConsistency`: 验证 Responses 渠道更新一致性
  - `TestUpdateGeminiUpstream_BaseURLConsistency`: 验证 Gemini 渠道更新一致性
  - `TestGetAllBaseURLs_Priority`: 验证 URL 获取优先级逻辑(4 场景)
  - `TestGetEffectiveBaseURL_Priority`: 验证有效 URL 选择逻辑(3 场景)
  - `TestDeduplicateBaseURLs`: 验证 URL 去重逻辑(7 场景,含末尾斜杠/井号差异)
  - `TestAddUpstream_BaseURLDeduplication`: 验证添加渠道时的 URL 去重
  - 涉及文件:`internal/config/config_baseurl_test.go`(新增 414 行)

### 🐛 修复

- **修复历史分桶边界导致边界点漏算** - 历史统计 API 的时间过滤条件从开区间 `(startTime, endTime)` 改为半开区间 `[startTime, endTime)`,避免恰好落在 startTime 的记录被遗漏
  - 涉及文件:`internal/metrics/channel_metrics.go`

- **修复历史图表时间戳错位** - 将返回的 Timestamp 从"桶结束时间"改为"桶起始时间",前端图表不再出现一格偏差
  - 涉及文件:`internal/metrics/channel_metrics.go`

- **修复成功计数可能重复记录** - 移除多渠道/单渠道成功路径上多余的 `RecordSuccess()` 调用,统一使用 `RecordSuccessWithUsage()` 作为唯一成功计数入口
  - Messages 路径:移除重复调用,保留流式/非流式末尾的 `RecordSuccessWithUsage`
  - Responses compact 路径:改用 `RecordSuccessWithUsage(nil)` 替代原 `RecordSuccess`,保持指标一致性
  - 涉及文件:`internal/handlers/messages/handler.go`、`internal/handlers/responses/compact.go`

- **修复多 BaseURL 故障转移时成功指标归属错误** - 当请求通过 fallback BaseURL 成功时,成功指标错误地记录到主 BaseURL 而非实际成功的 URL
  - 根本原因:`handleNormalResponse` 和 `HandleStreamResponse` 接收的是原始 `upstream` 而非设置了 `currentBaseURL` 的 `upstreamCopy`
  - 修复方式:将两处调用点的参数从 `upstream` 改为 `upstreamCopy`
  - 影响范围:多渠道/单渠道的流式与非流式响应处理
  - 涉及文件:`internal/handlers/messages/handler.go`

---

## [v2.4.24] - 2026-01-04

### ✨ 新功能

- **缓存命中率统计** - 按 Token 口径展示各渠道缓存读/写与命中率:
  - 后端:在 `timeWindows` 聚合统计中新增 `inputTokens`/`outputTokens`/`cacheCreationTokens`/`cacheReadTokens`/`cacheHitRate` 字段
  - 命中率定义:`cacheReadTokens / (cacheReadTokens + inputTokens) * 100`
  - 前端:渠道编排列表在 15 分钟有请求时额外显示缓存命中率,tooltip 中按 15m/1h/6h/24h 展示缓存统计
  - 新字段均为 `omitempty`,向后兼容

### 🎨 优化

- **调整渠道指标显示间距** - 优化缓存命中率 chip 与请求数之间的间距,避免布局拥挤

---

## [v2.4.23] - 2026-01-03

### ✨ 新功能

- **lowQuality 模式输出完整的 token 验证过程日志** - 启用低质量渠道时,日志会显示完整的验证过程:
  - 偏差 > 5% 时显示修补详情
  - 偏差 ≤ 5% 时显示保留上游值
  - 上游返回无效值时显示本地估算值

### 🐛 修复

- **修复渠道列表 API 未返回 `lowQuality` 字段** - 在 `GetUpstreams` 和 `GetChannelDashboard` 函数返回的 JSON 中补充 `lowQuality` 字段:
  - 之前前端编辑渠道时无法正确显示已保存的"低质量渠道"开关状态
  - 涉及文件:`handlers/messages/channels.go`、`handlers/responses/channels.go`、`handlers/gemini/channels.go`、`handlers/channel_metrics_handler.go`

---

## [v2.4.22] - 2026-01-02

### ✨ 新功能

- **低质量渠道处理机制** - 新增 `lowQuality` 渠道配置选项,用于处理返回不完整数据的上游渠道:
  - Token 偏差检测:启用后对比上游返回值与本地估算值,偏差 > 5% 时使用本地估算值
  - Model 一致性检查:验证响应中的 model 是否与请求一致,不一致则改写为请求的 model
  - 空 ID 补全:自动补全上游返回的空 `message.id`(生成 `msg_<uuid>` 格式)
  - 前端支持:渠道编辑 modal 新增"低质量渠道"开关

### 🐛 修复

- **暂停渠道时自动清除促销期** - 当用户暂停一个正在抢优先级的渠道时,自动清除其 `promotionUntil` 字段:
  - 避免暂停后仍显示促销期标识
  - 涉及三个渠道类型:Messages、Responses、Gemini
  - 涉及文件:`config_messages.go`、`config_responses.go`、`config_gemini.go`

- **修复 `lowQuality` 字段更新不持久化的问题** - 在 `UpdateUpstream` 系列函数中补充 `LowQuality` 字段处理:
  - 之前前端切换"低质量渠道"开关后变更不会被保存
  - 涉及文件:`config_messages.go`、`config_responses.go`、`config_gemini.go`

- **修复渠道列表 API 未返回 `lowQuality` 字段** - 在 `GetUpstreams` 和 `GetChannelDashboard` 函数返回的 JSON 中补充 `lowQuality` 字段:
  - 之前前端编辑渠道时无法正确显示已保存的"低质量渠道"开关状态
  - 涉及文件:`handlers/messages/channels.go`、`handlers/responses/channels.go`、`handlers/gemini/channels.go`、`handlers/channel_metrics_handler.go`

---

## [v2.4.21] - 2026-01-02

### 🐛 修复

- **修复流式响应 input_tokens 为 nil 时丢失的问题** - 当上游返回的顶层 usage 中 `input_tokens` 为 `nil` 时,之前从 `message.usage` 收集到的有效值无法被修补:
  - 原因:`patchUsageFieldsWithLog` 和 `checkUsageFieldsWithPatch` 函数中类型断言 `.(float64)` 失败时跳过了修补逻辑
  - 表现:日志显示 `InputTokens=<nil>` 而非之前收集到的有效值(如 10920)
  - 修复:在两处函数中新增 `input_tokens == nil` 检测,无论是否有缓存 token 都用收集到的值修补
  - 涉及文件:`backend-go/internal/handlers/common/stream.go`

---

## [v2.4.18] - 2025-12-31

### 🐛 修复

- **Gemini 日志和 Header 透传改进** - 修复 Gemini 接口的日志显示和请求头处理:
  - 修复 `contents`/`parts` 字段在日志中不显示的问题
  - 修复原生 Gemini handler 未透传客户端 Header 的问题
  - 新增 `compactGeminiContentsArray` 和 `compactGeminiPart` 函数
  - 涉及文件:`backend-go/internal/utils/json.go`、`backend-go/internal/handlers/gemini/handler.go`

### 🔧 重构

- **Gemini tools 日志简化支持** - 新增 `extractToolNames` 函数支持 Gemini 格式的工具提取:
  - 支持 Gemini `functionDeclarations` 数组格式
  - 兼容 Claude 和 OpenAI 格式
  - 日志中 tools 字段现在统一显示为 `["tool1", "tool2", ...]` 格式
  - 涉及文件:`backend-go/internal/utils/json.go`

- **移除非标准 Gemini API 路由** - 简化 API 端点,仅保留官方格式:
  - 移除:`POST /v1/models/{model}:generateContent`(非标准简化格式)
  - 保留:`POST /v1beta/models/{model}:generateContent`(Gemini 官方格式)
  - 更新前端预览 URL 显示完整路径格式 `/models/{model}:generateContent`
  - 涉及文件:`backend-go/main.go`、`frontend/src/components/AddChannelModal.vue`

---

## [v2.4.17] - 2025-12-30

### 🐛 修复

- **修复 ModelMapping 导致请求字段丢失** - 解决使用模型重定向时 Claude API 返回 403 的问题:
  - 原因:`ClaudeRequest` 结构体缺少 `metadata` 字段,JSON 反序列化时该字段被丢弃
  - 表现:配置 `modelMapping` 后请求被上游拒绝(如 `opus` → `claude-opus-4-5-20251101`)
  - 修复:在 `ClaudeRequest` 中添加 `Metadata map[string]interface{}` 字段
  - 涉及文件:`backend-go/internal/types/types.go`

---

## [v2.4.16] - 2025-12-30

### 🐛 修复

- **修复 Gemini 渠道预期请求 URL 预览** - 创建渠道时预览显示正确的 `/v1beta` 路径:
  - 原问题:Gemini 渠道预览错误显示 `/v1` 而后端实际使用 `/v1beta`
  - 修复:当 serviceType 为 gemini 时使用 `/v1beta` 作为版本前缀
  - 涉及文件:`frontend/src/components/AddChannelModal.vue`

---

## [v2.4.15] - 2025-12-30

### 🐛 修复

- **修复 Gemini API 路由注册失败** - 解决 Gin 框架路由 panic 问题:
  - 原因:Gin 不支持 `:param\:literal` 格式,即使转义冒号也会被解析为两个通配符
  - 方案:使用 `*modelAction` 通配符捕获 `model:action` 整体,在 handler 内解析
  - 涉及文件:`main.go`、`internal/handlers/gemini/handler.go`

### ✨ 新功能

- **Gemini 历史指标 API 完整实现** - 补全 Gemini 模块的历史数据端点:
  - `GET /api/gemini/channels/metrics/history` - 渠道级别指标历史
  - `GET /api/gemini/channels/:id/keys/metrics/history` - Key 级别指标历史
  - `GET /api/gemini/global/stats/history` - 全局统计历史
  - 涉及文件:`internal/handlers/channel_metrics_handler.go`、`main.go`

- **Gemini 前端管理界面完整实现** - 与 Messages/Responses 功能完全对齐:
  - 新增 Gemini Tab 切换,支持完整渠道 CRUD、Key 管理、状态/促销设置
  - KeyTrendChart 和 GlobalStatsChart 组件支持 Gemini 数据展示(移除降级显示)
  - 涉及文件:`frontend/src/App.vue`、`frontend/src/components/`、`frontend/src/services/api.ts`

---

## [v2.4.14] - 2025-12-29

### ✨ 新功能

- **新增 Gemini API 模块** - 与 `/v1/messages`、`/v1/responses` 同级的完整 Gemini 代理支持:
  - **代理端点**:`POST /v1/models/{model}:generateContent`(非流式)、`:streamGenerateContent`(流式)
  - **协议转换**:支持 Gemini 请求转发到 Claude/OpenAI/Gemini 上游,双向转换器自动处理格式差异
  - **渠道管理 API**:完整 CRUD、API Key 管理、状态/促销设置、指标监控(`/api/gemini/channels/*`)
  - **多渠道调度**:集成 ChannelScheduler,支持优先级、熔断、Trace 亲和性
  - **认证方式**:兼容 Gemini 原生格式(`x-goog-api-key` 头、`?key=` 参数)
  - 涉及文件:`internal/handlers/gemini/`、`internal/converters/gemini_converter.go`、`internal/types/gemini.go`

### 🔧 重构

- **config 包模块化拆分** - 将 1973 行的单文件拆分为 6 个职责清晰的模块:
  - `config.go`(297 行):核心类型定义 + 共享方法
  - `config_loader.go`(384 行):配置加载、迁移、验证、文件监听
  - `config_messages.go`(429 行):Messages 渠道 CRUD
  - `config_responses.go`(380 行):Responses 渠道 CRUD
  - `config_gemini.go`(361 行):Gemini 渠道 CRUD
  - `config_utils.go`(183 行):工具函数(去重、模型重定向、状态辅助)
  - 遵循单一职责原则,提升代码可维护性

---

## [v2.4.12] - 2025-12-29

### 🐛 修复

- **修复 Responses API 错误消息提取失败的问题** - 解决 upstream_error 字段无法被正确解析:
  - 扩展 `classifyByErrorMessage` 函数:支持多个消息字段(`message`, `upstream_error`, `detail`)
  - 支持嵌套对象格式:当 `upstream_error` 为对象时,提取其中的 `message` 字段
  - 之前仅检查 `error.message` 字段,导致 `{type, upstream_error}` 格式的错误无法被识别
  - 新增 4 个测试用例覆盖 upstream_error 字符串、嵌套对象、detail 字段等场景
  - 涉及文件:`internal/handlers/common/failover.go`, `internal/handlers/common/failover_test.go`

---

## [v2.4.11] - 2025-12-29

### 🐛 修复

- **修复 Fuzzy 模式下 403 + 预扣费消息未触发 Key 降级的问题** - 补充 v2.4.10 修复的遗漏场景:
  - 修改 `shouldRetryWithNextKeyFuzzy` 函数:新增 `bodyBytes` 参数,对非 402/429 状态码检查消息体中的配额关键词
  - 之前 Fuzzy 模式仅检查状态码(402/429 = quota),不解析消息体,导致 403 + "预扣费额度失败" 返回 `isQuotaRelated=false`
  - 新增 `TestShouldRetryWithNextKey_FuzzyMode_403WithQuotaMessage` 测试用例
  - 涉及文件:`internal/handlers/common/failover.go`, `internal/handlers/common/failover_test.go`

### 🔧 调试

- **添加 Key 降级调试日志** - 用于追踪 `isQuotaRelated` 值和密钥降级流程:
  - 在 `ShouldRetryWithNextKey` 调用后记录返回值(statusCode, shouldFailover, isQuotaRelated)
  - 在密钥标记为配额相关失败时记录日志
  - 涉及文件:`internal/handlers/messages/handler.go`
- **改进 .env.example 文档** - 添加日志配置默认值说明(默认启用,需显式设置 false 禁用)

---

## [v2.4.10] - 2025-12-29

### 🐛 修复

- **修复 403 预扣费额度不足的 Key 未被自动降级的问题** - 解决配额不足的密钥始终被优先尝试:
  - 修改 `shouldRetryWithNextKeyNormal` 逻辑:即使 HTTP 状态码已触发 failover,仍检查消息体确定是否为配额相关错误
  - 之前 403 状态码直接返回 `isQuotaRelated=false`,跳过消息体解析,导致 `DeprioritizeAPIKey` 未被调用
  - 新增 "预扣费" 关键词到 `quotaKeywords` 列表,确保匹配中文预扣费错误消息
  - 涉及文件:`internal/handlers/common/failover.go`

---

## [v2.4.9] - 2025-12-27

### 🔧 改进

- **重构 URL 预热机制为非阻塞动态排序** - 解决首次请求延迟 500ms+ 的问题:
  - 移除阻塞式 ping 预热(`URLWarmupManager`),改用非阻塞的 `URLManager`
  - 新排序策略:基于实际请求结果动态调整 URL 顺序
    - 请求成功:重置失败计数,URL 保持/提升位置
    - 请求失败:增加失败计数,URL 移到末尾
    - 冷却期机制:失败的 URL 在 30 秒后自动恢复可用
  - 排序规则:无失败记录优先 > 冷却期已过 > 仍在冷却期
  - 涉及文件:`warmup/url_manager.go`(新建)、`warmup/url_warmup.go`(删除)、`scheduler/channel_scheduler.go`、`messages/handler.go`、`responses/handler.go`、`main.go`

---

## [v2.4.8] - 2025-12-27

### 🐛 修复

- **修复多端点渠道密钥轮换时的并发竞争问题** - 解决高并发下 BaseURL 被错误修改导致密钥跨渠道混用:
  - 新增 `UpstreamConfig.Clone()` 深拷贝方法,避免并发修改共享对象
  - Messages/Responses Handler 改用深拷贝替代临时修改模式
  - 新增 `MarkWarmupURLFailed()` 方法,请求失败时触发预热缓存失效
  - HTTP 5xx 和网络超时均会触发预热缓存失效,确保失败端点被重新排序
  - 涉及文件:`config/config.go`、`messages/handler.go`、`responses/handler.go`、`scheduler/channel_scheduler.go`、`warmup/url_warmup.go`

---

## [v2.4.6] - 2025-12-27

### ✨ 新功能

- **多端点预热排序** - 渠道首次访问前自动 ping 所有端点,按延迟排序:
  - 新增 `internal/warmup/url_warmup.go` 预热管理器模块
  - 渠道首次访问时自动并发 ping 所有 BaseURL
  - 排序策略:成功的端点优先,同类型按延迟从低到高排序
  - ping 结果缓存 5 分钟,避免频繁测试
  - 支持并发安全的预热请求去重(多个请求同时触发时只执行一次预热)
  - Messages 和 Responses API 均支持预热排序

---

## [v2.4.5] - 2025-12-27

### 🔧 改进

- **统一日志前缀规范** - Messages 和 Responses 接口日志标签标准化:
  - Messages 流式处理日志统一使用 `[Messages-Stream]`、`[Messages-Stream-Token]` 前缀
  - Responses 流式处理日志保持 `[Responses-Stream]`、`[Responses-Stream-Token]` 前缀
  - 修复 3 处遗漏前缀的错误日志(`messages/handler.go`、`responses/handler.go`)
  - 更新 `backend-go/CLAUDE.md` 日志规范文档

---

## [v2.4.4] - 2025-12-27

### ✨ 新功能

- **全局流量和 Token 统计图表** - 新增全局统计可视化功能:
  - 后端新增 `/api/messages/global/stats/history` 和 `/api/responses/global/stats/history` API
  - 支持请求数量(成功/失败/总量)和 Token 总量(输入/输出)统计
  - 前端新增 `GlobalStatsChart.vue` 组件,支持流量/Token 双视图切换
  - 时间范围支持 1h / 6h / 24h / 今日 多档位切换
  - 用户偏好(时间范围、视图模式)按 Messages/Responses 分别保存到 localStorage
  - 以顶部可折叠卡片形式展示,随当前 Tab 自动切换对应 API 类型的统计

- **渠道 Key 趋势图表支持"今日"** - KeyTrendChart 新增今日时间范围选项:
  - 后端 `GetChannelKeyMetricsHistory` 支持 `duration=today` 参数
  - 前端添加"今日"按钮,动态计算从今日 0 点到当前的时长

---

## [v2.4.3] - 2025-12-27

### 🐛 修复

- **Responses API Token 统计修复** - 解决上游无 usage 时本地统计无数据的问题:
  - 修复 SSE 事件解析格式兼容性:支持 `data:` 和 `data: ` 两种格式(某些上游不带空格)
  - 修复 `handleSuccess` / `handleStreamSuccess` 不返回 usage 数据的问题
  - 修复调用点使用 `RecordSuccess` 而非 `RecordSuccessWithUsage` 导致 token 统计未入库
  - 涉及函数:`checkResponsesEventUsage`、`injectResponsesUsageToCompletedEvent`、`patchResponsesCompletedEventUsage`、`tryChannelWithAllKeys`

---

## [v2.4.2] - 2025-12-26

### 🐛 修复

- **原始请求日志修复** - 修复多渠道模式下原始请求头/请求体日志不显示的问题:
  - 将 `LogOriginalRequest` 调用移至 Handler 入口处,确保无论单/多渠道模式都只记录一次
  - 移除单渠道处理函数中重复的日志调用和未使用变量
  - 同时修复 Messages 和 Responses 两个处理器

### 🧹 清理

- **移除废弃环境变量 `LOAD_BALANCE_STRATEGY`** - 负载均衡策略已迁移至 config.json 热重载配置:
  - 删除 `env.go` 中 `LoadBalanceStrategy` 字段
  - 更新 `.env.example`、`docker-compose.yml`、`README.md` 移除相关配置
  - 更新 `CLAUDE.md` 添加配置方式说明

---

## [v2.4.0] - 2025-12-26

### ✨ 改进

- **渠道编辑表单优化** - 改进 AddChannelModal 用户体验:
  - 预期请求支持显示所有 BaseURL 端点,而非仅显示首个
  - 修复 Gemini 类型渠道预期请求显示错误端点的问题(应为 `/generateContent`)
  - 修复从快速模式切换到详细模式时 BaseURL 输入框为空的问题
  - 表单字段重排:TLS 验证开关和描述字段移至表单末尾
  - BaseURL 输入框不再自动修改用户输入,仅在提交时进行去重处理
  - 调整预期请求区域下方间距,改善视觉效果

- **API Key/BaseURL 策略简化** - 移除过度设计,采用纯 failover 模式:
  - 删除 `ResourceAffinityManager` 及相关代码(资源亲和性)
  - 移除 API Key 策略选择(round-robin/random/failover),始终使用优先级顺序
  - 移除 BaseURL 策略选择,始终使用优先级顺序并在失败时切换
  - 前端删除策略选择器,简化渠道配置界面
  - 保留渠道级 Trace 亲和性(TraceAffinityManager)用于会话一致性
  - 清理遗留无用代码:`requestCount`/`responsesRequestCount` 字段、`EnableStreamEventDedup` 环境变量

### 🐛 修复

- **多 BaseURL failover 失效** - 修复当所有 API Key 在首个 BaseURL 失败后不会切换到下一个 BaseURL 的问题:
  - 重构 `tryChannelWithAllKeys` 函数,采用嵌套循环遍历所有 BaseURL
  - 重构 `handleSingleChannel` 函数,单渠道模式也支持多 BaseURL failover
  - 每个 BaseURL 尝试所有 Key 后,若全部失败则自动切换下一个
  - 每次切换 BaseURL 时重置失败 Key 列表
  - 同时修复 Messages 和 Responses 两个处理器
  - 修复 `GetEffectiveBaseURL()` 优先级:临时设置的 `BaseURL` 字段优先于 `BaseURLs` 数组
  - 移除废弃代码:`MarkBaseURLFailed()`、`baseURLIndex` 字段

- **SSE 流式事件完整性** - 修复 Claude Provider 流式响应可能在事件边界处截断的问题:
  - 改用事件缓冲机制,按空行分隔完整 SSE 事件后再转发
  - 确保 `event:`/`data:`/`id:`/`retry:` 等字段作为整体发送
  - 处理上游未以空行结尾的边界情况

- **前端延迟测试结果被覆盖** - 修复 ping 延迟值显示几秒后消失的问题:
  - 新增 `mergeChannelsWithLocalData()` 函数保留本地延迟测试结果
  - 应用于自动刷新、Tab 切换、手动刷新三处数据更新点
  - 添加 5 分钟有效期检查,确保过期数据自动清除

---

## [v2.3.11] - 2025-12-26

### 🐛 修复

- **Responses API usage 字段缺失** - 修复当上游服务(OpenAI/Gemini)不返回 usage 信息时,`response.completed` 事件完全不包含 `usage` 字段的问题:
  - 转换器现在始终生成基础 `usage` 字段(`input_tokens`、`output_tokens`、`total_tokens`),即使值为 0
  - Handler 检测到 usage 存在后,会用本地 token 估算值替换 0 值
  - 确保下游客户端始终能获得合理的 token 使用估算

### ✨ 新功能

- **API Key/Base URL 去重** - 前后端全链路自动去重:
  - 前端详细表单模式输入时自动过滤重复 URL(忽略末尾 `/` 和 `#` 差异)
  - 后端 AddUpstream/UpdateUpstream 接口添加去重逻辑
  - 同时覆盖 Messages 和 Responses 渠道

### 🔧 改进

- **API Key 策略推荐调整** - 将默认推荐策略从"轮询"改为"故障转移",更符合实际使用场景
- **延迟测试结果持久显示** - 优化渠道延迟测试体验:
  - 测试结果直接显示在故障转移序列列表中,不再使用短暂 Toast 通知
  - 延迟结果保持显示 5 分钟后自动清除
  - 支持单个渠道测试和批量测试统一行为

---

## [v2.3.10] - 2025-12-25

### ✨ 新功能

- **快速添加支持等号分割** - 输入 `KEY=value` 格式时自动按等号分割,识别 `value` 为 API Key
- **快速添加支持多 Base URL** - 自动识别输入中所有 HTTP 链接作为 Base URL(最多 10 个)
- **多 URL 预期请求展示** - 快速添加模式下逐一展示每个 URL 的预期请求地址

---

## [v2.3.9] - 2025-12-25

### ✨ 新功能

- **渠道级 API Key 策略** - 每个渠道可独立配置 API Key 分配策略:
  - `round-robin`(默认):轮询分发请求到不同 Key
  - `random`:随机选择 Key
  - `failover`:故障转移,优先使用第一个 Key
  - 单 Key 时自动强制使用 `failover`,UI 显示禁用状态
- **多 BaseURL 支持** - 单个渠道可配置多个 BaseURL,支持三种策略:
  - `round-robin`(默认):轮询分发请求,自动分散负载
  - `random`:随机选择 URL
  - `failover`:手动故障转移(需配合外部监控切换)
- **促销期状态展示** - 渠道列表显示正在"抢优先级"的渠道,带火箭图标和剩余时间
- **延迟测试优化** - 批量测试时直接在列表显示每个渠道的延迟值,颜色根据延迟等级变化(绿/黄/红)
- **多 URL 延迟测试** - 当渠道配置多个 BaseURL 时,并发测试所有 URL 并显示最快的延迟
- **资源亲和性** - 记录用户成功使用的 BaseURL 和 API Key 索引,后续请求优先使用相同资源组合,减少不必要的资源切换

---

## [v2.3.8] - 2025-12-24

### 🔨 重构

- **日志输出规范化** - 移除所有 emoji 符号,统一使用 `[Component-Action]` 标签格式,确保跨平台兼容性

---

## [v2.3.7] - 2025-12-24

### 🐛 修复

- **滑动窗口重建逻辑优化** - 服务重启时只从最近 15 分钟的历史记录重建滑动窗口,避免历史失败记录导致渠道长期处于不健康状态

---

## [v2.3.6] - 2025-12-24

### ✨ 新功能

- **快速添加渠道 - API Key 识别增强** - 大幅改进 `quickInputParser` 的密钥识别能力
  - 新增各平台特定格式支持:OpenAI (sk-/sk-proj-)、Anthropic (sk-ant-api03-)、Google Gemini (AIza)、OpenRouter (sk-or-v1-)、Hugging Face (hf_)、Groq (gsk_)、Perplexity (pplx-)、Replicate (r8_)、智谱 AI (id.secret)、火山引擎 (UUID/AK)
  - 新增宽松兜底规则:常见前缀 (sk/api/key/ut/hf/gsk/cr/ms/r8/pplx) + 任意后缀,支持识别短密钥如 `sk-111`
  - 新增配置键名排除:全大写下划线分隔格式 (如 `API_TIMEOUT_MS`) 不再被误识别为密钥

### 🐛 修复

- **Claude Code settings.json 解析修复** - 粘贴 Claude Code 配置时,不再将键名 (`ANTHROPIC_AUTH_TOKEN` 等) 误识别为 API 密钥

---

## [v2.3.5] - 2025-12-24

### ✨ 新功能

- **Responses API Token 统计补全** - 为 Responses 接口添加完整的输入输出 Token 统计功能
  - 非流式响应:自动检测上游是否返回 usage,无 usage 时本地估算,修补虚假值(`input_tokens/output_tokens <= 1`)
  - 流式响应:累积收集流事件中的文本内容,在 `response.completed` 事件中检测并修补 Token 统计
  - 新增 `EstimateResponsesRequestTokens`、`EstimateResponsesOutputTokens` 专用估算函数
  - 支持缓存 Token 细分统计(5m/1h TTL)
  - 与 Messages API 保持一致的处理逻辑

### 🐛 修复

- **缓存 Token 5m/1h 字段检测完善** - 修复缓存 Token 检测逻辑,同时检测 `cache_creation_5m_input_tokens` 和 `cache_creation_1h_input_tokens` 字段
- **类型化 ResponsesItem 处理** - `EstimateResponsesOutputTokens` 现支持直接处理 `[]types.ResponsesItem` 类型
- **total_tokens 零值补全** - 修复当上游返回有效 `input_tokens/output_tokens` 但 `total_tokens` 为 0 时未自动补全的问题(非流式和流式均已修复)
- **特殊类型 Token 估算回退** - 当 `ResponsesItem` 的 `Type` 为 `function_call`、`reasoning` 等特殊类型时,自动序列化整个结构进行估算
- **流式 delta 类型扩展** - `extractResponsesTextFromEvent` 现支持更多 delta 事件类型:`output_json.delta`、`content_part.delta`、`audio.delta`、`audio_transcript.delta`
- **流式缓冲区内存保护** - `outputTextBuffer` 添加 1MB 大小上限,防止长流式响应导致内存溢出
- **Claude/OpenAI 缓存格式区分** - 新增 `HasClaudeCache` 标志,正确区分 Claude 原生缓存字段(`cache_creation/read_input_tokens`)和 OpenAI 格式(`input_tokens_details.cached_tokens`),避免 OpenAI 格式错误阻止 `input_tokens` 补全
- **流式缓存标志传播** - 修复 `updateResponsesStreamUsage` 未传播 `HasClaudeCache` 标志的问题,确保流式响应正确识别 Claude 缓存

---

## [v2.3.4] - 2025-12-23

### ✨ 新功能

- **Models API 增强** - `/v1/models` 端点重大改进
  - 使用调度器按故障转移顺序选择渠道(与 Messages/Responses API 一致)
  - 同时从 Messages 和 Responses 两种渠道获取模型列表并合并去重
  - 添加详细日志:渠道名称、脱敏 Key、选择原因
  - 移除对 Claude 原生渠道的跳过限制(第三方 Claude 代理通常支持 /models)
  - 移除不常用的 `DELETE /v1/models/:model` 端点

---

## [v2.3.3] - 2025-12-23

### ✨ 新功能

- **Models API 端点支持** - 新增 `/v1/models` 系列端点,转发到上游 OpenAI 兼容服务
  - `GET /v1/models` - 获取模型列表
  - `GET /v1/models/:model` - 获取单个模型详情
  - `DELETE /v1/models/:model` - 删除微调模型
  - 自动跳过不支持的 Claude 原生渠道,遍历所有上游直到成功或返回 404

---

## [v2.3.2] - 2025-12-23

### ✨ 新功能

- **快速添加渠道自动检测协议类型** - 根据 URL 路径自动选择正确的服务类型
  - `/messages` → Claude 协议
  - `/chat/completions` → OpenAI 协议
  - `/responses` → Responses 协议
  - `/generateContent` → Gemini 协议
- **快速添加支持 `%20` 分隔符** - 解析输入时自动将 URL 编码的空格转换为实际空格

---

## [v2.3.1] - 2025-12-22

### ✨ 新功能

- **HTTP 响应头超时可配置** - 新增 `RESPONSE_HEADER_TIMEOUT` 环境变量(默认 60 秒,范围 30-120 秒),解决上游响应慢导致的 `http2: timeout awaiting response headers` 错误

---

## [v2.3.0] - 2025-12-22

### ✨ 新功能

- **快速添加渠道支持引号内容提取** - 支持从双引号/单引号中提取 URL 和 API Key,可直接粘贴 Claude Code 环境变量 JSON 配置格式
- **SQLite 指标持久化存储** - 服务重启后不再丢失历史指标数据,启动时自动加载最近 24 小时数据
  - 新增 `METRICS_PERSISTENCE_ENABLED`(默认 true)和 `METRICS_RETENTION_DAYS`(默认 7)配置
  - 异步批量写入(100 条/批或每 30 秒),WAL 模式高并发,自动清理过期数据
- **完整的 Responses API Token Usage 统计** - 支持多格式自动检测(Claude/Gemini/OpenAI)、缓存 TTL 细分统计(5m/1h)
- **Messages API 缓存 TTL 细分统计** - 区分 5 分钟和 1 小时 TTL 的缓存创建统计

### 🔨 重构

- **SQLite 驱动切换为纯 Go 实现** - 从 `go-sqlite3`(CGO)切换为 `modernc.org/sqlite`,简化交叉编译

### 🐛 修复

- **Usage 解析数值类型健壮性** - 支持 `float64`/`int`/`int64`/`int32` 四种数值类型
- **CachedTokens 重复计算** - `CachedTokens` 仅包含 `cache_read`,不再包含 `cache_creation`
- **流式响应纯缓存场景 Usage 丢失** - 有任何 usage 字段时都记录

---

## [v2.2.0] - 2025-12-21

### 🔨 重构

- **Handlers 模块重构为同级子包结构** - 将 Messages/Responses API 处理器重构为同级模块,新增 `handlers/common/` 公共包,代码量减少约 180 行

### 🐛 修复

- **Stream 错误处理完善** - 流式传输错误时发送 SSE 错误事件并记录失败指标
- **CountTokens 端点安全加固** - 应用请求体大小限制
- **非 failover 错误指标记录** - 400/401/403 等错误正确记录失败指标

---

## [v2.1.35] - 2025-12-21

- **流量图表失败率可视化** - 失败率超过 10% 显示红色背景,Tooltip 显示详情

---

## [v2.1.34] - 2025-12-20

- **Key 级别使用趋势图表** - 支持流量/Token I/O/缓存三种视图,智能 Key 筛选
- **合并 Dashboard API** - 3 个并行请求优化为 1 个

---

## [v2.1.33] - 2025-12-20

- **Fuzzy Mode 错误处理开关** - 所有非 2xx 错误自动触发 failover
- **渠道指标历史数据 API** - 支持时间序列图表

---

## [v2.1.25] - 2025-12-18

### ✨ 新功能

- **TransformerMetadata 和 CacheControl 支持** - 转换器元数据保留原始格式信息,实现特性透传
- **FinishReason 统一映射函数** - OpenAI/Anthropic/Responses 三种协议间双向映射
- **原始日志输出开关** - `RAW_LOG_OUTPUT` 环境变量,开启后不进行格式化或截断

---

## [v2.1.23] - 2025-12-13

- 修复编辑渠道弹窗中基础 URL 布局和验证问题

---

## [v2.1.31] - 2025-12-19

- **前端显示版本号和更新检查** - 自动检查 GitHub 最新版本

---

## [v2.1.30] - 2025-12-19

- **强制探测模式** - 所有 Key 熔断时自动启用强制探测

---

## [v2.1.28] - 2025-12-19

- **BaseURL 支持 `#` 结尾跳过自动添加 `/v1`**

---

## [v2.1.27] - 2025-12-19

- 移除 Claude Provider 畸形 tool_call 修复逻辑

---

## [v2.1.26] - 2025-12-19

- Responses 渠道新增 `gpt-5.2-codex` 模型选项

---

## [v2.1.24] - 2025-12-17

- Responses 渠道新增 `gpt-5.2`、`gpt-5` 模型选项
- 移除 openaiold 服务类型支持

---

## [v2.1.23] - 2025-12-13

- 修复 402 状态码未触发 failover 的问题
- 重构 HTTP 状态码 failover 判断逻辑(两层分类策略)

---

## [v2.1.22] - 2025-12-13

### 🐛 修复

- **流式日志合成器类型修复** - 所有 Provider 的 HandleStreamResponse 都将响应转换为 Claude SSE 格式,日志合成器使用 "claude" 类型解析
- **insecureSkipVerify 字段提交修复** - 修复前端 insecureSkipVerify 为 false 时不提交的问题

---

## [v2.1.21] - 2025-12-13

### 🐛 修复

- **促销渠道绕过健康检查** - 促销渠道现在绕过健康检查直接尝试使用,只有本次请求实际失败后才跳过

---

## [v2.1.20] - 2025-12-12

- 渠道名称支持点击打开编辑弹窗

---

## [v2.1.19] - 2025-12-12

- 修复添加渠道弹窗密钥重复错误状态残留
- 新增 `/v1/responses/compact` 端点

---

## [v2.1.15] - 2025-12-12

### 🔒 安全加固

- **请求体大小限制** - 新增 `MAX_REQUEST_BODY_SIZE_MB` 环境变量(默认 50MB),超限返回 413
- **Goroutine 泄漏修复** - ConfigManager 添加 `stopChan` 和 `Close()` 方法释放资源
- **数据竞争修复** - 负载均衡计数器改用 `sync/atomic` 原子操作
- **优雅关闭** - 监听 SIGINT/SIGTERM,10 秒超时优雅关闭

---

## [v2.1.14] - 2025-12-12

- 修复流式响应 Token 计数中间更新被覆盖

---

## [v2.1.12] - 2025-12-11

- 支持 Claude 缓存 Token 计数

---

## [v2.1.10] - 2025-12-11

- 修复流式响应 Token 计数补全逻辑

---

## [v2.1.8] - 2025-12-11

- 重构过长方法,提升代码可读性

---

## [v2.1.7] - 2025-12-11

### 🐛 修复

- 修复前端 MDI 图标无法显示
- **Token 计数补全虚假值处理** - 当 `input_tokens <= 1` 或 `output_tokens == 0` 时用本地估算值覆盖

---

## [v2.1.6] - 2025-12-11

### ✨ 新功能

- **Messages API Token 计数补全** - 当上游不返回 usage 时,本地估算 token 数量并附加到响应中

---

## [v2.1.4] - 2025-12-11

- 修复前端渠道健康度统计不显示数据

---

## [v2.1.1] - 2025-12-11

- 新增 `QUIET_POLLING_LOGS` 环境变量(默认 true),过滤前端轮询日志噪音

---

## [v2.1.0] - 2025-12-11

### 🔨 重构

- **指标系统重构:Key 级别绑定** - 指标键改为 `hash(baseURL + apiKey)`,每个 Key 独立追踪
- **熔断器生效修复** - 在 `tryChannelWithAllKeys` 中调用 `ShouldSuspendKey()` 跳过熔断的 Key
- **单渠道路径指标记录** - 转换失败、发送失败、failover、成功时正确记录指标

---

## [v2.0.20-go] - 2025-12-08

- 修复单渠道模式渠道选择逻辑

---

## [v2.0.11-go] - 2025-12-06

### 🚀 多渠道智能调度器

- **ChannelScheduler** - 基于优先级的渠道选择、Trace 亲和性、失败率检测和自动熔断
- **MetricsManager** - 滑动窗口算法计算实时成功率
- **TraceAffinityManager** - 用户会话与渠道绑定

### 🎨 渠道编排面板

- 拖拽排序、实时指标、状态切换、备用池管理

---

## [v2.0.10-go] - 2025-12-06

### 🎨 复古像素主题

- Neo-Brutalism 设计语言:无圆角、等宽字体、粗实体边框、硬阴影

---

## [v2.0.5-go] - 2025-11-15

### 🚀 Responses API 转换器架构重构

- 策略模式 + 工厂模式实现多上游转换器
- 完整支持 Responses API 标准格式

---

## [v2.0.4-go] - 2025-11-14

### ✨ Responses API 透明转发

- Codex Responses API 端点 (`/v1/responses`)
- 会话管理系统(多轮对话跟踪)
- Messages API 多上游协议支持(Claude/OpenAI/Gemini)

---

## [v2.0.0-go] - 2025-10-15

### 🎉 Go 语言重写版本

- **性能提升**: 启动速度 20x,内存占用 -70%
- **单文件部署**: 前端资源嵌入二进制
- **完整功能移植**: 所有上游适配器、协议转换、流式响应、配置热重载

---

## 历史版本

<details>
<summary>v1.x TypeScript 版本</summary>

### v1.2.0 - 2025-09-19
- Web 管理界面、模型映射、渠道置顶、API 密钥故障转移

### v1.1.0 - 2025-09-17
- SSE 数据解析优化、Bearer Token 处理简化、代码重构

### v1.0.0 - 2025-09-13
- 初始版本:多上游支持、负载均衡、配置管理

</details>


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## 项目概述

Claude / Codex / Gemini API Proxy - 支持多上游 AI 服务的协议转换代理,提供 Web 管理界面和统一 API 入口。

**技术栈**: Go 1.22 (后端) + Vue 3 + Vuetify (前端) + Docker

## 常用命令

```bash
# 根目录(推荐)
make dev              # Go 后端热重载开发(不含前端)
make run              # 构建前端并运行 Go 后端
make frontend-dev     # 前端开发服务器
make build            # 构建前端并编译 Go 后端
make clean            # 清理构建文件
docker-compose up -d  # Docker 部署

# Go 后端开发 (backend-go/)
make dev              # 热重载开发模式
make test             # 运行所有测试
make test-cover       # 测试 + 覆盖率报告(生成 coverage.html)
make build            # 构建生产版本
make lint             # 代码检查(需要 golangci-lint)
make fmt              # 格式化代码
make deps             # 更新依赖

# 运行特定测试
go test -v ./internal/converters/...       # 运行单个包测试
go test -v -run TestName ./internal/...    # 运行单个测试

# 前端开发 (frontend/)
bun install && bun run dev    # 开发服务器
bun run build                 # 生产构建
```

## 架构概览

```
claude-proxy/
├── backend-go/                 # Go 后端(主程序)
│   ├── main.go                # 入口、路由配置
│   └── internal/
│       ├── handlers/          # HTTP 处理器 (proxy.go, responses.go, config.go)
│       ├── providers/         # 上游适配器 (openai.go, gemini.go, claude.go)
│       ├── converters/        # 协议转换器(工厂模式)
│       ├── scheduler/         # 多渠道调度器(优先级、熔断)
│       ├── session/           # 会话管理 + Trace 亲和性
│       ├── metrics/           # 渠道指标(滑动窗口算法)
│       ├── config/            # 配置管理(fsnotify 热重载)
│       └── middleware/        # 认证、CORS、日志过滤
├── frontend/                   # Vue 3 + Vuetify 前端
│   └── src/
│       ├── components/        # Vue 组件
│       └── services/          # API 服务封装
└── .config/                    # 运行时配置(热重载)
```

## 核心设计模式

1. **Provider Pattern** - `internal/providers/`: 所有上游实现统一 `Provider` 接口
2. **Converter Pattern** - `internal/converters/`: 协议转换,工厂模式创建转换器
3. **Session Manager** - `internal/session/`: 基于 `previous_response_id` 的多轮对话跟踪
4. **Scheduler Pattern** - `internal/scheduler/`: 优先级调度、Trace 亲和性、自动熔断

## API 端点

**代理端点**:
- `POST /v1/messages` - Claude Messages API(支持 OpenAI/Gemini 协议转换)
- `POST /v1/messages/count_tokens` - Token 计数
- `POST /v1/responses` - Codex Responses API(支持会话管理)
- `POST /v1/responses/compact` - 精简版 Responses API
- `GET /health` - 健康检查(无需认证)

**管理 API** (`/api/`):
- `/api/messages/channels` - Messages 渠道 CRUD
- `/api/responses/channels` - Responses 渠道 CRUD
- `/api/messages/channels/metrics` - 渠道指标
- `/api/messages/channels/scheduler/stats` - 调度器统计
- `/api/messages/ping/:id` - 渠道连通性测试

## 关键配置

| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `PORT` | 3000 | 服务器端口 |
| `ENV` | production | 运行环境 |
| `PROXY_ACCESS_KEY` | - | **必须设置** 访问密钥 |
| `QUIET_POLLING_LOGS` | true | 静默轮询日志 |
| `MAX_REQUEST_BODY_SIZE_MB` | 50 | 请求体最大大小 |

完整配置参考 `backend-go/.env.example`

## 常见任务

1. **添加新的上游服务**: 在 `internal/providers/` 实现 `Provider` 接口,在 `GetProvider()` 注册
2. **修改协议转换**: 编辑 `internal/converters/` 中的转换器
3. **调整调度策略**: 修改 `internal/scheduler/channel_scheduler.go`
4. **前端界面调整**: 编辑 `frontend/src/components/` 中的 Vue 组件

## 重要提示

- **Git 操作**: 未经用户明确要求,不要执行 git commit/push/branch 操作
- **配置热重载**: `backend-go/.config/config.json` 修改后自动生效,无需重启
- **环境变量变更**: 修改 `.env` 后需要重启服务
- **认证**: 所有端点(除 `/health`)需要 `x-api-key` 头或 `PROXY_ACCESS_KEY`

## Git 命令注意事项

- 执行 `git add`/`git commit` 前确保在项目根目录
- `git diff` 查看特定文件时使用 `--` 分隔符避免歧义:`git diff -- path/to/file`
- 错误示例:`git diff frontend/src/file.vue`(可能报 `unknown revision` 错误)
- 正确示例:`git diff -- frontend/src/file.vue`

## 模块文档

- [backend-go/CLAUDE.md](backend-go/CLAUDE.md) - Go 后端详细文档
- [frontend/CLAUDE.md](frontend/CLAUDE.md) - Vue 前端详细文档
- [ARCHITECTURE.md](ARCHITECTURE.md) - 详细架构设计
- [ENVIRONMENT.md](ENVIRONMENT.md) - 完整环境变量配置


================================================
FILE: CONTRIBUTING.md
================================================
# 贡献指南

本文档为项目贡献者提供了一套标准化的指导,以确保代码库的一致性和高质量。

## 如何贡献

欢迎通过提交 Issue 和 Pull Request 为本项目贡献力量!

1.  Fork 本项目。
2.  创建特性分支 (`git checkout -b feature/AmazingFeature`)。
3.  提交改动 (`git commit -m 'feat: Add some AmazingFeature'`)。
4.  推送到分支 (`git push origin feature/AmazingFeature`)。
5.  开启 Pull Request。

## 版本规范

项目遵循 **语义化版本 2.0.0 (Semantic Versioning)** 规范。版本格式为 `主版本号.次版本号.修订号` (MAJOR.MINOR.PATCH),版本号递增规则如下:

-   **主版本号 (MAJOR)**: 当你做了不兼容的 API 修改。
-   **次版本号 (MINOR)**: 当你做了向下兼容的功能性新增。
-   **修订号 (PATCH)**: 当你做了向下兼容的问题修正。

## 发布流程

本节为项目维护者提供了版本发布流程概述。贡献者通常不需要直接执行这些步骤。

1.  **准备工作**:
    *   确保本地 `main` 分支是最新的。
    *   确认所有计划内的功能和修复已合并。
    *   运行类型检查 (`bun run type-check`) 和构建验证 (`bun run build`)。
2.  **更新日志**: 更新 `CHANGELOG.md`,新增版本标题,并分类记录变更内容。
3.  **更新版本号**: 更新 `package.json` 中的 `version` 字段。
4.  **提交**: 提交 `CHANGELOG.md` 和 `package.json` 的修改,提交信息格式为 `chore(release): prepare for vX.Y.Z`。
5.  **创建标签**: 为此次提交创建附注标签 `git tag -a vX.Y.Z -m "Release vX.Y.Z"` 并推送到远程。
6.  **GitHub Release**: 在 GitHub 上创建 Release,将 `CHANGELOG.md` 中的对应版本内容复制到发布说明中。

## 编码规范

### 设计原则

项目严格遵循以下软件工程原则:

1.  **KISS 原则 (Keep It Simple, Stupid)**: 追求代码和设计的极致简洁,优先选择最直观的解决方案。
2.  **DRY 原则 (Don't Repeat Yourself)**: 消除重复代码,提取共享函数,统一相似功能的实现方式。
3.  **YAGNI 原则 (You Aren't Gonna Need It)**: 仅实现当前明确所需的功能,删除未使用的代码和依赖,避免过度设计。
4.  **函数式编程优先**: 优先使用 `map`、`reduce`、`filter` 等函数式方法和不可变数据操作。

### 代码质量标准

-   使用 TypeScript 严格模式,避免 `any` 类型。
-   所有函数都有明确的类型声明。
-   实现优雅的错误处理和日志记录。
-   遵循 Prettier 格式化(2空格、单引号、无分号、宽度120、LF EOL)。

### 文件命名规范

-   **文件名**: `kebab-case` (例: `config-manager.ts`)
-   **类名**: `PascalCase` (例: `ConfigManager`)
-   **Vue 组件名**: `PascalCase` (例: `ChannelCard.vue`)
-   **类型/接口名**: `PascalCase`
-   **函数名**: `camelCase` (例: `getNextApiKey`)
-   **常量名**: `SCREAMING_SNAKE_CASE` (例: `DEFAULT_CONFIG`)

### TypeScript 规范

-   使用严格的 TypeScript 配置。
-   所有函数和变量都有明确的类型声明。
-   使用接口定义数据结构。
-   避免使用 `any` 类型。

## 测试指南

### 开发测试

在提交代码前,请确保:
-   运行 TypeScript 类型检查:`bun run type-check`
-   运行构建验证:`bun run build`
-   通过健康检查端点 (`GET http://localhost:3000/health`) 进行冒烟测试。
-   对于 UI 变更,在 Pull Request 中包含简短的测试计划和截图/GIF。

### 提交与 Pull Request 指南

-   **Conventional Commits**: 提交信息遵循 `conventional-commits` 规范,例如 `feat:`, `fix:`, `refactor:`, `chore:`。
    -   示例: `feat(frontend): add ESC to close modal`, `fix(backend): redact Authorization header`。
-   **PR 内容**: Pull Request 必须包含:
    -   目的说明
    -   关联的 Issue (如果有)
    -   详细的测试步骤
    -   配置/环境变量变更说明
    -   UI 变更的截图/GIF

## 安全与配置提示

-   **切勿提交敏感信息**: 永远不要将密钥或敏感配置提交到版本控制中。使用 `.env` 文件和 `backend/config.json` 进行管理。
-   **访问密钥**: `PROXY_ACCESS_KEY` 是代理访问的必需密钥。避免在日志中记录完整的 API 密钥。

## Agent-Specific Notes

-   保持代码差异最小化,与现有代码风格保持一致。
-   当行为发生变化时,及时更新相关文档。
-   除非必要,否则避免进行重命名或大规模重构。


================================================
FILE: DEVELOPMENT.md
================================================
# 开发指南

本文档为开发者提供开发环境配置、工作流程、调试技巧和最佳实践。

> 📚 **相关文档**
> - 架构设计和技术选型: [ARCHITECTURE.md](ARCHITECTURE.md)
> - 环境变量配置: [ENVIRONMENT.md](ENVIRONMENT.md)
> - 贡献规范: [CONTRIBUTING.md](CONTRIBUTING.md)

---

## 🎯 推荐开发方式

| 开发方式 | 启动速度 | 热重载 | 适用场景 |
|---------|---------|-------|---------|
| **🚀 根目录 Make 命令** | ⚡ 极快 | ✅ 支持 | **推荐:日常开发** |
| **🔧 backend-go Make** | ⚡ 极快 | ✅ 支持 | Go 后端专项开发 |
| **🐳 Docker** | 🔄 中等 | ❌ 需重启 | 生产环境测试 |

---

## 方式一:🚀 根目录开发(推荐)

**适合日常开发,自动处理前端构建和后端启动**

### 快速开始

```bash
# 在项目根目录执行

# 查看所有可用命令
make help

# 开发模式(后端热重载)
make dev

# 构建前端并运行后端
make run

# 前端独立开发服务器
make frontend-dev

# 完整构建(前端 + 后端)
make build

# 清理构建产物
make clean
```

### 开发环境要求

- Go 1.22+
- Make(构建工具)
- Bun(前端构建)

---

## 方式二:🔧 backend-go 目录开发

**适合专注 Go 后端开发和调试**

```bash
cd backend-go

# 查看所有可用命令
make help

# 开发模式(支持热重载)
make dev

# 运行测试
make test

# 测试 + 覆盖率
make test-cover

# 构建当前平台二进制
make build-current

# 构建并运行
make build-run
```

---

## 🪟 Windows 环境配置

Windows 用户在开发本项目时可能遇到一些工具缺失的问题,以下是常见问题的解决方案。

### 问题 1: 没有 `make` 命令

Windows 默认不包含 `make` 工具,有以下几种解决方案:

#### 方案 A: 安装 Make (推荐)

```powershell
# 使用 Chocolatey (推荐)
choco install make

# 或使用 Scoop
scoop install make

# 或使用 winget
winget install GnuWin32.Make
```

#### 方案 B: 直接使用 Go 命令 (无需安装 make)

```powershell
cd backend-go

# 替代 make dev (需要先安装 air: go install github.com/air-verse/air@latest)
air

# 替代 make build
go build -o claude-proxy.exe .

# 替代 make run
go run main.go

# 替代 make test
go test ./...

# 替代 make fmt
go fmt ./...
```

### 问题 2: 没有 `vite` 命令

这是因为前端依赖未安装,`vite` 是项目的开发依赖。

#### 解决步骤

```powershell
cd frontend

# 使用 bun 安装依赖 (推荐)
bun install

# 或使用 npm
npm install

# 安装完成后运行开发服务器
bun run dev    # 或 npm run dev
```

### Windows 完整开发环境配置

#### 1. 安装包管理器 (可选但推荐)

```powershell
# 安装 Scoop (无需管理员权限)
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
irm get.scoop.sh | iex

# 或安装 Chocolatey (需要管理员权限)
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
```

#### 2. 安装开发工具

```powershell
# 使用 Scoop
scoop install git go bun make

# 或使用 Chocolatey
choco install git golang bun make -y
```

#### 3. 验证安装

```powershell
go version      # 应显示 go1.22+
bun --version   # 应显示版本号
make --version  # 应显示 GNU Make 版本
git --version   # 应显示 git 版本
```

### Windows 快速启动流程

```powershell
# 1. 克隆项目
git clone https://github.com/BenedictKing/claude-proxy
cd claude-proxy

# 2. 安装前端依赖
cd frontend
bun install    # 或 npm install

# 3. 配置环境变量
cd ../backend-go
copy .env.example .env
# 编辑 .env 文件设置 PROXY_ACCESS_KEY

# 4. 启动后端 (选择以下方式之一)

# 方式 A: 使用 make (如果已安装)
make dev

# 方式 B: 直接使用 Go
go run main.go

# 5. 另开终端,启动前端开发服务器 (如需单独开发前端)
cd frontend
bun run dev
```

### Windows 常见问题

#### PowerShell 执行策略限制

```powershell
# 如果遇到脚本执行限制,以管理员身份运行
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```

#### 端口被占用

```powershell
# 查看端口占用
netstat -ano | findstr :3000

# 终止占用进程 (替换 PID 为实际进程 ID)
taskkill /PID <PID> /F
```

#### 路径包含空格

确保项目路径不包含空格和中文字符,推荐使用如 `C:\projects\claude-proxy` 这样的路径。

---

## 方式三:🐳 Docker 开发

**适合测试生产环境或隔离开发**

```bash
# 使用 docker-compose 启动
docker-compose up -d

# 查看日志
docker-compose logs -f

# 重新构建并启动
docker-compose up -d --build

# 停止服务
docker-compose down
```

---

## 前端独立开发

前端使用 Vue 3 + Vuetify + Vite,可独立开发:

```bash
cd frontend

# 安装依赖
bun install

# 启动开发服务器(端口 5173)
bun run dev

# 构建生产版本
bun run build

# 预览构建结果
bun run preview
```

**开发服务器代理配置**:

Vite 开发服务器会自动将 `/api` 和 `/v1` 请求代理到后端(默认 `http://localhost:3000`):

```typescript
// frontend/vite.config.ts
server: {
  port: 5173,
  proxy: {
    '/api': { target: backendUrl, changeOrigin: true },
    '/v1': { target: backendUrl, changeOrigin: true }
  }
}
```

**环境变量**:
- `VITE_PROXY_TARGET` - 后端代理目标(默认 `http://localhost:3000`)
- `VITE_FRONTEND_PORT` - 前端开发服务器端口(默认 `5173`)

---

## 文件监听策略

### 配置文件(无需重启)

- `backend-go/.config/config.json` - 主配置文件

**变化时**: 自动重载配置,服务保持运行

### 环境变量文件(需要重启)

- `backend-go/.env` - 环境变量配置

**变化时**: 需要重启服务以加载新的环境变量

## 开发模式特性

### 1. 热重载开发 (`make dev`)

- ✅ Go 源码变化自动重新编译
- ✅ 配置文件变化自动重载(不重启)
- ✅ 优雅关闭处理
- ✅ 详细的开发日志

### 2. 配置热重载

- ✅ 配置文件变化自动重载
- ✅ 无需重启服务器
- ✅ 自动备份配置(最多 10 个)

---

## 🎯 代码质量标准

> 📚 完整的编码规范和设计模式请参考 [ARCHITECTURE.md](ARCHITECTURE.md)

### 编程原则

项目严格遵循以下软件工程原则:

#### 1. KISS 原则 (Keep It Simple, Stupid)
- 追求代码和设计的极致简洁
- 优先选择最直观的解决方案
- 使用正则表达式替代复杂的字符串处理逻辑

#### 2. DRY 原则 (Don't Repeat Yourself)  
- 消除重复代码,提取共享函数
- 统一相似功能的实现方式
- 例:`normalizeClaudeRole` 函数的提取和共享

#### 3. YAGNI 原则 (You Aren't Gonna Need It)
- 仅实现当前明确所需的功能
- 删除未使用的代码和依赖
- 避免过度设计和未来特性预留

#### 4. 函数式编程优先
- 使用 `map`、`reduce`、`filter` 等函数式方法
- 优先使用不可变数据操作
- 例:命令行参数解析使用 `reduce()` 替代传统循环

### 代码优化检查清单

在提交代码前,请确保:

- [ ] Go 代码通过 `make lint` 检查
- [ ] 通过 `make test` 测试
- [ ] 前端代码通过 `bun run build` 构建验证

### Go 代码规范

- 使用 `gofmt` 格式化代码
- 遵循 Go 官方代码规范
- 错误处理要完整
- 适当添加注释

### 命名规范

- **文件名**: snake_case (例: `config_manager.go`)
- **函数名**: PascalCase 导出 / camelCase 私有 (例: `GetProvider` / `parseRequest`)
- **常量名**: PascalCase 或 SCREAMING_SNAKE_CASE
- **接口名**: PascalCase,通常以 -er 结尾 (例: `Provider`)

### 错误处理

```go
result, err := riskyOperation()
if err != nil {
    log.Printf("Operation failed: %v", err)
    return fmt.Errorf("specific error: %w", err)
}
```

### 日志规范

使用 Go 标准日志或结构化日志:

```go
log.Printf("🎯 使用上游: %s", upstream.Name)
log.Printf("⚠️ 警告: %s", message)
log.Printf("💥 错误: %v", err)
```

## 🧪 测试策略

### 手动测试

#### 1. 基础功能测试

```bash
# 测试健康检查
curl http://localhost:3000/health

# 测试基础对话
curl -X POST http://localhost:3000/v1/messages \
  -H "x-api-key: test-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":100,"messages":[{"role":"user","content":"Hello"}]}'

# 测试流式响应
curl -X POST http://localhost:3000/v1/messages \
  -H "x-api-key: test-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-3-5-sonnet-20241022","stream":true,"max_tokens":100,"messages":[{"role":"user","content":"Count to 10"}]}'
```

#### 2. 负载均衡测试

```bash
# 添加多个 API 密钥
bun run config key test-upstream add key1 key2 key3

# 设置轮询策略
bun run config balance round-robin

# 发送多个请求观察密钥轮换
for i in {1..5}; do
  curl -X POST http://localhost:3000/v1/messages \
    -H "x-api-key: test-key" \
    -H "Content-Type: application/json" \
    -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":10,"messages":[{"role":"user","content":"Test '$i'"}]}'
done
```

### 集成测试

#### Claude Code 集成测试

1. 配置 Claude Code 使用本地代理
2. 测试基础对话功能
3. 测试工具调用功能
4. 测试流式响应
5. 验证错误处理

#### 压力测试

```bash
# 使用 ab (Apache Bench) 进行压力测试
ab -n 100 -c 10 -p request.json -T application/json \
  -H "x-api-key: test-key" \
  http://localhost:3000/v1/messages
```

## 🔧 调试技巧

### 1. 日志分析

```bash
# 实时查看日志
tail -f server.log

# 过滤错误日志
grep -i "error" server.log

# 分析请求模式
grep -o "POST /v1/messages" server.log | wc -l
```

### 2. 配置调试

```bash
# 验证配置文件
cat config.json | jq .

# 检查环境变量
env | grep -E "(PORT|LOG_LEVEL)"
```

### 3. 网络调试

```bash
# 测试上游连接
curl -I https://api.openai.com

# 检查 DNS 解析
nslookup api.openai.com

# 测试端口连通性
telnet localhost 3000
```

## 🚀 部署指南

### 开发环境部署

```bash
# 1. 克隆项目
git clone https://github.com/BenedictKing/claude-proxy
cd claude-proxy

# 2. 配置环境变量
cp backend-go/.env.example backend-go/.env
vim backend-go/.env

# 3. 启动开发服务器
make dev
```

### 生产环境部署

```bash
# 1. 构建生产版本
make build

# 2. 配置环境变量
cp backend-go/.env.example backend-go/.env
# 修改 ENV=production 和 PROXY_ACCESS_KEY

# 3. 运行服务
./backend-go/dist/claude-proxy
```

### Docker 部署

```bash
# 使用预构建镜像
docker-compose up -d

# 或本地构建
docker-compose build
docker-compose up -d
```

## 🤝 贡献与发布

### 贡献指南

欢迎提交 Issue 和 Pull Request!

> 📚 详细的贡献规范和提交指南请参考 [CONTRIBUTING.md](CONTRIBUTING.md)

### 版本发布

> 📚 维护者版本发布流程请参考 [RELEASE.md](RELEASE.md)


================================================
FILE: Dockerfile
================================================
# --- 阶段 1: 准备 Bun 运行时 ---
FROM oven/bun:alpine AS bun-runtime

# --- 阶段 2: 构建阶段 (Go + Bun) ---
FROM golang:1.22-alpine AS builder

# 声明 VERSION 构建参数(用于 CI 传入版本号,留空则从 VERSION 文件读取)
ARG VERSION

WORKDIR /src

# 安装必要的构建工具和 bun 依赖(libstdc++ libgcc 是 bun:alpine 运行所需)
RUN apk add --no-cache git make libstdc++ libgcc

# 从 bun-runtime 复制 bun 和 bunx 到 Go 镜像
COPY --from=bun-runtime /usr/local/bin/bun /usr/local/bin/bun
COPY --from=bun-runtime /usr/local/bin/bunx /usr/local/bin/bunx

# 将 bun 添加到 PATH
ENV PATH="/usr/local/bin:${PATH}"

# 复制项目必要文件(.dockerignore 会排除不需要的文件)
COPY Makefile VERSION ./
COPY frontend/ ./frontend/
COPY backend-go/ ./backend-go/

# 使用 bun 安装前端依赖(比 npm 快 10-100 倍)
RUN cd frontend && bun install

# 安装 Go 后端依赖(go mod tidy 确保 go.sum 完整)
RUN cd backend-go && go mod tidy && go mod download

# 使用 Makefile 构建整个项目(前端 + 后端)
# 如果 CI 传入了 VERSION 则使用,否则 Makefile 会从 VERSION 文件读取
RUN if [ -n "${VERSION}" ]; then VERSION=${VERSION} make build; else make build; fi

# --- 阶段 3: 运行时 ---
FROM alpine:latest AS runtime

WORKDIR /app

# 安装运行时依赖
RUN apk --no-cache add ca-certificates tzdata

# 从构建阶段复制 Go 二进制文件(已内嵌前端资源)
COPY --from=builder /src/dist/claude-proxy-go /app/claude-proxy

# 创建配置目录和日志目录
RUN mkdir -p /app/.config/backups /app/logs

# 设置时区(可选)
ENV TZ=Asia/Shanghai

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# 启动命令
CMD ["/app/claude-proxy"]


================================================
FILE: Dockerfile_China
================================================
# --- 阶段 1: 准备 Bun 运行时 ---
FROM docker.1ms.run/oven/bun:alpine AS bun-runtime

# --- 阶段 2: 构建阶段 (Go + Bun) ---
FROM docker.1ms.run/library/golang:1.22-alpine AS builder

# 声明 VERSION 构建参数(用于 CI 传入版本号,留空则从 VERSION 文件读取)
ARG VERSION

WORKDIR /src

# 配置 Go 代理(使用国内镜像)
ENV GOPROXY=https://goproxy.cn,direct
ENV GO111MODULE=on

# 配置国内镜像源并安装构建工具和 bun 依赖(libstdc++ libgcc 是 bun:alpine 运行所需)
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    apk add --no-cache git make libstdc++ libgcc

# 从 bun-runtime 复制 bun 和 bunx 到 Go 镜像
COPY --from=bun-runtime /usr/local/bin/bun /usr/local/bin/bun
COPY --from=bun-runtime /usr/local/bin/bunx /usr/local/bin/bunx

# 将 bun 添加到 PATH
ENV PATH="/usr/local/bin:${PATH}"

# 复制项目必要文件(.dockerignore 会排除不需要的文件)
COPY Makefile VERSION ./
COPY frontend/ ./frontend/
COPY backend-go/ ./backend-go/

# 使用 bun 安装前端依赖(使用国内镜像,比 npm 快 10-100 倍)
RUN cd frontend && bun install --registry https://registry.npmmirror.com

# 安装 Go 后端依赖(go mod tidy 确保 go.sum 完整)
RUN cd backend-go && go mod tidy && go mod download

# 使用 Makefile 构建整个项目(前端 + 后端)
# 如果 CI 传入了 VERSION 则使用,否则 Makefile 会从 VERSION 文件读取
RUN if [ -n "${VERSION}" ]; then VERSION=${VERSION} make build; else make build; fi

# --- 阶段 3: 运行时 ---
FROM docker.1ms.run/library/alpine:latest AS runtime

WORKDIR /app

# 配置国内镜像源并安装运行时依赖
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    apk --no-cache add ca-certificates tzdata

# 从构建阶段复制 Go 二进制文件(已内嵌前端资源)
COPY --from=builder /src/dist/claude-proxy-go /app/claude-proxy

# 创建配置目录和日志目录
RUN mkdir -p /app/.config/backups /app/logs

# 设置时区
ENV TZ=Asia/Shanghai

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# 启动命令
CMD ["/app/claude-proxy"]


================================================
FILE: ENVIRONMENT.md
================================================
# 环境变量配置指南

## 概述

本项目使用分层的环境变量配置系统,支持开发、生产等不同环境的端口和API配置。前端通过 Vite 的环境变量系统动态连接后端服务。

## 配置文件结构

```
claude-proxy/
├── frontend/
│   ├── .env                    # 前端默认配置
│   ├── .env.development        # 开发环境配置
│   ├── .env.production         # 生产环境配置
│   └── vite.config.ts          # Vite 构建配置
└── backend-go/
    └── .env                    # Go 后端环境配置
```

## 环境变量详解

### 前端配置变量

#### 开发环境变量

前端使用 Vite,环境变量需以 `VITE_` 前缀:

- `VITE_PROXY_TARGET` - 后端代理目标地址(默认 `http://localhost:3000`)
- `VITE_FRONTEND_PORT` - 前端开发服务器端口(默认 `5173`)
- `VITE_BACKEND_URL` - 开发环境后端 URL(用于 API 服务)
- `VITE_API_BASE_PATH` - API 基础路径(默认 `/api`)
- `VITE_PROXY_API_PATH` - 代理 API 路径(默认 `/v1`)
- `VITE_APP_ENV` - 应用环境标识

### 后端配置 (Go)

后端支持以下环境变量:

```bash
# 服务器配置
PORT=3000                              # 服务器端口

# 运行环境
ENV=production                         # 运行环境: development | production
# NODE_ENV=production                  # 向后兼容 (已弃用,请使用 ENV)

# 访问控制
PROXY_ACCESS_KEY=your-secret-key       # 访问密钥 (必须设置!)

# Web UI
ENABLE_WEB_UI=true                     # 是否启用 Web 管理界面

# 日志配置
LOG_LEVEL=info                         # 日志级别: debug | info | warn | error
ENABLE_REQUEST_LOGS=true               # 是否记录请求日志
ENABLE_RESPONSE_LOGS=false             # 是否记录响应日志
QUIET_POLLING_LOGS=true                # 静默前端轮询端点日志(/api/channels 等)

# 性能配置
REQUEST_TIMEOUT=300000                 # 请求超时时间(毫秒)
MAX_REQUEST_BODY_SIZE_MB=50            # 请求体最大大小(MB,默认 50)

# CORS 配置
ENABLE_CORS=false                      # 是否启用 CORS
CORS_ORIGIN=*                          # CORS 允许的源

# 熔断指标配置
METRICS_WINDOW_SIZE=10                 # 滑动窗口大小(最小 3,默认 10)
METRICS_FAILURE_THRESHOLD=0.5          # 失败率阈值(0-1,默认 0.5 即 50%)
```

#### 日志等级说明

项目采用标准的四级日志系统,等级从高到低:

| 等级 | 值 | 说明 | 典型场景 |
|------|----|----|---------|
| `error` | 0 | 错误日志(最高优先级) | 致命错误、异常情况 |
| `warn` | 1 | 警告日志 | 非致命问题、降级操作 |
| `info` | 2 | 信息日志(默认) | 常规操作、状态变化 |
| `debug` | 3 | 调试日志(最低优先级) | 详细调试信息、敏感数据 |

**等级控制规则**:设置 `LOG_LEVEL=info` 时,会输出 `error`、`warn`、`info` 级别的日志,但不输出 `debug` 级别。

#### 日志控制机制

项目使用三种机制来控制日志输出:

##### 1. 显式等级控制(推荐)
```go
// 代码示例
if envCfg.ShouldLog("info") {
    log.Printf("🎯 使用上游: %s", upstream.Name)
}
```
- **适用场景**:通用信息输出
- **控制变量**:`LOG_LEVEL`

##### 2. 开关控制(分类日志)
```go
// 代码示例
if envCfg.EnableRequestLogs {
    log.Printf("📥 收到请求: %s", c.Request.URL.Path)
}
```
- **适用场景**:请求/响应类日志
- **控制变量**:`ENABLE_REQUEST_LOGS`、`ENABLE_RESPONSE_LOGS`

##### 3. 环境门控(开发专用)
```go
// 代码示例
if envCfg.EnableRequestLogs && envCfg.IsDevelopment() {
    log.Printf("📄 原始请求体:\n%s", formattedBody)
}
```
- **适用场景**:敏感/详细信息(请求体、请求头等)
- **控制变量**:`ENV=development`

#### 日志输出对照表

| 日志内容 | 控制条件 | 等效等级 | 生产环境 | 开发环境 |
|---------|---------|---------|---------|---------|
| `📄 原始请求体` | `EnableRequestLogs && IsDevelopment()` | debug | ❌ 不输出 | ✅ 输出 |
| `📋 实际请求头` | `EnableRequestLogs && IsDevelopment()` | debug | ❌ 不输出 | ✅ 输出 |
| `📦 响应体` | `EnableResponseLogs && IsDevelopment()` | debug | ❌ 不输出 | ✅ 输出 |
| `📥 收到请求` | `EnableRequestLogs` | info | ⚙️ 可配置 | ✅ 输出 |
| `⏱️ 响应完成` | `EnableResponseLogs` | info | ⚙️ 可配置 | ✅ 输出 |
| `🎯 使用上游` | `ShouldLog("info")` | info | ⚙️ 可配置 | ✅ 输出 |
| `ℹ️ 客户端中断` | `ShouldLog("info")` | info | ⚙️ 可配置 | ✅ 输出 |
| `⚠️ API密钥失败` | 无条件 | warn | ✅ 输出 | ✅ 输出 |
| `💥 所有密钥失败` | 无条件 | error | ✅ 输出 | ✅ 输出 |

#### 配置组合效果

**开发环境 + 完整日志**:
```env
ENV=development
LOG_LEVEL=debug
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true
```
- ✅ 输出所有日志,包括完整请求体、请求头、响应体
- ✅ 适合本地开发调试
- ⚠️ 可能包含敏感信息,不要在生产环境使用

**生产环境 + 最小日志**:
```env
ENV=production
LOG_LEVEL=warn
ENABLE_REQUEST_LOGS=false
ENABLE_RESPONSE_LOGS=false
```
- ✅ 只输出警告和错误
- ✅ 最小性能影响
- ✅ 不输出敏感信息
- ⚠️ 排查问题时信息较少

**生产环境 + 适度日志**(推荐):
```env
ENV=production
LOG_LEVEL=info
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=false
```
- ✅ 输出基本请求信息(如 `📥 收到请求`)
- ✅ 不输出详细内容(请求体、响应体等)
- ✅ 平衡了可观测性和性能
- ✅ 不泄露敏感信息

**调试模式**:
```env
ENV=development
LOG_LEVEL=debug
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true
```
- ✅ 最详细的日志输出
- ✅ 查看完整的请求/响应数据流
- ⚠️ 仅用于故障排查,排查完成后应恢复正常配置

#### 性能影响说明

| 配置 | CPU 影响 | 内存影响 | 磁盘 I/O |
|-----|---------|---------|----------|
| `LOG_LEVEL=error` | 极低 | 极低 | 极低 |
| `LOG_LEVEL=warn` | 极低 | 极低 | 低 |
| `LOG_LEVEL=info` | 低 | 低 | 中 |
| `LOG_LEVEL=debug` | 中 | 中 | 高 |
| `ENABLE_REQUEST_LOGS=true` | 低 | 低 | 中 |
| `ENABLE_RESPONSE_LOGS=true` | 低-中 | 中-高 | 高 |

**生产环境建议**:
- 日常运行:`LOG_LEVEL=info`,`ENABLE_RESPONSE_LOGS=false`
- 故障排查:临时开启 `ENABLE_RESPONSE_LOGS=true`
- 高负载场景:使用 `LOG_LEVEL=warn` 减少开销

### ENV 变量影响

| 配置项 | `development` | `production` |
|--------|---------------|--------------|
| Gin 模式 | DebugMode | ReleaseMode |
| `/admin/dev/info` | ✅ 开启 | ❌ 关闭 |
| CORS | 宽松(localhost自动允许)| 严格 |
| 日志 | 详细 | 最小 |

## 配置文件内容

### frontend/.env
```env
# 前端环境配置

# 后端API服务器配置
VITE_BACKEND_URL=http://localhost:3000

# 前端开发服务器配置
VITE_FRONTEND_PORT=5173

# API路径配置
VITE_API_BASE_PATH=/api
VITE_PROXY_API_PATH=/v1
```

### frontend/.env.development
```env
# 开发环境配置

# 后端API服务器配置
VITE_BACKEND_URL=http://localhost:3000

# 前端开发服务器配置
VITE_FRONTEND_PORT=5173

# API路径配置
VITE_API_BASE_PATH=/api
VITE_PROXY_API_PATH=/v1

# 开发模式标识
VITE_APP_ENV=development
```

### frontend/.env.production
```env
# 生产环境配置
VITE_API_BASE_PATH=/api
VITE_PROXY_API_PATH=/v1
VITE_APP_ENV=production
```

### backend-go/.env.example
```env
# 服务器配置
PORT=3000

# 运行环境
ENV=production

# 访问控制 (必须修改!)
PROXY_ACCESS_KEY=your-super-strong-secret-key

# Web UI
ENABLE_WEB_UI=true

# 日志配置
LOG_LEVEL=info
ENABLE_REQUEST_LOGS=false
ENABLE_RESPONSE_LOGS=false
```

## API 基础URL 生成逻辑

前端通过以下逻辑动态确定API基础URL:

```typescript
const getApiBase = () => {
  // 生产环境:直接使用当前域名
  if (import.meta.env.PROD) {
    return '/api'
  }

  // 开发环境:使用配置的后端URL
  const backendUrl = import.meta.env.VITE_BACKEND_URL
  const apiBasePath = import.meta.env.VITE_API_BASE_PATH || '/api'

  if (backendUrl) {
    return `${backendUrl}${apiBasePath}`
  }

  // 回退到默认配置
  return '/api'
}
```

## 开发服务器代理配置

Vite 开发服务器自动配置代理,将前端请求转发到后端:

```typescript
// vite.config.ts
server: {
  port: Number(env.VITE_FRONTEND_PORT) || 5173,
  proxy: {
    '/api': {
      target: backendUrl,
      changeOrigin: true,
      secure: false
    }
  }
}
```

## 环境切换

### 开发环境启动
```bash
# 方式 1: 根目录启动(推荐)
make dev

# 方式 2: 分别启动
# 启动后端 (端口 3000)
cd backend-go && make dev

# 启动前端 (端口 5173)
cd frontend && bun run dev
```

### 生产环境构建
```bash
# 完整构建
make build

# Docker 部署
docker-compose up -d
```

## 端口配置优先级

1. **环境变量** - 从 `.env.*` 文件读取
2. **默认值** - 代码中定义的回退值
3. **系统环境变量** - `PORT` (后端)

## 常见配置场景

### 场景1:更改后端端口到 8080
```env
# backend-go/.env
PORT=8080

# frontend/.env.development
VITE_BACKEND_URL=http://localhost:8080
```

### 场景2:使用远程后端服务
```env
# frontend/.env.development
VITE_BACKEND_URL=https://api.example.com
```

### 场景3:自定义前端开发端口
```env
# frontend/.env.development
VITE_FRONTEND_PORT=3000
```

### 场景4:生产环境配置

#### 4.1 高性能模式(最小日志)
```env
# backend-go/.env
ENV=production
PORT=3000
PROXY_ACCESS_KEY=$(openssl rand -base64 32)

# 最小日志输出
LOG_LEVEL=warn
ENABLE_REQUEST_LOGS=false
ENABLE_RESPONSE_LOGS=false

ENABLE_WEB_UI=true
```
- ✅ 适合:高并发场景、性能敏感应用
- ✅ 特点:最低资源消耗,只记录警告和错误
- ⚠️ 注意:排查问题时信息较少

#### 4.2 标准模式(推荐)
```env
# backend-go/.env
ENV=production
PORT=3000
PROXY_ACCESS_KEY=$(openssl rand -base64 32)

# 适度日志输出
LOG_LEVEL=info
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=false

ENABLE_WEB_UI=true
```
- ✅ 适合:大多数生产环境
- ✅ 特点:平衡可观测性和性能,不泄露敏感信息
- ✅ 优势:足够的信息用于监控和问题排查

#### 4.3 调试模式(临时排查)
```env
# backend-go/.env
ENV=production
PORT=3000
PROXY_ACCESS_KEY=$(openssl rand -base64 32)

# 详细日志输出(临时使用)
LOG_LEVEL=info
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true

ENABLE_WEB_UI=true
```
- ⚠️ 适合:故障排查时临时启用
- ⚠️ 注意:会输出完整响应内容,增加日志量
- 🔄 建议:问题解决后立即恢复标准配置

#### 4.4 开发环境配置
```env
# backend-go/.env
ENV=development
PORT=3000
PROXY_ACCESS_KEY=dev-test-key

# 完整日志输出
LOG_LEVEL=debug
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true

ENABLE_WEB_UI=true
```
- ✅ 适合:本地开发和调试
- ✅ 特点:输出所有详细信息,包括请求体、响应体
- ⚠️ 警告:包含敏感信息,仅限开发环境使用

## 调试配置

开发环境下,前端会在控制台输出当前API配置:

```javascript
console.log('🔗 API Configuration:', {
  API_BASE: '/api',
  BACKEND_URL: 'http://localhost:3000',
  IS_DEV: true,
  IS_PROD: false
})
```

## 注意事项

1. **变量前缀**:前端环境变量必须以 `VITE_` 开头才能在浏览器中访问
2. **构建时解析**:Vite 在构建时静态替换环境变量,运行时无法修改
3. **生产环境**:生产环境不需要指定后端URL,通过反向代理或一体化部署处理
4. **类型安全**:使用 `Number()` 转换端口号确保类型正确
5. **密钥安全**:切勿在版本控制中提交 `.env` 文件,使用 `.env.example` 作为模板

## 安全最佳实践

### 生成强密钥
```bash
# 生成随机密钥
PROXY_ACCESS_KEY=$(openssl rand -base64 32)
echo "生成的密钥: $PROXY_ACCESS_KEY"
```

### 生产环境配置清单
```bash
# 1. 强密钥 (必须!)
PROXY_ACCESS_KEY=<strong-random-key>

# 2. 生产模式
ENV=production

# 3. 适度日志(推荐)
LOG_LEVEL=info
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=false

# 4. 启用 Web UI (可选)
ENABLE_WEB_UI=true
```

### 日志安全建议

#### 敏感信息保护
项目已自动对以下信息进行脱敏处理:
- ✅ API密钥:只显示前4位和后4位(如 `sk-a***b`)
- ✅ Authorization 请求头:完全隐藏
- ✅ x-api-key 请求头:完全隐藏

#### 推荐配置
```bash
# 生产环境:不输出详细内容
ENV=production
ENABLE_REQUEST_LOGS=true    # ✅ 基本请求信息
ENABLE_RESPONSE_LOGS=false  # ❌ 不输出响应体

# 开发环境:可以输出详细内容
ENV=development
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true
```

#### 日志存储注意事项
1. **日志轮转**:定期清理旧日志,避免磁盘空间耗尽
2. **访问控制**:限制日志文件的访问权限
   ```bash
   chmod 600 /var/log/claude-proxy/*.log
   ```
3. **敏感数据**:即使有脱敏,也应定期审查日志内容
4. **合规要求**:根据数据保护法规(GDPR、CCPA等)管理日志

#### 故障排查时的安全做法
```bash
# ✅ 推荐:临时开启详细日志,排查完成后恢复
ENABLE_RESPONSE_LOGS=true  # 临时启用

# 🔄 排查完成后立即恢复
ENABLE_RESPONSE_LOGS=false

# ❌ 不推荐:在生产环境长期开启 debug 级别
LOG_LEVEL=debug  # 可能泄露敏感信息
```

## 故障排除

### 问题:前端无法连接后端
1. 检查后端是否在正确端口启动
   ```bash
   curl http://localhost:3000/health
   ```
2. 确认 `VITE_BACKEND_URL` 配置正确
3. 查看浏览器控制台的API配置输出

### 问题:构建后API请求失败
1. 确认生产环境配置了正确的反向代理或使用一体化部署
2. 检查 `VITE_API_BASE_PATH` 设置
3. 验证后端API路径匹配

### 问题:环境变量不生效
1. 确认变量名以 `VITE_` 开头 (前端) 或在后端代码中正确读取
2. 重启开发服务器
3. 检查 `.env` 文件语法正确 (无多余空格、引号等)

### 问题:认证失败
```bash
# 检查密钥设置
echo $PROXY_ACCESS_KEY

# 测试认证
curl -H "x-api-key: $PROXY_ACCESS_KEY" http://localhost:3000/health
```

### 问题:日志输出过多或过少

#### 日志过多(影响性能)
**症状**:日志文件快速增长,磁盘空间不足,或系统性能下降

**解决方案**:
1. 降低日志等级
   ```env
   LOG_LEVEL=warn  # 从 info 或 debug 降级
   ```

2. 关闭详细日志
   ```env
   ENABLE_REQUEST_LOGS=false
   ENABLE_RESPONSE_LOGS=false
   ```

3. 使用日志轮转(推荐)
   ```bash
   # 使用 systemd 日志轮转
   journalctl --vacuum-time=7d

   # 或使用 logrotate
   # /etc/logrotate.d/claude-proxy
   /var/log/claude-proxy/*.log {
       daily
       rotate 7
       compress
       delaycompress
       missingok
       notifempty
   }
   ```

#### 日志过少(排查困难)
**症状**:出现问题时没有足够的日志信息

**解决方案**:
1. 提高日志等级
   ```env
   LOG_LEVEL=info  # 从 warn 提升
   ```

2. 临时开启详细日志
   ```env
   ENABLE_REQUEST_LOGS=true
   ENABLE_RESPONSE_LOGS=true
   ```

3. 使用开发模式(仅限测试环境)
   ```env
   ENV=development
   LOG_LEVEL=debug
   ```

#### 看不到请求体/响应体
**症状**:日志中没有详细的请求/响应内容

**原因**:详细内容只在开发环境 (`ENV=development`) 输出

**解决方案**:
```env
# 方案1:临时切换到开发模式(不推荐生产环境)
ENV=development
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true

# 方案2:查看是否开启了日志开关
ENABLE_REQUEST_LOGS=true   # 必须为 true
ENABLE_RESPONSE_LOGS=true  # 必须为 true

# 方案3:检查当前环境
echo $ENV  # 必须是 development
```

**安全提醒**:
- ⚠️ 请求体和响应体可能包含敏感信息(API密钥、用户数据等)
- ⚠️ 生产环境建议关闭 `ENABLE_RESPONSE_LOGS`
- ⚠️ 排查完成后立即恢复安全配置

### 问题:日志格式混乱
**症状**:日志输出格式不统一或难以阅读

**检查项**:
1. 确认是否混用了多个日志系统
2. 检查是否有第三方库输出了额外日志
3. 验证环境变量是否正确加载
   ```bash
   # 打印当前日志配置
   curl -H "x-api-key: $PROXY_ACCESS_KEY" http://localhost:3000/health
   ```

## 文档资源

- **项目架构**: 参见 [ARCHITECTURE.md](ARCHITECTURE.md)
- **快速开始**: 参见 [README.md](README.md)
- **贡献指南**: 参见 [CONTRIBUTING.md](CONTRIBUTING.md)


================================================
FILE: LICENSE
================================================
Copyright (c) 2025 wangyusong

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. 

================================================
FILE: Makefile
================================================
# Claude Proxy Makefile

GREEN=\033[0;32m
YELLOW=\033[0;33m
NC=\033[0m

.PHONY: help dev run build clean frontend-dev frontend-build embed-frontend

help:
	@echo "$(GREEN)Claude Proxy - 可用命令:$(NC)"
	@echo ""
	@echo "$(YELLOW)开发:$(NC)"
	@echo "  make dev            - Go 后端热重载开发(不含前端)"
	@echo "  make run            - 构建前端并运行 Go 后端"
	@echo "  make frontend-dev   - 前端开发服务器"
	@echo ""
	@echo "$(YELLOW)构建:$(NC)"
	@echo "  make build          - 构建前端并编译 Go 后端"
	@echo "  make frontend-build - 仅构建前端"
	@echo "  make clean          - 清理构建文件"

dev:
	@echo "$(GREEN)🚀 启动前后端开发模式...$(NC)"
	@cd frontend && bun run dev &
	@cd backend-go && $(MAKE) dev

run: embed-frontend
	@cd backend-go && $(MAKE) run

build: embed-frontend
	@cd backend-go && $(MAKE) build

embed-frontend:
	@echo "$(GREEN)📦 构建前端...$(NC)"
	@cd frontend && bun run build
	@echo "$(GREEN)📋 嵌入前端到 Go 后端...$(NC)"
	@rm -rf backend-go/frontend/dist
	@mkdir -p backend-go/frontend/dist
	@cp -r frontend/dist/* backend-go/frontend/dist/

clean:
	@cd backend-go && $(MAKE) clean
	@rm -rf frontend/dist

frontend-dev:
	@cd frontend && bun run dev

frontend-build:
	@cd frontend && bun run build


================================================
FILE: README.md
================================================
> ⚠️ **项目已重命名**: 本项目已重命名为 **[CCX](https://github.com/BenedictKing/ccx)**,请访问新仓库获取最新版本和更新。本仓库已归档,不再维护。

---

# Claude / Codex / Gemini API Proxy

[![GitHub release](https://img.shields.io/github/v/release/BenedictKing/claude-proxy)](https://github.com/BenedictKing/claude-proxy/releases/latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

一个高性能的 Claude API 代理服务器,支持多种上游 AI 服务提供商(Claude、Codex、Gemini),提供故障转移、多 API 密钥管理和统一入口访问。

## 🚀 功能特性

- **🖥️ 一体化架构**: 后端集成前端,单容器部署,完全替代 Nginx
- **🔐 统一认证**: 一个密钥保护所有入口(前端界面、管理 API、代理 API)
- **📱 Web 管理面板**: 现代化可视化界面,支持渠道管理、实时监控和配置
- **三 API 支持**: 同时支持 Claude Messages API (`/v1/messages`)、Codex Responses API (`/v1/responses`) 和 Gemini API
- **统一入口**: 通过统一端点访问不同的 AI 服务
- **多上游支持**: 支持 Claude、Codex 和 Gemini 等多种上游服务
- **🔌 协议转换**: Messages API 支持协议自动转换,统一接入不同上游服务
- **🎯 智能调度**: 多渠道智能调度器,支持优先级排序、健康检查和自动熔断
- **📊 渠道编排**: 可视化渠道管理,拖拽调整优先级,实时查看健康状态
- **🔄 Trace 亲和**: 同一用户会话自动绑定到同一渠道,提升一致性体验
- **故障转移**: 自动切换到可用渠道,确保服务高可用
- **多 API 密钥**: 每个上游可配置多个 API 密钥,自动轮换使用(推荐 failover 策略以最大化利用 Prompt Caching)
- **🧠 缓存统计**: 按 Token 口径展示各渠道缓存读/写与命中率(命中率 = `cache_read_tokens / (cache_read_tokens + input_tokens)`)
- **增强的稳定性**: 内置上游请求超时与重试机制,确保服务在网络波动时依然可靠
- **自动重试与密钥降级**: 检测到额度/余额不足等错误时自动切换下一个可用密钥;若后续请求成功,再将失败密钥移动到末尾(降级);所有密钥均失败时按上游原始错误返回
- **⚡ 自动熔断**: 基于滑动窗口算法检测渠道健康度,失败率过高自动熔断,15 分钟后自动恢复
- **双重配置**: 支持命令行工具和 Web 界面管理上游配置
- **环境变量**: 通过 `.env` 文件灵活配置服务器参数
- **健康检查**: 内置健康检查端点和实时状态监控
- **日志系统**: 完整的请求/响应日志记录
- **📡 支持流式和非流式响应**
- **🛠️ 支持工具调用**
- **💬 会话管理**: Responses API 支持多轮对话的会话跟踪和上下文保持

## 📄 许可证

本项目基于 MIT 许可证开源 - 查看 [LICENSE](LICENSE) 文件了解详情。


================================================
FILE: RELEASE.md
================================================
# 发布指南

本文档为项目维护者提供了一套标准的版本发布流程,以确保版本迭代的一致性和清晰度。

## 版本规范

项目遵循**语义化版本 2.0.0 (Semantic Versioning)** 规范。版本格式为 `主版本号.次版本号.修订号` (MAJOR.MINOR.PATCH),版本号递增规则如下:

-   **主版本号 (MAJOR)**: 当你做了不兼容的 API 修改。
-   **次版本号 (MINOR)**: 当你做了向下兼容的功能性新增。
-   **修订号 (PATCH)**: 当你做了向下兼容的问题修正。

## 发布流程

### 步骤 1: 准备工作

1.  确保本地的 `main` 分支是最新且稳定的。
    ```bash
    git checkout main
    git pull origin main
    ```

2.  确认所有计划内的功能和修复都已合并到 `main` 分支。

3.  验证代码质量和构建状态。
    ```bash
    # TypeScript 类型检查
    bun run type-check
    
    # 构建验证
    bun run build
    ```

### 步骤 2: 更新版本日志 (`CHANGELOG.md`)

1.  打开 `CHANGELOG.md` 文件。
2.  在文件顶部新增一个版本标题,格式为 `## vX.Y.Z - YYYY-MM-DD`。
3.  在标题下,根据本次版本的变更内容,添加相应的分类,如:
    -   `### ✨ 新功能`
    -   `### 🐛 Bug 修复`
    -   `### ♻️ 重构`
    -   `### 📚 文档`
    -   `### ⚙️ 其他`

4.  运行以下命令,查看自上一个版本以来的所有提交记录,以帮助你整理更新日志。
    ```bash
    # 将 v1.0.0 替换为上一个版本的标签
    git log v1.0.0...HEAD --oneline
    ```

### 步骤 3: 更新 `package.json` 中的版本号

1.  打开 `package.json` 文件。
2.  将 `version` 字段的值更新为新的版本号。

    例如,从 `1.0.0` 更新到 `1.0.1`:
    ```diff
    - "version": "1.0.0",
    + "version": "1.0.1",
    ```

### 步骤 4: 提交版本更新

1.  将 `CHANGELOG.md` 和 `package.json` 的修改提交到暂存区。
    ```bash
    git add CHANGELOG.md package.json
    ```

2.  使用标准化的提交信息进行提交。
    ```bash
    # 将 vX.Y.Z 替换为新版本号
    git commit -m "chore(release): prepare for vX.Y.Z"
    ```

3.  将提交推送到远程 `main` 分支。
    ```bash
    git push origin main
    ```

### 步骤 5: 创建并推送 Git 标签

1.  为此次提交创建一个附注标签(annotated tag)。
    ```bash
    # 将 vX.Y.Z 替换为新版本号
    git tag -a vX.Y.Z -m "Release vX.Y.Z"
    ```

2.  将新创建的标签推送到远程仓库。
    ```bash
    # 将 vX.Y.Z 替换为新版本号
    git push origin vX.Y.Z
    ```

3.  推送 tag 后,GitHub Actions 会自动触发以下构建任务(**三平台并行执行**):
    -   `release-linux.yml` - 构建 Linux amd64/arm64 版本
    -   `release-macos.yml` - 构建 macOS amd64/arm64 版本
    -   `release-windows.yml` - 构建 Windows amd64/arm64 版本
    -   `docker-build.yml` - 构建并推送 Docker 镜像

    > **注意**: 各平台使用独立的 concurrency group (`${{ github.workflow }}-${{ github.ref }}`),确保并行构建互不阻塞。

### 步骤 6: 在 GitHub 上创建 Release (可选但推荐)

1.  访问项目的 GitHub 页面,进入 "Releases" 部分。
2.  点击 "Draft a new release"。
3.  从 "Choose a tag" 下拉菜单中选择你刚刚推送的标签(如 `vX.Y.Z`)。
4.  将 `CHANGELOG.md` 中对应版本的更新内容复制到发布说明中。
5.  点击 "Publish release"。

至此,新版本的发布流程已全部完成。


================================================
FILE: VERSION
================================================
v2.5.13


================================================
FILE: backend-go/.air.toml
================================================
# Air 配置文件 - Go热重载工具
# 文档: https://github.com/air-verse/air

# 工作目录
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

# 构建配置
[build]
  # 编译命令 (可以添加自定义参数)
  cmd = "go build -o ./tmp/main ."
  # 运行的二进制文件
  bin = "tmp/main"
  # 自定义运行时参数 (比如: ["run", "test", "-v"])
  full_bin = ""
  # 监听的文件扩展名
  include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml", "toml", "env"]
  # 忽略的文件/文件夹
  exclude_dir = ["assets", "tmp", "vendor", "testdata", "frontend", "dist", ".git", ".github", ".vscode", ".idea"]
  # 监听指定的目录
  include_dir = []
  # 监听指定的文件(包括 .env)
  include_file = [".env"]
  # 忽略的文件
  exclude_file = []
  # 忽略未修改的文件
  exclude_unchanged = false
  # 监听符号链接的目录
  follow_symlink = false
  # 文件变更延迟触发编译 (毫秒)
  delay = 1000
  # 编译错误时是否停止旧的二进制文件
  stop_on_error = true
  # 运行二进制文件时的日志文件
  log = "build-errors.log"
  # air启动时是否发送中断信号
  send_interrupt = true
  # 中断信号延迟 (毫秒)
  kill_delay = 500
  # 在发送中断信号时添加额外的参数
  rerun = false
  # 运行后的延迟 (毫秒)
  rerun_delay = 500
  # 添加额外的参数
  args_bin = []

# 自定义shell命令
[build.pre_cmd]
  # 数组形式的命令会按顺序执行
  # enable = true
  # cmds = [
  #   "echo 'Building...'"
  # ]

[build.post_cmd]
  # enable = true
  # cmds = [
  #   "echo 'Build complete!'"
  # ]

# 日志配置
[log]
  # 是否开启主进程日志
  main_only = false
  # 时间格式
  time = true

# 颜色配置
[color]
  # 自定义颜色
  main = "magenta"
  watcher = "cyan"
  build = "yellow"
  runner = "green"
  # app日志颜色
  app = ""

# 其他配置
[misc]
  # 是否在退出时清理tmp目录
  clean_on_exit = true

# 屏幕清理
[screen]
  # 重新编译时清屏
  clear_on_rebuild = true
  # 保持滚动
  keep_scroll = true

================================================
FILE: backend-go/.env.example
================================================
# 环境变量示例配置
# 复制此文件为 .env 并修改配置

# ============ 服务器配置 ============
PORT=3000

# 运行环境: development | production
# 影响:
#   - production: Gin ReleaseMode(高性能)、关闭/admin/dev/info、严格CORS
#   - development: Gin DebugMode(详细日志)、开启/admin/dev/info、宽松CORS
# 生产环境务必设置为 production!
ENV=production

# ============ Web UI 配置 ============
# 是否启用 Web 管理界面
ENABLE_WEB_UI=true

# ============ 访问控制 ============
# 代理访问密钥(必须修改!)
PROXY_ACCESS_KEY=your-secure-access-key-here

# ============ 日志配置 ============
# 日志级别: error | warn | info | debug
LOG_LEVEL=info

# 是否启用请求/响应日志
# 注意:默认值为 true,注释掉此项等于启用日志
# 要禁用日志必须显式设置为 false,不能通过注释来禁用
ENABLE_REQUEST_LOGS=false
ENABLE_RESPONSE_LOGS=false

# 静默前端轮询端点日志(调试时避免刷屏)
QUIET_POLLING_LOGS=true

# 原始日志输出(不缩进、不截断、不重排序,直接输出完整请求/响应内容)
RAW_LOG_OUTPUT=false

# SSE 调试级别: off | summary | full
# full: 记录每个 SSE 事件的类型、长度、content_block 详情
# summary: 仅在流结束时记录事件统计摘要
# off: 关闭 SSE 调试日志(默认)
SSE_DEBUG_LEVEL=off

# 是否改写响应中的 model 字段为请求的 model(默认 false)
# 启用后,当上游返回的 model 与请求的 model 不一致时,会自动改写为请求的 model
# 注意:仅影响 Messages API 的流式响应,不影响 Responses API 和 Gemini API
REWRITE_RESPONSE_MODEL=false

# ============ 性能配置 ============
# 请求超时时间(毫秒)
REQUEST_TIMEOUT=300000

# 请求体最大大小(MB),默认 50
MAX_REQUEST_BODY_SIZE_MB=50

# 等待上游响应头超时时间(秒),默认 60,范围 30-120
# 如果遇到 "http2: timeout awaiting response headers" 错误,可以适当调高
RESPONSE_HEADER_TIMEOUT=60

# ============ CORS 配置 ============
ENABLE_CORS=false
CORS_ORIGIN=*

# ============ 熔断指标配置 ============
# 滑动窗口大小(最小 3,默认 10)
METRICS_WINDOW_SIZE=10
# 失败率阈值(0-1,默认 0.5 即 50%)
METRICS_FAILURE_THRESHOLD=0.5

# ============ 指标持久化配置 ============
# 是否启用 SQLite 持久化(默认 true)
# 启用后重启服务不会丢失历史指标数据
METRICS_PERSISTENCE_ENABLED=true
# 数据保留天数(3-30,默认 7)
METRICS_RETENTION_DAYS=7


================================================
FILE: backend-go/.gitignore
================================================
# 忽略编译产物
dist/
*.exe
claude-proxy-*
claude-proxy
stream_verify

# 忽略前端资源(会在构建时复制)
frontend/

# 忽略配置文件
.env
.config/

# Go 相关
vendor/

# 操作系统文件
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/
*.swp
*.swo
*~
tmp/

!docs/

================================================
FILE: backend-go/CLAUDE.md
================================================
# backend-go 模块文档

[← 根目录](../CLAUDE.md)

## 模块职责

Go 后端核心服务:HTTP API、多上游适配、协议转换、智能调度、会话管理、配置热重载。

## 启动命令

```bash
make dev          # 热重载开发
make test         # 运行测试
make test-cover   # 测试 + 覆盖率
make build        # 构建二进制
```

## API 端点

| 端点 | 方法 | 功能 |
|------|------|------|
| `/health` | GET | 健康检查(无需认证) |
| `/v1/messages` | POST | Claude Messages API |
| `/v1/messages/count_tokens` | POST | Token 计数 |
| `/v1/responses` | POST | Codex Responses API |
| `/v1/responses/compact` | POST | 精简版 Responses API |
| `/api/messages/channels` | CRUD | Messages 渠道管理 |
| `/api/responses/channels` | CRUD | Responses 渠道管理 |
| `/api/messages/ping/:id` | GET | 渠道连通性测试 |
| `/api/messages/channels/metrics` | GET | 渠道指标 |
| `/api/messages/channels/scheduler/stats` | GET | 调度器统计 |

## 指标历史数据聚合粒度

`/api/messages/channels/:id/keys/metrics/history` 端点根据查询时间范围自动选择聚合间隔:

| 时间范围 | 聚合间隔 | 数据点数 |
|----------|----------|----------|
| 1h       | 1 分钟   | ~60 点   |
| 6h       | 5 分钟   | ~72 点   |
| 24h      | 15 分钟  | ~96 点   |

可通过 `interval` 参数手动指定(最小 1 分钟)。

## Provider 接口

所有上游服务实现 `internal/providers/Provider` 接口:

```go
type Provider interface {
    ConvertToProviderRequest(c *gin.Context, upstream *config.UpstreamConfig, apiKey string) (*http.Request, []byte, error)
    ConvertToClaudeResponse(providerResp *types.ProviderResponse) (*types.ClaudeResponse, error)
    HandleStreamResponse(body io.ReadCloser) (<-chan string, <-chan error, error)
}
```

**实现**: `ClaudeProvider`, `OpenAIProvider`, `GeminiProvider`

## 核心模块

| 模块 | 职责 |
|------|------|
| `handlers/` | HTTP 处理器(proxy.go, responses.go) |
| `providers/` | 上游适配器 |
| `converters/` | 协议转换器(工厂模式) |
| `scheduler/` | 多渠道调度(优先级、熔断) |
| `session/` | 会话管理(Trace 亲和性) |
| `config/` | 配置管理(热重载) |

## 日志规范

所有日志输出使用 `[Component-Action]` 标签格式,禁止使用 emoji 符号(确保跨平台兼容性)。

**格式规范**:
```go
// 标准格式
log.Printf("[Component-Action] 消息内容: %v", value)

// 警告信息
log.Printf("[Component] 警告: 消息内容")
```

**标签命名示例**:

| 组件 | 标签 | 用途 |
|------|------|------|
| 调度器 | `[Scheduler-Channel]` | 渠道选择 |
| 调度器 | `[Scheduler-Promotion]` | 促销渠道 |
| 调度器 | `[Scheduler-Affinity]` | Trace 亲和性 |
| 调度器 | `[Scheduler-Fallback]` | 降级选择 |
| 认证 | `[Auth-Failed]` | 认证失败 |
| 认证 | `[Auth-Success]` | 认证成功 |
| 指标 | `[Metrics-Store]` | 指标存储 |
| 会话 | `[Session-Manager]` | 会话管理 |
| 配置 | `[Config-Watcher]` | 配置热重载 |
| 压缩 | `[Gzip]` | Gzip 解压缩 |
| Messages | `[Messages-Stream]` | Messages 流式处理 |
| Messages | `[Messages-Stream-Token]` | Messages Token 统计 |
| Messages | `[Messages-Models]` | Messages Models API 操作 |
| Responses | `[Responses-Stream]` | Responses 流式处理 |
| Responses | `[Responses-Stream-Token]` | Responses Token 统计 |
| Responses | `[Responses-Models]` | Responses Models API 操作 |
| Models | `[Models]` | 跨接口的模型列表合并操作 |

## 扩展指南

**添加新上游服务**:
1. 在 `internal/providers/` 创建新文件
2. 实现 `Provider` 接口
3. 在 `GetProvider()` 注册

**调度优先级规则**:
1. 促销期渠道优先
2. Priority 字段排序
3. Trace 亲和性绑定
4. 熔断状态过滤

## 工具使用注意事项

**Edit 工具与 Tab 缩进**:
- Go 文件使用 tab 缩进,`Edit` 工具匹配时可能因空白字符差异失败
- 失败时可用 `sed -i '' 's/old/new/g' file.go` 替代


================================================
FILE: backend-go/DEV_GUIDE.md
================================================
# Go 后端开发指南 - 热重载模式

## 🚀 快速开始

### 1. 安装 Air 热重载工具

Air 项目已迁移至新仓库 `github.com/air-verse/air`(原 `cosmtrek/air`)

```bash
# 推荐方式
make install-air

# 或手动安装
go install github.com/air-verse/air@latest
```

### 2. 启动热重载开发模式

```bash
# 启动开发模式
make dev

# 输出示例:
# 🚀 启动开发模式 (热重载开启)
# 📝 监听文件变化: *.go, *.yaml, *.toml, *.env
# 🔄 修改代码后将自动重启...
```

## 📁 热重载配置

### 监听的文件类型
- `*.go` - Go 源代码
- `*.yaml`, `*.yml` - YAML 配置文件
- `*.toml` - TOML 配置文件
- `*.env` - 环境变量文件
- `*.html`, `*.tpl`, `*.tmpl` - 模板文件

### 忽略的目录
- `tmp/` - Air 临时编译目录
- `vendor/` - Go 依赖目录
- `frontend/` - 前端源码(不影响后端)
- `dist/` - 构建输出目录
- `.git/`, `.github/` - Git 相关
- `.vscode/`, `.idea/` - IDE 配置

### 性能优化设置
- **编译延迟**: 1000ms(避免保存过程中频繁编译)
- **错误处理**: 编译错误时保持旧版本运行
- **信号处理**: 优雅关闭,500ms 延迟
- **清屏设置**: 每次重编译时自动清屏

## 🎯 开发流程

### 典型开发场景

1. **修改业务逻辑**
   ```bash
   # 编辑 handlers/proxy.go
   # 保存文件 → Air 检测到变化 → 1秒后自动重编译 → 重启服务
   ```

2. **更新配置文件**
   ```bash
   # 编辑 .env
   # 保存文件 → Air 重新加载配置 → 服务自动重启
   ```

3. **处理编译错误**
   ```bash
   # 代码有语法错误
   # Air 显示错误信息 → 保持旧版本运行
   # 修复错误并保存 → 自动重新编译 → 恢复正常
   ```

## 🛠️ Make 命令参考

| 命令 | 说明 | 使用场景 |
|------|------|---------|
| `make dev` | 启动热重载开发模式 | 日常开发主要命令 |
| `make install-air` | 安装 Air 工具 | 首次设置或更新 Air |
| `make run` | 直接运行(无热重载) | 快速测试 |
| `make build` | 构建生产版本 | 部署准备 |
| `make build-local` | 构建本地版本 | 本地测试 |
| `make test` | 运行测试 | 功能验证 |
| `make fmt` | 格式化代码 | 代码规范化 |
| `make clean` | 清理临时文件 | 清理环境 |

## 🔧 Air 高级配置

### 自定义 .air.toml

```toml
# 添加预编译命令
[build.pre_cmd]
  enable = true
  cmds = [
    "echo '开始编译...'",
    "go mod tidy"
  ]

# 添加后编译命令
[build.post_cmd]
  enable = true
  cmds = [
    "echo '编译完成!'"
  ]

# 自定义运行参数
[build]
  # 添加构建标签
  cmd = "go build -tags dev -o ./tmp/main ."

  # 传递运行时参数
  args_bin = ["--debug", "--verbose"]
```

### 环境变量

```bash
# 开发模式专用环境变量
ENV=development
LOG_LEVEL=debug
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true
```

## 🐛 问题排查

### Air 命令未找到
```bash
# 检查安装
which air

# 添加到 PATH
export PATH=$PATH:$(go env GOPATH)/bin

# 或添加到 shell 配置
echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.zshrc
source ~/.zshrc
```

### 热重载不触发
```bash
# 检查 Air 进程
ps aux | grep air

# 查看 Air 日志
tail -f build-errors.log

# 清理并重启
make clean && make dev
```

### 端口占用
```bash
# 查找占用端口的进程
lsof -i :3000

# 终止进程
kill -9 <PID>
```

### 文件权限问题
```bash
# 修复权限
chmod -R 755 .
chmod 644 .air.toml
```

## 📊 性能对比

| 操作 | 无热重载 | 有热重载 | 提升 |
|------|---------|---------|------|
| 修改代码后重启 | 手动 10-15秒 | 自动 1-2秒 | **10倍** |
| 处理编译错误 | 中断→修复→重启 | 保持运行→修复→自动恢复 | **无中断** |
| 配置更新 | 停止→修改→启动 | 修改→自动重启 | **3倍** |
| 开发效率 | 低 | 高 | **显著提升** |

## 💡 最佳实践

1. **保持 Air 运行**: 开发期间始终使用 `make dev`
2. **合理设置延迟**: 1秒延迟平衡了响应速度和性能
3. **利用彩色输出**: 不同颜色快速区分日志类型
4. **定期清理**: 使用 `make clean` 清理临时文件
5. **版本控制**: `.air.toml` 应该加入版本控制

## 🔗 相关资源

- [Air 官方文档](https://github.com/air-verse/air)
- [Air 配置示例](https://github.com/air-verse/air/blob/master/air_example.toml)
- [Gin 开发模式文档](https://gin-gonic.com/docs/quickstart/)

---

**提示**: 如果遇到任何问题,请先运行 `make clean && make install-air` 重置环境。

================================================
FILE: backend-go/Makefile
================================================
# Makefile for Claude Proxy Go Backend

# 变量定义
BINARY_NAME=claude-proxy-go
MAIN_PATH=.
BUILD_DIR=../dist
AIR_VERSION=v1.52.0
# 支持环境变量覆盖 VERSION(用于 Docker 构建时传入)
VERSION?=$(shell cat ../VERSION 2>/dev/null || echo "v0.0.0-dev")
BUILD_TIME=$(shell date '+%Y-%m-%d_%H:%M:%S_%Z')
GIT_COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
LDFLAGS=-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)

# 颜色输出
GREEN=\033[0;32m
YELLOW=\033[0;33m
NC=\033[0m # No Color

.PHONY: help dev build run clean install-air test copy-frontend

# 默认目标 - 显示帮助
help:
	@echo "$(GREEN)Claude Proxy Go Backend - 可用命令:$(NC)"
	@echo ""
	@echo "$(YELLOW)开发命令:$(NC)"
	@echo "  make dev          - 启动开发模式 (热重载)"
	@echo "  make install-air  - 安装 Air 热重载工具"
	@echo ""
	@echo "$(YELLOW)构建命令:$(NC)"
	@echo "  make build        - 构建生产版本"
	@echo "  make run          - 直接运行 (自动复制前端)"
	@echo "  make copy-frontend- 复制前端资源"
	@echo "  make clean        - 清理构建文件"
	@echo ""
	@echo "$(YELLOW)测试命令:$(NC)"
	@echo "  make test         - 运行测试"
	@echo "  make test-cover   - 运行测试并生成覆盖率"

# 安装 Air 工具
install-air:
	@echo "$(GREEN)正在安装 Air 热重载工具...$(NC)"
	@go install github.com/air-verse/air@latest
	@echo "$(GREEN)✅ Air 安装完成!$(NC)"
	@echo "使用方法: make dev"

# 开发模式 - 使用 Air 热重载
dev:
	@if ! command -v air &> /dev/null; then \
		echo "$(YELLOW)⚠️  Air 未安装,正在自动安装...$(NC)"; \
		$(MAKE) install-air; \
	fi
	@echo "$(GREEN)🚀 启动开发模式 (热重载开启)$(NC)"
	@echo "$(YELLOW)📝 监听文件变化: *.go, *.yaml, *.toml, *.env$(NC)"
	@echo "$(YELLOW)🔄 修改代码后将自动重启...$(NC)"
	@echo ""
	@air

# 开发模式 - 使用系统环境变量
dev-with-env:
	@echo "$(GREEN)🚀 启动开发模式 (使用系统环境变量)$(NC)"
	@ENV_MODE=development air

# 构建生产版本
build:
	@echo "$(GREEN)📦 构建生产版本...$(NC)"
	@CGO_ENABLED=0 go build -ldflags="$(LDFLAGS) -s -w" -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PATH)
	@echo "$(GREEN)✅ 构建完成: $(BUILD_DIR)/$(BINARY_NAME)$(NC)"

# 构建当前平台版本
build-local:
	@echo "$(GREEN)📦 构建本地版本...$(NC)"
	@go build -o $(BINARY_NAME) $(MAIN_PATH)
	@echo "$(GREEN)✅ 构建完成: ./$(BINARY_NAME)$(NC)"

# 直接运行 (带版本信息)
run: copy-frontend
	@echo "$(GREEN)▶️  直接运行...$(NC)"
	@go run -ldflags="$(LDFLAGS)" $(MAIN_PATH)

# 运行测试
test:
	@echo "$(GREEN)🧪 运行测试...$(NC)"
	@go test -v ./...

# 运行测试并生成覆盖率报告
test-cover:
	@echo "$(GREEN)🧪 运行测试并生成覆盖率...$(NC)"
	@go test -v -cover -coverprofile=coverage.out ./...
	@go tool cover -html=coverage.out -o coverage.html
	@echo "$(GREEN)✅ 覆盖率报告已生成: coverage.html$(NC)"

# 清理构建文件
clean:
	@echo "$(YELLOW)🧹 清理构建文件...$(NC)"
	@rm -rf tmp/
	@rm -rf frontend/dist/
	@rm -f $(BINARY_NAME)
	@rm -f $(BUILD_DIR)/$(BINARY_NAME)
	@rm -f coverage.out coverage.html
	@rm -f build-errors.log
	@echo "$(GREEN)✅ 清理完成$(NC)"

# 复制前端资源
copy-frontend:
	@echo "$(GREEN)📦 复制前端资源...$(NC)"
	@rm -rf frontend/dist
	@mkdir -p frontend/dist
	@if [ -d "../frontend/dist" ]; then \
		cp -r ../frontend/dist/* frontend/dist/; \
		echo "$(GREEN)✅ 前端资源复制完成$(NC)"; \
	else \
		echo "$(YELLOW)⚠️  前端构建产物不存在,请先构建前端: cd ../frontend && bun run build$(NC)"; \
	fi

# 代码格式化
fmt:
	@echo "$(GREEN)🎨 格式化代码...$(NC)"
	@go fmt ./...
	@echo "$(GREEN)✅ 格式化完成$(NC)"

# 代码检查
lint:
	@echo "$(GREEN)🔍 检查代码...$(NC)"
	@if ! command -v golangci-lint &> /dev/null; then \
		echo "$(YELLOW)安装 golangci-lint...$(NC)"; \
		go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest; \
	fi
	@golangci-lint run

# 依赖管理
deps:
	@echo "$(GREEN)📚 更新依赖...$(NC)"
	@go mod tidy
	@go mod download
	@echo "$(GREEN)✅ 依赖更新完成$(NC)"

# 查看依赖树
deps-tree:
	@echo "$(GREEN)🌳 依赖树:$(NC)"
	@go mod graph

# 安装开发工具
install-tools: install-air
	@echo "$(GREEN)🔧 安装开发工具...$(NC)"
	@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
	@echo "$(GREEN)✅ 所有工具安装完成$(NC)"

================================================
FILE: backend-go/README.md
================================================
# Claude / Codex / Gemini API Proxy - Go 版本

> 🚀 高性能的 Claude / Codex / Gemini API Proxy - Go 语言实现,支持多种上游AI服务提供商,内置前端管理界面

## 特性

- ✅ **完整的 TypeScript 后端功能移植**:所有原 TS 后端功能完整实现
- 🚀 **高性能**:Go 语言实现,性能优于 Node.js 版本
- 📦 **单文件部署**:前端资源嵌入二进制文件,无需额外配置
- 🔄 **协议转换**:自动转换 Claude 格式请求到不同上游服务商格式
- ⚖️ **故障转移**:支持多 API 密钥的智能分配和自动切换
- 🖥️ **Web 管理界面**:内置的前端管理界面(嵌入式)
- 🛡️ **高可用性**:健康检查、错误处理和优雅降级

## 支持的上游服务

- ✅ OpenAI (GPT-4, GPT-3.5 等)
- ✅ Gemini (Google AI)
- ✅ Claude (Anthropic)
- ✅ OpenAI Old (旧版兼容)

## 最新更新 (v2.0.1)

### 🐛 重要修复
- ✅ 修复前端资源加载问题(Vite base 路径配置)
- ✅ 修复静态文件 MIME 类型错误(favicon.ico 等)
- ✅ 修复 API 路由与前端不匹配问题
- ✅ 修复版本信息未注入问题

### ⚡ 性能优化
- ✅ 智能前端构建缓存(无变更时 0.07秒启动,提升 142 倍)
- ✅ 优化代码分割(vue-vendor 独立打包)

### 📝 改进
- ✅ ENV 环境变量标准化(替代 NODE_ENV,向后兼容)
- ✅ 添加 favicon 支持(SVG 格式)
- ✅ 完善文档和开发指南

---

## 快速开始

### 方式1:下载预编译二进制文件(推荐)

1. 从 [Releases](https://github.com/yourusername/claude-proxy/releases) 下载对应平台的二进制文件
2. 创建 `.env` 文件:

```bash
# 复制示例配置
cp .env.example .env

# 编辑配置
nano .env
```

3. 运行服务器:

```bash
# Linux / macOS
./claude-proxy-linux-amd64

# Windows
claude-proxy-windows-amd64.exe
```

### 方式2:从源码构建

#### 前置要求

- Go 1.22 或更高版本
- Node.js 18+ (用于构建前端)

#### 构建步骤

```bash
# 1. 克隆项目
git clone https://github.com/yourusername/claude-proxy.git
cd claude-proxy

# 2. 构建前端
cd frontend
npm install
npm run build
cd ..

# 3. 构建 Go 后端(包含前端资源)
cd backend-go
./build.sh

# 构建产物位于 dist/ 目录
```

## 配置说明

### 环境变量配置 (.env)

```env
# ============ 服务器配置 ============
PORT=3000

# 运行环境: development | production
# 影响:
#   - production: Gin ReleaseMode(高性能)、关闭/admin/dev/info、严格CORS
#   - development: Gin DebugMode(详细日志)、开启/admin/dev/info、宽松CORS
ENV=production

# ============ Web UI 配置 ============
ENABLE_WEB_UI=true

# ============ 访问控制 ============
# 代理访问密钥(必须修改!)
PROXY_ACCESS_KEY=your-secure-access-key

# ============ 日志配置 ============
# 日志级别: error | warn | info | debug
LOG_LEVEL=info

# 是否启用请求/响应日志
ENABLE_REQUEST_LOGS=true
ENABLE_RESPONSE_LOGS=true

# ============ 性能配置 ============
# 请求超时时间(毫秒)
REQUEST_TIMEOUT=30000

# ============ CORS 配置 ============
ENABLE_CORS=true
CORS_ORIGIN=*
```

### 环境模式详解

| 配置项 | development | production |
|--------|-------------|------------|
| **Gin 模式** | DebugMode (详细日志) | ReleaseMode (高性能) |
| **开发端点** | `/admin/dev/info` 开启 | `/admin/dev/info` 关闭 |
| **CORS 策略** | 自动允许所有 localhost 源 | 严格使用 CORS_ORIGIN 配置 |
| **日志输出** | 路由注册、请求详情 | 仅错误和警告 |
| **安全性** | 低(暴露调试信息) | 高(最小信息暴露) |

**建议**:
- 开发测试时使用 `ENV=development`
- 生产部署时务必使用 `ENV=production`

### 渠道配置

服务启动后,通过 Web 管理界面 (http://localhost:3000) 配置上游渠道和 API 密钥。

或者直接编辑配置文件 `.config/config.json`:

```json
{
  "upstream": [
    {
      "name": "OpenAI",
      "baseUrl": "https://api.openai.com/v1",
      "apiKeys": ["sk-your-api-key"],
      "serviceType": "openai",
      "status": "active"
    }
  ],
  "loadBalance": "failover"
}
```

### 渠道状态自动变化

以下场景会触发渠道状态的自动变化:

| 场景 | 触发条件 | 自动行为 |
|------|----------|----------|
| **单 Key 更换自动激活** | 渠道只有 1 个 Key,且更新为不同的 Key | 1. 状态从 `suspended` 变为 `active`<br>2. 重置熔断状态(清除错误计数) |
| **熔断自动恢复** | 渠道熔断后超过恢复时间(默认 15 分钟) | 自动清除熔断标记,渠道恢复可用 |
| **无 Key 自动暂停** | 渠道配置为 `active` 但没有 API Key | 状态自动设为 `suspended` |

**设计说明:**
- 单 Key 更换时自动激活,因为用户明显想要使用新 Key
- 多 Key 场景不会自动激活,避免误操作(用户可能只是添加/删除部分 Key)
- `disabled` 状态不受影响,用户主动禁用的渠道不会被自动激活

### 渠道促销期(Promotion)

促销期机制用于临时提升某个渠道的优先级,让新渠道能够快速获得流量进行测试。

**促销期特性:**
- 处于促销期的渠道会被**优先选择**,忽略 trace 亲和性
- 同一时间**只能有一个渠道**处于促销期(设置新渠道会自动清除旧渠道的促销期)
- 促销期有**时间限制**,到期后自动失效
- 促销渠道如果**不健康**(熔断/无可用密钥),会自动跳过

**自动触发场景:**

| 场景 | 触发条件 | 自动行为 |
|------|----------|----------|
| **快速添加渠道** | 通过 Web UI 快速添加新渠道 | 1. 新渠道排序到第一位<br>2. 设置 5 分钟促销期 |

**API 使用:**
```bash
# 设置渠道促销期(600秒 = 10分钟)
curl -X POST http://localhost:3000/api/channels/0/promotion \
  -H "x-api-key: your-proxy-access-key" \
  -H "Content-Type: application/json" \
  -d '{"duration": 600}'

# 清除渠道促销期
curl -X POST http://localhost:3000/api/channels/0/promotion \
  -H "x-api-key: your-proxy-access-key" \
  -H "Content-Type: application/json" \
  -d '{"duration": 0}'
```

**适用场景:**
- 新增渠道后,临时提升优先级进行测试
- 更换 Key 后,验证新 Key 是否正常工作
- 临时将流量切换到特定渠道

## 使用方法

### 访问 Web 管理界面

打开浏览器访问: http://localhost:3000

首次访问需要输入 `PROXY_ACCESS_KEY`

### API 调用

```bash
curl -X POST http://localhost:3000/v1/messages \
  -H "x-api-key: your-proxy-access-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-3-5-sonnet-20241022",
    "max_tokens": 1024,
    "messages": [
      {"role": "user", "content": "Hello, Claude!"}
    ]
  }'
```

## 架构对比

| 特性 | TypeScript 版本 | Go 版本 |
|------|----------------|---------|
| 运行时 | Node.js/Bun | Go (编译型) |
| 性能 | 中等 | 高 |
| 内存占用 | 较高 | 较低 |
| 部署 | 需要 Node.js 环境 | 单文件可执行 |
| 启动速度 | 较慢 | 快速 |
| 并发处理 | 事件循环 | Goroutine(原生并发)|

## 目录结构

```
backend-go/
├── main.go                 # 主程序入口
├── go.mod                  # Go 模块定义
├── build.sh                # 构建脚本
├── internal/
│   ├── config/             # 配置管理
│   │   ├── env.go          # 环境变量配置
│   │   └── config.go       # 配置文件管理
│   ├── providers/          # 上游服务适配器
│   │   ├── provider.go     # Provider 接口
│   │   ├── openai.go       # OpenAI 适配器
│   │   ├── gemini.go       # Gemini 适配器
│   │   └── claude.go       # Claude 适配器
│   ├── middleware/         # HTTP 中间件
│   │   ├── cors.go         # CORS 中间件
│   │   └── auth.go         # 认证中间件
│   ├── handlers/           # HTTP 处理器
│   │   ├── health.go       # 健康检查
│   │   ├── config.go       # 配置管理 API
│   │   ├── proxy.go        # 代理处理逻辑
│   │   └── frontend.go     # 前端资源服务
│   └── types/              # 类型定义
│       └── types.go        # 请求/响应类型
└── frontend/dist/          # 嵌入的前端资源(构建时生成)
```

## 性能优化

Go 版本相比 TypeScript 版本的性能优势:

1. **更低的内存占用**:Go 的垃圾回收机制更高效
2. **更快的启动速度**:编译型语言,无需运行时解析
3. **更好的并发性能**:原生 Goroutine 支持
4. **更小的部署包**:单文件可执行,无需 node_modules

## 常见问题

### 1. 如何更新前端资源?

重新构建前端后,运行 `./build.sh` 重新打包。

### 2. 如何禁用 Web UI?

在 `.env` 文件中设置 `ENABLE_WEB_UI=false`

### 3. 支持热重载配置吗?

支持!配置文件(`.config/config.json`)变更会自动重载,无需重启服务器。

### 4. 如何添加自定义上游服务?

实现 `providers.Provider` 接口并在 `providers.GetProvider` 中注册即可。

## 开发

### 🔥 热重载开发模式(新增)

Go 版本现在支持代码热重载,修改代码后自动重新编译和重启!

#### 安装热重载工具

```bash
# 方式一:使用 make(推荐)
make install-air

# 方式二:使用 npm/bun
npm run dev:go:install

# 方式三:直接安装
go install github.com/air-verse/air@latest
```

#### 启动热重载开发模式

```bash
# 方式一:使用 make(推荐)
make dev              # 自动检测并安装 Air,启动热重载

# 方式二:使用 npm/bun
npm run dev:go        # 或 bun run dev:go

# 方式三:直接使用 air
cd backend-go && air
```

**热重载特性:**
- ✅ **自动重启** - 修改 `.go` 文件后自动重新编译和重启
- ✅ **配置监听** - 修改 `.yaml`, `.toml`, `.env` 文件也会触发重启
- ✅ **错误恢复** - 编译错误时保持运行,修复后自动恢复
- ✅ **彩色日志** - 不同类型日志使用不同颜色,便于调试
- ✅ **性能优化** - 1秒延迟编译,避免频繁重启

### 推荐开发流程(智能缓存)

```bash
# 使用 Makefile - 自动管理前端构建缓存
make dev              # 🔥 热重载开发模式(推荐)
make run              # 首次构建前端,后续仅在源文件变更时重新编译
make build            # 构建生产版本
make clean            # 清除所有构建缓存和临时文件

# 手动控制
make build-local      # 构建本地版本
make test             # 运行测试
make test-cover       # 生成测试覆盖率报告
make fmt              # 格式化代码
make lint             # 代码检查
make deps             # 更新依赖
```

**智能缓存机制:**
- ✅ `make run` 自动检测 `frontend/src` 目录文件变更
- ✅ 未变更时跳过编译,**秒级启动**服务器
- ✅ 首次运行或源文件修改后自动重新编译
- ✅ 使用标记文件 `.build-marker` 追踪构建状态

### Air 配置说明

`.air.toml` 文件定义了热重载行为:

```toml
# 监听的文件类型
include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml", "toml", "env"]

# 忽略的目录
exclude_dir = ["assets", "tmp", "vendor", "frontend", "dist"]

# 编译延迟(毫秒)
delay = 1000

# 编译错误时是否停止
stop_on_error = true
```

### 传统开发方式

```bash
# 直接运行(不推荐 - 无版本信息)
go run main.go

# 运行测试
go test ./...

# 格式化代码
go fmt ./...

# 静态检查
go vet ./...
```

### 开发技巧

1. **使用热重载**:`make dev` 启动后,专注于代码编写,无需手动重启
2. **查看日志**:热重载模式下日志有颜色区分,更易阅读
3. **错误处理**:编译错误会显示在控制台,修复后自动重新编译
4. **配置更新**:修改 `.env` 或配置文件也会触发重启

## 版本管理

### 升级版本

只需修改根目录的 `VERSION` 文件:

```bash
# 编辑 VERSION 文件
echo "v1.1.0" > ../VERSION

# 重新构建即可
make build
```

所有构建产物会自动包含新版本号,无需修改代码!

### 查看版本信息

```bash
# 查看项目版本信息
make info

# 启动服务器后查看版本
curl http://localhost:3000/health | jq '.version'

# 输出示例:
# {
#   "version": "v1.0.0",
#   "buildTime": "2025-01-15_10:30:45_UTC",
#   "gitCommit": "abc1234"
# }
```

## 许可证

MIT License

## 贡献

欢迎提交 Issue 和 Pull Request!

---

**注意**: 这是 Claude Proxy 的 Go 语言重写版本,完整实现了原 TypeScript 版本的所有功能,并提供了更好的性能和部署体验。


================================================
FILE: backend-go/build.sh
================================================
#!/bin/bash

# Claude Proxy Go 版本构建脚本

set -e

# 版本信息 - 从根目录 VERSION 文件读取
VERSION=$(cat ../VERSION 2>/dev/null || echo "v0.0.0-dev")
BUILD_TIME=$(date '+%Y-%m-%d_%H:%M:%S_%Z')
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")

# 构建标志
LDFLAGS="-X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.GitCommit=${GIT_COMMIT}"

echo "🚀 开始构建 Claude Proxy Go 版本..."
echo "📌 版本: ${VERSION}"
echo "🕐 构建时间: ${BUILD_TIME}"
echo "🔖 Git提交: ${GIT_COMMIT}"
echo ""

# 检查前端构建产物是否存在
if [ ! -d "../frontend/dist" ]; then
    echo "❌ 前端构建产物不存在,请先构建前端:"
    echo "   cd ../frontend && npm run build"
    exit 1
fi

# 创建 frontend/dist 目录并复制前端资源
echo "📦 复制前端资源..."
rm -rf frontend/dist
mkdir -p frontend/dist
cp -r ../frontend/dist/* frontend/dist/

# 下载依赖
echo "📥 下载 Go 依赖..."
go mod download
go mod tidy

# 创建输出目录
mkdir -p dist

# 构建二进制文件
echo "🔨 构建二进制文件..."

# Linux
echo "  - 构建 Linux (amd64)..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o dist/claude-proxy-linux-amd64 .

# Linux ARM64
echo "  - 构建 Linux (arm64)..."
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o dist/claude-proxy-linux-arm64 .

# macOS
echo "  - 构建 macOS (amd64)..."
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o dist/claude-proxy-darwin-amd64 .

# macOS ARM64 (M1/M2)
echo "  - 构建 macOS (arm64)..."
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o dist/claude-proxy-darwin-arm64 .

# Windows
echo "  - 构建 Windows (amd64)..."
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o dist/claude-proxy-windows-amd64.exe .

echo ""
echo "✅ 构建完成!"
echo ""
echo "📁 构建产物位于 dist/ 目录:"
ls -lh dist/

echo ""
echo "💡 使用方法:"
echo "  1. 复制对应平台的二进制文件到目标机器"
echo "  2. 创建 .env 文件配置环境变量"
echo "  3. 运行: ./claude-proxy-linux-amd64"
echo ""
echo "📌 版本信息已注入到二进制文件中"



================================================
FILE: backend-go/docs/MALFORMED_TOOLCALL_MEMO.md
================================================
# 畸形 tool_call 问题备忘录

> 创建时间: 2025-12-19
> 状态: 待观察,暂不修复

## 问题描述

上游 Claude API 在流式返回时,偶尔会在同一个 `content_block` 中错误地发送多个工具调用的参数。

### 表现形式

1. **参数拼接**:两个工具的 JSON 参数被拼接成无效格式
   ```json
   {"command": "git diff --stat", "description": "..."}{"command": "git diff xxx", "description": "..."}
   ```

2. **元数据缺失**:第二个工具缺少必要的 `name` 和 `id`

3. **下游解析失败**:客户端(如 Claude Code)收到畸形数据后可能无法正确解析

### 日志示例

```
2025/12/19 11:00:51.203101 🛰️  上游流式响应合成内容:
...
Tool Call: Bash({"command": "git diff --stat", "description": "获取变更统计摘要"}{"command": "git diff backend-go/internal/providers/claude.go", "description": "获取 claude.go 的详细变更内容"}) [ID: toolu_01S6L3ngcGA9XKQrT1o2PLQa]
```

## 曾尝试的修复方案

### 方案 1: 实时流处理修复(已放弃)

在 SSE 流传输过程中实时检测并修复畸形数据。

**实现内容**:
- `toolCallFixer` 结构体跟踪状态
- `findJSONObjectBoundary()` 使用状态机检测 JSON 边界
- `inferToolName()` 根据参数推断工具名称
- `shouldFilterStop()` 过滤重复的 stop 事件
- `cleanupOnStop()` 清理内存

**放弃原因**:
- 逻辑复杂,经过多轮 Codex Review 仍有边缘问题
- 合成 block 的 index 可能与后续上游 index 冲突
- 需要处理重复 `content_block_stop` 事件
- 内存管理复杂

### 方案 2: 流结束后修复(未实现)

在流式响应完全结束后,检测并修复拼接的 tool_call。

**优势**:
- 逻辑简单:只在流结束时处理一次
- 无状态冲突:不需要实时跟踪 block index

**未实现原因**:
- 问题发生频率较低
- 等待上游修复

## 工具名称推断规则

如果将来需要实现修复,可根据参数 key 组合推断工具类型:

| 参数组合 | 工具名称 |
|---------|---------|
| `file_path` + `content` | Write |
| `file_path` + `old_string` + `new_string` | Edit |
| `file_path` (仅) | Read |
| `command` | Bash |
| `pattern` + `output_mode`/`glob`/`type` | Grep |
| `pattern` (仅) | Glob |
| `url` | WebFetch |
| `query` | WebSearch |
| `todos` | TodoWrite |
| `prompt` + `subagent_type` | Task |

## 相关文件

- `internal/providers/claude.go` - Claude Provider 流式处理
- `internal/utils/stream_synthesizer.go` - 日志合成器

## 后续行动

- [ ] 持续观察问题发生频率
- [ ] 如频繁触发,考虑实现"流结束后修复"方案
- [ ] 关注上游 Claude API 是否修复此问题


================================================
FILE: backend-go/go.mod
================================================
module github.com/BenedictKing/claude-proxy

go 1.22

require (
	github.com/fsnotify/fsnotify v1.7.0
	github.com/gin-gonic/gin v1.10.0
	github.com/google/uuid v1.6.0
	github.com/joho/godotenv v1.5.1
	github.com/stretchr/testify v1.9.0
	github.com/tidwall/gjson v1.18.0
	github.com/tidwall/sjson v1.2.5
	gopkg.in/natefinch/lumberjack.v2 v2.2.1
	modernc.org/sqlite v1.34.4
)

require (
	github.com/bytedance/sonic v1.11.6 // indirect
	github.com/bytedance/sonic/loader v0.1.1 // indirect
	github.com/cloudwego/base64x v0.1.4 // indirect
	github.com/cloudwego/iasm v0.2.0 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.20.0 // indirect
	github.com/goccy/go-json v0.10.2 // indirect
	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.7 // indirect
	github.com/leodido/go-urn v1.4.0 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/ncruces/go-strftime v0.1.9 // indirect
	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
	github.com/tidwall/match v1.1.1 // indirect
	github.com/tidwall/pretty v1.2.0 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.2.12 // indirect
	golang.org/x/arch v0.8.0 // indirect
	golang.org/x/crypto v0.23.0 // indirect
	golang.org/x/net v0.25.0 // indirect
	golang.org/x/sys v0.22.0 // indirect
	golang.org/x/text v0.15.0 // indirect
	google.golang.org/protobuf v1.34.1 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
	modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
	modernc.org/libc v1.55.3 // indirect
	modernc.org/mathutil v1.6.0 // indirect
	modernc.org/memory v1.8.0 // indirect
	modernc.org/strutil v1.2.0 // indirect
	modernc.org/token v1.1.0 // indirect
)


================================================
FILE: backend-go/go.sum
================================================
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=


================================================
FILE: backend-go/internal/config/config.go
================================================
package config

import (
	"fmt"
	"log"
	"sync"
	"time"

	"github.com/BenedictKing/claude-proxy/internal/utils"

	"github.com/fsnotify/fsnotify"
)

// ============== 核心类型定义 ==============

// UpstreamConfig 上游配置
type UpstreamConfig struct {
	BaseURL            string            `json:"baseUrl"`
	BaseURLs           []string          `json:"baseUrls,omitempty"` // 多 BaseURL 支持(failover 模式)
	APIKeys            []string          `json:"apiKeys"`
	HistoricalAPIKeys  []string          `json:"historicalApiKeys,omitempty"` // 历史 API Key(用于统计聚合,换 Key 后保留旧 Key 的统计数据)
	ServiceType        string            `json:"serviceType"`                 // gemini, openai, claude
	Name               string            `json:"name,omitempty"`
	Description        string            `json:"description,omitempty"`
	Website            string            `json:"website,omitempty"`
	InsecureSkipVerify bool              `json:"insecureSkipVerify,omitempty"`
	ModelMapping       map[string]string `json:"modelMapping,omitempty"`
	// 多渠道调度相关字段
	Priority       int        `json:"priority"`                 // 渠道优先级(数字越小优先级越高,默认按索引)
	Status         string     `json:"status"`                   // 渠道状态:active(正常), suspended(暂停), disabled(备用池)
	PromotionUntil *time.Time `json:"promotionUntil,omitempty"` // 促销期截止时间,在此期间内优先使用此渠道(忽略trace亲和)
	LowQuality     bool       `json:"lowQuality,omitempty"`     // 低质量渠道标记:启用后强制本地估算 token,偏差>5%时使用本地值
	// Gemini 特定配置
	InjectDummyThoughtSignature bool `json:"injectDummyThoughtSignature,omitempty"` // 给空 thought_signature 注入 dummy 值(兼容 x666.me 等要求必须有该字段的 API)
	StripThoughtSignature       bool `json:"stripThoughtSignature,omitempty"`       // 移除 thought_signature 字段(兼容旧版 Gemini API)
}

// UpstreamUpdate 用于部分更新 UpstreamConfig
type UpstreamUpdate struct {
	Name               *string           `json:"name"`
	ServiceType        *string           `json:"serviceType"`
	BaseURL            *string           `json:"baseUrl"`
	BaseURLs           []string          `json:"baseUrls"`
	APIKeys            []string          `json:"apiKeys"`
	Description        *string           `json:"description"`
	Website            *string           `json:"website"`
	InsecureSkipVerify *bool             `json:"insecureSkipVerify"`
	ModelMapping       map[string]string `json:"modelMapping"`
	// 多渠道调度相关字段
	Priority       *int       `json:"priority"`
	Status         *string    `json:"status"`
	PromotionUntil *time.Time `json:"promotionUntil"`
	LowQuality     *bool      `json:"lowQuality"`
	// Gemini 特定配置
	InjectDummyThoughtSignature *bool `json:"injectDummyThoughtSignature"`
	StripThoughtSignature       *bool `json:"stripThoughtSignature"`
}

// Config 配置结构
type Config struct {
	Upstream        []UpstreamConfig `json:"upstream"`
	CurrentUpstream int              `json:"currentUpstream,omitempty"` // 已废弃:旧格式兼容用
	LoadBalance     string           `json:"loadBalance"`               // round-robin, random, failover

	// Responses 接口专用配置(独立于 /v1/messages)
	ResponsesUpstream        []UpstreamConfig `json:"responsesUpstream"`
	CurrentResponsesUpstream int              `json:"currentResponsesUpstream,omitempty"` // 已废弃:旧格式兼容用
	ResponsesLoadBalance     string           `json:"responsesLoadBalance"`

	// Gemini 接口专用配置(独立于 /v1/messages 和 /v1/responses)
	GeminiUpstream    []UpstreamConfig `json:"geminiUpstream"`
	GeminiLoadBalance string           `json:"geminiLoadBalance"`

	// Fuzzy 模式:启用时模糊处理错误,所有非 2xx 错误都尝试 failover
	FuzzyModeEnabled bool `json:"fuzzyModeEnabled"`
}

// FailedKey 失败密钥记录
type FailedKey struct {
	Timestamp    time.Time
	FailureCount int
}

// ConfigManager 配置管理器
type ConfigManager struct {
	mu              sync.RWMutex
	config          Config
	configFile      string
	watcher         *fsnotify.Watcher
	failedKeysCache map[string]*FailedKey
	keyRecoveryTime time.Duration
	maxFailureCount int
	stopChan        chan struct{} // 用于通知 goroutine 停止
	closeOnce       sync.Once     // 确保 Close 只执行一次
}

// ============== 核心共享方法 ==============

// GetConfig 获取配置(返回深拷贝,确保并发安全)
func (cm *ConfigManager) GetConfig() Config {
	cm.mu.RLock()
	defer cm.mu.RUnlock()

	// 深拷贝整个 Config 结构体
	cloned := cm.config

	// 深拷贝 Upstream slice
	if cm.config.Upstream != nil {
		cloned.Upstream = make([]UpstreamConfig, len(cm.config.Upstream))
		for i := range cm.config.Upstream {
			cloned.Upstream[i] = *cm.config.Upstream[i].Clone()
		}
	}

	// 深拷贝 ResponsesUpstream slice
	if cm.config.ResponsesUpstream != nil {
		cloned.ResponsesUpstream = make([]UpstreamConfig, len(cm.config.ResponsesUpstream))
		for i := range cm.config.ResponsesUpstream {
			cloned.ResponsesUpstream[i] = *cm.config.ResponsesUpstream[i].Clone()
		}
	}

	// 深拷贝 GeminiUpstream slice
	if cm.config.GeminiUpstream != nil {
		cloned.GeminiUpstream = make([]UpstreamConfig, len(cm.config.GeminiUpstream))
		for i := range cm.config.GeminiUpstream {
			cloned.GeminiUpstream[i] = *cm.config.GeminiUpstream[i].Clone()
		}
	}

	return cloned
}

// GetNextAPIKey 获取下一个 API 密钥(纯 failover 模式)
// apiType: 接口类型(Messages/Responses/Gemini),用于日志标签前缀
func (cm *ConfigManager) GetNextAPIKey(upstream *UpstreamConfig, failedKeys map[string]bool, apiType string) (string, error) {
	if len(upstream.APIKeys) == 0 {
		return "", fmt.Errorf("上游 %s 没有可用的API密钥", upstream.Name)
	}

	// 单 Key 直接返回
	if len(upstream.APIKeys) == 1 {
		return upstream.APIKeys[0], nil
	}

	// 筛选可用密钥:排除临时失败密钥和内存中的失败密钥
	availableKeys := []string{}
	for _, key := range upstream.APIKeys {
		if !failedKeys[key] && !cm.isKeyFailed(key) {
			availableKeys = append(availableKeys, key)
		}
	}

	if len(availableKeys) == 0 {
		// 如果所有密钥都失效,尝试选择失败时间最早的密钥(恢复尝试)
		var oldestFailedKey string
		oldestTime := time.Now()

		cm.mu.RLock()
		for _, key := range upstream.APIKeys {
			if !failedKeys[key] { // 排除本次请求已经尝试过的密钥
				if failure, exists := cm.failedKeysCache[key]; exists {
					if failure.Timestamp.Before(oldestTime) {
						oldestTime = failure.Timestamp
						oldestFailedKey = key
					}
				}
			}
		}
		cm.mu.RUnlock()

		if oldestFailedKey != "" {
			log.Printf("[%s-Key] 警告: 所有密钥都失效,尝试最早失败的密钥: %s", apiType, utils.MaskAPIKey(oldestFailedKey))
			return oldestFailedKey, nil
		}

		return "", fmt.Errorf("上游 %s 的所有API密钥都暂时不可用", upstream.Name)
	}

	// 纯 failover:按优先级顺序选择第一个可用密钥
	selectedKey := availableKeys[0]
	// 获取该密钥在原始列表中的索引
	keyIndex := 0
	for i, key := range upstream.APIKeys {
		if key == selectedKey {
			keyIndex = i + 1
			break
		}
	}
	log.Printf("[%s-Key] 故障转移选择密钥 %s (%d/%d)", apiType, utils.MaskAPIKey(selectedKey), keyIndex, len(upstream.APIKeys))
	return selectedKey, nil
}

// MarkKeyAsFailed 标记密钥失败
// apiType: 接口类型(Messages/Responses/Gemini),用于日志标签前缀
func (cm *ConfigManager) MarkKeyAsFailed(apiKey string, apiType string) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if failure, exists := cm.failedKeysCache[apiKey]; exists {
		failure.FailureCount++
		failure.Timestamp = time.Now()
	} else {
		cm.failedKeysCache[apiKey] = &FailedKey{
			Timestamp:    time.Now(),
			FailureCount: 1,
		}
	}

	failure := cm.failedKeysCache[apiKey]
	recoveryTime := cm.keyRecoveryTime
	if failure.FailureCount > cm.maxFailureCount {
		recoveryTime = cm.keyRecoveryTime * 2
	}

	log.Printf("[%s-Key] 标记API密钥失败: %s (失败次数: %d, 恢复时间: %v)",
		apiType, utils.MaskAPIKey(apiKey), failure.FailureCount, recoveryTime)
}

// isKeyFailed 检查密钥是否失败
func (cm *ConfigManager) isKeyFailed(apiKey string) bool {
	cm.mu.RLock()
	defer cm.mu.RUnlock()

	failure, exists := cm.failedKeysCache[apiKey]
	if !exists {
		return false
	}

	recoveryTime := cm.keyRecoveryTime
	if failure.FailureCount > cm.maxFailureCount {
		recoveryTime = cm.keyRecoveryTime * 2
	}

	return time.Since(failure.Timestamp) < recoveryTime
}

// IsKeyFailed 检查 Key 是否在冷却期(公开方法)
func (cm *ConfigManager) IsKeyFailed(apiKey string) bool {
	return cm.isKeyFailed(apiKey)
}

// clearFailedKeysForUpstream 清理指定渠道的所有失败 key 记录
// 当渠道被删除时调用,避免内存泄漏和冷却状态残留
// apiType: 接口类型(Messages/Responses/Gemini),用于日志标签前缀
func (cm *ConfigManager) clearFailedKeysForUpstream(upstream *UpstreamConfig, apiType string) {
	for _, key := range upstream.APIKeys {
		if _, exists := cm.failedKeysCache[key]; exists {
			delete(cm.failedKeysCache, key)
			log.Printf("[%s-Key] 已清理被删除渠道 %s 的失败密钥记录: %s", apiType, upstream.Name, utils.MaskAPIKey(key))
		}
	}
}

// cleanupExpiredFailures 清理过期的失败记录
func (cm *ConfigManager) cleanupExpiredFailures() {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()

	for {
		select {
		case <-cm.stopChan:
			return
		case <-ticker.C:
			cm.mu.Lock()
			now := time.Now()
			for key, failure := range cm.failedKeysCache {
				recoveryTime := cm.keyRecoveryTime
				if failure.FailureCount > cm.maxFailureCount {
					recoveryTime = cm.keyRecoveryTime * 2
				}

				if now.Sub(failure.Timestamp) > recoveryTime {
					delete(cm.failedKeysCache, key)
					log.Printf("[Config-Key] API密钥 %s 已从失败列表中恢复", utils.MaskAPIKey(key))
				}
			}
			cm.mu.Unlock()
		}
	}
}

// ============== Fuzzy 模式相关方法 ==============

// GetFuzzyModeEnabled 获取 Fuzzy 模式状态
func (cm *ConfigManager) GetFuzzyModeEnabled() bool {
	cm.mu.RLock()
	defer cm.mu.RUnlock()
	return cm.config.FuzzyModeEnabled
}

// SetFuzzyModeEnabled 设置 Fuzzy 模式状态
func (cm *ConfigManager) SetFuzzyModeEnabled(enabled bool) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	cm.config.FuzzyModeEnabled = enabled

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	status := "关闭"
	if enabled {
		status = "启用"
	}
	log.Printf("[Config-FuzzyMode] Fuzzy 模式已%s", status)
	return nil
}


================================================
FILE: backend-go/internal/config/config_baseurl_test.go
================================================
package config

import (
	"os"
	"path/filepath"
	"testing"
)

// TestUpdateUpstream_BaseURLConsistency 测试更新 baseUrl 时的一致性
// 覆盖场景:
// 1. 只更新 baseUrl 时,baseUrls 应被清空
// 2. 只更新 baseUrls 时,baseUrl 应保持不变
// 3. 同时更新 baseUrl 和 baseUrls 时,两者应独立更新
// 4. 都不更新时,两者应保持原值
func TestUpdateUpstream_BaseURLConsistency(t *testing.T) {
	// 创建临时配置文件
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "config.json")
	initialConfig := `{
		"upstream": [{
			"name": "test-channel",
			"baseUrl": "https://old.example.com",
			"baseUrls": ["https://old-1.example.com", "https://old-2.example.com"],
			"apiKeys": ["test-key"],
			"serviceType": "claude"
		}],
		"loadBalance": "failover"
	}`
	if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil {
		t.Fatalf("写入初始配置失败: %v", err)
	}

	// 初始化配置管理器
	cm, err := NewConfigManager(configPath)
	if err != nil {
		t.Fatalf("初始化配置管理器失败: %v", err)
	}
	defer cm.Close()

	tests := []struct {
		name            string
		updates         UpstreamUpdate
		wantBaseURL     string
		wantBaseURLs    []string
		wantBaseURLsNil bool // 期望 baseUrls 为 nil(而非空切片)
	}{
		{
			name: "只更新 baseUrl 时 baseUrls 应被清空",
			updates: UpstreamUpdate{
				BaseURL: strPtr("https://new.example.com"),
			},
			wantBaseURL:     "https://new.example.com",
			wantBaseURLsNil: true,
		},
		{
			name: "只更新 baseUrls 时 baseUrl 应保持不变",
			updates: UpstreamUpdate{
				BaseURLs: []string{"https://urls-1.example.com", "https://urls-2.example.com"},
			},
			wantBaseURL:  "https://new.example.com", // 保持上个测试的值
			wantBaseURLs: []string{"https://urls-1.example.com", "https://urls-2.example.com"},
		},
		{
			name: "同时更新 baseUrl 和 baseUrls 时两者独立更新",
			updates: UpstreamUpdate{
				BaseURL:  strPtr("https://both-base.example.com"),
				BaseURLs: []string{"https://both-urls-1.example.com", "https://both-urls-2.example.com"},
			},
			wantBaseURL:  "https://both-base.example.com",
			wantBaseURLs: []string{"https://both-urls-1.example.com", "https://both-urls-2.example.com"},
		},
		{
			name:         "都不更新时保持原值",
			updates:      UpstreamUpdate{Name: strPtr("renamed-channel")},
			wantBaseURL:  "https://both-base.example.com",
			wantBaseURLs: []string{"https://both-urls-1.example.com", "https://both-urls-2.example.com"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := cm.UpdateUpstream(0, tt.updates)
			if err != nil {
				t.Fatalf("UpdateUpstream 失败: %v", err)
			}

			cfg := cm.GetConfig()
			upstream := cfg.Upstream[0]

			if upstream.BaseURL != tt.wantBaseURL {
				t.Errorf("BaseURL = %q, want %q", upstream.BaseURL, tt.wantBaseURL)
			}

			if tt.wantBaseURLsNil {
				if upstream.BaseURLs != nil {
					t.Errorf("BaseURLs = %v, want nil", upstream.BaseURLs)
				}
			} else {
				if len(upstream.BaseURLs) != len(tt.wantBaseURLs) {
					t.Errorf("BaseURLs length = %d, want %d", len(upstream.BaseURLs), len(tt.wantBaseURLs))
				} else {
					for i, url := range upstream.BaseURLs {
						if url != tt.wantBaseURLs[i] {
							t.Errorf("BaseURLs[%d] = %q, want %q", i, url, tt.wantBaseURLs[i])
						}
					}
				}
			}
		})
	}
}

// TestUpdateResponsesUpstream_BaseURLConsistency 测试 Responses 渠道的 baseUrl 一致性
func TestUpdateResponsesUpstream_BaseURLConsistency(t *testing.T) {
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "config.json")
	initialConfig := `{
		"upstream": [],
		"responsesUpstream": [{
			"name": "responses-channel",
			"baseUrl": "https://old.responses.com",
			"baseUrls": ["https://old-1.responses.com", "https://old-2.responses.com"],
			"apiKeys": ["test-key"],
			"serviceType": "claude"
		}],
		"loadBalance": "failover",
		"responsesLoadBalance": "failover"
	}`
	if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil {
		t.Fatalf("写入初始配置失败: %v", err)
	}

	cm, err := NewConfigManager(configPath)
	if err != nil {
		t.Fatalf("初始化配置管理器失败: %v", err)
	}
	defer cm.Close()

	// 测试:只更新 baseUrl 时 baseUrls 应被清空
	t.Run("只更新 baseUrl 时 baseUrls 应被清空", func(t *testing.T) {
		_, err := cm.UpdateResponsesUpstream(0, UpstreamUpdate{
			BaseURL: strPtr("https://new.responses.com"),
		})
		if err != nil {
			t.Fatalf("UpdateResponsesUpstream 失败: %v", err)
		}

		cfg := cm.GetConfig()
		upstream := cfg.ResponsesUpstream[0]

		if upstream.BaseURL != "https://new.responses.com" {
			t.Errorf("BaseURL = %q, want %q", upstream.BaseURL, "https://new.responses.com")
		}
		if upstream.BaseURLs != nil {
			t.Errorf("BaseURLs = %v, want nil", upstream.BaseURLs)
		}
	})
}

// TestUpdateGeminiUpstream_BaseURLConsistency 测试 Gemini 渠道的 baseUrl 一致性
func TestUpdateGeminiUpstream_BaseURLConsistency(t *testing.T) {
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "config.json")
	initialConfig := `{
		"upstream": [],
		"geminiUpstream": [{
			"name": "gemini-channel",
			"baseUrl": "https://old.gemini.com",
			"baseUrls": ["https://old-1.gemini.com", "https://old-2.gemini.com"],
			"apiKeys": ["test-key"],
			"serviceType": "gemini"
		}],
		"loadBalance": "failover",
		"geminiLoadBalance": "failover"
	}`
	if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil {
		t.Fatalf("写入初始配置失败: %v", err)
	}

	cm, err := NewConfigManager(configPath)
	if err != nil {
		t.Fatalf("初始化配置管理器失败: %v", err)
	}
	defer cm.Close()

	// 测试:只更新 baseUrl 时 baseUrls 应被清空
	t.Run("只更新 baseUrl 时 baseUrls 应被清空", func(t *testing.T) {
		_, err := cm.UpdateGeminiUpstream(0, UpstreamUpdate{
			BaseURL: strPtr("https://new.gemini.com"),
		})
		if err != nil {
			t.Fatalf("UpdateGeminiUpstream 失败: %v", err)
		}

		cfg := cm.GetConfig()
		upstream := cfg.GeminiUpstream[0]

		if upstream.BaseURL != "https://new.gemini.com" {
			t.Errorf("BaseURL = %q, want %q", upstream.BaseURL, "https://new.gemini.com")
		}
		if upstream.BaseURLs != nil {
			t.Errorf("BaseURLs = %v, want nil", upstream.BaseURLs)
		}
	})
}

// TestGetAllBaseURLs_Priority 测试 GetAllBaseURLs 的优先级逻辑
func TestGetAllBaseURLs_Priority(t *testing.T) {
	tests := []struct {
		name     string
		upstream UpstreamConfig
		want     []string
	}{
		{
			name: "baseUrls 非空时优先返回 baseUrls",
			upstream: UpstreamConfig{
				BaseURL:  "https://single.example.com",
				BaseURLs: []string{"https://multi-1.example.com", "https://multi-2.example.com"},
			},
			want: []string{"https://multi-1.example.com", "https://multi-2.example.com"},
		},
		{
			name: "baseUrls 为空时回退到 baseUrl",
			upstream: UpstreamConfig{
				BaseURL:  "https://single.example.com",
				BaseURLs: nil,
			},
			want: []string{"https://single.example.com"},
		},
		{
			name: "两者都为空时返回 nil",
			upstream: UpstreamConfig{
				BaseURL:  "",
				BaseURLs: nil,
			},
			want: nil,
		},
		{
			name: "baseUrls 为空切片时回退到 baseUrl",
			upstream: UpstreamConfig{
				BaseURL:  "https://single.example.com",
				BaseURLs: []string{},
			},
			want: []string{"https://single.example.com"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.upstream.GetAllBaseURLs()

			if len(got) != len(tt.want) {
				t.Errorf("GetAllBaseURLs() length = %d, want %d", len(got), len(tt.want))
				return
			}
			for i := range got {
				if got[i] != tt.want[i] {
					t.Errorf("GetAllBaseURLs()[%d] = %q, want %q", i, got[i], tt.want[i])
				}
			}
		})
	}
}

// TestGetEffectiveBaseURL_Priority 测试 GetEffectiveBaseURL 的优先级逻辑
func TestGetEffectiveBaseURL_Priority(t *testing.T) {
	tests := []struct {
		name     string
		upstream UpstreamConfig
		want     string
	}{
		{
			name: "baseUrl 非空时优先返回 baseUrl",
			upstream: UpstreamConfig{
				BaseURL:  "https://single.example.com",
				BaseURLs: []string{"https://multi-1.example.com", "https://multi-2.example.com"},
			},
			want: "https://single.example.com",
		},
		{
			name: "baseUrl 为空时回退到 baseUrls[0]",
			upstream: UpstreamConfig{
				BaseURL:  "",
				BaseURLs: []string{"https://multi-1.example.com", "https://multi-2.example.com"},
			},
			want: "https://multi-1.example.com",
		},
		{
			name: "两者都为空时返回空字符串",
			upstream: UpstreamConfig{
				BaseURL:  "",
				BaseURLs: nil,
			},
			want: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.upstream.GetEffectiveBaseURL()
			if got != tt.want {
				t.Errorf("GetEffectiveBaseURL() = %q, want %q", got, tt.want)
			}
		})
	}
}

// TestDeduplicateBaseURLs 测试 BaseURLs 去重逻辑
func TestDeduplicateBaseURLs(t *testing.T) {
	tests := []struct {
		name  string
		input []string
		want  []string
	}{
		{
			name:  "精确重复应去重",
			input: []string{"https://a.com", "https://b.com", "https://a.com"},
			want:  []string{"https://a.com", "https://b.com"},
		},
		{
			name:  "末尾斜杠差异应视为相同",
			input: []string{"https://a.com", "https://a.com/"},
			want:  []string{"https://a.com"},
		},
		{
			name:  "末尾井号差异应视为相同",
			input: []string{"https://a.com", "https://a.com#"},
			want:  []string{"https://a.com"},
		},
		{
			name:  "保持原始顺序",
			input: []string{"https://c.com", "https://a.com", "https://b.com"},
			want:  []string{"https://c.com", "https://a.com", "https://b.com"},
		},
		{
			name:  "单个元素不变",
			input: []string{"https://only.com"},
			want:  []string{"https://only.com"},
		},
		{
			name:  "空切片返回空切片",
			input: []string{},
			want:  []string{},
		},
		{
			name:  "nil 返回 nil",
			input: nil,
			want:  nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := deduplicateBaseURLs(tt.input)

			if len(got) != len(tt.want) {
				t.Errorf("deduplicateBaseURLs() length = %d, want %d", len(got), len(tt.want))
				return
			}
			for i := range got {
				if got[i] != tt.want[i] {
					t.Errorf("deduplicateBaseURLs()[%d] = %q, want %q", i, got[i], tt.want[i])
				}
			}
		})
	}
}

// TestAddUpstream_BaseURLDeduplication 测试添加渠道时的 BaseURLs 去重
func TestAddUpstream_BaseURLDeduplication(t *testing.T) {
	tempDir := t.TempDir()
	configPath := filepath.Join(tempDir, "config.json")
	initialConfig := `{"upstream": [], "loadBalance": "failover"}`
	if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil {
		t.Fatalf("写入初始配置失败: %v", err)
	}

	cm, err := NewConfigManager(configPath)
	if err != nil {
		t.Fatalf("初始化配置管理器失败: %v", err)
	}
	defer cm.Close()

	// 添加包含重复 URL 的渠道
	err = cm.AddUpstream(UpstreamConfig{
		Name:        "dedup-test",
		BaseURL:     "https://main.example.com",
		BaseURLs:    []string{"https://a.com", "https://b.com", "https://a.com/", "https://c.com"},
		APIKeys:     []string{"key1"},
		ServiceType: "claude",
	})
	if err != nil {
		t.Fatalf("AddUpstream 失败: %v", err)
	}

	cfg := cm.GetConfig()
	upstream := cfg.Upstream[0]

	// 期望去重后只有 3 个 URL
	expectedURLs := []string{"https://a.com", "https://b.com", "https://c.com"}
	if len(upstream.BaseURLs) != len(expectedURLs) {
		t.Errorf("BaseURLs length = %d, want %d", len(upstream.BaseURLs), len(expectedURLs))
	}
	for i, url := range upstream.BaseURLs {
		if url != expectedURLs[i] {
			t.Errorf("BaseURLs[%d] = %q, want %q", i, url, expectedURLs[i])
		}
	}
}

// strPtr 辅助函数:返回字符串指针
func strPtr(s string) *string {
	return &s
}


================================================
FILE: backend-go/internal/config/config_gemini.go
================================================
package config

import (
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/BenedictKing/claude-proxy/internal/utils"
)

// ============== Gemini 渠道方法 ==============

// GetCurrentGeminiUpstream 获取当前 Gemini 上游配置
// 优先选择第一个 active 状态的渠道,若无则回退到第一个渠道
func (cm *ConfigManager) GetCurrentGeminiUpstream() (*UpstreamConfig, error) {
	cm.mu.RLock()
	defer cm.mu.RUnlock()

	if len(cm.config.GeminiUpstream) == 0 {
		return nil, fmt.Errorf("未配置任何 Gemini 渠道")
	}

	// 优先选择第一个 active 状态的渠道
	for i := range cm.config.GeminiUpstream {
		status := cm.config.GeminiUpstream[i].Status
		if status == "" || status == "active" {
			return &cm.config.GeminiUpstream[i], nil
		}
	}

	// 没有 active 渠道,回退到第一个渠道
	return &cm.config.GeminiUpstream[0], nil
}

// AddGeminiUpstream 添加 Gemini 上游
func (cm *ConfigManager) AddGeminiUpstream(upstream UpstreamConfig) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	// 新建渠道默认设为 active
	if upstream.Status == "" {
		upstream.Status = "active"
	}

	// 去重 API Keys 和 Base URLs
	upstream.APIKeys = deduplicateStrings(upstream.APIKeys)
	upstream.BaseURLs = deduplicateBaseURLs(upstream.BaseURLs)

	cm.config.GeminiUpstream = append(cm.config.GeminiUpstream, upstream)

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	log.Printf("[Config-Upstream] 已添加 Gemini 上游: %s", upstream.Name)
	return nil
}

// UpdateGeminiUpstream 更新 Gemini 上游
// 返回值:shouldResetMetrics 表示是否需要重置渠道指标(熔断状态)
func (cm *ConfigManager) UpdateGeminiUpstream(index int, updates UpstreamUpdate) (shouldResetMetrics bool, err error) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if index < 0 || index >= len(cm.config.GeminiUpstream) {
		return false, fmt.Errorf("无效的 Gemini 上游索引: %d", index)
	}

	upstream := &cm.config.GeminiUpstream[index]

	if updates.Name != nil {
		upstream.Name = *updates.Name
	}
	if updates.BaseURL != nil {
		upstream.BaseURL = *updates.BaseURL
		// 当 BaseURL 被更新且 BaseURLs 未被显式设置时,清空 BaseURLs 保持一致性
		// 避免出现 baseUrl 和 baseUrls[0] 不一致的情况
		if updates.BaseURLs == nil {
			upstream.BaseURLs = nil
		}
	}
	if updates.BaseURLs != nil {
		upstream.BaseURLs = deduplicateBaseURLs(updates.BaseURLs)
	}
	if updates.ServiceType != nil {
		upstream.ServiceType = *updates.ServiceType
	}
	if updates.Description != nil {
		upstream.Description = *updates.Description
	}
	if updates.Website != nil {
		upstream.Website = *updates.Website
	}
	if updates.APIKeys != nil {
		// 记录被移除的 Key 到历史列表(用于统计聚合)
		newKeys := make(map[string]bool)
		for _, key := range updates.APIKeys {
			newKeys[key] = true
		}

		// 找出被移除的 Key(在旧列表中但不在新列表中)
		for _, key := range upstream.APIKeys {
			if !newKeys[key] {
				// 检查是否已在历史列表中
				alreadyInHistory := false
				for _, hk := range upstream.HistoricalAPIKeys {
					if hk == key {
						alreadyInHistory = true
						break
					}
				}
				if !alreadyInHistory {
					upstream.HistoricalAPIKeys = append(upstream.HistoricalAPIKeys, key)
					log.Printf("[Config-Upstream] Gemini 渠道 [%d] %s: Key %s 已移入历史列表", index, upstream.Name, utils.MaskAPIKey(key))
				}
			}
		}

		// 如果新 Key 在历史列表中,从历史列表移除(换回来了)
		var newHistoricalKeys []string
		for _, hk := range upstream.HistoricalAPIKeys {
			if !newKeys[hk] {
				newHistoricalKeys = append(newHistoricalKeys, hk)
			} else {
				log.Printf("[Config-Upstream] Gemini 渠道 [%d] %s: Key %s 已从历史列表恢复", index, upstream.Name, utils.MaskAPIKey(hk))
			}
		}
		upstream.HistoricalAPIKeys = newHistoricalKeys

		// 只有单 key 场景且 key 被更换时,才自动激活并重置熔断
		if len(upstream.APIKeys) == 1 && len(updates.APIKeys) == 1 &&
			upstream.APIKeys[0] != updates.APIKeys[0] {
			shouldResetMetrics = true
			if upstream.Status == "suspended" {
				upstream.Status = "active"
				log.Printf("[Config-Upstream] Gemini 渠道 [%d] %s 已从暂停状态自动激活(单 key 更换)", index, upstream.Name)
			}
		}
		upstream.APIKeys = deduplicateStrings(updates.APIKeys)
	}
	if updates.ModelMapping != nil {
		upstream.ModelMapping = updates.ModelMapping
	}
	if updates.InsecureSkipVerify != nil {
		upstream.InsecureSkipVerify = *updates.InsecureSkipVerify
	}
	if updates.Priority != nil {
		upstream.Priority = *updates.Priority
	}
	if updates.Status != nil {
		upstream.Status = *updates.Status
	}
	if updates.PromotionUntil != nil {
		upstream.PromotionUntil = updates.PromotionUntil
	}
	if updates.LowQuality != nil {
		upstream.LowQuality = *updates.LowQuality
	}
	if updates.InjectDummyThoughtSignature != nil {
		upstream.InjectDummyThoughtSignature = *updates.InjectDummyThoughtSignature
	}
	if updates.StripThoughtSignature != nil {
		upstream.StripThoughtSignature = *updates.StripThoughtSignature
	}

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return false, err
	}

	log.Printf("[Config-Upstream] 已更新 Gemini 上游: [%d] %s", index, cm.config.GeminiUpstream[index].Name)
	return shouldResetMetrics, nil
}

// RemoveGeminiUpstream 删除 Gemini 上游
func (cm *ConfigManager) RemoveGeminiUpstream(index int) (*UpstreamConfig, error) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if index < 0 || index >= len(cm.config.GeminiUpstream) {
		return nil, fmt.Errorf("无效的 Gemini 上游索引: %d", index)
	}

	removed := cm.config.GeminiUpstream[index]
	cm.config.GeminiUpstream = append(cm.config.GeminiUpstream[:index], cm.config.GeminiUpstream[index+1:]...)

	// 清理被删除渠道的失败 key 冷却记录
	cm.clearFailedKeysForUpstream(&removed, "Gemini")

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return nil, err
	}

	log.Printf("[Config-Upstream] 已删除 Gemini 上游: %s", removed.Name)
	return &removed, nil
}

// AddGeminiAPIKey 添加 Gemini 上游的 API 密钥
func (cm *ConfigManager) AddGeminiAPIKey(index int, apiKey string) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if index < 0 || index >= len(cm.config.GeminiUpstream) {
		return fmt.Errorf("无效的上游索引: %d", index)
	}

	// 检查密钥是否已存在
	for _, key := range cm.config.GeminiUpstream[index].APIKeys {
		if key == apiKey {
			return fmt.Errorf("API密钥已存在")
		}
	}

	cm.config.GeminiUpstream[index].APIKeys = append(cm.config.GeminiUpstream[index].APIKeys, apiKey)

	// 如果该 Key 在历史列表中,从历史列表移除(换回来了)
	var newHistoricalKeys []string
	for _, hk := range cm.config.GeminiUpstream[index].HistoricalAPIKeys {
		if hk != apiKey {
			newHistoricalKeys = append(newHistoricalKeys, hk)
		} else {
			log.Printf("[Gemini-Key] 上游 [%d] %s: Key %s 已从历史列表恢复", index, cm.config.GeminiUpstream[index].Name, utils.MaskAPIKey(hk))
		}
	}
	cm.config.GeminiUpstream[index].HistoricalAPIKeys = newHistoricalKeys

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	log.Printf("[Gemini-Key] 已添加API密钥到 Gemini 上游 [%d] %s", index, cm.config.GeminiUpstream[index].Name)
	return nil
}

// RemoveGeminiAPIKey 删除 Gemini 上游的 API 密钥
func (cm *ConfigManager) RemoveGeminiAPIKey(index int, apiKey string) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if index < 0 || index >= len(cm.config.GeminiUpstream) {
		return fmt.Errorf("无效的上游索引: %d", index)
	}

	// 查找并删除密钥
	keys := cm.config.GeminiUpstream[index].APIKeys
	found := false
	for i, key := range keys {
		if key == apiKey {
			cm.config.GeminiUpstream[index].APIKeys = append(keys[:i], keys[i+1:]...)
			found = true
			break
		}
	}

	if !found {
		return fmt.Errorf("API密钥不存在")
	}

	// 将被移除的 Key 添加到历史列表(用于统计聚合)
	alreadyInHistory := false
	for _, hk := range cm.config.GeminiUpstream[index].HistoricalAPIKeys {
		if hk == apiKey {
			alreadyInHistory = true
			break
		}
	}
	if !alreadyInHistory {
		cm.config.GeminiUpstream[index].HistoricalAPIKeys = append(cm.config.GeminiUpstream[index].HistoricalAPIKeys, apiKey)
		log.Printf("[Gemini-Key] 上游 [%d] %s: Key %s 已移入历史列表", index, cm.config.GeminiUpstream[index].Name, utils.MaskAPIKey(apiKey))
	}

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	log.Printf("[Gemini-Key] 已从 Gemini 上游 [%d] %s 删除API密钥", index, cm.config.GeminiUpstream[index].Name)
	return nil
}

// GetNextGeminiAPIKey 获取下一个 Gemini API 密钥(纯 failover 模式)
func (cm *ConfigManager) GetNextGeminiAPIKey(upstream *UpstreamConfig, failedKeys map[string]bool) (string, error) {
	return cm.GetNextAPIKey(upstream, failedKeys, "Gemini")
}

// MoveGeminiAPIKeyToTop 将指定 Gemini 渠道的 API 密钥移到最前面
func (cm *ConfigManager) MoveGeminiAPIKeyToTop(upstreamIndex int, apiKey string) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if upstreamIndex < 0 || upstreamIndex >= len(cm.config.GeminiUpstream) {
		return fmt.Errorf("无效的上游索引: %d", upstreamIndex)
	}

	upstream := &cm.config.GeminiUpstream[upstreamIndex]
	index := -1
	for i, key := range upstream.APIKeys {
		if key == apiKey {
			index = i
			break
		}
	}

	if index <= 0 {
		return nil
	}

	upstream.APIKeys = append([]string{apiKey}, append(upstream.APIKeys[:index], upstream.APIKeys[index+1:]...)...)
	return cm.saveConfigLocked(cm.config)
}

// MoveGeminiAPIKeyToBottom 将指定 Gemini 渠道的 API 密钥移到最后面
func (cm *ConfigManager) MoveGeminiAPIKeyToBottom(upstreamIndex int, apiKey string) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if upstreamIndex < 0 || upstreamIndex >= len(cm.config.GeminiUpstream) {
		return fmt.Errorf("无效的上游索引: %d", upstreamIndex)
	}

	upstream := &cm.config.GeminiUpstream[upstreamIndex]
	index := -1
	for i, key := range upstream.APIKeys {
		if key == apiKey {
			index = i
			break
		}
	}

	if index == -1 || index == len(upstream.APIKeys)-1 {
		return nil
	}

	upstream.APIKeys = append(upstream.APIKeys[:index], upstream.APIKeys[index+1:]...)
	upstream.APIKeys = append(upstream.APIKeys, apiKey)
	return cm.saveConfigLocked(cm.config)
}

// ReorderGeminiUpstreams 重新排序 Gemini 渠道优先级
// order 是渠道索引数组,按新的优先级顺序排列(只更新传入的渠道,支持部分排序)
func (cm *ConfigManager) ReorderGeminiUpstreams(order []int) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if len(order) == 0 {
		return fmt.Errorf("排序数组不能为空")
	}

	seen := make(map[int]bool)
	for _, idx := range order {
		if idx < 0 || idx >= len(cm.config.GeminiUpstream) {
			return fmt.Errorf("无效的渠道索引: %d", idx)
		}
		if seen[idx] {
			return fmt.Errorf("重复的渠道索引: %d", idx)
		}
		seen[idx] = true
	}

	// 更新传入渠道的优先级(未传入的渠道保持原优先级不变)
	// 注意:priority 从 1 开始,避免 omitempty 吞掉 0 值
	for i, idx := range order {
		cm.config.GeminiUpstream[idx].Priority = i + 1
	}

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	log.Printf("[Config-Reorder] 已更新 Gemini 渠道优先级顺序 (%d 个渠道)", len(order))
	return nil
}

// SetGeminiChannelStatus 设置 Gemini 渠道状态
func (cm *ConfigManager) SetGeminiChannelStatus(index int, status string) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if index < 0 || index >= len(cm.config.GeminiUpstream) {
		return fmt.Errorf("无效的上游索引: %d", index)
	}

	// 状态值转为小写,支持大小写不敏感
	status = strings.ToLower(status)
	if status != "active" && status != "suspended" && status != "disabled" {
		return fmt.Errorf("无效的状态: %s (允许值: active, suspended, disabled)", status)
	}

	cm.config.GeminiUpstream[index].Status = status

	// 暂停时清除促销期
	if status == "suspended" && cm.config.GeminiUpstream[index].PromotionUntil != nil {
		cm.config.GeminiUpstream[index].PromotionUntil = nil
		log.Printf("[Config-Status] 已清除 Gemini 渠道 [%d] %s 的促销期", index, cm.config.GeminiUpstream[index].Name)
	}

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	log.Printf("[Config-Status] 已设置 Gemini 渠道 [%d] %s 状态为: %s", index, cm.config.GeminiUpstream[index].Name, status)
	return nil
}

// SetGeminiChannelPromotion 设置 Gemini 渠道促销期
func (cm *ConfigManager) SetGeminiChannelPromotion(index int, duration time.Duration) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if index < 0 || index >= len(cm.config.GeminiUpstream) {
		return fmt.Errorf("无效的 Gemini 上游索引: %d", index)
	}

	if duration <= 0 {
		cm.config.GeminiUpstream[index].PromotionUntil = nil
		log.Printf("[Config-Promotion] 已清除 Gemini 渠道 [%d] %s 的促销期", index, cm.config.GeminiUpstream[index].Name)
	} else {
		// 清除其他渠道的促销期(同一时间只允许一个促销渠道)
		for i := range cm.config.GeminiUpstream {
			if i != index && cm.config.GeminiUpstream[i].PromotionUntil != nil {
				cm.config.GeminiUpstream[i].PromotionUntil = nil
			}
		}
		promotionEnd := time.Now().Add(duration)
		cm.config.GeminiUpstream[index].PromotionUntil = &promotionEnd
		log.Printf("[Config-Promotion] 已设置 Gemini 渠道 [%d] %s 进入促销期,截止: %s", index, cm.config.GeminiUpstream[index].Name, promotionEnd.Format(time.RFC3339))
	}

	return cm.saveConfigLocked(cm.config)
}

// GetPromotedGeminiChannel 获取当前处于促销期的 Gemini 渠道索引
func (cm *ConfigManager) GetPromotedGeminiChannel() (int, bool) {
	cm.mu.RLock()
	defer cm.mu.RUnlock()

	for i, upstream := range cm.config.GeminiUpstream {
		if IsChannelInPromotion(&upstream) && GetChannelStatus(&upstream) == "active" {
			return i, true
		}
	}
	return -1, false
}

// SetGeminiLoadBalance 设置 Gemini 负载均衡策略
func (cm *ConfigManager) SetGeminiLoadBalance(strategy string) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if err := validateLoadBalanceStrategy(strategy); err != nil {
		return err
	}

	cm.config.GeminiLoadBalance = strategy

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	log.Printf("[Config-LoadBalance] 已设置 Gemini 负载均衡策略: %s", strategy)
	return nil
}


================================================
FILE: backend-go/internal/config/config_loader.go
================================================
package config

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/fsnotify/fsnotify"
)

const (
	maxBackups      = 10
	keyRecoveryTime = 5 * time.Minute
	maxFailureCount = 3
)

// NewConfigManager 创建配置管理器
func NewConfigManager(configFile string) (*ConfigManager, error) {
	cm := &ConfigManager{
		configFile:      configFile,
		failedKeysCache: make(map[string]*FailedKey),
		keyRecoveryTime: keyRecoveryTime,
		maxFailureCount: maxFailureCount,
		stopChan:        make(chan struct{}),
	}

	// 加载配置
	if err := cm.loadConfig(); err != nil {
		return nil, err
	}

	// 启动文件监听
	if err := cm.startWatcher(); err != nil {
		log.Printf("[Config-Watcher] 警告: 启动配置文件监听失败: %v", err)
	}

	// 启动定期清理
	go cm.cleanupExpiredFailures()

	return cm, nil
}

// loadConfig 加载配置
func (cm *ConfigManager) loadConfig() error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	// 如果配置文件不存在,创建默认配置
	if _, err := os.Stat(cm.configFile); os.IsNotExist(err) {
		return cm.createDefaultConfig()
	}

	// 读取配置文件
	data, err := os.ReadFile(cm.configFile)
	if err != nil {
		return err
	}

	if err := json.Unmarshal(data, &cm.config); err != nil {
		return err
	}

	// 兼容旧配置:检查 FuzzyModeEnabled 字段是否存在
	// 如果不存在,默认设为 true(新功能默认启用)
	needSaveDefaults := cm.applyConfigDefaults(data)

	// 兼容旧格式:检测是否需要迁移
	needMigration := cm.migrateOldFormat()

	// 如果有默认值迁移或格式迁移,保存配置
	if needSaveDefaults || needMigration {
		if err := cm.saveConfigLocked(cm.config); err != nil {
			log.Printf("[Config-Migration] 警告: 保存迁移后的配置失败: %v", err)
			return err
		}
		if needMigration {
			log.Printf("[Config-Migration] 配置迁移完成")
		}
	}

	// 自检:没有配置 key 的渠道自动暂停
	if cm.validateChannelKeys() {
		if err := cm.saveConfigLocked(cm.config); err != nil {
			log.Printf("[Config-Validate] 警告: 保存自检后的配置失败: %v", err)
			return err
		}
	}

	return nil
}

// createDefaultConfig 创建默认配置
func (cm *ConfigManager) createDefaultConfig() error {
	defaultConfig := Config{
		Upstream:                 []UpstreamConfig{},
		CurrentUpstream:          0,
		LoadBalance:              "failover",
		ResponsesUpstream:        []UpstreamConfig{},
		CurrentResponsesUpstream: 0,
		ResponsesLoadBalance:     "failover",
		GeminiUpstream:           []UpstreamConfig{},
		GeminiLoadBalance:        "failover",
		FuzzyModeEnabled:         true, // 默认启用 Fuzzy 模式
	}

	if err := os.MkdirAll(filepath.Dir(cm.configFile), 0755); err != nil {
		return err
	}

	return cm.saveConfigLocked(defaultConfig)
}

// applyConfigDefaults 应用配置默认值
// rawJSON: 原始 JSON 数据,用于检测字段是否存在
// 返回: 是否有字段需要迁移(需要保存配置)
func (cm *ConfigManager) applyConfigDefaults(rawJSON []byte) bool {
	needSave := false

	if cm.config.LoadBalance == "" {
		cm.config.LoadBalance = "failover"
	}
	if cm.config.ResponsesLoadBalance == "" {
		cm.config.ResponsesLoadBalance = cm.config.LoadBalance
	}
	if cm.config.GeminiLoadBalance == "" {
		cm.config.GeminiLoadBalance = "failover"
	}

	// FuzzyModeEnabled 默认值处理:
	// 由于 bool 零值是 false,无法区分"用户设为 false"和"字段不存在"
	// 通过检查原始 JSON 是否包含该字段来判断
	var rawMap map[string]json.RawMessage
	if err := json.Unmarshal(rawJSON, &rawMap); err == nil {
		if _, exists := rawMap["fuzzyModeEnabled"]; !exists {
			// 字段不存在,设为默认值 true
			cm.config.FuzzyModeEnabled = true
			needSave = true
			log.Printf("[Config-Migration] FuzzyModeEnabled 字段不存在,设为默认值 true")
		}
	}

	return needSave
}

// migrateOldFormat 迁移旧格式配置,返回是否有迁移
func (cm *ConfigManager) migrateOldFormat() bool {
	needMigration := false

	// 迁移 Messages 渠道
	if cm.migrateUpstreams(cm.config.Upstream, cm.config.CurrentUpstream, "Messages") {
		needMigration = true
	}

	// 迁移 Responses 渠道
	if cm.migrateUpstreams(cm.config.ResponsesUpstream, cm.config.CurrentResponsesUpstream, "Responses") {
		needMigration = true
	}

	if needMigration {
		log.Printf("[Config-Migration] 检测到旧格式配置,正在迁移到新格式...")
	}

	return needMigration
}

// migrateUpstreams 迁移单个渠道列表
func (cm *ConfigManager) migrateUpstreams(upstreams []UpstreamConfig, currentIdx int, name string) bool {
	if len(upstreams) == 0 {
		return false
	}

	// 检查是否已有 status 字段
	for _, up := range upstreams {
		if up.Status != "" {
			return false
		}
	}

	// 需要迁移
	if currentIdx < 0 || currentIdx >= len(upstreams) {
		currentIdx = 0
	}

	for i := range upstreams {
		if i == currentIdx {
			upstreams[i].Status = "active"
		} else {
			upstreams[i].Status = "disabled"
		}
	}

	log.Printf("[Config-Migration] %s 渠道 [%d] %s 已设置为 active,其他 %d 个渠道已设为 disabled",
		name, currentIdx, upstreams[currentIdx].Name, len(upstreams)-1)

	return true
}

// validateChannelKeys 自检渠道密钥配置
// 没有配置 API key 的渠道,即使状态为 active 也应暂停
// 返回 true 表示有配置被修改,需要保存
func (cm *ConfigManager) validateChannelKeys() bool {
	modified := false

	// 检查 Messages 渠道
	for i := range cm.config.Upstream {
		upstream := &cm.config.Upstream[i]
		status := upstream.Status
		if status == "" {
			status = "active"
		}

		// 如果是 active 状态但没有配置 key,自动设为 suspended
		if status == "active" && len(upstream.APIKeys) == 0 {
			upstream.Status = "suspended"
			modified = true
			log.Printf("[Config-Validate] 警告: Messages 渠道 [%d] %s 没有配置 API key,已自动暂停", i, upstream.Name)
		}
	}

	// 检查 Responses 渠道
	for i := range cm.config.ResponsesUpstream {
		upstream := &cm.config.ResponsesUpstream[i]
		status := upstream.Status
		if status == "" {
			status = "active"
		}

		// 如果是 active 状态但没有配置 key,自动设为 suspended
		if status == "active" && len(upstream.APIKeys) == 0 {
			upstream.Status = "suspended"
			modified = true
			log.Printf("[Config-Validate] 警告: Responses 渠道 [%d] %s 没有配置 API key,已自动暂停", i, upstream.Name)
		}
	}

	// 检查 Gemini 渠道
	for i := range cm.config.GeminiUpstream {
		upstream := &cm.config.GeminiUpstream[i]
		status := upstream.Status
		if status == "" {
			status = "active"
		}

		// 如果是 active 状态但没有配置 key,自动设为 suspended
		if status == "active" && len(upstream.APIKeys) == 0 {
			upstream.Status = "suspended"
			modified = true
			log.Printf("[Config-Validate] 警告: Gemini 渠道 [%d] %s 没有配置 API key,已自动暂停", i, upstream.Name)
		}
	}

	return modified
}

// saveConfigLocked 保存配置(已加锁)
func (cm *ConfigManager) saveConfigLocked(config Config) error {
	// 备份当前配置
	cm.backupConfig()

	// 清理已废弃字段,确保不会被序列化到 JSON
	config.CurrentUpstream = 0
	config.CurrentResponsesUpstream = 0

	data, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		return err
	}

	cm.config = config
	return os.WriteFile(cm.configFile, data, 0644)
}

// SaveConfig 保存配置
func (cm *ConfigManager) SaveConfig() error {
	cm.mu.Lock()
	defer cm.mu.Unlock()
	return cm.saveConfigLocked(cm.config)
}

// backupConfig 备份配置
func (cm *ConfigManager) backupConfig() {
	if _, err := os.Stat(cm.configFile); os.IsNotExist(err) {
		return
	}

	backupDir := filepath.Join(filepath.Dir(cm.configFile), "backups")
	if err := os.MkdirAll(backupDir, 0755); err != nil {
		log.Printf("[Config-Backup] 警告: 创建备份目录失败: %v", err)
		return
	}

	// 读取当前配置
	data, err := os.ReadFile(cm.configFile)
	if err != nil {
		log.Printf("[Config-Backup] 警告: 读取配置文件失败: %v", err)
		return
	}

	// 创建备份文件
	timestamp := time.Now().Format("2006-01-02T15-04-05")
	backupFile := filepath.Join(backupDir, fmt.Sprintf("config-%s.json", timestamp))
	if err := os.WriteFile(backupFile, data, 0644); err != nil {
		log.Printf("[Config-Backup] 警告: 写入备份文件失败: %v", err)
		return
	}

	// 清理旧备份
	cm.cleanupOldBackups(backupDir)
}

// cleanupOldBackups 清理旧备份
func (cm *ConfigManager) cleanupOldBackups(backupDir string) {
	entries, err := os.ReadDir(backupDir)
	if err != nil {
		return
	}

	if len(entries) <= maxBackups {
		return
	}

	// 删除最旧的备份
	for i := 0; i < len(entries)-maxBackups; i++ {
		os.Remove(filepath.Join(backupDir, entries[i].Name()))
	}
}

// startWatcher 启动文件监听
func (cm *ConfigManager) startWatcher() error {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return err
	}

	cm.watcher = watcher

	go func() {
		for {
			select {
			case <-cm.stopChan:
				return
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				if event.Op&fsnotify.Write == fsnotify.Write {
					log.Printf("[Config-Watcher] 检测到配置文件变化,重载配置...")
					if err := cm.loadConfig(); err != nil {
						log.Printf("[Config-Watcher] 警告: 配置重载失败: %v", err)
					} else {
						log.Printf("[Config-Watcher] 配置已重载")
					}
				}
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Printf("[Config-Watcher] 警告: 文件监听错误: %v", err)
			}
		}
	}()

	return watcher.Add(cm.configFile)
}

// Close 关闭 ConfigManager 并释放资源(幂等,可安全多次调用)
func (cm *ConfigManager) Close() error {
	var closeErr error
	cm.closeOnce.Do(func() {
		// 通知所有 goroutine 停止
		if cm.stopChan != nil {
			close(cm.stopChan)
		}

		// 关闭文件监听器
		if cm.watcher != nil {
			closeErr = cm.watcher.Close()
		}
	})
	return closeErr
}


================================================
FILE: backend-go/internal/config/config_messages.go
================================================
package config

import (
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/BenedictKing/claude-proxy/internal/utils"
)

// ============== Messages 渠道方法 ==============

// GetCurrentUpstream 获取当前上游配置
// 优先选择第一个 active 状态的渠道,若无则回退到第一个渠道
func (cm *ConfigManager) GetCurrentUpstream() (*UpstreamConfig, error) {
	cm.mu.RLock()
	defer cm.mu.RUnlock()

	if len(cm.config.Upstream) == 0 {
		return nil, fmt.Errorf("未配置任何上游渠道")
	}

	// 优先选择第一个 active 状态的渠道
	for i := range cm.config.Upstream {
		status := cm.config.Upstream[i].Status
		if status == "" || status == "active" {
			return &cm.config.Upstream[i], nil
		}
	}

	// 没有 active 渠道,回退到第一个渠道
	return &cm.config.Upstream[0], nil
}

// AddUpstream 添加上游
func (cm *ConfigManager) AddUpstream(upstream UpstreamConfig) error {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	// 新建渠道默认设为 active
	if upstream.Status == "" {
		upstream.Status = "active"
	}

	// 去重 API Keys 和 Base URLs
	upstream.APIKeys = deduplicateStrings(upstream.APIKeys)
	upstream.BaseURLs = deduplicateBaseURLs(upstream.BaseURLs)

	cm.config.Upstream = append(cm.config.Upstream, upstream)

	if err := cm.saveConfigLocked(cm.config); err != nil {
		return err
	}

	log.Printf("[Config-Upstream] 已添加上游: %s", upstream.Name)
	return nil
}

// UpdateUpstream 更新上游
// 返回值:shouldResetMetrics 表示是否需要重置渠道指标(熔断状态)
func (cm *ConfigManager) UpdateUpstream(index int, updates UpstreamUpdate) (shouldResetMetrics bool, err error) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if index < 0 || index >= len(cm.config.Upstream) {
		return false, fmt.Errorf("无效的上游索引: %d", index)
	}

	upstream := &cm.config.Upstream[index]

	if updates.Name != nil {
		upstream.Name = *updates.Name
	}
	if updates.BaseURL != nil {
		upstream.BaseURL = *updates.BaseURL
		// 当 BaseURL 被更新且 BaseURLs 未被显式设置时,清空 BaseURLs 保持一致性
		// 避免出现 baseUrl 和 baseUrls[0] 不一致的情况
		if updates.BaseURLs == nil {
			upstream.BaseURLs = nil
		}
	}
	if updates.BaseURLs != nil {
		upstream.BaseURLs = deduplicateBaseURLs(updates.BaseURLs)
	}
	if updates.ServiceType != nil {
		upstream.ServiceType = *updates.ServiceType
	}
	if updates.Description != nil {
		upstream.Description = *updates.Description
	}
	if updates.Website != nil {
		upstream.Website = *updates.Website
	}
	if updates.APIKeys != nil {
		// 记录被移除的 Key 到历史列表(用于统计聚合)
		newKeys := make(map[string]bool)
		for _, key := range updates.APIKeys {
			newKeys[key] = true
		}

		// 找出被移除的 Key(在旧列表中但不在新列表中)
		for _, key := range upstream.APIKeys {
			if !newKeys[key] {
				// 检查是否已在历史列表中
				alreadyInHistory := false
				for _, hk := range upstream.HistoricalAPIKeys {
					if hk == key {
						alreadyInHistory = true
						break
					}
				}
				if !alreadyInHistory {
					upstream.HistoricalAPIKeys = append(upstream.HistoricalAPIKeys, key)
					log.Printf("[Config-Upstream] 渠道 [%d] %s: Key %s 已移入历史列表", index, upstream.Name, utils.MaskAPIKey(key))
				}
			}
		}

		// 如果新 Key 在历史列表中,从历史列表移除(换回来了)
		var newHistoricalKeys []string
		for _, hk := range upstream.HistoricalAPIKeys {
			if !newKeys[hk] {
				newHistoricalKeys = append(newHistoricalKeys, hk)
			} else {
				log.Printf("[Config-Upstream] 渠道 [%d] %s: Key %s 已从历史列表恢复", index, upstream.Name, utils.MaskAPIKey(hk))
			}
		}
		upstream.HistoricalAPIKeys = newHistoricalKeys

		// 只有单 key 场景且 key 被更换时,才自动激活并重置熔断
		if len(upstream.APIKeys) == 1 && len(updates.APIKeys) == 1 &&
			upstream.APIKeys[0] != updates.APIKeys[0] {
			shouldResetMetrics = true
			if upstream.Status == "suspended" {
				upstream.Status = "active"
				log.Printf("[Config-Upstream] 渠道 [%d] %s 已从暂停状态自动激活(单 key 更换)", index, upstream.Name)
			}
		}
		upstream.APIKeys = deduplicateStrings(updates.APIKeys)
	}
	if upda
Download .txt
gitextract_1jsidgpo/

├── .claudeignore
├── .dockerignore
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── docker-build.yml
│       ├── release-linux.yml
│       ├── release-macos.yml
│       └── release-windows.yml
├── .gitignore
├── .prettierrc
├── AGENTS.md
├── ARCHITECTURE.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── Dockerfile
├── Dockerfile_China
├── ENVIRONMENT.md
├── LICENSE
├── Makefile
├── README.md
├── RELEASE.md
├── VERSION
├── backend-go/
│   ├── .air.toml
│   ├── .env.example
│   ├── .gitignore
│   ├── CLAUDE.md
│   ├── DEV_GUIDE.md
│   ├── Makefile
│   ├── README.md
│   ├── build.sh
│   ├── docs/
│   │   └── MALFORMED_TOOLCALL_MEMO.md
│   ├── go.mod
│   ├── go.sum
│   ├── internal/
│   │   ├── config/
│   │   │   ├── config.go
│   │   │   ├── config_baseurl_test.go
│   │   │   ├── config_gemini.go
│   │   │   ├── config_loader.go
│   │   │   ├── config_messages.go
│   │   │   ├── config_responses.go
│   │   │   ├── config_utils.go
│   │   │   └── env.go
│   │   ├── converters/
│   │   │   ├── chat_to_responses.go
│   │   │   ├── chat_to_responses_test.go
│   │   │   ├── claude_converter.go
│   │   │   ├── converter.go
│   │   │   ├── converter_test.go
│   │   │   ├── factory.go
│   │   │   ├── gemini_converter.go
│   │   │   ├── gemini_converter_test.go
│   │   │   ├── openai_converter.go
│   │   │   ├── responses_converter.go
│   │   │   ├── responses_passthrough.go
│   │   │   └── responses_to_chat.go
│   │   ├── handlers/
│   │   │   ├── channel_metrics_handler.go
│   │   │   ├── common/
│   │   │   │   ├── client_error_test.go
│   │   │   │   ├── failover.go
│   │   │   │   ├── failover_test.go
│   │   │   │   ├── multi_channel_failover.go
│   │   │   │   ├── request.go
│   │   │   │   ├── stream.go
│   │   │   │   ├── stream_test.go
│   │   │   │   └── upstream_failover.go
│   │   │   ├── frontend.go
│   │   │   ├── gemini/
│   │   │   │   ├── channels.go
│   │   │   │   ├── dashboard.go
│   │   │   │   ├── dashboard_test.go
│   │   │   │   ├── handler.go
│   │   │   │   ├── handler_test.go
│   │   │   │   └── stream.go
│   │   │   ├── global_stats_handler.go
│   │   │   ├── health.go
│   │   │   ├── messages/
│   │   │   │   ├── channels.go
│   │   │   │   ├── handler.go
│   │   │   │   └── models.go
│   │   │   ├── responses/
│   │   │   │   ├── channels.go
│   │   │   │   ├── compact.go
│   │   │   │   └── handler.go
│   │   │   └── settings.go
│   │   ├── httpclient/
│   │   │   └── client.go
│   │   ├── logger/
│   │   │   └── logger.go
│   │   ├── metrics/
│   │   │   ├── channel_metrics.go
│   │   │   ├── channel_metrics_activity_test.go
│   │   │   ├── channel_metrics_cache_stats_test.go
│   │   │   ├── persistence.go
│   │   │   └── sqlite_store.go
│   │   ├── middleware/
│   │   │   ├── auth.go
│   │   │   ├── auth_test.go
│   │   │   ├── cors.go
│   │   │   └── logger.go
│   │   ├── providers/
│   │   │   ├── claude.go
│   │   │   ├── gemini.go
│   │   │   ├── openai.go
│   │   │   ├── provider.go
│   │   │   ├── request_context_test.go
│   │   │   ├── responses.go
│   │   │   └── url_builder_test.go
│   │   ├── scheduler/
│   │   │   ├── channel_scheduler.go
│   │   │   └── channel_scheduler_test.go
│   │   ├── session/
│   │   │   ├── manager.go
│   │   │   └── trace_affinity.go
│   │   ├── types/
│   │   │   ├── gemini.go
│   │   │   ├── gemini_test.go
│   │   │   ├── responses.go
│   │   │   └── types.go
│   │   ├── utils/
│   │   │   ├── compression.go
│   │   │   ├── headers.go
│   │   │   ├── headers_test.go
│   │   │   ├── json.go
│   │   │   ├── json_compact_test.go
│   │   │   ├── json_test.go
│   │   │   ├── stream_synthesizer.go
│   │   │   ├── token_counter.go
│   │   │   └── token_counter_test.go
│   │   └── warmup/
│   │       └── url_manager.go
│   ├── main.go
│   └── version.go
├── docker-compose.yml
└── frontend/
    ├── .env.example
    ├── CLAUDE.md
    ├── ESLINT.md
    ├── eslint.config.js
    ├── index.html
    ├── package.json
    ├── src/
    │   ├── App.vue
    │   ├── assets/
    │   │   └── style.css
    │   ├── components/
    │   │   ├── AddChannelModal.vue
    │   │   ├── ChannelCard.vue
    │   │   ├── ChannelMetricsChart.vue
    │   │   ├── ChannelOrchestration.vue
    │   │   ├── ChannelStatusBadge.vue
    │   │   ├── GlobalStatsChart.vue
    │   │   └── KeyTrendChart.vue
    │   ├── composables/
    │   │   └── useTheme.ts
    │   ├── env.d.ts
    │   ├── main.ts
    │   ├── plugins/
    │   │   └── vuetify.ts
    │   ├── router/
    │   │   └── index.ts
    │   ├── services/
    │   │   ├── api.ts
    │   │   └── version.ts
    │   ├── stores/
    │   │   ├── auth.ts
    │   │   ├── channel.ts
    │   │   ├── dialog.ts
    │   │   ├── index.ts
    │   │   ├── preferences.ts
    │   │   └── system.ts
    │   ├── styles/
    │   │   └── settings.scss
    │   ├── utils/
    │   │   ├── quickInputParser.test.ts
    │   │   └── quickInputParser.ts
    │   └── views/
    │       └── ChannelsView.vue
    ├── tsconfig.json
    └── vite.config.ts
Download .txt
SYMBOL INDEX (954 symbols across 91 files)

FILE: backend-go/internal/config/config.go
  type UpstreamConfig (line 17) | type UpstreamConfig struct
  type UpstreamUpdate (line 39) | type UpstreamUpdate struct
  type Config (line 60) | type Config struct
  type FailedKey (line 79) | type FailedKey struct
  type ConfigManager (line 85) | type ConfigManager struct
    method GetConfig (line 100) | func (cm *ConfigManager) GetConfig() Config {
    method GetNextAPIKey (line 136) | func (cm *ConfigManager) GetNextAPIKey(upstream *UpstreamConfig, faile...
    method MarkKeyAsFailed (line 196) | func (cm *ConfigManager) MarkKeyAsFailed(apiKey string, apiType string) {
    method isKeyFailed (line 221) | func (cm *ConfigManager) isKeyFailed(apiKey string) bool {
    method IsKeyFailed (line 239) | func (cm *ConfigManager) IsKeyFailed(apiKey string) bool {
    method clearFailedKeysForUpstream (line 246) | func (cm *ConfigManager) clearFailedKeysForUpstream(upstream *Upstream...
    method cleanupExpiredFailures (line 256) | func (cm *ConfigManager) cleanupExpiredFailures() {
    method GetFuzzyModeEnabled (line 286) | func (cm *ConfigManager) GetFuzzyModeEnabled() bool {
    method SetFuzzyModeEnabled (line 293) | func (cm *ConfigManager) SetFuzzyModeEnabled(enabled bool) error {

FILE: backend-go/internal/config/config_baseurl_test.go
  function TestUpdateUpstream_BaseURLConsistency (line 15) | func TestUpdateUpstream_BaseURLConsistency(t *testing.T) {
  function TestUpdateResponsesUpstream_BaseURLConsistency (line 114) | func TestUpdateResponsesUpstream_BaseURLConsistency(t *testing.T) {
  function TestUpdateGeminiUpstream_BaseURLConsistency (line 161) | func TestUpdateGeminiUpstream_BaseURLConsistency(t *testing.T) {
  function TestGetAllBaseURLs_Priority (line 208) | func TestGetAllBaseURLs_Priority(t *testing.T) {
  function TestGetEffectiveBaseURL_Priority (line 266) | func TestGetEffectiveBaseURL_Priority(t *testing.T) {
  function TestDeduplicateBaseURLs (line 309) | func TestDeduplicateBaseURLs(t *testing.T) {
  function TestAddUpstream_BaseURLDeduplication (line 370) | func TestAddUpstream_BaseURLDeduplication(t *testing.T) {
  function strPtr (line 412) | func strPtr(s string) *string {

FILE: backend-go/internal/config/config_gemini.go
  method GetCurrentGeminiUpstream (line 16) | func (cm *ConfigManager) GetCurrentGeminiUpstream() (*UpstreamConfig, er...
  method AddGeminiUpstream (line 37) | func (cm *ConfigManager) AddGeminiUpstream(upstream UpstreamConfig) error {
  method UpdateGeminiUpstream (line 62) | func (cm *ConfigManager) UpdateGeminiUpstream(index int, updates Upstrea...
  method RemoveGeminiUpstream (line 176) | func (cm *ConfigManager) RemoveGeminiUpstream(index int) (*UpstreamConfi...
  method AddGeminiAPIKey (line 199) | func (cm *ConfigManager) AddGeminiAPIKey(index int, apiKey string) error {
  method RemoveGeminiAPIKey (line 236) | func (cm *ConfigManager) RemoveGeminiAPIKey(index int, apiKey string) er...
  method GetNextGeminiAPIKey (line 281) | func (cm *ConfigManager) GetNextGeminiAPIKey(upstream *UpstreamConfig, f...
  method MoveGeminiAPIKeyToTop (line 286) | func (cm *ConfigManager) MoveGeminiAPIKeyToTop(upstreamIndex int, apiKey...
  method MoveGeminiAPIKeyToBottom (line 312) | func (cm *ConfigManager) MoveGeminiAPIKeyToBottom(upstreamIndex int, api...
  method ReorderGeminiUpstreams (line 340) | func (cm *ConfigManager) ReorderGeminiUpstreams(order []int) error {
  method SetGeminiChannelStatus (line 374) | func (cm *ConfigManager) SetGeminiChannelStatus(index int, status string...
  method SetGeminiChannelPromotion (line 405) | func (cm *ConfigManager) SetGeminiChannelPromotion(index int, duration t...
  method GetPromotedGeminiChannel (line 432) | func (cm *ConfigManager) GetPromotedGeminiChannel() (int, bool) {
  method SetGeminiLoadBalance (line 445) | func (cm *ConfigManager) SetGeminiLoadBalance(strategy string) error {

FILE: backend-go/internal/config/config_loader.go
  constant maxBackups (line 15) | maxBackups      = 10
  constant keyRecoveryTime (line 16) | keyRecoveryTime = 5 * time.Minute
  constant maxFailureCount (line 17) | maxFailureCount = 3
  function NewConfigManager (line 21) | func NewConfigManager(configFile string) (*ConfigManager, error) {
  method loadConfig (line 47) | func (cm *ConfigManager) loadConfig() error {
  method createDefaultConfig (line 96) | func (cm *ConfigManager) createDefaultConfig() error {
  method applyConfigDefaults (line 119) | func (cm *ConfigManager) applyConfigDefaults(rawJSON []byte) bool {
  method migrateOldFormat (line 149) | func (cm *ConfigManager) migrateOldFormat() bool {
  method migrateUpstreams (line 170) | func (cm *ConfigManager) migrateUpstreams(upstreams []UpstreamConfig, cu...
  method validateChannelKeys (line 204) | func (cm *ConfigManager) validateChannelKeys() bool {
  method saveConfigLocked (line 259) | func (cm *ConfigManager) saveConfigLocked(config Config) error {
  method SaveConfig (line 277) | func (cm *ConfigManager) SaveConfig() error {
  method backupConfig (line 284) | func (cm *ConfigManager) backupConfig() {
  method cleanupOldBackups (line 315) | func (cm *ConfigManager) cleanupOldBackups(backupDir string) {
  method startWatcher (line 332) | func (cm *ConfigManager) startWatcher() error {
  method Close (line 370) | func (cm *ConfigManager) Close() error {

FILE: backend-go/internal/config/config_messages.go
  method GetCurrentUpstream (line 16) | func (cm *ConfigManager) GetCurrentUpstream() (*UpstreamConfig, error) {
  method AddUpstream (line 37) | func (cm *ConfigManager) AddUpstream(upstream UpstreamConfig) error {
  method UpdateUpstream (line 62) | func (cm *ConfigManager) UpdateUpstream(index int, updates UpstreamUpdat...
  method RemoveUpstream (line 173) | func (cm *ConfigManager) RemoveUpstream(index int) (*UpstreamConfig, err...
  method AddAPIKey (line 196) | func (cm *ConfigManager) AddAPIKey(index int, apiKey string) error {
  method RemoveAPIKey (line 233) | func (cm *ConfigManager) RemoveAPIKey(index int, apiKey string) error {
  method SetLoadBalance (line 278) | func (cm *ConfigManager) SetLoadBalance(strategy string) error {
  method MoveAPIKeyToTop (line 297) | func (cm *ConfigManager) MoveAPIKeyToTop(upstreamIndex int, apiKey strin...
  method MoveAPIKeyToBottom (line 324) | func (cm *ConfigManager) MoveAPIKeyToBottom(upstreamIndex int, apiKey st...
  method ReorderUpstreams (line 353) | func (cm *ConfigManager) ReorderUpstreams(order []int) error {
  method SetChannelStatus (line 388) | func (cm *ConfigManager) SetChannelStatus(index int, status string) error {
  method SetChannelPromotion (line 420) | func (cm *ConfigManager) SetChannelPromotion(index int, duration time.Du...
  method GetPromotedChannel (line 447) | func (cm *ConfigManager) GetPromotedChannel() (int, bool) {
  method DeprioritizeAPIKey (line 460) | func (cm *ConfigManager) DeprioritizeAPIKey(apiKey string) error {

FILE: backend-go/internal/config/config_responses.go
  method GetCurrentResponsesUpstream (line 16) | func (cm *ConfigManager) GetCurrentResponsesUpstream() (*UpstreamConfig,...
  method AddResponsesUpstream (line 37) | func (cm *ConfigManager) AddResponsesUpstream(upstream UpstreamConfig) e...
  method UpdateResponsesUpstream (line 62) | func (cm *ConfigManager) UpdateResponsesUpstream(index int, updates Upst...
  method RemoveResponsesUpstream (line 170) | func (cm *ConfigManager) RemoveResponsesUpstream(index int) (*UpstreamCo...
  method AddResponsesAPIKey (line 193) | func (cm *ConfigManager) AddResponsesAPIKey(index int, apiKey string) er...
  method RemoveResponsesAPIKey (line 230) | func (cm *ConfigManager) RemoveResponsesAPIKey(index int, apiKey string)...
  method GetNextResponsesAPIKey (line 275) | func (cm *ConfigManager) GetNextResponsesAPIKey(upstream *UpstreamConfig...
  method SetResponsesLoadBalance (line 280) | func (cm *ConfigManager) SetResponsesLoadBalance(strategy string) error {
  method MoveResponsesAPIKeyToTop (line 299) | func (cm *ConfigManager) MoveResponsesAPIKeyToTop(upstreamIndex int, api...
  method MoveResponsesAPIKeyToBottom (line 325) | func (cm *ConfigManager) MoveResponsesAPIKeyToBottom(upstreamIndex int, ...
  method ReorderResponsesUpstreams (line 353) | func (cm *ConfigManager) ReorderResponsesUpstreams(order []int) error {
  method SetResponsesChannelStatus (line 387) | func (cm *ConfigManager) SetResponsesChannelStatus(index int, status str...
  method SetResponsesChannelPromotion (line 418) | func (cm *ConfigManager) SetResponsesChannelPromotion(index int, duratio...
  method GetPromotedResponsesChannel (line 445) | func (cm *ConfigManager) GetPromotedResponsesChannel() (int, bool) {

FILE: backend-go/internal/config/config_utils.go
  function deduplicateStrings (line 12) | func deduplicateStrings(items []string) []string {
  function deduplicateBaseURLs (line 28) | func deduplicateBaseURLs(urls []string) []string {
  function validateLoadBalanceStrategy (line 45) | func validateLoadBalanceStrategy(strategy string) error {
  type ConfigError (line 55) | type ConfigError struct
    method Error (line 59) | func (e *ConfigError) Error() string {
  function RedirectModel (line 66) | func RedirectModel(model string, upstream *UpstreamConfig) string {
  function GetChannelStatus (line 104) | func GetChannelStatus(upstream *UpstreamConfig) string {
  function GetChannelPriority (line 112) | func GetChannelPriority(upstream *UpstreamConfig, index int) int {
  function IsChannelInPromotion (line 120) | func IsChannelInPromotion(upstream *UpstreamConfig) bool {
  method Clone (line 132) | func (u *UpstreamConfig) Clone() *UpstreamConfig {
  method GetEffectiveBaseURL (line 164) | func (u *UpstreamConfig) GetEffectiveBaseURL() string {
  method GetAllBaseURLs (line 179) | func (u *UpstreamConfig) GetAllBaseURLs() []string {

FILE: backend-go/internal/config/env.go
  type EnvConfig (line 8) | type EnvConfig struct
    method IsDevelopment (line 88) | func (c *EnvConfig) IsDevelopment() bool {
    method IsProduction (line 93) | func (c *EnvConfig) IsProduction() bool {
    method ShouldLog (line 98) | func (c *EnvConfig) ShouldLog(level string) bool {
  function NewEnvConfig (line 44) | func NewEnvConfig() *EnvConfig {
  function getEnv (line 120) | func getEnv(key, defaultValue string) string {
  function getEnvAsInt (line 128) | func getEnvAsInt(key string, defaultValue int) int {
  function getEnvAsInt64 (line 138) | func getEnvAsInt64(key string, defaultValue int64) int64 {
  function getEnvAsFloat (line 148) | func getEnvAsFloat(key string, defaultValue float64) float64 {
  function clampInt (line 158) | func clampInt(value, minVal, maxVal int) int {

FILE: backend-go/internal/converters/chat_to_responses.go
  type chatToResponsesState (line 15) | type chatToResponsesState struct
    method closeReasoningBlock (line 403) | func (st *chatToResponsesState) closeReasoningBlock(nextSeq func() int...
    method closeTextBlock (line 440) | func (st *chatToResponsesState) closeTextBlock(nextSeq func() int) []s...
    method closeFuncBlocks (line 480) | func (st *chatToResponsesState) closeFuncBlocks(nextSeq func() int) []...
    method generateCompletedEvents (line 542) | func (st *chatToResponsesState) generateCompletedEvents(originalReques...
  function emitResponsesEvent (line 50) | func emitResponsesEvent(event string, payload string) string {
  function ConvertOpenAIChatToResponses (line 61) | func ConvertOpenAIChatToResponses(ctx context.Context, modelName string,...
  function ConvertOpenAIChatToResponsesNonStream (line 721) | func ConvertOpenAIChatToResponsesNonStream(_ context.Context, _ string, ...

FILE: backend-go/internal/converters/chat_to_responses_test.go
  function TestConvertResponsesToOpenAIChatRequest (line 12) | func TestConvertResponsesToOpenAIChatRequest(t *testing.T) {
  function TestConvertOpenAIChatToResponses_Stream (line 135) | func TestConvertOpenAIChatToResponses_Stream(t *testing.T) {
  function TestConvertOpenAIChatToResponses_ToolCall (line 197) | func TestConvertOpenAIChatToResponses_ToolCall(t *testing.T) {
  function TestConvertOpenAIChatToResponsesNonStream (line 247) | func TestConvertOpenAIChatToResponsesNonStream(t *testing.T) {
  function TestConvertOpenAIChatToResponsesNonStream_ToolCalls (line 313) | func TestConvertOpenAIChatToResponsesNonStream_ToolCalls(t *testing.T) {

FILE: backend-go/internal/converters/claude_converter.go
  type ClaudeConverter (line 11) | type ClaudeConverter struct
    method ToProviderRequest (line 14) | func (c *ClaudeConverter) ToProviderRequest(sess *session.Session, req...
    method FromProviderResponse (line 51) | func (c *ClaudeConverter) FromProviderResponse(resp map[string]interfa...
    method GetProviderName (line 56) | func (c *ClaudeConverter) GetProviderName() string {

FILE: backend-go/internal/converters/converter.go
  function OpenAIFinishReasonToAnthropic (line 12) | func OpenAIFinishReasonToAnthropic(reason string) string {
  function AnthropicStopReasonToOpenAI (line 31) | func AnthropicStopReasonToOpenAI(reason string) string {
  function OpenAIFinishReasonToResponses (line 52) | func OpenAIFinishReasonToResponses(reason string) string {
  type ResponsesConverter (line 69) | type ResponsesConverter interface

FILE: backend-go/internal/converters/converter_test.go
  function TestExtractTextFromContent_String (line 12) | func TestExtractTextFromContent_String(t *testing.T) {
  function TestExtractTextFromContent_ContentBlockArray (line 21) | func TestExtractTextFromContent_ContentBlockArray(t *testing.T) {
  function TestExtractTextFromContent_MixedTypes (line 41) | func TestExtractTextFromContent_MixedTypes(t *testing.T) {
  function TestExtractTextFromContent_EmptyArray (line 65) | func TestExtractTextFromContent_EmptyArray(t *testing.T) {
  function TestOpenAIChatConverter_WithInstructions (line 76) | func TestOpenAIChatConverter_WithInstructions(t *testing.T) {
  function TestOpenAIChatConverter_WithMessageType (line 141) | func TestOpenAIChatConverter_WithMessageType(t *testing.T) {
  function TestClaudeConverter_WithInstructions (line 186) | func TestClaudeConverter_WithInstructions(t *testing.T) {
  function TestConverterFactory (line 229) | func TestConverterFactory(t *testing.T) {
  function TestOpenAIChatConverter_WithSessionHistory (line 256) | func TestOpenAIChatConverter_WithSessionHistory(t *testing.T) {
  function TestOpenAIFinishReasonToAnthropic (line 306) | func TestOpenAIFinishReasonToAnthropic(t *testing.T) {
  function TestAnthropicStopReasonToOpenAI (line 330) | func TestAnthropicStopReasonToOpenAI(t *testing.T) {
  function TestOpenAIFinishReasonToResponses (line 355) | func TestOpenAIFinishReasonToResponses(t *testing.T) {

FILE: backend-go/internal/converters/factory.go
  function NewConverter (line 8) | func NewConverter(serviceType string) ResponsesConverter {

FILE: backend-go/internal/converters/gemini_converter.go
  function GeminiToClaudeRequest (line 14) | func GeminiToClaudeRequest(geminiReq *types.GeminiRequest, model string)...
  function GeminiToOpenAIRequest (line 92) | func GeminiToOpenAIRequest(geminiReq *types.GeminiRequest, model string)...
  function ClaudeResponseToGemini (line 166) | func ClaudeResponseToGemini(claudeResp map[string]interface{}) (*types.G...
  function OpenAIResponseToGemini (line 249) | func OpenAIResponseToGemini(openaiResp map[string]interface{}) (*types.G...
  function geminiContentToClaudeMessage (line 340) | func geminiContentToClaudeMessage(content *types.GeminiContent) (map[str...
  function geminiContentToOpenAIMessage (line 408) | func geminiContentToOpenAIMessage(content *types.GeminiContent) (map[str...
  function extractTextFromGeminiParts (line 491) | func extractTextFromGeminiParts(parts []types.GeminiPart) string {
  function claudeStopReasonToGemini (line 502) | func claudeStopReasonToGemini(stopReason string) string {
  function openaiFinishReasonToGemini (line 516) | func openaiFinishReasonToGemini(finishReason string) string {
  function geminiFinishReasonToClaude (line 532) | func geminiFinishReasonToClaude(finishReason string) string {
  function geminiFinishReasonToOpenAI (line 546) | func geminiFinishReasonToOpenAI(finishReason string) string {
  function JSONMarshal (line 560) | func JSONMarshal(v interface{}) ([]byte, error) {
  function JSONUnmarshal (line 565) | func JSONUnmarshal(data []byte, v interface{}) error {

FILE: backend-go/internal/converters/gemini_converter_test.go
  function TestClaudeResponseToGemini_WithThoughtSignature (line 10) | func TestClaudeResponseToGemini_WithThoughtSignature(t *testing.T) {
  function TestOpenAIResponseToGemini_WithThoughtSignature (line 79) | func TestOpenAIResponseToGemini_WithThoughtSignature(t *testing.T) {

FILE: backend-go/internal/converters/openai_converter.go
  type OpenAIChatConverter (line 11) | type OpenAIChatConverter struct
    method ToProviderRequest (line 14) | func (c *OpenAIChatConverter) ToProviderRequest(sess *session.Session,...
    method FromProviderResponse (line 58) | func (c *OpenAIChatConverter) FromProviderResponse(resp map[string]int...
    method GetProviderName (line 63) | func (c *OpenAIChatConverter) GetProviderName() string {
  type OpenAICompletionsConverter (line 70) | type OpenAICompletionsConverter struct
    method ToProviderRequest (line 73) | func (c *OpenAICompletionsConverter) ToProviderRequest(sess *session.S...
    method FromProviderResponse (line 119) | func (c *OpenAICompletionsConverter) FromProviderResponse(resp map[str...
    method GetProviderName (line 124) | func (c *OpenAICompletionsConverter) GetProviderName() string {

FILE: backend-go/internal/converters/responses_converter.go
  function ResponsesToClaudeMessages (line 16) | func ResponsesToClaudeMessages(sess *session.Session, newInput interface...
  function responsesItemToClaudeMessage (line 50) | func responsesItemToClaudeMessage(item types.ResponsesItem) (*types.Clau...
  function ClaudeResponseToResponses (line 113) | func ClaudeResponseToResponses(claudeResp map[string]interface{}, sessio...
  function ResponsesToOpenAIChatMessages (line 155) | func ResponsesToOpenAIChatMessages(sess *session.Session, newInput inter...
  function responsesItemToOpenAIMessage (line 191) | func responsesItemToOpenAIMessage(item types.ResponsesItem) map[string]i...
  function OpenAIChatResponseToResponses (line 234) | func OpenAIChatResponseToResponses(openaiResp map[string]interface{}, se...
  function extractTextFromContent (line 276) | func extractTextFromContent(content interface{}) string {
  function parseResponsesInput (line 313) | func parseResponsesInput(input interface{}) ([]types.ResponsesItem, erro...
  function generateResponseID (line 353) | func generateResponseID() string {
  function getCurrentTimestamp (line 358) | func getCurrentTimestamp() int64 {
  function ExtractTextFromResponses (line 363) | func ExtractTextFromResponses(sess *session.Session, newInput interface{...
  function OpenAICompletionsResponseToResponses (line 393) | func OpenAICompletionsResponseToResponses(completionsResp map[string]int...
  function JSONToMap (line 425) | func JSONToMap(data []byte) (map[string]interface{}, error) {
  function getIntFromMap (line 433) | func getIntFromMap(m map[string]interface{}, key string) (int, bool) {
  function parseResponsesUsage (line 454) | func parseResponsesUsage(usageRaw interface{}) types.ResponsesUsage {
  function parseClaudeUsage (line 513) | func parseClaudeUsage(usageRaw interface{}) types.ResponsesUsage {
  function parseGeminiUsage (line 585) | func parseGeminiUsage(usageRaw interface{}) types.ResponsesUsage {
  function ExtractUsageMetrics (line 631) | func ExtractUsageMetrics(usageRaw interface{}) types.ResponsesUsage {

FILE: backend-go/internal/converters/responses_passthrough.go
  type ResponsesPassthroughConverter (line 12) | type ResponsesPassthroughConverter struct
    method ToProviderRequest (line 15) | func (c *ResponsesPassthroughConverter) ToProviderRequest(sess *sessio...
    method FromProviderResponse (line 36) | func (c *ResponsesPassthroughConverter) FromProviderResponse(resp map[...
    method GetProviderName (line 76) | func (c *ResponsesPassthroughConverter) GetProviderName() string {

FILE: backend-go/internal/converters/responses_to_chat.go
  function ConvertResponsesToOpenAIChatRequest (line 24) | func ConvertResponsesToOpenAIChatRequest(modelName string, inputRawJSON ...
  function convertInputArrayToMessages (line 118) | func convertInputArrayToMessages(input gjson.Result, out string) string {
  function convertMessageItem (line 145) | func convertMessageItem(item gjson.Result, out string) string {
  function convertFunctionCallItem (line 197) | func convertFunctionCallItem(item gjson.Result, out string) string {
  function convertFunctionCallOutputItem (line 222) | func convertFunctionCallOutputItem(item gjson.Result, out string) string {
  function convertToolsToOpenAIFormat (line 239) | func convertToolsToOpenAIFormat(tools gjson.Result, out string) string {

FILE: backend-go/internal/handlers/channel_metrics_handler.go
  function GetChannelMetricsWithConfig (line 15) | func GetChannelMetricsWithConfig(metricsManager *metrics.MetricsManager,...
  function GetAllKeyMetrics (line 62) | func GetAllKeyMetrics(metricsManager *metrics.MetricsManager) gin.Handle...
  function GetChannelMetrics (line 107) | func GetChannelMetrics(metricsManager *metrics.MetricsManager) gin.Handl...
  function GetResponsesChannelMetrics (line 153) | func GetResponsesChannelMetrics(metricsManager *metrics.MetricsManager) ...
  function ResumeChannel (line 159) | func ResumeChannel(sch *scheduler.ChannelScheduler, isResponses bool) gi...
  function GetSchedulerStats (line 183) | func GetSchedulerStats(sch *scheduler.ChannelScheduler) gin.HandlerFunc {
  function SetChannelPromotion (line 216) | func SetChannelPromotion(cfgManager ConfigManager) gin.HandlerFunc {
  function SetResponsesChannelPromotion (line 256) | func SetResponsesChannelPromotion(cfgManager ResponsesConfigManager) gin...
  type ConfigManager (line 295) | type ConfigManager interface
  type ResponsesConfigManager (line 300) | type ResponsesConfigManager interface
  type MetricsHistoryResponse (line 305) | type MetricsHistoryResponse struct
  function GetChannelMetricsHistory (line 315) | func GetChannelMetricsHistory(metricsManager *metrics.MetricsManager, cf...
  type ChannelKeyMetricsHistoryResponse (line 384) | type ChannelKeyMetricsHistoryResponse struct
  type KeyMetricsHistoryResult (line 391) | type KeyMetricsHistoryResult struct
  function GetChannelKeyMetricsHistory (line 408) | func GetChannelKeyMetricsHistory(metricsManager *metrics.MetricsManager,...
  function truncateKeyMask (line 525) | func truncateKeyMask(keyMask string, maxLen int) string {
  function GetChannelDashboard (line 535) | func GetChannelDashboard(cfgManager *config.ConfigManager, sch *schedule...
  function GetGeminiChannelMetricsHistory (line 648) | func GetGeminiChannelMetricsHistory(metricsManager *metrics.MetricsManag...
  function GetGeminiChannelKeyMetricsHistory (line 709) | func GetGeminiChannelKeyMetricsHistory(metricsManager *metrics.MetricsMa...
  function GetGeminiChannelMetrics (line 817) | func GetGeminiChannelMetrics(metricsManager *metrics.MetricsManager, cfg...

FILE: backend-go/internal/handlers/common/client_error_test.go
  function TestIsClientSideError (line 10) | func TestIsClientSideError(t *testing.T) {

FILE: backend-go/internal/handlers/common/failover.go
  type FailoverError (line 13) | type FailoverError struct
  function ShouldRetryWithNextKey (line 32) | func ShouldRetryWithNextKey(statusCode int, bodyBytes []byte, fuzzyMode ...
  function shouldRetryWithNextKeyFuzzy (line 44) | func shouldRetryWithNextKeyFuzzy(statusCode int, bodyBytes []byte, apiTy...
  function shouldRetryWithNextKeyNormal (line 79) | func shouldRetryWithNextKeyNormal(statusCode int, bodyBytes []byte, apiT...
  function classifyByStatusCode (line 112) | func classifyByStatusCode(statusCode int) (bool, bool) {
  function classifyByErrorMessage (line 159) | func classifyByErrorMessage(bodyBytes []byte, apiType string) (bool, boo...
  function classifyMessage (line 215) | func classifyMessage(msg string) (bool, bool) {
  function classifyErrorType (line 261) | func classifyErrorType(errType string) (bool, bool) {
  function HandleAllChannelsFailed (line 305) | func HandleAllChannelsFailed(c *gin.Context, fuzzyMode bool, lastFailove...
  function HandleAllKeysFailed (line 343) | func HandleAllKeysFailed(c *gin.Context, fuzzyMode bool, lastFailoverErr...
  function getMapKeys (line 381) | func getMapKeys(m map[string]interface{}) []string {
  function isNonRetryableErrorCode (line 391) | func isNonRetryableErrorCode(code string) bool {
  function isNonRetryableError (line 414) | func isNonRetryableError(bodyBytes []byte) bool {

FILE: backend-go/internal/handlers/common/failover_test.go
  function TestClassifyByStatusCode (line 9) | func TestClassifyByStatusCode(t *testing.T) {
  function TestClassifyMessage (line 64) | func TestClassifyMessage(t *testing.T) {
  function TestClassifyErrorType (line 114) | func TestClassifyErrorType(t *testing.T) {
  function TestClassifyByErrorMessage (line 158) | func TestClassifyByErrorMessage(t *testing.T) {
  function TestClassifyByErrorMessage_InvalidJSON (line 298) | func TestClassifyByErrorMessage_InvalidJSON(t *testing.T) {
  function TestShouldRetryWithNextKey_403WithPredeductQuotaError (line 316) | func TestShouldRetryWithNextKey_403WithPredeductQuotaError(t *testing.T) {
  function TestClassifyMessage_ChineseQuotaKeywords (line 331) | func TestClassifyMessage_ChineseQuotaKeywords(t *testing.T) {
  function TestShouldRetryWithNextKey (line 357) | func TestShouldRetryWithNextKey(t *testing.T) {
  function TestShouldRetryWithNextKeyFuzzyMode (line 477) | func TestShouldRetryWithNextKeyFuzzyMode(t *testing.T) {
  function TestShouldRetryWithNextKey_FuzzyMode_403WithQuotaMessage (line 590) | func TestShouldRetryWithNextKey_FuzzyMode_403WithQuotaMessage(t *testing...
  function TestIsNonRetryableErrorCode (line 649) | func TestIsNonRetryableErrorCode(t *testing.T) {
  function TestShouldRetryWithNextKey_SensitiveWordsDetected (line 691) | func TestShouldRetryWithNextKey_SensitiveWordsDetected(t *testing.T) {

FILE: backend-go/internal/handlers/common/multi_channel_failover.go
  type MultiChannelAttemptResult (line 14) | type MultiChannelAttemptResult struct
  type TrySelectedChannelFunc (line 25) | type TrySelectedChannelFunc
  type OnMultiChannelHandledFunc (line 28) | type OnMultiChannelHandledFunc
  type HandleAllFailedFunc (line 31) | type HandleAllFailedFunc
  function HandleMultiChannelFailover (line 35) | func HandleMultiChannelFailover(

FILE: backend-go/internal/handlers/common/request.go
  function ReadRequestBody (line 23) | func ReadRequestBody(c *gin.Context, maxBodySize int64) ([]byte, error) {
  function RestoreRequestBody (line 44) | func RestoreRequestBody(c *gin.Context, bodyBytes []byte) {
  function SendRequest (line 51) | func SendRequest(req *http.Request, upstream *config.UpstreamConfig, env...
  function logRequestDetails (line 79) | func logRequestDetails(req *http.Request, envCfg *config.EnvConfig, apiT...
  function LogOriginalRequest (line 112) | func LogOriginalRequest(c *gin.Context, bodyBytes []byte, envCfg *config...
  function AreAllKeysSuspended (line 147) | func AreAllKeysSuspended(metricsManager *metrics.MetricsManager, baseURL...
  function RemoveEmptySignatures (line 165) | func RemoveEmptySignatures(bodyBytes []byte, enableLog bool, apiType str...
  function removeEmptySignaturesInMessages (line 193) | func removeEmptySignaturesInMessages(data map[string]interface{}) (bool,...
  function ExtractUserID (line 237) | func ExtractUserID(bodyBytes []byte) string {
  function ExtractConversationID (line 251) | func ExtractConversationID(c *gin.Context, bodyBytes []byte) string {

FILE: backend-go/internal/handlers/common/stream.go
  type StreamContext (line 22) | type StreamContext struct
  type CollectedUsageData (line 46) | type CollectedUsageData struct
  function NewStreamContext (line 58) | func NewStreamContext(envCfg *config.EnvConfig) *StreamContext {
  function seedSynthesizerFromRequest (line 73) | func seedSynthesizerFromRequest(ctx *StreamContext, requestBody []byte) {
  function SetupStreamHeaders (line 113) | func SetupStreamHeaders(c *gin.Context, resp *http.Response) {
  function ProcessStreamEvents (line 124) | func ProcessStreamEvents(
  function ProcessStreamEvent (line 166) | func ProcessStreamEvent(
  function updateCollectedUsage (line 326) | func updateCollectedUsage(collected *CollectedUsageData, usageData Colle...
  function inferImplicitCacheRead (line 355) | func inferImplicitCacheRead(ctx *StreamContext, enableLog bool) {
  function logStreamCompletion (line 386) | func logStreamCompletion(ctx *StreamContext, envCfg *config.EnvConfig, s...
  function logPartialResponse (line 431) | func logPartialResponse(ctx *StreamContext, envCfg *config.EnvConfig) {
  function logSynthesizedContent (line 438) | func logSynthesizedContent(ctx *StreamContext) {
  function IsClientDisconnectError (line 462) | func IsClientDisconnectError(err error) bool {
  function HandleStreamResponse (line 468) | func HandleStreamResponse(
  function CheckEventUsageStatus (line 506) | func CheckEventUsageStatus(event string, enableLog bool) (bool, bool, bo...
  function checkUsageFieldsWithPatch (line 548) | func checkUsageFieldsWithPatch(usage interface{}) (bool, bool, bool) {
  function extractUsageFromMap (line 580) | func extractUsageFromMap(usage map[string]interface{}) CollectedUsageData {
  function logUsageDetection (line 618) | func logUsageDetection(location string, usage map[string]interface{}, ne...
  function HasEventWithUsage (line 629) | func HasEventWithUsage(event string) bool {
  function PatchTokensInEvent (line 655) | func PatchTokensInEvent(event string, estimatedInputTokens, estimatedOut...
  function PatchTokensInEventWithCache (line 703) | func PatchTokensInEventWithCache(event string, estimatedInputTokens, est...
  function PatchMessageStartInputTokensIfNeeded (line 771) | func PatchMessageStartInputTokensIfNeeded(event string, requestBody []by...
  function patchUsageFieldsWithLog (line 799) | func patchUsageFieldsWithLog(usage map[string]interface{}, estimatedInpu...
  function abs (line 891) | func abs(x int) int {
  function BuildStreamErrorEvent (line 899) | func BuildStreamErrorEvent(err error) string {
  function BuildUsageEvent (line 912) | func BuildUsageEvent(requestBody []byte, outputText string) string {
  function IsMessageStartEvent (line 928) | func IsMessageStartEvent(event string) bool {
  function PatchMessageStartEvent (line 934) | func PatchMessageStartEvent(event string, requestModel string, rewriteMo...
  function IsMessageStopEvent (line 1005) | func IsMessageStopEvent(event string) bool {
  function IsMessageDeltaEvent (line 1029) | func IsMessageDeltaEvent(event string) bool {
  function ExtractInputTokensFromEvent (line 1051) | func ExtractInputTokensFromEvent(event string) int {
  function ExtractTextFromEvent (line 1083) | func ExtractTextFromEvent(event string, buf *bytes.Buffer) {
  function extractSSEEventInfo (line 1115) | func extractSSEEventInfo(event string) (eventType string, blockIndex int...
  function truncateForLog (line 1143) | func truncateForLog(s string, maxLen int) string {

FILE: backend-go/internal/handlers/common/stream_test.go
  function TestPatchUsageFieldsWithLog_NilInputTokens (line 11) | func TestPatchUsageFieldsWithLog_NilInputTokens(t *testing.T) {
  function TestPatchMessageStartInputTokensIfNeeded (line 63) | func TestPatchMessageStartInputTokensIfNeeded(t *testing.T) {
  function TestInferImplicitCacheRead (line 168) | func TestInferImplicitCacheRead(t *testing.T) {
  function TestPatchTokensInEventWithCache (line 276) | func TestPatchTokensInEventWithCache(t *testing.T) {

FILE: backend-go/internal/handlers/common/upstream_failover.go
  function isClientSideError (line 23) | func isClientSideError(err error) bool {
  type NextAPIKeyFunc (line 32) | type NextAPIKeyFunc
  type BuildRequestFunc (line 35) | type BuildRequestFunc
  type DeprioritizeKeyFunc (line 38) | type DeprioritizeKeyFunc
  type HandleSuccessFunc (line 42) | type HandleSuccessFunc
  function TryUpstreamWithAllKeys (line 51) | func TryUpstreamWithAllKeys(
  function BuildDefaultURLResults (line 240) | func BuildDefaultURLResults(urls []string) []warmup.URLLatencyResult {

FILE: backend-go/internal/handlers/frontend.go
  function ServeFrontend (line 13) | func ServeFrontend(r *gin.Engine, frontendFS embed.FS) {
  function isAPIPath (line 76) | func isAPIPath(path string) bool {
  function getContentType (line 94) | func getContentType(path string) string {
  function getErrorPage (line 141) | func getErrorPage() string {

FILE: backend-go/internal/handlers/gemini/channels.go
  function GetUpstreams (line 17) | func GetUpstreams(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function AddUpstream (line 55) | func AddUpstream(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function UpdateUpstream (line 73) | func UpdateUpstream(cfgManager *config.ConfigManager, sch *scheduler.Cha...
  function DeleteUpstream (line 104) | func DeleteUpstream(cfgManager *config.ConfigManager, sch *scheduler.Cha...
  function AddApiKey (line 131) | func AddApiKey(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function DeleteApiKey (line 167) | func DeleteApiKey(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function MoveApiKeyToTop (line 200) | func MoveApiKeyToTop(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function MoveApiKeyToBottom (line 214) | func MoveApiKeyToBottom(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function ReorderChannels (line 228) | func ReorderChannels(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function SetChannelStatus (line 251) | func SetChannelStatus(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function SetChannelPromotion (line 286) | func SetChannelPromotion(cfgManager *config.ConfigManager) gin.HandlerFu...
  function PingChannel (line 325) | func PingChannel(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function PingAllChannels (line 379) | func PingAllChannels(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function UpdateLoadBalance (line 436) | func UpdateLoadBalance(cfgManager *config.ConfigManager) gin.HandlerFunc {

FILE: backend-go/internal/handlers/gemini/dashboard.go
  function GetDashboard (line 14) | func GetDashboard(cfgManager *config.ConfigManager, sch *scheduler.Chann...

FILE: backend-go/internal/handlers/gemini/dashboard_test.go
  function TestGetDashboard_IncludesStripThoughtSignature (line 20) | func TestGetDashboard_IncludesStripThoughtSignature(t *testing.T) {

FILE: backend-go/internal/handlers/gemini/handler.go
  function Handler (line 26) | func Handler(
  function extractModelName (line 103) | func extractModelName(param string) string {
  function handleMultiChannel (line 115) | func handleMultiChannel(
  function handleSingleChannel (line 196) | func handleSingleChannel(
  function ensureThoughtSignatures (line 280) | func ensureThoughtSignatures(geminiReq *types.GeminiRequest) {
  function stripThoughtSignature (line 293) | func stripThoughtSignature(geminiReq *types.GeminiRequest) {
  function cloneGeminiRequest (line 306) | func cloneGeminiRequest(req *types.GeminiRequest) *types.GeminiRequest {
  function buildProviderRequest (line 314) | func buildProviderRequest(
  function handleSuccess (line 448) | func handleSuccess(
  function handleAllChannelsFailed (line 547) | func handleAllChannelsFailed(c *gin.Context, failoverErr *common.Failove...
  function handleAllKeysFailed (line 568) | func handleAllKeysFailed(c *gin.Context, failoverErr *common.FailoverErr...

FILE: backend-go/internal/handlers/gemini/handler_test.go
  function TestHandler_RequiresProxyAccessKeyEvenWhenGeminiKeyProvided (line 16) | func TestHandler_RequiresProxyAccessKeyEvenWhenGeminiKeyProvided(t *test...
  function TestStripThoughtSignature (line 54) | func TestStripThoughtSignature(t *testing.T) {
  function TestBuildProviderRequest_StripThoughtSignature (line 249) | func TestBuildProviderRequest_StripThoughtSignature(t *testing.T) {
  function TestBuildProviderRequest_InjectDummyThoughtSignature_PreservesThoughtSignatureAtPartLevel (line 359) | func TestBuildProviderRequest_InjectDummyThoughtSignature_PreservesThoug...

FILE: backend-go/internal/handlers/gemini/stream.go
  function handleStreamSuccess (line 18) | func handleStreamSuccess(
  function streamGeminiToGemini (line 60) | func streamGeminiToGemini(
  function streamClaudeToGemini (line 105) | func streamClaudeToGemini(
  function streamOpenAIToGemini (line 212) | func streamOpenAIToGemini(
  function openaiFinishReasonToGemini (line 353) | func openaiFinishReasonToGemini(finishReason string) string {

FILE: backend-go/internal/handlers/global_stats_handler.go
  function GetGlobalStatsHistory (line 12) | func GetGlobalStatsHistory(metricsManager *metrics.MetricsManager) gin.H...

FILE: backend-go/internal/handlers/health.go
  function HealthCheck (line 11) | func HealthCheck(envCfg *config.EnvConfig, cfgManager *config.ConfigMana...
  function getVersion (line 33) | func getVersion() gin.H {
  function getVersionString (line 51) | func getVersionString() string { return versionString }
  function getBuildTime (line 52) | func getBuildTime() string     { return buildTime }
  function getGitCommit (line 53) | func getGitCommit() string     { return gitCommit }
  function SetVersionInfo (line 56) | func SetVersionInfo(version, build, commit string) {
  function SaveConfigHandler (line 63) | func SaveConfigHandler(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function DevInfo (line 90) | func DevInfo(envCfg *config.EnvConfig, cfgManager *config.ConfigManager)...

FILE: backend-go/internal/handlers/messages/channels.go
  function GetUpstreams (line 18) | func GetUpstreams(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function AddUpstream (line 54) | func AddUpstream(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function UpdateUpstream (line 75) | func UpdateUpstream(cfgManager *config.ConfigManager, sch *scheduler.Cha...
  function DeleteUpstream (line 113) | func DeleteUpstream(cfgManager *config.ConfigManager, sch *scheduler.Cha...
  function AddApiKey (line 143) | func AddApiKey(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function DeleteApiKey (line 179) | func DeleteApiKey(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function MoveApiKeyToTop (line 212) | func MoveApiKeyToTop(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function MoveApiKeyToBottom (line 237) | func MoveApiKeyToBottom(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function UpdateLoadBalance (line 262) | func UpdateLoadBalance(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function ReorderChannels (line 289) | func ReorderChannels(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function SetChannelStatus (line 309) | func SetChannelStatus(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function SetChannelPromotion (line 337) | func SetChannelPromotion(cfgManager *config.ConfigManager) gin.HandlerFu...
  function PingChannel (line 377) | func PingChannel(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function pingChannelURLs (line 399) | func pingChannelURLs(ch *config.UpstreamConfig) gin.H {
  function pingURL (line 466) | func pingURL(testURL string, insecureSkipVerify bool) gin.H {
  function PingAllChannels (line 484) | func PingAllChannels(cfgManager *config.ConfigManager) gin.HandlerFunc {

FILE: backend-go/internal/handlers/messages/handler.go
  function Handler (line 24) | func Handler(envCfg *config.EnvConfig, cfgManager *config.ConfigManager,...
  function handleMultiChannel (line 69) | func handleMultiChannel(
  function handleSingleChannel (line 159) | func handleSingleChannel(
  function handleNormalResponse (line 238) | func handleNormalResponse(
  function CountTokensHandler (line 362) | func CountTokensHandler(envCfg *config.EnvConfig, cfgManager *config.Con...

FILE: backend-go/internal/handlers/messages/models.go
  constant modelsRequestTimeout (line 21) | modelsRequestTimeout = 30 * time.Second
  type ModelsResponse (line 24) | type ModelsResponse struct
  type ModelEntry (line 30) | type ModelEntry struct
  function ModelsHandler (line 38) | func ModelsHandler(envCfg *config.EnvConfig, cfgManager *config.ConfigMa...
  function ModelsDetailHandler (line 75) | func ModelsDetailHandler(envCfg *config.EnvConfig, cfgManager *config.Co...
  function fetchModelsFromChannels (line 115) | func fetchModelsFromChannels(c *gin.Context, cfgManager *config.ConfigMa...
  function mergeModels (line 135) | func mergeModels(models1, models2 []ModelEntry) []ModelEntry {
  function tryModelsRequest (line 159) | func tryModelsRequest(c *gin.Context, cfgManager *config.ConfigManager, ...
  function buildModelsURL (line 241) | func buildModelsURL(baseURL string) string {

FILE: backend-go/internal/handlers/responses/channels.go
  function GetUpstreams (line 14) | func GetUpstreams(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function AddUpstream (line 50) | func AddUpstream(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function UpdateUpstream (line 68) | func UpdateUpstream(cfgManager *config.ConfigManager, sch *scheduler.Cha...
  function DeleteUpstream (line 99) | func DeleteUpstream(cfgManager *config.ConfigManager, sch *scheduler.Cha...
  function AddApiKey (line 126) | func AddApiKey(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function DeleteApiKey (line 162) | func DeleteApiKey(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function MoveApiKeyToTop (line 195) | func MoveApiKeyToTop(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function MoveApiKeyToBottom (line 209) | func MoveApiKeyToBottom(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function UpdateLoadBalance (line 223) | func UpdateLoadBalance(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function ReorderChannels (line 250) | func ReorderChannels(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function SetChannelStatus (line 273) | func SetChannelStatus(cfgManager *config.ConfigManager) gin.HandlerFunc {

FILE: backend-go/internal/handlers/responses/compact.go
  type compactError (line 22) | type compactError struct
  function CompactHandler (line 30) | func CompactHandler(
  function handleSingleChannelCompact (line 65) | func handleSingleChannelCompact(
  function handleMultiChannelCompact (line 130) | func handleMultiChannelCompact(
  function tryCompactChannelWithAllKeys (line 190) | func tryCompactChannelWithAllKeys(
  function tryCompactWithKey (line 249) | func tryCompactWithKey(
  function buildCompactURL (line 291) | func buildCompactURL(upstream *config.UpstreamConfig) string {

FILE: backend-go/internal/handlers/responses/handler.go
  function Handler (line 29) | func Handler(
  function handleMultiChannel (line 75) | func handleMultiChannel(
  function handleSingleChannel (line 158) | func handleSingleChannel(
  function handleSuccess (line 231) | func handleSuccess(
  function patchResponsesUsage (line 339) | func patchResponsesUsage(resp *types.ResponsesResponse, requestBody []by...
  function estimateResponsesOutputFromItems (line 399) | func estimateResponsesOutputFromItems(output []types.ResponsesItem) int {
  function handleStreamSuccess (line 457) | func handleStreamSuccess(
  type responsesStreamUsage (line 633) | type responsesStreamUsage struct
  function extractResponsesTextFromEvent (line 646) | func extractResponsesTextFromEvent(event string, buf *bytes.Buffer) {
  function checkResponsesEventUsage (line 696) | func checkResponsesEventUsage(event string, enableLog bool) (bool, bool,...
  function extractResponsesUsageFromMap (line 750) | func extractResponsesUsageFromMap(usage map[string]interface{}) response...
  function updateResponsesStreamUsage (line 818) | func updateResponsesStreamUsage(collected *responsesStreamUsage, usageDa...
  function isResponsesCompletedEvent (line 850) | func isResponsesCompletedEvent(event string) bool {
  function isClientDisconnectError (line 856) | func isClientDisconnectError(err error) bool {
  function injectResponsesUsageToCompletedEvent (line 863) | func injectResponsesUsageToCompletedEvent(event string, requestBody []by...
  function patchResponsesCompletedEventUsage (line 1042) | func patchResponsesCompletedEventUsage(event string, requestBody []byte,...
  function parseInputToItems (line 1124) | func parseInputToItems(input interface{}) ([]types.ResponsesItem, error) {

FILE: backend-go/internal/handlers/settings.go
  function GetFuzzyMode (line 10) | func GetFuzzyMode(cfgManager *config.ConfigManager) gin.HandlerFunc {
  function SetFuzzyMode (line 19) | func SetFuzzyMode(cfgManager *config.ConfigManager) gin.HandlerFunc {

FILE: backend-go/internal/httpclient/client.go
  type ClientManager (line 14) | type ClientManager struct
    method GetStandardClient (line 30) | func (cm *ClientManager) GetStandardClient(timeout time.Duration, inse...
    method GetStreamClient (line 77) | func (cm *ClientManager) GetStreamClient(insecure bool) *http.Client {
  function GetManager (line 24) | func GetManager() *ClientManager {

FILE: backend-go/internal/logger/logger.go
  type Config (line 14) | type Config struct
  function DefaultConfig (line 32) | func DefaultConfig() *Config {
  function Setup (line 45) | func Setup(cfg *Config) error {

FILE: backend-go/internal/metrics/channel_metrics.go
  type RequestRecord (line 16) | type RequestRecord struct
  type KeyMetrics (line 26) | type KeyMetrics struct
  type ChannelMetrics (line 47) | type ChannelMetrics struct
  type TimeWindowStats (line 63) | type TimeWindowStats struct
  type MetricsManager (line 79) | type MetricsManager struct
    method loadFromStore (line 158) | func (m *MetricsManager) loadFromStore() error {
    method getOrCreateKeyLocked (line 238) | func (m *MetricsManager) getOrCreateKeyLocked(baseURL, metricsKey, key...
    method getOrCreateKey (line 266) | func (m *MetricsManager) getOrCreateKey(baseURL, apiKey string) *KeyMe...
    method RecordSuccess (line 283) | func (m *MetricsManager) RecordSuccess(baseURL, apiKey string) {
    method RecordSuccessWithUsage (line 288) | func (m *MetricsManager) RecordSuccessWithUsage(baseURL, apiKey string...
    method recordSuccessWithUsageLocked (line 295) | func (m *MetricsManager) recordSuccessWithUsageLocked(baseURL, apiKey ...
    method RecordFailure (line 346) | func (m *MetricsManager) RecordFailure(baseURL, apiKey string) {
    method recordFailureLocked (line 353) | func (m *MetricsManager) recordFailureLocked(baseURL, apiKey string, n...
    method RecordRequestConnected (line 392) | func (m *MetricsManager) RecordRequestConnected(baseURL, apiKey string...
    method RecordRequestConnectedAt (line 397) | func (m *MetricsManager) RecordRequestConnectedAt(baseURL, apiKey stri...
    method RecordRequestFinalizeSuccess (line 424) | func (m *MetricsManager) RecordRequestFinalizeSuccess(baseURL, apiKey ...
    method RecordRequestFinalizeFailure (line 498) | func (m *MetricsManager) RecordRequestFinalizeFailure(baseURL, apiKey ...
    method RecordRequestFinalizeClientCancel (line 559) | func (m *MetricsManager) RecordRequestFinalizeClientCancel(baseURL, ap...
    method RecordRequestStart (line 593) | func (m *MetricsManager) RecordRequestStart(baseURL, apiKey string) {
    method RecordRequestEnd (line 602) | func (m *MetricsManager) RecordRequestEnd(baseURL, apiKey string) {
    method isKeyCircuitBroken (line 615) | func (m *MetricsManager) isKeyCircuitBroken(metrics *KeyMetrics) bool {
    method calculateKeyFailureRateInternal (line 625) | func (m *MetricsManager) calculateKeyFailureRateInternal(metrics *KeyM...
    method appendToWindowKey (line 639) | func (m *MetricsManager) appendToWindowKey(metrics *KeyMetrics, succes...
    method appendToHistoryKey (line 648) | func (m *MetricsManager) appendToHistoryKey(metrics *KeyMetrics, times...
    method cleanupHistoryLocked (line 654) | func (m *MetricsManager) cleanupHistoryLocked(metrics *KeyMetrics) {
    method appendToHistoryKeyWithUsage (line 696) | func (m *MetricsManager) appendToHistoryKeyWithUsage(metrics *KeyMetri...
    method IsKeyHealthy (line 711) | func (m *MetricsManager) IsKeyHealthy(baseURL, apiKey string) bool {
    method IsChannelHealthyWithKeys (line 726) | func (m *MetricsManager) IsChannelHealthyWithKeys(baseURL string, acti...
    method CalculateKeyFailureRate (line 767) | func (m *MetricsManager) CalculateKeyFailureRate(baseURL, apiKey strin...
    method CalculateChannelFailureRate (line 781) | func (m *MetricsManager) CalculateChannelFailureRate(baseURL string, a...
    method GetKeyMetrics (line 812) | func (m *MetricsManager) GetKeyMetrics(baseURL, apiKey string) *KeyMet...
    method GetChannelAggregatedMetrics (line 836) | func (m *MetricsManager) GetChannelAggregatedMetrics(channelIndex int,...
    method GetChannelKeyUsageInfo (line 890) | func (m *MetricsManager) GetChannelKeyUsageInfo(baseURL string, apiKey...
    method GetChannelKeyUsageInfoMultiURL (line 943) | func (m *MetricsManager) GetChannelKeyUsageInfoMultiURL(baseURLs []str...
    method GetAllKeyMetrics (line 1054) | func (m *MetricsManager) GetAllKeyMetrics() []*KeyMetrics {
    method GetTimeWindowStatsForKey (line 1077) | func (m *MetricsManager) GetTimeWindowStatsForKey(baseURL, apiKey stri...
    method GetAllTimeWindowStatsForKey (line 1115) | func (m *MetricsManager) GetAllTimeWindowStatsForKey(baseURL, apiKey s...
    method ResetKeyFailureState (line 1126) | func (m *MetricsManager) ResetKeyFailureState(baseURL, apiKey string) {
    method ResetKey (line 1140) | func (m *MetricsManager) ResetKey(baseURL, apiKey string) {
    method ResetAll (line 1167) | func (m *MetricsManager) ResetAll() {
    method Stop (line 1175) | func (m *MetricsManager) Stop() {
    method DeleteKeysForChannel (line 1183) | func (m *MetricsManager) DeleteKeysForChannel(baseURLs, apiKeys []stri...
    method DeleteChannelMetrics (line 1212) | func (m *MetricsManager) DeleteChannelMetrics(baseURLs, apiKeys []stri...
    method cleanupCircuitBreakers (line 1233) | func (m *MetricsManager) cleanupCircuitBreakers() {
    method recoverExpiredCircuitBreakers (line 1254) | func (m *MetricsManager) recoverExpiredCircuitBreakers() {
    method cleanupStaleKeys (line 1274) | func (m *MetricsManager) cleanupStaleKeys() {
    method GetCircuitRecoveryTime (line 1305) | func (m *MetricsManager) GetCircuitRecoveryTime() time.Duration {
    method GetFailureThreshold (line 1310) | func (m *MetricsManager) GetFailureThreshold() float64 {
    method GetWindowSize (line 1315) | func (m *MetricsManager) GetWindowSize() int {
    method ToResponseMultiURL (line 1353) | func (m *MetricsManager) ToResponseMultiURL(channelIndex int, baseURLs...
    method ToResponse (line 1519) | func (m *MetricsManager) ToResponse(channelIndex int, baseURL string, ...
    method calculateAggregatedTimeWindowsInternal (line 1619) | func (m *MetricsManager) calculateAggregatedTimeWindowsInternal(baseUR...
    method calculateAggregatedTimeWindowsMultiURL (line 1683) | func (m *MetricsManager) calculateAggregatedTimeWindowsMultiURL(baseUR...
    method IsChannelHealthy (line 1754) | func (m *MetricsManager) IsChannelHealthy(channelIndex int) bool {
    method CalculateFailureRate (line 1760) | func (m *MetricsManager) CalculateFailureRate(channelIndex int) float64 {
    method CalculateSuccessRate (line 1765) | func (m *MetricsManager) CalculateSuccessRate(channelIndex int) float64 {
    method Reset (line 1770) | func (m *MetricsManager) Reset(channelIndex int) {
    method GetMetrics (line 1775) | func (m *MetricsManager) GetMetrics(channelIndex int) *ChannelMetrics {
    method GetAllMetrics (line 1780) | func (m *MetricsManager) GetAllMetrics() []*ChannelMetrics {
    method GetTimeWindowStats (line 1785) | func (m *MetricsManager) GetTimeWindowStats(channelIndex int, duration...
    method GetAllTimeWindowStats (line 1790) | func (m *MetricsManager) GetAllTimeWindowStats(channelIndex int) map[s...
    method ShouldSuspend (line 1800) | func (m *MetricsManager) ShouldSuspend(channelIndex int) bool {
    method ShouldSuspendKey (line 1805) | func (m *MetricsManager) ShouldSuspendKey(baseURL, apiKey string) bool {
    method GetHistoricalStats (line 1851) | func (m *MetricsManager) GetHistoricalStats(baseURL string, activeKeys...
    method GetHistoricalStatsMultiURL (line 1924) | func (m *MetricsManager) GetHistoricalStatsMultiURL(baseURLs []string,...
    method GetAllKeysHistoricalStats (line 2005) | func (m *MetricsManager) GetAllKeysHistoricalStats(duration, interval ...
    method GetKeyHistoricalStats (line 2073) | func (m *MetricsManager) GetKeyHistoricalStats(baseURL, apiKey string,...
    method GetKeyHistoricalStatsMultiURL (line 2162) | func (m *MetricsManager) GetKeyHistoricalStatsMultiURL(baseURLs []stri...
    method GetGlobalHistoricalStatsWithTokens (line 2305) | func (m *MetricsManager) GetGlobalHistoricalStatsWithTokens(duration, ...
    method GetRecentActivityMultiURL (line 2466) | func (m *MetricsManager) GetRecentActivityMultiURL(channelIndex int, b...
  function NewMetricsManager (line 94) | func NewMetricsManager() *MetricsManager {
  function NewMetricsManagerWithConfig (line 108) | func NewMetricsManagerWithConfig(windowSize int, failureThreshold float6...
  function NewMetricsManagerWithPersistence (line 128) | func NewMetricsManagerWithPersistence(windowSize int, failureThreshold f...
  function generateMetricsKey (line 254) | func generateMetricsKey(baseURL, apiKey string) string {
  function GenerateMetricsKey (line 261) | func GenerateMetricsKey(baseURL, apiKey string) string {
  type KeyUsageInfo (line 881) | type KeyUsageInfo struct
  function SelectTopKeys (line 1016) | func SelectTopKeys(infos []KeyUsageInfo, maxDisplay int) []KeyUsageInfo {
  type MetricsResponse (line 1322) | type MetricsResponse struct
  type KeyMetricsResponse (line 1340) | type KeyMetricsResponse struct
  type HistoryDataPoint (line 1827) | type HistoryDataPoint struct
  type KeyHistoryDataPoint (line 1836) | type KeyHistoryDataPoint struct
  type bucketData (line 1999) | type bucketData struct
  type keyBucketData (line 2259) | type keyBucketData struct
  type GlobalHistoryDataPoint (line 2272) | type GlobalHistoryDataPoint struct
  type GlobalStatsSummary (line 2285) | type GlobalStatsSummary struct
  type GlobalStatsHistoryResponse (line 2298) | type GlobalStatsHistoryResponse struct
  type globalBucketData (line 2420) | type globalBucketData struct
  function CalculateTodayDuration (line 2431) | func CalculateTodayDuration() time.Duration {
  type ActivitySegment (line 2440) | type ActivitySegment struct
  type ChannelRecentActivity (line 2449) | type ChannelRecentActivity struct

FILE: backend-go/internal/metrics/channel_metrics_activity_test.go
  function floatEquals (line 10) | func floatEquals(a, b, epsilon float64) bool {
  function TestGetRecentActivityMultiURL_EmptyInputs (line 14) | func TestGetRecentActivityMultiURL_EmptyInputs(t *testing.T) {
  function TestGetRecentActivityMultiURL_SegmentBoundaries (line 37) | func TestGetRecentActivityMultiURL_SegmentBoundaries(t *testing.T) {
  function TestGetRecentActivityMultiURL_FailureCount (line 116) | func TestGetRecentActivityMultiURL_FailureCount(t *testing.T) {
  function TestGetRecentActivityMultiURL_MultipleURLs (line 161) | func TestGetRecentActivityMultiURL_MultipleURLs(t *testing.T) {
  function TestGetRecentActivityMultiURL_MultipleKeys (line 204) | func TestGetRecentActivityMultiURL_MultipleKeys(t *testing.T) {
  function TestGetRecentActivityMultiURL_MultipleURLsAndKeys (line 247) | func TestGetRecentActivityMultiURL_MultipleURLsAndKeys(t *testing.T) {

FILE: backend-go/internal/metrics/channel_metrics_cache_stats_test.go
  function TestToResponse_TimeWindowsIncludesCacheStats (line 10) | func TestToResponse_TimeWindowsIncludesCacheStats(t *testing.T) {
  function TestRecordSuccessWithUsage_CacheCreationFallbackFromTTLBreakdown (line 53) | func TestRecordSuccessWithUsage_CacheCreationFallbackFromTTLBreakdown(t ...

FILE: backend-go/internal/metrics/persistence.go
  type PersistenceStore (line 8) | type PersistenceStore interface
  type PersistentRecord (line 27) | type PersistentRecord struct

FILE: backend-go/internal/metrics/sqlite_store.go
  type SQLiteStore (line 17) | type SQLiteStore struct
    method AddRecord (line 142) | func (s *SQLiteStore) AddRecord(record PersistentRecord) {
    method flush (line 165) | func (s *SQLiteStore) flush() {
    method batchInsertRecords (line 192) | func (s *SQLiteStore) batchInsertRecords(records []PersistentRecord) e...
    method LoadRecords (line 232) | func (s *SQLiteStore) LoadRecords(since time.Time, apiType string) ([]...
    method CleanupOldRecords (line 269) | func (s *SQLiteStore) CleanupOldRecords(before time.Time) (int64, erro...
    method DeleteRecordsByMetricsKeys (line 282) | func (s *SQLiteStore) DeleteRecordsByMetricsKeys(metricsKeys []string,...
    method flushLoop (line 331) | func (s *SQLiteStore) flushLoop() {
    method cleanupLoop (line 354) | func (s *SQLiteStore) cleanupLoop() {
    method doCleanup (line 374) | func (s *SQLiteStore) doCleanup() {
    method Close (line 385) | func (s *SQLiteStore) Close() error {
    method GetRecordCount (line 402) | func (s *SQLiteStore) GetRecordCount() (int64, error) {
  type SQLiteStoreConfig (line 39) | type SQLiteStoreConfig struct
  constant defaultBatchSize (line 46) | defaultBatchSize     = 100
  constant defaultFlushInterval (line 47) | defaultFlushInterval = 30 * time.Second
  function NewSQLiteStore (line 51) | func NewSQLiteStore(cfg *SQLiteStoreConfig) (*SQLiteStore, error) {
  function initSchema (line 111) | func initSchema(db *sql.DB) error {

FILE: backend-go/internal/middleware/auth.go
  function WebAuthMiddleware (line 13) | func WebAuthMiddleware(envCfg *config.EnvConfig, cfgManager *config.Conf...
  function isPollingEndpoint (line 90) | func isPollingEndpoint(path string) bool {
  function isStaticResource (line 108) | func isStaticResource(path string) bool {
  function getAPIKey (line 124) | func getAPIKey(c *gin.Context) string {
  function ProxyAuthMiddleware (line 144) | func ProxyAuthMiddleware(envCfg *config.EnvConfig) gin.HandlerFunc {

FILE: backend-go/internal/middleware/auth_test.go
  function setupRouterWithAuth (line 13) | func setupRouterWithAuth(envCfg *config.EnvConfig) *gin.Engine {
  function TestWebAuthMiddleware_APIRequiresKey (line 42) | func TestWebAuthMiddleware_APIRequiresKey(t *testing.T) {
  function TestWebAuthMiddleware_SPAPassesThrough (line 85) | func TestWebAuthMiddleware_SPAPassesThrough(t *testing.T) {
  function TestWebAuthMiddleware_AdminRequiresKey (line 102) | func TestWebAuthMiddleware_AdminRequiresKey(t *testing.T) {
  function TestWebAuthMiddleware_DevInfoRequiresKeyInDevelopment (line 133) | func TestWebAuthMiddleware_DevInfoRequiresKeyInDevelopment(t *testing.T) {
  function TestWebAuthMiddleware_AllowsV1BetaRoutesWhenWebUIDisabled (line 165) | func TestWebAuthMiddleware_AllowsV1BetaRoutesWhenWebUIDisabled(t *testin...

FILE: backend-go/internal/middleware/cors.go
  function CORSMiddleware (line 11) | func CORSMiddleware(envCfg *config.EnvConfig) gin.HandlerFunc {

FILE: backend-go/internal/middleware/logger.go
  function FilteredLogger (line 24) | func FilteredLogger(envCfg *config.EnvConfig, skipPrefixes ...string) gi...

FILE: backend-go/internal/providers/claude.go
  type ClaudeProvider (line 19) | type ClaudeProvider struct
    method ConvertToProviderRequest (line 53) | func (p *ClaudeProvider) ConvertToProviderRequest(c *gin.Context, upst...
    method ConvertToClaudeResponse (line 116) | func (p *ClaudeProvider) ConvertToClaudeResponse(providerResp *types.P...
    method HandleStreamResponse (line 125) | func (p *ClaudeProvider) HandleStreamResponse(body io.ReadCloser) (<-c...
  function redirectModelInBody (line 23) | func redirectModelInBody(bodyBytes []byte, upstream *config.UpstreamConf...

FILE: backend-go/internal/providers/gemini.go
  type GeminiProvider (line 19) | type GeminiProvider struct
    method ConvertToProviderRequest (line 22) | func (p *GeminiProvider) ConvertToProviderRequest(c *gin.Context, upst...
    method convertToGeminiRequest (line 67) | func (p *GeminiProvider) convertToGeminiRequest(claudeReq *types.Claud...
    method convertMessages (line 112) | func (p *GeminiProvider) convertMessages(claudeMessages []types.Claude...
    method convertMessage (line 126) | func (p *GeminiProvider) convertMessage(msg types.ClaudeMessage) map[s...
    method convertTools (line 209) | func (p *GeminiProvider) convertTools(claudeTools []types.ClaudeTool) ...
    method ConvertToClaudeResponse (line 255) | func (p *GeminiProvider) ConvertToClaudeResponse(providerResp *types.P...
    method HandleStreamResponse (line 354) | func (p *GeminiProvider) HandleStreamResponse(body io.ReadCloser) (<-c...
  function normalizeGeminiParameters (line 225) | func normalizeGeminiParameters(schema interface{}) map[string]interface{} {

FILE: backend-go/internal/providers/openai.go
  type OpenAIProvider (line 21) | type OpenAIProvider struct
    method ConvertToProviderRequest (line 24) | func (p *OpenAIProvider) ConvertToProviderRequest(c *gin.Context, upst...
    method convertMessages (line 100) | func (p *OpenAIProvider) convertMessages(claudeReq *types.ClaudeReques...
    method convertMessage (line 124) | func (p *OpenAIProvider) convertMessage(msg types.ClaudeMessage) []typ...
    method convertTools (line 226) | func (p *OpenAIProvider) convertTools(claudeTools []types.ClaudeTool) ...
    method ConvertToClaudeResponse (line 297) | func (p *OpenAIProvider) ConvertToClaudeResponse(providerResp *types.P...
    method HandleStreamResponse (line 357) | func (p *OpenAIProvider) HandleStreamResponse(body io.ReadCloser) (<-c...
  function cleanJsonSchema (line 244) | func cleanJsonSchema(schema interface{}) interface{} {
  type ToolCallAccumulator (line 564) | type ToolCallAccumulator struct
  function processToolUsePart (line 571) | func processToolUsePart(id, name string, input interface{}, index int) [...
  function extractSystemText (line 613) | func extractSystemText(system interface{}) string {
  function normalizeRole (line 641) | func normalizeRole(role string) string {
  function generateID (line 651) | func generateID() string {

FILE: backend-go/internal/providers/provider.go
  type Provider (line 13) | type Provider interface
  function GetProvider (line 25) | func GetProvider(serviceType string) Provider {

FILE: backend-go/internal/providers/request_context_test.go
  type testContextKey (line 14) | type testContextKey
  function newGinContext (line 16) | func newGinContext(method, url string, body []byte, ctx context.Context)...
  function TestConvertToProviderRequest_PropagatesContext (line 28) | func TestConvertToProviderRequest_PropagatesContext(t *testing.T) {

FILE: backend-go/internal/providers/responses.go
  type ResponsesProvider (line 21) | type ResponsesProvider struct
    method ConvertToProviderRequest (line 26) | func (p *ResponsesProvider) ConvertToProviderRequest(
    method buildTargetURL (line 124) | func (p *ResponsesProvider) buildTargetURL(upstream *config.UpstreamCo...
    method ConvertToClaudeResponse (line 156) | func (p *ResponsesProvider) ConvertToClaudeResponse(providerResp *type...
    method ConvertToResponsesResponse (line 162) | func (p *ResponsesProvider) ConvertToResponsesResponse(
    method HandleStreamResponse (line 179) | func (p *ResponsesProvider) HandleStreamResponse(body io.ReadCloser) (...

FILE: backend-go/internal/providers/url_builder_test.go
  function buildOpenAIURL (line 12) | func buildOpenAIURL(baseURL string) string {
  function buildClaudeURL (line 30) | func buildClaudeURL(baseURL, requestPath string) string {
  function TestOpenAIURL_SkipVersionWithHash (line 45) | func TestOpenAIURL_SkipVersionWithHash(t *testing.T) {
  function TestClaudeURL_SkipVersionWithHash (line 68) | func TestClaudeURL_SkipVersionWithHash(t *testing.T) {
  function TestBuildTargetURL_SkipVersionWithHash (line 93) | func TestBuildTargetURL_SkipVersionWithHash(t *testing.T) {

FILE: backend-go/internal/scheduler/channel_scheduler.go
  type ChannelScheduler (line 18) | type ChannelScheduler struct
    method getMetricsManager (line 59) | func (s *ChannelScheduler) getMetricsManager(kind ChannelKind) *metric...
    method SelectChannel (line 79) | func (s *ChannelScheduler) SelectChannel(
    method findPromotedChannel (line 195) | func (s *ChannelScheduler) findPromotedChannel(activeChannels []Channe...
    method selectFallbackChannel (line 214) | func (s *ChannelScheduler) selectFallbackChannel(
    method getActiveChannels (line 270) | func (s *ChannelScheduler) getActiveChannels(kind ChannelKind) []Chann...
    method getUpstreamByIndex (line 317) | func (s *ChannelScheduler) getUpstreamByIndex(index int, kind ChannelK...
    method RecordSuccess (line 339) | func (s *ChannelScheduler) RecordSuccess(baseURL, apiKey string, kind ...
    method RecordSuccessWithUsage (line 344) | func (s *ChannelScheduler) RecordSuccessWithUsage(baseURL, apiKey stri...
    method RecordFailure (line 349) | func (s *ChannelScheduler) RecordFailure(baseURL, apiKey string, kind ...
    method RecordRequestStart (line 354) | func (s *ChannelScheduler) RecordRequestStart(baseURL, apiKey string, ...
    method RecordRequestEnd (line 359) | func (s *ChannelScheduler) RecordRequestEnd(baseURL, apiKey string, ki...
    method SetTraceAffinity (line 364) | func (s *ChannelScheduler) SetTraceAffinity(userID string, channelInde...
    method UpdateTraceAffinity (line 371) | func (s *ChannelScheduler) UpdateTraceAffinity(userID string) {
    method GetMessagesMetricsManager (line 378) | func (s *ChannelScheduler) GetMessagesMetricsManager() *metrics.Metric...
    method GetResponsesMetricsManager (line 383) | func (s *ChannelScheduler) GetResponsesMetricsManager() *metrics.Metri...
    method GetGeminiMetricsManager (line 388) | func (s *ChannelScheduler) GetGeminiMetricsManager() *metrics.MetricsM...
    method GetTraceAffinityManager (line 393) | func (s *ChannelScheduler) GetTraceAffinityManager() *session.TraceAff...
    method ResetChannelMetrics (line 399) | func (s *ChannelScheduler) ResetChannelMetrics(channelIndex int, kind ...
    method ResetKeyMetrics (line 415) | func (s *ChannelScheduler) ResetKeyMetrics(baseURL, apiKey string, kin...
    method DeleteChannelMetrics (line 421) | func (s *ChannelScheduler) DeleteChannelMetrics(upstream *config.Upstr...
    method GetActiveChannelCount (line 436) | func (s *ChannelScheduler) GetActiveChannelCount(kind ChannelKind) int {
    method IsMultiChannelMode (line 441) | func (s *ChannelScheduler) IsMultiChannelMode(kind ChannelKind) bool {
    method GetSortedURLsForChannel (line 455) | func (s *ChannelScheduler) GetSortedURLsForChannel(
    method MarkURLSuccess (line 476) | func (s *ChannelScheduler) MarkURLSuccess(kind ChannelKind, channelInd...
    method MarkURLFailure (line 483) | func (s *ChannelScheduler) MarkURLFailure(kind ChannelKind, channelInd...
    method InvalidateURLCache (line 490) | func (s *ChannelScheduler) InvalidateURLCache(kind ChannelKind, channe...
    method GetURLManagerStats (line 497) | func (s *ChannelScheduler) GetURLManagerStats() map[string]interface{} {
  type ChannelKind (line 31) | type ChannelKind
  constant ChannelKindMessages (line 34) | ChannelKindMessages  ChannelKind = "messages"
  constant ChannelKindResponses (line 35) | ChannelKindResponses ChannelKind = "responses"
  constant ChannelKindGemini (line 36) | ChannelKindGemini    ChannelKind = "gemini"
  function NewChannelScheduler (line 40) | func NewChannelScheduler(
  type SelectionResult (line 71) | type SelectionResult struct
  type ChannelInfo (line 262) | type ChannelInfo struct
  function maskUserID (line 446) | func maskUserID(userID string) string {
  function kindSchedulerLogPrefix (line 504) | func kindSchedulerLogPrefix(kind ChannelKind) string {
  function urlManagerChannelKey (line 515) | func urlManagerChannelKey(kind ChannelKind, channelIndex int) int {
  function urlManagerChannelKeyOrdinal (line 520) | func urlManagerChannelKeyOrdinal(kind ChannelKind) int {

FILE: backend-go/internal/scheduler/channel_scheduler_test.go
  function createTestConfigManager (line 18) | func createTestConfigManager(t *testing.T, cfg config.Config) (*config.C...
  function createTestScheduler (line 56) | func createTestScheduler(t *testing.T, cfg config.Config) (*ChannelSched...
  function TestPromotedChannelBypassesHealthCheck (line 77) | func TestPromotedChannelBypassesHealthCheck(t *testing.T) {
  function TestPromotedChannelSkippedAfterFailure (line 136) | func TestPromotedChannelSkippedAfterFailure(t *testing.T) {
  function TestNonPromotedChannelStillChecksHealth (line 183) | func TestNonPromotedChannelStillChecksHealth(t *testing.T) {
  function TestExpiredPromotionNotBypassHealthCheck (line 228) | func TestExpiredPromotionNotBypassHealthCheck(t *testing.T) {

FILE: backend-go/internal/session/manager.go
  type Session (line 15) | type Session struct
  type SessionManager (line 25) | type SessionManager struct
    method GetOrCreateSession (line 53) | func (sm *SessionManager) GetOrCreateSession(previousResponseID string...
    method RecordResponseMapping (line 86) | func (sm *SessionManager) RecordResponseMapping(responseID, sessionID ...
    method AppendMessage (line 95) | func (sm *SessionManager) AppendMessage(sessionID string, item types.R...
    method UpdateLastResponseID (line 112) | func (sm *SessionManager) UpdateLastResponseID(sessionID, responseID s...
    method GetSession (line 126) | func (sm *SessionManager) GetSession(sessionID string) (*Session, erro...
    method cleanupLoop (line 139) | func (sm *SessionManager) cleanupLoop() {
    method cleanup (line 149) | func (sm *SessionManager) cleanup() {
    method GetStats (line 200) | func (sm *SessionManager) GetStats() map[string]interface{} {
  function NewSessionManager (line 37) | func NewSessionManager(maxAge time.Duration, maxMessages int, maxTokens ...
  function generateID (line 211) | func generateID(prefix string) string {

FILE: backend-go/internal/session/trace_affinity.go
  type TraceAffinity (line 15) | type TraceAffinity struct
  type TraceAffinityManager (line 21) | type TraceAffinityManager struct
    method GetPreferredChannel (line 61) | func (m *TraceAffinityManager) GetPreferredChannel(userID string) (int...
    method SetPreferredChannel (line 83) | func (m *TraceAffinityManager) SetPreferredChannel(userID string, chan...
    method UpdateLastUsed (line 114) | func (m *TraceAffinityManager) UpdateLastUsed(userID string) {
    method Remove (line 128) | func (m *TraceAffinityManager) Remove(userID string) {
    method RemoveByChannel (line 146) | func (m *TraceAffinityManager) RemoveByChannel(channelIndex int) {
    method Cleanup (line 163) | func (m *TraceAffinityManager) Cleanup() int {
    method cleanupLoop (line 184) | func (m *TraceAffinityManager) cleanupLoop() {
    method Stop (line 199) | func (m *TraceAffinityManager) Stop() {
    method Size (line 204) | func (m *TraceAffinityManager) Size() int {
    method GetTTL (line 211) | func (m *TraceAffinityManager) GetTTL() time.Duration {
    method GetAll (line 216) | func (m *TraceAffinityManager) GetAll() map[string]TraceAffinity {
  function NewTraceAffinityManager (line 29) | func NewTraceAffinityManager() *TraceAffinityManager {
  function NewTraceAffinityManagerWithTTL (line 43) | func NewTraceAffinityManagerWithTTL(ttl time.Duration) *TraceAffinityMan...
  function maskUserID (line 229) | func maskUserID(userID string) string {

FILE: backend-go/internal/types/gemini.go
  constant DummyThoughtSignature (line 11) | DummyThoughtSignature = "skip_thought_signature_validator"
  constant StripThoughtSignatureMarker (line 15) | StripThoughtSignatureMarker = "__STRIP_THOUGHT_SIGNATURE__"
  type GeminiRequest (line 22) | type GeminiRequest struct
  type GeminiContent (line 31) | type GeminiContent struct
  type GeminiPart (line 37) | type GeminiPart struct
    method UnmarshalJSON (line 53) | func (p *GeminiPart) UnmarshalJSON(data []byte) error {
    method MarshalJSON (line 80) | func (p GeminiPart) MarshalJSON() ([]byte, error) {
  type GeminiInlineData (line 100) | type GeminiInlineData struct
  type GeminiFileData (line 106) | type GeminiFileData struct
  type GeminiFunctionCall (line 116) | type GeminiFunctionCall struct
  type GeminiFunctionResponse (line 123) | type GeminiFunctionResponse struct
  type GeminiTool (line 129) | type GeminiTool struct
  type GeminiFunctionDeclaration (line 134) | type GeminiFunctionDeclaration struct
    method UnmarshalJSON (line 144) | func (fd *GeminiFunctionDeclaration) UnmarshalJSON(data []byte) error {
  function sanitizeGeminiToolSchema (line 184) | func sanitizeGeminiToolSchema(v interface{}) interface{} {
  type GeminiGenerationConfig (line 223) | type GeminiGenerationConfig struct
  type GeminiThinkingConfig (line 235) | type GeminiThinkingConfig struct
  type GeminiSafetySetting (line 242) | type GeminiSafetySetting struct
  type GeminiResponse (line 252) | type GeminiResponse struct
  type GeminiCandidate (line 260) | type GeminiCandidate struct
  type GeminiPromptFeedback (line 268) | type GeminiPromptFeedback struct
  type GeminiSafetyRating (line 274) | type GeminiSafetyRating struct
  type GeminiUsageMetadata (line 280) | type GeminiUsageMetadata struct
  type GeminiStreamChunk (line 293) | type GeminiStreamChunk struct
  type GeminiError (line 303) | type GeminiError struct
  type GeminiErrorDetail (line 308) | type GeminiErrorDetail struct

FILE: backend-go/internal/types/gemini_test.go
  function TestGeminiPart_UnmarshalJSON_ThoughtSignatureAtPartLevel (line 8) | func TestGeminiPart_UnmarshalJSON_ThoughtSignatureAtPartLevel(t *testing...
  function TestGeminiFunctionDeclaration_UnmarshalJSON_ParametersJsonSchema (line 90) | func TestGeminiFunctionDeclaration_UnmarshalJSON_ParametersJsonSchema(t ...
  function TestGeminiFunctionDeclaration_UnmarshalJSON_SanitizeParametersSchema (line 130) | func TestGeminiFunctionDeclaration_UnmarshalJSON_SanitizeParametersSchem...

FILE: backend-go/internal/types/responses.go
  type ResponsesRequest (line 6) | type ResponsesRequest struct
  type ResponsesItem (line 29) | type ResponsesItem struct
  type ContentBlock (line 37) | type ContentBlock struct
  type ToolUse (line 43) | type ToolUse struct
  type ResponsesResponse (line 50) | type ResponsesResponse struct
  type ResponsesUsage (line 63) | type ResponsesUsage struct
  type InputTokensDetails (line 79) | type InputTokensDetails struct
  type OutputTokensDetails (line 84) | type OutputTokensDetails struct
  type ResponsesStreamEvent (line 89) | type ResponsesStreamEvent struct
  type ResponsesDelta (line 101) | type ResponsesDelta struct

FILE: backend-go/internal/types/types.go
  type ClaudeRequest (line 4) | type ClaudeRequest struct
  type ClaudeMessage (line 16) | type ClaudeMessage struct
  type CacheControl (line 23) | type CacheControl struct
  type ClaudeContent (line 28) | type ClaudeContent struct
  type ClaudeTool (line 39) | type ClaudeTool struct
  type ClaudeResponse (line 47) | type ClaudeResponse struct
  type OpenAIRequest (line 57) | type OpenAIRequest struct
  type OpenAIMessage (line 68) | type OpenAIMessage struct
  type OpenAIToolCall (line 76) | type OpenAIToolCall struct
  type OpenAIToolCallFunction (line 83) | type OpenAIToolCallFunction struct
  type OpenAITool (line 89) | type OpenAITool struct
  type OpenAIToolFunction (line 95) | type OpenAIToolFunction struct
  type OpenAIResponse (line 102) | type OpenAIResponse struct
  type OpenAIChoice (line 109) | type OpenAIChoice struct
  type Usage (line 116) | type Usage struct
  type ProviderRequest (line 131) | type ProviderRequest struct
  type ProviderResponse (line 139) | type ProviderResponse struct

FILE: backend-go/internal/utils/compression.go
  function DecompressGzipIfNeeded (line 14) | func DecompressGzipIfNeeded(resp *http.Response, bodyBytes []byte) []byte {

FILE: backend-go/internal/utils/headers.go
  function PrepareUpstreamHeaders (line 13) | func PrepareUpstreamHeaders(c *gin.Context, targetHost string) http.Head...
  function PrepareMinimalHeaders (line 34) | func PrepareMinimalHeaders(targetHost string) http.Header {
  function SetAuthenticationHeader (line 46) | func SetAuthenticationHeader(headers http.Header, apiKey string) {
  function SetGeminiAuthenticationHeader (line 64) | func SetGeminiAuthenticationHeader(headers http.Header, apiKey string) {
  function EnsureCompatibleUserAgent (line 71) | func EnsureCompatibleUserAgent(headers http.Header, serviceType string) {
  function ForwardResponseHeaders (line 84) | func ForwardResponseHeaders(upstreamHeaders http.Header, clientWriter ht...

FILE: backend-go/internal/utils/headers_test.go
  function TestPrepareUpstreamHeaders (line 11) | func TestPrepareUpstreamHeaders(t *testing.T) {
  function TestSetAuthenticationHeader (line 89) | func TestSetAuthenticationHeader(t *testing.T) {
  function TestSetGeminiAuthenticationHeader (line 140) | func TestSetGeminiAuthenticationHeader(t *testing.T) {
  function TestEnsureCompatibleUserAgent (line 159) | func TestEnsureCompatibleUserAgent(t *testing.T) {

FILE: backend-go/internal/utils/json.go
  function MarshalJSONNoEscape (line 12) | func MarshalJSONNoEscape(v interface{}) ([]byte, error) {
  function TruncateJSONIntelligently (line 25) | func TruncateJSONIntelligently(data interface{}, maxTextLength int) inte...
  function SimplifyToolsArray (line 61) | func SimplifyToolsArray(data interface{}) interface{} {
  function compactContentArray (line 109) | func compactContentArray(contents []interface{}) []interface{} {
  function compactGeminiContentsArray (line 230) | func compactGeminiContentsArray(contents []interface{}) []interface{} {
  function compactGeminiPart (line 264) | func compactGeminiPart(partMap map[string]interface{}) map[string]interf...
  function truncateInputValues (line 334) | func truncateInputValues(data interface{}, maxLength int) interface{} {
  function extractToolNames (line 363) | func extractToolNames(toolsArray []interface{}) []interface{} {
  function extractToolName (line 409) | func extractToolName(tool interface{}) interface{} {
  function SimplifyToolsInJSON (line 432) | func SimplifyToolsInJSON(jsonData []byte) []byte {
  function FormatJSONForLog (line 450) | func FormatJSONForLog(data interface{}, maxTextLength int) string {
  function formatMapAsOneLine (line 463) | func formatMapAsOneLine(m map[string]interface{}) string {
  function formatInputMapCompact (line 515) | func formatInputMapCompact(m map[string]interface{}) string {
  function formatMessageAsOneLine (line 533) | func formatMessageAsOneLine(m map[string]interface{}) string {
  function formatJSONWithCompactArrays (line 583) | func formatJSONWithCompactArrays(data interface{}, indent string, depth ...
  function FormatJSONBytesForLog (line 752) | func FormatJSONBytesForLog(jsonData []byte, maxTextLength int) string {
  function MaskSensitiveHeaders (line 767) | func MaskSensitiveHeaders(headers map[string]string) map[string]string {
  function MaskAPIKey (line 792) | func MaskAPIKey(key string) string {
  function FormatJSONBytesRaw (line 809) | func FormatJSONBytesRaw(jsonData []byte) string {

FILE: backend-go/internal/utils/json_compact_test.go
  function TestCompactContentArray (line 9) | func TestCompactContentArray(t *testing.T) {
  function TestContentArrayCompactFormat (line 79) | func TestContentArrayCompactFormat(t *testing.T) {
  function TestNoTruncationInMiddleOfJSON (line 166) | func TestNoTruncationInMiddleOfJSON(t *testing.T) {
  function TestFormatJSONBytesForLog (line 208) | func TestFormatJSONBytesForLog(t *testing.T) {
  function TestCodexResponsesFormat (line 244) | func TestCodexResponsesFormat(t *testing.T) {
  function TestGeminiContentsFormat (line 332) | func TestGeminiContentsFormat(t *testing.T) {
  function TestGeminiToolsFormat (line 417) | func TestGeminiToolsFormat(t *testing.T) {

FILE: backend-go/internal/utils/json_test.go
  function TestTruncateJSONIntelligently (line 9) | func TestTruncateJSONIntelligently(t *testing.T) {
  function TestSimplifyToolsArray (line 59) | func TestSimplifyToolsArray(t *testing.T) {
  function TestFormatJSONForLog (line 123) | func TestFormatJSONForLog(t *testing.T) {
  function TestMaskAPIKey (line 167) | func TestMaskAPIKey(t *testing.T) {

FILE: backend-go/internal/utils/stream_synthesizer.go
  type StreamSynthesizer (line 12) | type StreamSynthesizer struct
    method ProcessLine (line 39) | func (s *StreamSynthesizer) ProcessLine(line string) {
    method processResponses (line 87) | func (s *StreamSynthesizer) processResponses(data map[string]interface...
    method processGemini (line 202) | func (s *StreamSynthesizer) processGemini(data map[string]interface{}) {
    method processOpenAI (line 249) | func (s *StreamSynthesizer) processOpenAI(data map[string]interface{}) {
    method processClaude (line 306) | func (s *StreamSynthesizer) processClaude(data map[string]interface{}) {
    method GetSynthesizedContent (line 402) | func (s *StreamSynthesizer) GetSynthesizedContent() string {
    method mergeSplitToolCalls (line 488) | func (s *StreamSynthesizer) mergeSplitToolCalls() {
    method IsParseFailed (line 543) | func (s *StreamSynthesizer) IsParseFailed() bool {
    method HasToolCalls (line 548) | func (s *StreamSynthesizer) HasToolCalls() bool {
  type ToolCall (line 23) | type ToolCall struct
  function NewStreamSynthesizer (line 30) | func NewStreamSynthesizer(serviceType string) *StreamSynthesizer {

FILE: backend-go/internal/utils/token_counter.go
  function EstimateTokens (line 14) | func EstimateTokens(text string) int {
  function EstimateMessagesTokens (line 38) | func EstimateMessagesTokens(messages interface{}) int {
  function EstimateRequestTokens (line 59) | func EstimateRequestTokens(bodyBytes []byte) int {
  function EstimateResponseTokens (line 100) | func EstimateResponseTokens(content interface{}) int {
  function isCJK (line 137) | func isCJK(r rune) bool {
  function EstimateResponsesRequestTokens (line 148) | func EstimateResponsesRequestTokens(bodyBytes []byte) int {
  function estimateResponsesInputTokens (line 179) | func estimateResponsesInputTokens(input interface{}) int {
  function estimateContentTokens (line 216) | func estimateContentTokens(content interface{}) int {
  function EstimateResponsesOutputTokens (line 241) | func EstimateResponsesOutputTokens(output interface{}) int {
  function estimateResponsesItemTokens (line 307) | func estimateResponsesItemTokens(item types.ResponsesItem) int {

FILE: backend-go/internal/utils/token_counter_test.go
  function TestEstimateTokens (line 10) | func TestEstimateTokens(t *testing.T) {
  function TestEstimateResponsesRequestTokens (line 33) | func TestEstimateResponsesRequestTokens(t *testing.T) {
  function TestEstimateResponsesOutputTokens (line 95) | func TestEstimateResponsesOutputTokens(t *testing.T) {
  function TestEstimateResponsesOutputTokensWithTypedItems (line 159) | func TestEstimateResponsesOutputTokensWithTypedItems(t *testing.T) {
  function TestEstimateRequestTokens (line 181) | func TestEstimateRequestTokens(t *testing.T) {

FILE: backend-go/internal/warmup/url_manager.go
  type URLLatencyResult (line 12) | type URLLatencyResult struct
  type URLState (line 19) | type URLState struct
  type ChannelURLState (line 30) | type ChannelURLState struct
  type URLManager (line 37) | type URLManager struct
    method GetSortedURLs (line 64) | func (m *URLManager) GetSortedURLs(channelIndex int, urls []string) []...
    method MarkSuccess (line 97) | func (m *URLManager) MarkSuccess(channelIndex int, url string) {
    method MarkFailure (line 121) | func (m *URLManager) MarkFailure(channelIndex int, url string) {
    method ensureChannelState (line 148) | func (m *URLManager) ensureChannelState(channelIndex int, urls []strin...
    method urlsMatch (line 189) | func (m *URLManager) urlsMatch(states []*URLState, urls []string) bool {
    method sortURLs (line 210) | func (m *URLManager) sortURLs(state *ChannelURLState) {
    method InvalidateChannel (line 251) | func (m *URLManager) InvalidateChannel(channelIndex int) {
    method InvalidateAll (line 259) | func (m *URLManager) InvalidateAll() {
    method GetStats (line 267) | func (m *URLManager) GetStats() map[string]interface{} {
  function NewURLManager (line 45) | func NewURLManager(failureCooldown time.Duration, maxFailCount int) *URL...

FILE: backend-go/main.go
  function main (line 32) | func main() {

FILE: frontend/src/composables/useTheme.ts
  constant RETRO_THEME (line 4) | const RETRO_THEME = {
  function useAppTheme (line 9) | function useAppTheme() {

FILE: frontend/src/services/api.ts
  class ApiError (line 4) | class ApiError extends Error {
    method constructor (line 8) | constructor(message: string, status: number, details?: unknown) {
  constant API_BASE (line 35) | const API_BASE = getApiBase()
  type ChannelStatus (line 48) | type ChannelStatus = 'active' | 'suspended' | 'disabled'
  type TimeWindowStats (line 52) | interface TimeWindowStats {
  type ChannelMetrics (line 64) | interface ChannelMetrics {
  type Channel (line 84) | interface Channel {
  type ChannelsResponse (line 109) | interface ChannelsResponse {
  type ChannelDashboardResponse (line 116) | interface ChannelDashboardResponse {
  type PingResult (line 132) | interface PingResult {
  type HistoryDataPoint (line 140) | interface HistoryDataPoint {
  type MetricsHistoryResponse (line 149) | interface MetricsHistoryResponse {
  type KeyHistoryDataPoint (line 156) | interface KeyHistoryDataPoint {
  type KeyHistoryData (line 169) | interface KeyHistoryData {
  type ChannelKeyMetricsHistoryResponse (line 176) | interface ChannelKeyMetricsHistoryResponse {
  type GlobalHistoryDataPoint (line 185) | interface GlobalHistoryDataPoint {
  type GlobalStatsSummary (line 198) | interface GlobalStatsSummary {
  type GlobalStatsHistoryResponse (line 211) | interface GlobalStatsHistoryResponse {
  type ActivitySegment (line 219) | interface ActivitySegment {
  type ChannelRecentActivity (line 228) | interface ChannelRecentActivity {
  type ModelEntry (line 237) | interface ModelEntry {
  type ModelsResponse (line 244) | interface ModelsResponse {
  function buildModelsURL (line 253) | function buildModelsURL(baseURL: string): string {
  function fetchUpstreamModels (line 277) | async function fetchUpstreamModels(
  class ApiService (line 318) | class ApiService {
    method getApiKey (line 320) | private getApiKey(): string | null {
    method parseResponseBody (line 325) | private async parseResponseBody(response: Response): Promise<unknown> {
    method request (line 336) | private async request(url: string, options: RequestInit = {}): Promise...
    method getChannels (line 382) | async getChannels(): Promise<ChannelsResponse> {
    method addChannel (line 386) | async addChannel(channel: Omit<Channel, 'index' | 'latency' | 'status'...
    method updateChannel (line 393) | async updateChannel(id: number, channel: Partial<Channel>): Promise<vo...
    method deleteChannel (line 400) | async deleteChannel(id: number): Promise<void> {
    method addApiKey (line 406) | async addApiKey(channelId: number, apiKey: string): Promise<void> {
    method removeApiKey (line 413) | async removeApiKey(channelId: number, apiKey: string): Promise<void> {
    method pingChannel (line 419) | async pingChannel(id: number): Promise<PingResult> {
    method pingAllChannels (line 423) | async pingAllChannels(): Promise<Array<{ id: number; name: string; lat...
    method updateLoadBalance (line 427) | async updateLoadBalance(strategy: string): Promise<void> {
    method updateResponsesLoadBalance (line 434) | async updateResponsesLoadBalance(strategy: string): Promise<void> {
    method getResponsesChannels (line 443) | async getResponsesChannels(): Promise<ChannelsResponse> {
    method addResponsesChannel (line 447) | async addResponsesChannel(channel: Omit<Channel, 'index' | 'latency' |...
    method updateResponsesChannel (line 454) | async updateResponsesChannel(id: number, channel: Partial<Channel>): P...
    method deleteResponsesChannel (line 461) | async deleteResponsesChannel(id: number): Promise<void> {
    method addResponsesApiKey (line 467) | async addResponsesApiKey(channelId: number, apiKey: string): Promise<v...
    method removeResponsesApiKey (line 474) | async removeResponsesApiKey(channelId: number, apiKey: string): Promis...
    method moveApiKeyToTop (line 480) | async moveApiKeyToTop(channelId: number, apiKey: string): Promise<void> {
    method moveApiKeyToBottom (line 486) | async moveApiKeyToBottom(channelId: number, apiKey: string): Promise<v...
    method moveResponsesApiKeyToTop (line 492) | async moveResponsesApiKeyToTop(channelId: number, apiKey: string): Pro...
    method moveResponsesApiKeyToBottom (line 498) | async moveResponsesApiKeyToBottom(channelId: number, apiKey: string): ...
    method reorderChannels (line 507) | async reorderChannels(order: number[]): Promise<void> {
    method setChannelStatus (line 515) | async setChannelStatus(channelId: number, status: ChannelStatus): Prom...
    method resumeChannel (line 523) | async resumeChannel(channelId: number): Promise<void> {
    method getChannelMetrics (line 530) | async getChannelMetrics(): Promise<ChannelMetrics[]> {
    method getSchedulerStats (line 535) | async getSchedulerStats(type?: 'messages' | 'responses' | 'gemini'): P...
    method getChannelDashboard (line 559) | async getChannelDashboard(type: 'messages' | 'responses' | 'gemini' = ...
    method reorderResponsesChannels (line 571) | async reorderResponsesChannels(order: number[]): Promise<void> {
    method setResponsesChannelStatus (line 579) | async setResponsesChannelStatus(channelId: number, status: ChannelStat...
    method resumeResponsesChannel (line 587) | async resumeResponsesChannel(channelId: number): Promise<void> {
    method getResponsesChannelMetrics (line 594) | async getResponsesChannelMetrics(): Promise<ChannelMetrics[]> {
    method setChannelPromotion (line 601) | async setChannelPromotion(channelId: number, durationSeconds: number):...
    method setResponsesChannelPromotion (line 609) | async setResponsesChannelPromotion(channelId: number, durationSeconds:...
    method getFuzzyMode (line 619) | async getFuzzyMode(): Promise<{ fuzzyModeEnabled: boolean }> {
    method setFuzzyMode (line 624) | async setFuzzyMode(enabled: boolean): Promise<void> {
    method getChannelMetricsHistory (line 634) | async getChannelMetricsHistory(duration: '1h' | '6h' | '24h' = '24h'):...
    method getResponsesChannelMetricsHistory (line 639) | async getResponsesChannelMetricsHistory(duration: '1h' | '6h' | '24h' ...
    method getChannelKeyMetricsHistory (line 646) | async getChannelKeyMetricsHistory(channelId: number, duration: '1h' | ...
    method getResponsesChannelKeyMetricsHistory (line 651) | async getResponsesChannelKeyMetricsHistory(channelId: number, duration...
    method getMessagesGlobalStats (line 658) | async getMessagesGlobalStats(duration: '1h' | '6h' | '24h' | 'today' =...
    method getResponsesGlobalStats (line 663) | async getResponsesGlobalStats(duration: '1h' | '6h' | '24h' | 'today' ...
    method getGeminiChannels (line 669) | async getGeminiChannels(): Promise<ChannelsResponse> {
    method addGeminiChannel (line 673) | async addGeminiChannel(channel: Omit<Channel, 'index' | 'latency' | 's...
    method updateGeminiChannel (line 680) | async updateGeminiChannel(id: number, channel: Partial<Channel>): Prom...
    method deleteGeminiChannel (line 687) | async deleteGeminiChannel(id: number): Promise<void> {
    method addGeminiApiKey (line 693) | async addGeminiApiKey(channelId: number, apiKey: string): Promise<void> {
    method removeGeminiApiKey (line 700) | async removeGeminiApiKey(channelId: number, apiKey: string): Promise<v...
    method moveGeminiApiKeyToTop (line 706) | async moveGeminiApiKeyToTop(channelId: number, apiKey: string): Promis...
    method moveGeminiApiKeyToBottom (line 712) | async moveGeminiApiKeyToBottom(channelId: number, apiKey: string): Pro...
    method reorderGeminiChannels (line 720) | async reorderGeminiChannels(order: number[]): Promise<void> {
    method setGeminiChannelStatus (line 727) | async setGeminiChannelStatus(channelId: number, status: ChannelStatus)...
    method resumeGeminiChannel (line 735) | async resumeGeminiChannel(channelId: number): Promise<void> {
    method getGeminiChannelMetrics (line 739) | async getGeminiChannelMetrics(): Promise<ChannelMetrics[]> {
    method setGeminiChannelPromotion (line 743) | async setGeminiChannelPromotion(channelId: number, durationSeconds: nu...
    method updateGeminiLoadBalance (line 750) | async updateGeminiLoadBalance(strategy: string): Promise<void> {
    method getGeminiChannelMetricsHistory (line 760) | async getGeminiChannelMetricsHistory(duration: '1h' | '6h' | '24h' = '...
    method getGeminiChannelKeyMetricsHistory (line 765) | async getGeminiChannelKeyMetricsHistory(channelId: number, duration: '...
    method getGeminiGlobalStats (line 770) | async getGeminiGlobalStats(duration: '1h' | '6h' | '24h' | 'today' = '...
    method pingGeminiChannel (line 774) | async pingGeminiChannel(id: number): Promise<PingResult> {
    method pingAllGeminiChannels (line 778) | async pingAllGeminiChannels(): Promise<Array<{ id: number; name: strin...
    method getGeminiChannelDashboard (line 790) | async getGeminiChannelDashboard(): Promise<ChannelDashboardResponse> {
  type HealthResponse (line 796) | interface HealthResponse {

FILE: frontend/src/services/version.ts
  constant CACHE_KEY (line 6) | const CACHE_KEY = 'claude-proxy-version-info'
  constant CACHE_DURATION (line 7) | const CACHE_DURATION = 30 * 60 * 1000 // 30分钟缓存
  constant ERROR_CACHE_DURATION (line 8) | const ERROR_CACHE_DURATION = 5 * 60 * 1000 // 错误状态缓存5分钟,避免频繁请求
  constant GITHUB_API_TIMEOUT (line 9) | const GITHUB_API_TIMEOUT = 10000 // 10秒超时
  type GitHubRelease (line 11) | interface GitHubRelease {
  type VersionInfo (line 19) | interface VersionInfo {
  constant PRERELEASE_PATTERN (line 30) | const PRERELEASE_PATTERN = /-(alpha|beta|rc|dev|pre|canary|nightly)/i
  class VersionService (line 32) | class VersionService {
    method isPrerelease (line 38) | private isPrerelease(version: string): boolean {
    method setCurrentVersion (line 45) | setCurrentVersion(version: string): void {
    method getCurrentVersion (line 52) | getCurrentVersion(): string {
    method getCachedVersionInfo (line 59) | private getCachedVersionInfo(): VersionInfo | null {
    method setCachedVersionInfo (line 96) | private setCachedVersionInfo(info: VersionInfo): void {
    method clearCache (line 107) | clearCache(): void {
    method compareVersions (line 115) | private compareVersions(current: string, latest: string): number {
    method fetchLatestVersion (line 138) | private async fetchLatestVersion(): Promise<GitHubRelease | null> {
    method checkForUpdates (line 174) | async checkForUpdates(): Promise<VersionInfo> {

FILE: frontend/src/stores/auth.ts
  function setApiKey (line 53) | function setApiKey(key: string | null) {
  function clearAuth (line 63) | function clearAuth() {
  function initializeAuth (line 69) | function initializeAuth() {
  function setAuthError (line 81) | function setAuthError(error: string) {
  function incrementAuthAttempts (line 85) | function incrementAuthAttempts() {
  function resetAuthAttempts (line 89) | function resetAuthAttempts() {
  function setAuthLockout (line 93) | function setAuthLockout(lockoutTime: Date | null) {
  function setAutoAuthenticating (line 97) | function setAutoAuthenticating(value: boolean) {
  function setInitialized (line 101) | function setInitialized(value: boolean) {
  function setAuthLoading (line 105) | function setAuthLoading(value: boolean) {
  function setAuthKeyInput (line 109) | function setAuthKeyInput(value: string) {

FILE: frontend/src/stores/channel.ts
  type ApiTab (line 19) | type ApiTab = 'messages' | 'responses' | 'gemini'
  type DashboardCache (line 55) | interface DashboardCache {
  function mergeChannelsWithLocalData (line 130) | function mergeChannelsWithLocalData(newChannels: Channel[], existingChan...
  function refreshChannels (line 153) | async function refreshChannels() {
  function saveChannel (line 231) | async function saveChannel(
  function deleteChannel (line 314) | async function deleteChannel(channelId: number) {
  function pingChannel (line 329) | async function pingChannel(channelId: number) {
  function pingAllChannels (line 350) | async function pingAllChannels() {
  function updateLoadBalance (line 381) | async function updateLoadBalance(strategy: string) {
  function startAutoRefresh (line 398) | function startAutoRefresh() {
  function stopAutoRefresh (line 425) | function stopAutoRefresh() {
  function clearChannels (line 435) | function clearChannels() {

FILE: frontend/src/stores/dialog.ts
  function openAddChannelModal (line 30) | function openAddChannelModal() {
  function openEditChannelModal (line 38) | function openEditChannelModal(channel: Channel) {
  function closeAddChannelModal (line 46) | function closeAddChannelModal() {
  function openAddKeyModal (line 54) | function openAddKeyModal(channelId: number) {
  function closeAddKeyModal (line 63) | function closeAddKeyModal() {
  function resetDialogState (line 72) | function resetDialogState() {

FILE: frontend/src/stores/preferences.ts
  function setDarkMode (line 30) | function setDarkMode(mode: 'light' | 'dark' | 'auto') {
  function toggleDarkMode (line 37) | function toggleDarkMode() {
  function setFuzzyMode (line 47) | function setFuzzyMode(enabled: boolean) {
  function toggleFuzzyMode (line 54) | function toggleFuzzyMode() {
  function toggleGlobalStats (line 61) | function toggleGlobalStats() {

FILE: frontend/src/stores/system.ts
  type SystemStatus (line 17) | type SystemStatus = 'running' | 'error' | 'connecting'
  function setSystemStatus (line 71) | function setSystemStatus(status: SystemStatus) {
  function setVersionInfo (line 78) | function setVersionInfo(info: VersionInfo) {
  function setCurrentVersion (line 85) | function setCurrentVersion(version: string) {
  function setCheckingVersion (line 92) | function setCheckingVersion(checking: boolean) {
  function setFuzzyModeLoading (line 99) | function setFuzzyModeLoading(loading: boolean) {
  function setFuzzyModeLoadError (line 106) | function setFuzzyModeLoadError(error: boolean) {
  function resetSystemState (line 113) | function resetSystemState() {

FILE: frontend/src/utils/quickInputParser.ts
  constant PLATFORM_KEY_PATTERNS (line 43) | const PLATFORM_KEY_PATTERNS: RegExp[] = [
  constant MAX_BASE_URLS (line 197) | const MAX_BASE_URLS = 10
Condensed preview — 152 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,526K chars).
[
  {
    "path": ".claudeignore",
    "chars": 204,
    "preview": "\n# 忽略常规依赖\n**/node_modules\n**/frontend/node_modules\n**/dist\n**/.git\n**/.next\n**/.vercel\n\n# 忽略构建产物和二进制\n**/target\n**/bin\n**"
  },
  {
    "path": ".dockerignore",
    "chars": 575,
    "preview": "# Git 相关\n.git\n.gitignore\n.github\n\n# Node 模块\nnode_modules\nfrontend/node_modules\nbackend/node_modules\n\n# 构建产物(会在 Docker 中重"
  },
  {
    "path": ".gitattributes",
    "chars": 451,
    "preview": "# Bun lockfile - 配置 Git 以显示可读的差异\n# bun.lockb (二进制格式) - 需要 textconv 转换\n*.lockb binary diff=lockb\n# bun.lock (文本格式) - 直接作为"
  },
  {
    "path": ".github/workflows/docker-build.yml",
    "chars": 1457,
    "preview": "name: Build Docker Image\n\non:\n  push:\n    tags:\n      - 'v*'\n\nenv:\n  REGISTRY: crpi-i19l8zl0ugidq97v.cn-hangzhou.persona"
  },
  {
    "path": ".github/workflows/release-linux.yml",
    "chars": 1963,
    "preview": "name: Release Linux Build\npermissions:\n  contents: write\n\non:\n  push:\n    tags:\n      - \"v*\"\n\nconcurrency:\n  group: ${{ "
  },
  {
    "path": ".github/workflows/release-macos.yml",
    "chars": 1959,
    "preview": "name: Release MacOS Build\npermissions:\n  contents: write\n\non:\n  push:\n    tags:\n      - \"v*\"\n\nconcurrency:\n  group: ${{ "
  },
  {
    "path": ".github/workflows/release-windows.yml",
    "chars": 2029,
    "preview": "name: Release Windows Build\npermissions:\n  contents: write\n\non:\n  push:\n    tags:\n      - \"v*\"\n\nconcurrency:\n  group: ${"
  },
  {
    "path": ".gitignore",
    "chars": 2884,
    "preview": "# Logs\n\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic report"
  },
  {
    "path": ".prettierrc",
    "chars": 201,
    "preview": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"none\",\n  \"printWidth\": 120,\n  \"tabWidth\": 2,\n  \"arrowParen"
  },
  {
    "path": "AGENTS.md",
    "chars": 1311,
    "preview": "# 仓库协作指南\n\n## 重要约定\n- **始终使用简体中文回复**。\n- 遵循 SOLID / KISS / DRY / YAGNI;优先修复根因,避免无关重构。\n\n## 项目结构与模块\n- `backend-go/`:主 Go 服务(G"
  },
  {
    "path": "ARCHITECTURE.md",
    "chars": 16395,
    "preview": "# 项目架构与设计\n\n本文档详细介绍 Claude / Codex / Gemini API Proxy 的架构设计、技术选型和实现细节。\n\n## 项目结构\n\n项目采用一体化架构,Go 后端嵌入前端构建产物,实现单二进制部署:\n\n```\nc"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 44579,
    "preview": "# 版本历史\n\n> **注意**: v2.0.0 开始为 Go 语言重写版本,v1.x 为 TypeScript 版本\n\n---\n\n## [v2.5.13] - 2026-01-31\n\n### 修复\n\n- **Gemini function"
  },
  {
    "path": "CLAUDE.md",
    "chars": 3768,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2725,
    "preview": "# 贡献指南\n\n本文档为项目贡献者提供了一套标准化的指导,以确保代码库的一致性和高质量。\n\n## 如何贡献\n\n欢迎通过提交 Issue 和 Pull Request 为本项目贡献力量!\n\n1.  Fork 本项目。\n2.  创建特性分支 ("
  },
  {
    "path": "DEVELOPMENT.md",
    "chars": 7871,
    "preview": "# 开发指南\n\n本文档为开发者提供开发环境配置、工作流程、调试技巧和最佳实践。\n\n> 📚 **相关文档**\n> - 架构设计和技术选型: [ARCHITECTURE.md](ARCHITECTURE.md)\n> - 环境变量配置: [ENV"
  },
  {
    "path": "Dockerfile",
    "chars": 1500,
    "preview": "# --- 阶段 1: 准备 Bun 运行时 ---\nFROM oven/bun:alpine AS bun-runtime\n\n# --- 阶段 2: 构建阶段 (Go + Bun) ---\nFROM golang:1.22-alpine "
  },
  {
    "path": "Dockerfile_China",
    "chars": 1868,
    "preview": "# --- 阶段 1: 准备 Bun 运行时 ---\nFROM docker.1ms.run/oven/bun:alpine AS bun-runtime\n\n# --- 阶段 2: 构建阶段 (Go + Bun) ---\nFROM dock"
  },
  {
    "path": "ENVIRONMENT.md",
    "chars": 11366,
    "preview": "# 环境变量配置指南\n\n## 概述\n\n本项目使用分层的环境变量配置系统,支持开发、生产等不同环境的端口和API配置。前端通过 Vite 的环境变量系统动态连接后端服务。\n\n## 配置文件结构\n\n```\nclaude-proxy/\n├── f"
  },
  {
    "path": "LICENSE",
    "chars": 1054,
    "preview": "Copyright (c) 2025 wangyusong\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this soft"
  },
  {
    "path": "Makefile",
    "chars": 1144,
    "preview": "# Claude Proxy Makefile\n\nGREEN=\\033[0;32m\nYELLOW=\\033[0;33m\nNC=\\033[0m\n\n.PHONY: help dev run build clean frontend-dev fr"
  },
  {
    "path": "README.md",
    "chars": 1633,
    "preview": "> ⚠️ **项目已重命名**: 本项目已重命名为 **[CCX](https://github.com/BenedictKing/ccx)**,请访问新仓库获取最新版本和更新。本仓库已归档,不再维护。\n\n---\n\n# Claude / C"
  },
  {
    "path": "RELEASE.md",
    "chars": 2268,
    "preview": "# 发布指南\n\n本文档为项目维护者提供了一套标准的版本发布流程,以确保版本迭代的一致性和清晰度。\n\n## 版本规范\n\n项目遵循**语义化版本 2.0.0 (Semantic Versioning)** 规范。版本格式为 `主版本号.次版本号"
  },
  {
    "path": "VERSION",
    "chars": 8,
    "preview": "v2.5.13\n"
  },
  {
    "path": "backend-go/.air.toml",
    "chars": 1524,
    "preview": "# Air 配置文件 - Go热重载工具\n# 文档: https://github.com/air-verse/air\n\n# 工作目录\nroot = \".\"\ntestdata_dir = \"testdata\"\ntmp_dir = \"tmp\""
  },
  {
    "path": "backend-go/.env.example",
    "chars": 1702,
    "preview": "# 环境变量示例配置\n# 复制此文件为 .env 并修改配置\n\n# ============ 服务器配置 ============\nPORT=3000\n\n# 运行环境: development | production\n# 影响:\n#   "
  },
  {
    "path": "backend-go/.gitignore",
    "chars": 213,
    "preview": "# 忽略编译产物\ndist/\n*.exe\nclaude-proxy-*\nclaude-proxy\nstream_verify\n\n# 忽略前端资源(会在构建时复制)\nfrontend/\n\n# 忽略配置文件\n.env\n.config/\n\n# G"
  },
  {
    "path": "backend-go/CLAUDE.md",
    "chars": 3040,
    "preview": "# backend-go 模块文档\n\n[← 根目录](../CLAUDE.md)\n\n## 模块职责\n\nGo 后端核心服务:HTTP API、多上游适配、协议转换、智能调度、会话管理、配置热重载。\n\n## 启动命令\n\n```bash\nmake"
  },
  {
    "path": "backend-go/DEV_GUIDE.md",
    "chars": 3015,
    "preview": "# Go 后端开发指南 - 热重载模式\n\n## 🚀 快速开始\n\n### 1. 安装 Air 热重载工具\n\nAir 项目已迁移至新仓库 `github.com/air-verse/air`(原 `cosmtrek/air`)\n\n```bash"
  },
  {
    "path": "backend-go/Makefile",
    "chars": 3663,
    "preview": "# Makefile for Claude Proxy Go Backend\n\n# 变量定义\nBINARY_NAME=claude-proxy-go\nMAIN_PATH=.\nBUILD_DIR=../dist\nAIR_VERSION=v1."
  },
  {
    "path": "backend-go/README.md",
    "chars": 8205,
    "preview": "# Claude / Codex / Gemini API Proxy - Go 版本\n\n> 🚀 高性能的 Claude / Codex / Gemini API Proxy - Go 语言实现,支持多种上游AI服务提供商,内置前端管理界面"
  },
  {
    "path": "backend-go/build.sh",
    "chars": 1859,
    "preview": "#!/bin/bash\n\n# Claude Proxy Go 版本构建脚本\n\nset -e\n\n# 版本信息 - 从根目录 VERSION 文件读取\nVERSION=$(cat ../VERSION 2>/dev/null || echo \""
  },
  {
    "path": "backend-go/docs/MALFORMED_TOOLCALL_MEMO.md",
    "chars": 1768,
    "preview": "# 畸形 tool_call 问题备忘录\n\n> 创建时间: 2025-12-19\n> 状态: 待观察,暂不修复\n\n## 问题描述\n\n上游 Claude API 在流式返回时,偶尔会在同一个 `content_block` 中错误地发送多个工"
  },
  {
    "path": "backend-go/go.mod",
    "chars": 2361,
    "preview": "module github.com/BenedictKing/claude-proxy\n\ngo 1.22\n\nrequire (\n\tgithub.com/fsnotify/fsnotify v1.7.0\n\tgithub.com/gin-gon"
  },
  {
    "path": "backend-go/go.sum",
    "chars": 12786,
    "preview": "github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=\ngithub.com/bytedance/sonic v1.11.6/go"
  },
  {
    "path": "backend-go/internal/config/config.go",
    "chars": 9461,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/utils\"\n\n\t\"github"
  },
  {
    "path": "backend-go/internal/config/config_baseurl_test.go",
    "chars": 10989,
    "preview": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\n// TestUpdateUpstream_BaseURLConsistency 测试更新 baseUrl 时的一致"
  },
  {
    "path": "backend-go/internal/config/config_gemini.go",
    "chars": 12905,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/utils\"\n)\n\n// "
  },
  {
    "path": "backend-go/internal/config/config_loader.go",
    "chars": 8653,
    "preview": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/fsnotify/fsnotify\"\n"
  },
  {
    "path": "backend-go/internal/config/config_messages.go",
    "chars": 13359,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/utils\"\n)\n\n// "
  },
  {
    "path": "backend-go/internal/config/config_responses.go",
    "chars": 13030,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/utils\"\n)\n\n// "
  },
  {
    "path": "backend-go/internal/config/config_utils.go",
    "chars": 4323,
    "preview": "package config\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\n// ============== 工具函数 ==============\n\n// deduplicateStrings 去重字符"
  },
  {
    "path": "backend-go/internal/config/env.go",
    "chars": 4790,
    "preview": "package config\n\nimport (\n\t\"os\"\n\t\"strconv\"\n)\n\ntype EnvConfig struct {\n\tPort                 int\n\tEnv                  str"
  },
  {
    "path": "backend-go/internal/converters/chat_to_responses.go",
    "chars": 33453,
    "preview": "package converters\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/tidwall/gjson\"\n\t\"github.com/tid"
  },
  {
    "path": "backend-go/internal/converters/chat_to_responses_test.go",
    "chars": 11307,
    "preview": "package converters\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/tidwall/gjson\"\n)\n\nfunc Test"
  },
  {
    "path": "backend-go/internal/converters/claude_converter.go",
    "chars": 1526,
    "preview": "package converters\n\nimport (\n\t\"github.com/BenedictKing/claude-proxy/internal/session\"\n\t\"github.com/BenedictKing/claude-p"
  },
  {
    "path": "backend-go/internal/converters/converter.go",
    "chars": 2049,
    "preview": "package converters\n\nimport (\n\t\"github.com/BenedictKing/claude-proxy/internal/session\"\n\t\"github.com/BenedictKing/claude-p"
  },
  {
    "path": "backend-go/internal/converters/converter_test.go",
    "chars": 8593,
    "preview": "package converters\n\nimport (\n\t\"testing\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/session\"\n\t\"github.com/BenedictK"
  },
  {
    "path": "backend-go/internal/converters/factory.go",
    "chars": 448,
    "preview": "package converters\n\n// ConverterFactory 转换器工厂\n// 根据上游服务类型返回对应的转换器实例\n\n// NewConverter 创建转换器实例\n// serviceType: \"openai\", \""
  },
  {
    "path": "backend-go/internal/converters/gemini_converter.go",
    "chars": 14235,
    "preview": "package converters\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/types\"\n"
  },
  {
    "path": "backend-go/internal/converters/gemini_converter_test.go",
    "chars": 4492,
    "preview": "package converters\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// TestClaudeResponseToGemini_WithThou"
  },
  {
    "path": "backend-go/internal/converters/openai_converter.go",
    "chars": 3456,
    "preview": "package converters\n\nimport (\n\t\"github.com/BenedictKing/claude-proxy/internal/session\"\n\t\"github.com/BenedictKing/claude-p"
  },
  {
    "path": "backend-go/internal/converters/responses_converter.go",
    "chars": 16048,
    "preview": "package converters\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/session"
  },
  {
    "path": "backend-go/internal/converters/responses_passthrough.go",
    "chars": 2401,
    "preview": "package converters\n\nimport (\n\t\"github.com/BenedictKing/claude-proxy/internal/session\"\n\t\"github.com/BenedictKing/claude-p"
  },
  {
    "path": "backend-go/internal/converters/responses_to_chat.go",
    "chars": 7724,
    "preview": "package converters\n\nimport (\n\t\"github.com/tidwall/gjson\"\n\t\"github.com/tidwall/sjson\"\n)\n\n// ConvertResponsesToOpenAIChatR"
  },
  {
    "path": "backend-go/internal/handlers/channel_metrics_handler.go",
    "chars": 24577,
    "preview": "package handlers\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"gith"
  },
  {
    "path": "backend-go/internal/handlers/common/client_error_test.go",
    "chars": 1695,
    "preview": "package common\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestIsClientSideError(t *testing.T) {\n\ttests := "
  },
  {
    "path": "backend-go/internal/handlers/common/failover.go",
    "chars": 11650,
    "preview": "// Package common 提供 handlers 模块的公共功能\npackage common\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/gin-goni"
  },
  {
    "path": "backend-go/internal/handlers/common/failover_test.go",
    "chars": 20210,
    "preview": "package common\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\n// TestClassifyByStatusCode 测试基于状态码的分类\nfunc TestClassifyByStatus"
  },
  {
    "path": "backend-go/internal/handlers/common/multi_channel_failover.go",
    "chars": 3489,
    "preview": "package common\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"github.com/BenedictKin"
  },
  {
    "path": "backend-go/internal/handlers/common/request.go",
    "chars": 7992,
    "preview": "// Package common 提供 handlers 模块的公共功能\npackage common\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\""
  },
  {
    "path": "backend-go/internal/handlers/common/stream.go",
    "chars": 35304,
    "preview": "// Package common 提供 handlers 模块的公共功能\npackage common\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"str"
  },
  {
    "path": "backend-go/internal/handlers/common/stream_test.go",
    "chars": 11285,
    "preview": "package common\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/utils\"\n"
  },
  {
    "path": "backend-go/internal/handlers/common/upstream_failover.go",
    "chars": 8508,
    "preview": "// Package common 提供 handlers 模块的公共功能\npackage common\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"g"
  },
  {
    "path": "backend-go/internal/handlers/frontend.go",
    "chars": 3775,
    "preview": "package handlers\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// ServeFrontend 提供前"
  },
  {
    "path": "backend-go/internal/handlers/gemini/channels.go",
    "chars": 11667,
    "preview": "// Package gemini 提供 Gemini API 的渠道管理\npackage gemini\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"githu"
  },
  {
    "path": "backend-go/internal/handlers/gemini/dashboard.go",
    "chars": 3836,
    "preview": "package gemini\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"github.c"
  },
  {
    "path": "backend-go/internal/handlers/gemini/dashboard_test.go",
    "chars": 2668,
    "preview": "package gemini\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\""
  },
  {
    "path": "backend-go/internal/handlers/gemini/handler.go",
    "chars": 15950,
    "preview": "// Package gemini 提供 Gemini API 的处理器\npackage gemini\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n"
  },
  {
    "path": "backend-go/internal/handlers/gemini/handler_test.go",
    "chars": 12538,
    "preview": "package gemini\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/Bened"
  },
  {
    "path": "backend-go/internal/handlers/gemini/stream.go",
    "chars": 8495,
    "preview": "package gemini\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/BenedictKi"
  },
  {
    "path": "backend-go/internal/handlers/global_stats_handler.go",
    "chars": 1916,
    "preview": "package handlers\n\nimport (\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/metrics\"\n\t\"github.com/gin-gonic/gin\""
  },
  {
    "path": "backend-go/internal/handlers/health.go",
    "chars": 2467,
    "preview": "package handlers\n\nimport (\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"github.com/gin-gonic/gin\"\n"
  },
  {
    "path": "backend-go/internal/handlers/messages/channels.go",
    "chars": 12962,
    "preview": "// Package messages 提供 Claude Messages API 的渠道管理\npackage messages\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\""
  },
  {
    "path": "backend-go/internal/handlers/messages/handler.go",
    "chars": 12057,
    "preview": "// Package messages 提供 Claude Messages API 的处理器\npackage messages\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/ht"
  },
  {
    "path": "backend-go/internal/handlers/messages/models.go",
    "chars": 7197,
    "preview": "// Package messages 提供 Claude Messages API 的处理器\npackage messages\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"r"
  },
  {
    "path": "backend-go/internal/handlers/responses/channels.go",
    "chars": 7991,
    "preview": "// Package responses 提供 Responses API 的渠道管理\npackage responses\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/BenedictKing"
  },
  {
    "path": "backend-go/internal/handlers/responses/compact.go",
    "chars": 8161,
    "preview": "// Package responses 提供 Responses API 的处理器\npackage responses\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"str"
  },
  {
    "path": "backend-go/internal/handlers/responses/handler.go",
    "chars": 35672,
    "preview": "// Package responses 提供 Responses API 的处理器\npackage responses\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t"
  },
  {
    "path": "backend-go/internal/handlers/settings.go",
    "chars": 909,
    "preview": "// Package handlers 提供 HTTP 处理器\npackage handlers\n\nimport (\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"git"
  },
  {
    "path": "backend-go/internal/httpclient/client.go",
    "chars": 2726,
    "preview": "package httpclient\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/in"
  },
  {
    "path": "backend-go/internal/logger/logger.go",
    "chars": 1525,
    "preview": "package logger\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"gopkg.in/natefinch/lumberjack.v2\"\n)\n\n// Config 日志"
  },
  {
    "path": "backend-go/internal/metrics/channel_metrics.go",
    "chars": 74807,
    "preview": "package metrics\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"log\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/cla"
  },
  {
    "path": "backend-go/internal/metrics/channel_metrics_activity_test.go",
    "chars": 8275,
    "preview": "package metrics\n\nimport (\n\t\"math\"\n\t\"testing\"\n\t\"time\"\n)\n\n// floatEquals 使用容差比较浮点数\nfunc floatEquals(a, b, epsilon float64)"
  },
  {
    "path": "backend-go/internal/metrics/channel_metrics_cache_stats_test.go",
    "chars": 2245,
    "preview": "package metrics\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/types\"\n)\n\nfunc TestToRespo"
  },
  {
    "path": "backend-go/internal/metrics/persistence.go",
    "chars": 1144,
    "preview": "package metrics\n\nimport (\n\t\"time\"\n)\n\n// PersistenceStore 持久化存储接口\ntype PersistenceStore interface {\n\t// AddRecord 添加记录到写入"
  },
  {
    "path": "backend-go/internal/metrics/sqlite_store.go",
    "chars": 9143,
    "preview": "package metrics\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t_ \"modernc.o"
  },
  {
    "path": "backend-go/internal/middleware/auth.go",
    "chars": 3645,
    "preview": "package middleware\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"github"
  },
  {
    "path": "backend-go/internal/middleware/auth_test.go",
    "chars": 4855,
    "preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/BenedictKing/claude-proxy/interna"
  },
  {
    "path": "backend-go/internal/middleware/cors.go",
    "chars": 1068,
    "preview": "package middleware\n\nimport (\n\t\"strings\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"github.com/gin-gonic/"
  },
  {
    "path": "backend-go/internal/middleware/logger.go",
    "chars": 1099,
    "preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"github.co"
  },
  {
    "path": "backend-go/internal/providers/claude.go",
    "chars": 5130,
    "preview": "package providers\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Bene"
  },
  {
    "path": "backend-go/internal/providers/gemini.go",
    "chars": 12638,
    "preview": "package providers\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/Benedic"
  },
  {
    "path": "backend-go/internal/providers/openai.go",
    "chars": 16375,
    "preview": "package providers\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\""
  },
  {
    "path": "backend-go/internal/providers/provider.go",
    "chars": 949,
    "preview": "package providers\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n\t\"github.com/Bene"
  },
  {
    "path": "backend-go/internal/providers/request_context_test.go",
    "chars": 2922,
    "preview": "package providers\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/BenedictKing/c"
  },
  {
    "path": "backend-go/internal/providers/responses.go",
    "chars": 5180,
    "preview": "package providers\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Benedi"
  },
  {
    "path": "backend-go/internal/providers/url_builder_test.go",
    "chars": 4743,
    "preview": "package providers\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/config\"\n)\n\n"
  },
  {
    "path": "backend-go/internal/scheduler/channel_scheduler.go",
    "chars": 15649,
    "preview": "package scheduler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/co"
  },
  {
    "path": "backend-go/internal/scheduler/channel_scheduler_test.go",
    "chars": 7250,
    "preview": "package scheduler\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Benedict"
  },
  {
    "path": "backend-go/internal/session/manager.go",
    "chars": 5287,
    "preview": "package session\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/BenedictKing/claude"
  },
  {
    "path": "backend-go/internal/session/trace_affinity.go",
    "chars": 5163,
    "preview": "package session\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\n// affinityDebug 控制亲和性日志是否输出\n// 通过环境变量 AFFINITY_DEBUG=true 启用\n"
  },
  {
    "path": "backend-go/internal/types/gemini.go",
    "chars": 9896,
    "preview": "package types\n\nimport \"encoding/json\"\n\n// ============================================================================\n/"
  },
  {
    "path": "backend-go/internal/types/gemini_test.go",
    "chars": 6761,
    "preview": "package types\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\nfunc TestGeminiPart_UnmarshalJSON_ThoughtSignatureAtPartLevel(t *"
  },
  {
    "path": "backend-go/internal/types/responses.go",
    "chars": 4398,
    "preview": "package types\n\n// ============== Responses API 类型定义 ==============\n\n// ResponsesRequest Responses API 请求\ntype ResponsesR"
  },
  {
    "path": "backend-go/internal/types/types.go",
    "chars": 4959,
    "preview": "package types\n\n// ClaudeRequest Claude 请求结构\ntype ClaudeRequest struct {\n\tModel       string                 `json:\"model"
  },
  {
    "path": "backend-go/internal/utils/compression.go",
    "chars": 704,
    "preview": "package utils\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n)\n\n// DecompressGzipIfNeeded 检测并解压缩 gzip 响应体\n"
  },
  {
    "path": "backend-go/internal/utils/headers.go",
    "chars": 2810,
    "preview": "package utils\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// PrepareUpstreamHeaders 准备上游请求头(统一头部处理逻"
  },
  {
    "path": "backend-go/internal/utils/headers_test.go",
    "chars": 5519,
    "preview": "package utils\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc TestPrepareUpst"
  },
  {
    "path": "backend-go/internal/utils/json.go",
    "chars": 21372,
    "preview": "package utils\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strings\"\n)\n\n// MarshalJSONNoEscape 序列化 JSON 并禁用 HTML 字符转义\n// 使用 json"
  },
  {
    "path": "backend-go/internal/utils/json_compact_test.go",
    "chars": 13799,
    "preview": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestCompactContentArray(t *testing.T) {\n\tinput :="
  },
  {
    "path": "backend-go/internal/utils/json_test.go",
    "chars": 4328,
    "preview": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestTruncateJSONIntelligently(t *testing.T) {\n\tte"
  },
  {
    "path": "backend-go/internal/utils/stream_synthesizer.go",
    "chars": 13214,
    "preview": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// StreamSynthesizer 流式响应内容合成器\ntype "
  },
  {
    "path": "backend-go/internal/utils/token_counter.go",
    "chars": 7161,
    "preview": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"unicode\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/types\"\n)\n\n// Estima"
  },
  {
    "path": "backend-go/internal/utils/token_counter_test.go",
    "chars": 4975,
    "preview": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/BenedictKing/claude-proxy/internal/types\"\n)\n\nfunc Test"
  },
  {
    "path": "backend-go/internal/warmup/url_manager.go",
    "chars": 7080,
    "preview": "// Package warmup 提供多端点渠道的 URL 管理和动态排序功能\npackage warmup\n\nimport (\n\t\"log\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n)\n\n// URLLatencyResult "
  },
  {
    "path": "backend-go/main.go",
    "chars": 13791,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com"
  },
  {
    "path": "backend-go/version.go",
    "chars": 241,
    "preview": "package main\n\n// 版本信息变量 - 在构建时通过 -ldflags 注入\n// 实际值从根目录 VERSION 文件读取\nvar (\n\t// Version 当前版本号(构建时从 VERSION 文件注入)\n\tVersion"
  },
  {
    "path": "docker-compose.yml",
    "chars": 1419,
    "preview": "services:\n  # Claude API 代理服务 (一体化架构: 后端 + 前端界面)\n  claude-proxy:\n    image: crpi-i19l8zl0ugidq97v.cn-hangzhou.personal.c"
  },
  {
    "path": "frontend/.env.example",
    "chars": 248,
    "preview": "# 开发环境配置示例\n# 复制此文件为 .env 并根据需要修改\n\n# 后端API服务器地址(需与 backend-go/.env 中的 PORT 一致)\nVITE_BACKEND_URL=http://localhost:3000\n\n# "
  },
  {
    "path": "frontend/CLAUDE.md",
    "chars": 1133,
    "preview": "# frontend 模块文档\n\n[← 根目录](../CLAUDE.md)\n\n## 模块职责\n\nVue 3 + Vuetify 3 Web 管理界面:渠道配置、实时监控、拖拽排序、主题切换。\n\n## 启动命令\n\n```bash\nbun r"
  },
  {
    "path": "frontend/ESLINT.md",
    "chars": 3018,
    "preview": "# ESLint 配置说明\n\n## 已安装的包\n\n- `eslint` - ESLint 核心\n- `@eslint/js` - ESLint JavaScript 推荐规则\n- `eslint-plugin-vue` - Vue 3 专用"
  },
  {
    "path": "frontend/eslint.config.js",
    "chars": 4034,
    "preview": "import js from '@eslint/js'\nimport pluginVue from 'eslint-plugin-vue'\nimport vueParser from 'vue-eslint-parser'\nimport t"
  },
  {
    "path": "frontend/index.html",
    "chars": 369,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\" data-theme=\"emerald\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content"
  },
  {
    "path": "frontend/package.json",
    "chars": 1240,
    "preview": "{\n  \"name\": \"claude-proxy-frontend\",\n  \"version\": \"1.1.1\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"type\": \"module\",\n  "
  },
  {
    "path": "frontend/src/App.vue",
    "chars": 47824,
    "preview": "<template>\n  <v-app>\n    <!-- 自动认证加载提示 - 只在真正进行自动认证时显示 -->\n    <v-overlay\n      :model-value=\"authStore.isAutoAuthentica"
  },
  {
    "path": "frontend/src/assets/style.css",
    "chars": 232,
    "preview": "/* 全局基础样式 */\nhtml {\n  font-family: 'Courier New', Consolas, 'Liberation Mono', monospace;\n}\n\n/* 过渡动画 */\n.fade-enter-acti"
  },
  {
    "path": "frontend/src/components/AddChannelModal.vue",
    "chars": 54730,
    "preview": "<template>\n  <v-dialog :model-value=\"show\" max-width=\"800\" persistent @update:model-value=\"$emit('update:show', $event)\""
  },
  {
    "path": "frontend/src/components/ChannelCard.vue",
    "chars": 21422,
    "preview": "<template>\n  <v-card\n    class=\"channel-card h-100\"\n    :style=\"serviceStyle\"\n    :data-pinned=\"channel.pinned\"\n    elev"
  },
  {
    "path": "frontend/src/components/ChannelMetricsChart.vue",
    "chars": 7891,
    "preview": "<template>\n  <div class=\"channel-chart-container\">\n    <!-- Snackbar for error notification -->\n    <v-snackbar v-model="
  },
  {
    "path": "frontend/src/components/ChannelOrchestration.vue",
    "chars": 51373,
    "preview": "<template>\n  <v-card elevation=\"0\" rounded=\"lg\" class=\"channel-orchestration\" variant=\"flat\">\n    <!-- 调度器统计信息 -->\n    <"
  },
  {
    "path": "frontend/src/components/ChannelStatusBadge.vue",
    "chars": 8075,
    "preview": "<template>\n  <div class=\"status-badge\" :class=\"[statusClass, { 'has-metrics': showMetrics }]\">\n    <v-tooltip location=\""
  },
  {
    "path": "frontend/src/components/GlobalStatsChart.vue",
    "chars": 13896,
    "preview": "<template>\n  <div class=\"global-stats-chart-container\">\n    <!-- Snackbar for error notification -->\n    <v-snackbar v-m"
  },
  {
    "path": "frontend/src/components/KeyTrendChart.vue",
    "chars": 24696,
    "preview": "<template>\n  <div class=\"key-trend-chart-container\">\n    <!-- Snackbar for error notification -->\n    <v-snackbar v-mode"
  },
  {
    "path": "frontend/src/composables/useTheme.ts",
    "chars": 465,
    "preview": "import { useTheme as useVuetifyTheme } from 'vuetify'\n\n// 复古像素主题配置\nexport const RETRO_THEME = {\n  name: '复古像素',\n  font: "
  },
  {
    "path": "frontend/src/env.d.ts",
    "chars": 329,
    "preview": "/// <reference types=\"vite/client\" />\n\n// Allow importing .vue files in TS\ndeclare module '*.vue' {\n  import type { Defi"
  },
  {
    "path": "frontend/src/main.ts",
    "chars": 568,
    "preview": "import { createApp } from 'vue'\nimport { createPinia } from 'pinia'\nimport piniaPluginPersistedstate from 'pinia-plugin-"
  },
  {
    "path": "frontend/src/plugins/vuetify.ts",
    "chars": 8512,
    "preview": "import { createVuetify } from 'vuetify'\nimport { h } from 'vue'\nimport type { IconSet, IconProps, ThemeDefinition } from"
  },
  {
    "path": "frontend/src/router/index.ts",
    "chars": 774,
    "preview": "import { createRouter, createWebHistory } from 'vue-router'\nimport { useAuthStore } from '@/stores/auth'\n\nconst routes ="
  },
  {
    "path": "frontend/src/services/api.ts",
    "chars": 23498,
    "preview": "// API服务模块\nimport { useAuthStore } from '@/stores/auth'\n\nexport class ApiError extends Error {\n  readonly status: number"
  },
  {
    "path": "frontend/src/services/version.ts",
    "chars": 5792,
    "preview": "/**\n * 版本检查服务\n * 参考 gpt-load 项目实现\n */\n\nconst CACHE_KEY = 'claude-proxy-version-info'\nconst CACHE_DURATION = 30 * 60 * 10"
  },
  {
    "path": "frontend/src/stores/auth.ts",
    "chars": 2931,
    "preview": "import { defineStore } from 'pinia'\nimport { ref, computed } from 'vue'\n\n/**\n * 认证状态管理 Store\n *\n * 职责:\n * - 管理 API Key 的"
  },
  {
    "path": "frontend/src/stores/channel.ts",
    "chars": 14297,
    "preview": "import { defineStore } from 'pinia'\nimport { ref, computed, watch } from 'vue'\nimport { useRouter } from 'vue-router'\nim"
  },
  {
    "path": "frontend/src/stores/dialog.ts",
    "chars": 1849,
    "preview": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\nimport type { Channel } from '@/services/api'\n\n/**\n * 对话框状"
  },
  {
    "path": "frontend/src/stores/index.ts",
    "chars": 247,
    "preview": "/**\n * Pinia Stores 统一导出\n */\nexport { useAuthStore } from './auth'\nexport { useChannelStore } from './channel'\nexport { "
  },
  {
    "path": "frontend/src/stores/preferences.ts",
    "chars": 1662,
    "preview": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\n/**\n * 用户偏好设置 Store\n *\n * 职责:\n * - 管理暗色模式偏好(light/dark/au"
  },
  {
    "path": "frontend/src/stores/system.ts",
    "chars": 2871,
    "preview": "import { defineStore } from 'pinia'\nimport { ref, computed } from 'vue'\nimport type { VersionInfo } from '@/services/ver"
  },
  {
    "path": "frontend/src/styles/settings.scss",
    "chars": 605,
    "preview": "// Vuetify 样式变量配置 - 复古像素主题\n\n// 复古像素主题:无圆角\n$border-radius-root: 0px !default;\n$btn-border-radius: 0px !default;\n$card-bor"
  },
  {
    "path": "frontend/src/utils/quickInputParser.test.ts",
    "chars": 17354,
    "preview": "/**\n * 快速添加渠道 - 输入解析测试\n *\n * 测试 isValidApiKey 和 isValidUrl 工具函数\n */\n\nimport { describe, it, expect } from 'vitest'\nimpor"
  },
  {
    "path": "frontend/src/utils/quickInputParser.ts",
    "chars": 6887,
    "preview": "/**\n * 快速添加渠道 - 输入解析工具\n *\n * 用于识别 API Key 和 URL 格式\n */\n\n/**\n * 检测字符串是否看起来像配置键名(全大写 + 下划线分隔的单词)\n * 例如:API_TIMEOUT_MS, ANT"
  },
  {
    "path": "frontend/src/views/ChannelsView.vue",
    "chars": 1701,
    "preview": "<template>\n  <!-- 渠道编排(高密度列表模式) -->\n  <ChannelOrchestration\n    v-if=\"channelStore.currentChannelsData.channels?.length\""
  },
  {
    "path": "frontend/tsconfig.json",
    "chars": 497,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"lib\": ["
  },
  {
    "path": "frontend/vite.config.ts",
    "chars": 1485,
    "preview": "import { defineConfig, loadEnv } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport vuetify from 'vite-plugin-vueti"
  }
]

About this extraction

This page contains the full source code of the BenedictKing/claude-proxy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 152 files (1.2 MB), approximately 393.6k tokens, and a symbol index with 954 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!