Selaa lähdekoodia

feat: openapi mcp server and sse mcp proxy (#137)

* feat: openapi mcp

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: content type

* feat: server from

* feat: Authorization

* fix: log request index

* fix: ci

* feat: dynamic swag host

* feat: impl mcp proxy

* feat: per req price

* feat: mcp proxy

* chore: rename public

* feat: redis store support

* feat: init openapi

* feat: mpsc ctx

* feat: anthropic endpoint support

* fix: go mod

* fix: go version

* fix: docker build

* fix: docker build

* fix: docker build

* fix: db migrate

* fix: openapi mcp

* chore: code optimize

* fix: ci

* fix: claude endpoint

* fix: ci

* fix: ci

* fix: claude stream

* feat: model config retry times

* feat: group model config retry times

* fix: delete at json

* feat: jina rerank

* feat: jina rerank error detail

* fix: claude err chunk

* feat: gemini thinking config support

* feat: claude image url conv to base64

* fix: ci lint

* fix: ci lint

* chore: swag
zijiren 8 kuukautta sitten
vanhempi
sitoutus
e552c64efa
100 muutettua tiedostoa jossa 3603 lisäystä ja 437 poistoa
  1. 23 2
      .github/workflows/ci.yml
  2. 92 0
      .github/workflows/release-openapi-mcp.yml
  3. 7 3
      .github/workflows/release.yml
  4. 5 5
      Dockerfile
  5. 0 0
      core/.gitignore
  6. 0 1
      core/.golangci.yml
  7. 1 1
      core/common/audio/audio.go
  8. 1 1
      core/common/balance/balance.go
  9. 1 1
      core/common/balance/mock.go
  10. 4 4
      core/common/balance/sealos.go
  11. 0 0
      core/common/color.go
  12. 1 1
      core/common/config/config.go
  13. 0 0
      core/common/constants.go
  14. 22 6
      core/common/consume/consume.go
  15. 2 2
      core/common/consume/record.go
  16. 0 0
      core/common/conv/any.go
  17. 1 1
      core/common/database.go
  18. 1 1
      core/common/env/helper.go
  19. 1 1
      core/common/fastJSONSerializer/fastJSONSerializer.go
  20. 0 9
      core/common/gin.go
  21. 1 1
      core/common/image/image.go
  22. 0 0
      core/common/image/svg.go
  23. 1 1
      core/common/ipblack/main.go
  24. 0 0
      core/common/ipblack/mem.go
  25. 1 1
      core/common/ipblack/redis.go
  26. 48 0
      core/common/mcpproxy/session.go
  27. 212 0
      core/common/mcpproxy/sse.go
  28. 81 0
      core/common/mcpproxy/sse_test.go
  29. 0 0
      core/common/network/ip.go
  30. 1 1
      core/common/network/ip_test.go
  31. 2 2
      core/common/notify/feishu.go
  32. 1 1
      core/common/notify/feishu_test.go
  33. 0 0
      core/common/notify/notify.go
  34. 2 2
      core/common/notify/std.go
  35. 0 0
      core/common/redis.go
  36. 6 12
      core/common/render/event.go
  37. 1 1
      core/common/render/render.go
  38. 1 1
      core/common/rpmlimit/main.go
  39. 0 0
      core/common/rpmlimit/mem.go
  40. 1 1
      core/common/rpmlimit/redis.go
  41. 0 0
      core/common/splitter/splitter.go
  42. 1 1
      core/common/splitter/think.go
  43. 1 1
      core/common/tiktoken/assest.go
  44. 0 0
      core/common/tiktoken/assets/.gitkeep
  45. 0 0
      core/common/tiktoken/tiktoken.go
  46. 1 1
      core/common/trunc.go
  47. 1 1
      core/common/trylock/lock.go
  48. 1 1
      core/common/trylock/lock_test.go
  49. 6 6
      core/controller/channel-billing.go
  50. 10 19
      core/controller/channel-test.go
  51. 5 5
      core/controller/channel.go
  52. 3 3
      core/controller/dashboard.go
  53. 10 3
      core/controller/group.go
  54. 2 2
      core/controller/import.go
  55. 2 2
      core/controller/log.go
  56. 175 0
      core/controller/mcpopenapi.go
  57. 502 0
      core/controller/mcpproxy.go
  58. 2 2
      core/controller/misc.go
  59. 4 4
      core/controller/model.go
  60. 2 2
      core/controller/modelconfig.go
  61. 2 2
      core/controller/monitor.go
  62. 2 2
      core/controller/option.go
  63. 224 0
      core/controller/publicmcp.go
  64. 29 33
      core/controller/relay-controller.go
  65. 5 5
      core/controller/relay-dashboard.go
  66. 2 2
      core/controller/relay-model.go
  67. 27 3
      core/controller/relay.go
  68. 3 3
      core/controller/token.go
  69. 0 0
      core/controller/utils.go
  70. 0 0
      core/deploy/Kubefile
  71. 0 0
      core/deploy/manifests/aiproxy-config.yaml.tmpl
  72. 0 0
      core/deploy/manifests/deploy.yaml.tmpl
  73. 0 0
      core/deploy/manifests/ingress.yaml.tmpl
  74. 0 0
      core/deploy/manifests/pgsql-log.yaml
  75. 0 0
      core/deploy/manifests/pgsql.yaml
  76. 0 0
      core/deploy/manifests/redis.yaml
  77. 0 0
      core/deploy/scripts/init.sh
  78. 0 0
      core/docker-compose.yaml
  79. 694 69
      core/docs/docs.go
  80. 697 70
      core/docs/swagger.json
  81. 461 47
      core/docs/swagger.yaml
  82. 21 13
      core/go.mod
  83. 38 22
      core/go.sum
  84. 20 16
      core/main.go
  85. 11 6
      core/middleware/auth.go
  86. 0 0
      core/middleware/cors.go
  87. 0 0
      core/middleware/ctxkey.go
  88. 17 13
      core/middleware/distributor.go
  89. 1 1
      core/middleware/distributor_test.go
  90. 1 1
      core/middleware/ipblack.go
  91. 0 0
      core/middleware/log.go
  92. 86 0
      core/middleware/mcp.go
  93. 0 0
      core/middleware/reqid.go
  94. 1 1
      core/middleware/utils.go
  95. 1 1
      core/model/batch.go
  96. 4 4
      core/model/cache.go
  97. 5 5
      core/model/channel.go
  98. 1 1
      core/model/channeltest.go
  99. 0 0
      core/model/configkey.go
  100. 1 1
      core/model/consumeerr.go

+ 23 - 2
.github/workflows/ci.yml

@@ -19,7 +19,8 @@ on:
       - "**/*.yaml"
 
 jobs:
-  golangci-lint:
+  golangci-lint-aiproxy:
+    name: Lint AI Proxy
     runs-on: ubuntu-24.04
     steps:
       - name: Checkout
@@ -34,4 +35,24 @@ jobs:
         uses: golangci/golangci-lint-action@v6
         with:
           version: v1.64.6
-          args: "--out-format colored-line-number --max-issues-per-linter 0 --max-same-issues 0"
+          working-directory: core
+          args: "--out-format colored-line-number --max-issues-per-linter 0 --max-same-issues 0 --config ./.golangci.yml"
+
+  golangci-lint-openapimcp:
+    name: Lint OpneAPI MCP
+    runs-on: ubuntu-24.04
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Setup Golang with cache
+        uses: magnetikonline/action-golang-cache@v5
+        with:
+          go-version: "1.23"
+
+      - name: Run Linter
+        uses: golangci/golangci-lint-action@v6
+        with:
+          version: v1.64.6
+          working-directory: openapi-mcp
+          args: "--out-format colored-line-number --max-issues-per-linter 0 --max-same-issues 0 --config ./.golangci.yml"

+ 92 - 0
.github/workflows/release-openapi-mcp.yml

@@ -0,0 +1,92 @@
+name: Release OpenAPI MCP
+
+on:
+  push:
+    branches:
+      - "**"
+    tags:
+      - "v*.*.*"
+    paths-ignore:
+      - "**/*.md"
+      - "**/*.yaml"
+  pull_request:
+    branches:
+      - "**"
+    paths-ignore:
+      - "**/*.md"
+      - "**/*.yaml"
+
+jobs:
+  release-openapi-mcp:
+    name: Release OpenAPI MCP
+    runs-on: ubuntu-24.04
+    permissions:
+      contents: write
+    strategy:
+      fail-fast: false
+      matrix:
+        targets:
+          - GOOS: linux
+            GOARCH: arm64
+          - GOOS: linux
+            GOARCH: amd64
+          - GOOS: darwin
+            GOARCH: arm64
+          - GOOS: darwin
+            GOARCH: amd64
+          - GOOS: windows
+            GOARCH: amd64
+            EXT: .exe
+          - GOOS: windows
+            GOARCH: arm64
+            EXT: .exe
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Setup Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: "1.23"
+
+      - name: Build
+        working-directory: openapi-mcp
+        run: |
+          export GOOS=${{ matrix.targets.GOOS }}
+          export GOARCH=${{ matrix.targets.GOARCH }}
+          go build -trimpath -tags "jsoniter" -ldflags "-s -w" -o openapi-mcp-${{ matrix.targets.GOOS }}-${{ matrix.targets.GOARCH }}${{ matrix.targets.EXT }}
+
+      - name: Get release meta
+        if: ${{ startsWith(github.ref, 'refs/tags/') }}
+        id: release_meta
+        run: |
+          version=${GITHUB_REF/refs\/tags\/v/}
+          echo "version: ${version}"
+          prerelease=$(echo ${version} | grep -E 'rc|beta|alpha' || true)
+          release_name="Version ${version}"
+          echo "release_name: ${release_name}"
+          if [ -n "${prerelease}" ]; then
+            prerelease=true
+            release_name="${release_name} (Prerelease)"
+          fi
+          tag_name="v${version}"
+          echo "prerelease: ${prerelease}"
+          echo "tag_name: ${tag_name}"
+
+          echo "PRERELEASE=${prerelease}" >> $GITHUB_OUTPUT
+          echo "RELEASE_NAME=${release_name}" >> $GITHUB_OUTPUT
+          echo "TAG_NAME=${tag_name}" >> $GITHUB_OUTPUT
+
+      - name: Release
+        uses: softprops/action-gh-release@v2
+        if: ${{ startsWith(github.ref, 'refs/tags/') }}
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          draft: false
+          prerelease: ${{ steps.release_meta.outputs.PRERELEASE }}
+          append_body: false
+          fail_on_unmatched_files: true
+          name: ${{ steps.release_meta.outputs.RELEASE_NAME }}
+          tag_name: ${{ steps.release_meta.outputs.TAG_NAME }}
+          files: |
+            openapi-mcp/openapi-mcp-${{ matrix.targets.GOOS }}-${{ matrix.targets.GOARCH }}${{ matrix.targets.EXT }}

+ 7 - 3
.github/workflows/release.yml

@@ -24,7 +24,7 @@ env:
 
 jobs:
   release:
-    name: Release
+    name: Release AI Proxy
     runs-on: ubuntu-24.04
     permissions:
       contents: write
@@ -51,6 +51,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: Download tiktoken
+        working-directory: core
         run: |
           bash scripts/tiktoken.sh
 
@@ -60,11 +61,13 @@ jobs:
           go-version: "1.23"
 
       - name: Generate Swagger
+        working-directory: core
         run: |
           go install github.com/swaggo/swag/cmd/swag@latest
           bash scripts/swag.sh
 
       - name: Build
+        working-directory: core
         run: |
           export GOOS=${{ matrix.targets.GOOS }}
           export GOARCH=${{ matrix.targets.GOARCH }}
@@ -72,7 +75,7 @@ jobs:
 
       - name: Get release meta
         if: ${{ startsWith(github.ref, 'refs/tags/') }}
-        id: release_meta
+        working-directory: core
         run: |
           version=${GITHUB_REF/refs\/tags\/v/}
           echo "version: ${version}"
@@ -103,7 +106,7 @@ jobs:
           name: ${{ steps.release_meta.outputs.RELEASE_NAME }}
           tag_name: ${{ steps.release_meta.outputs.TAG_NAME }}
           files: |
-            aiproxy-${{ matrix.targets.GOOS }}-${{ matrix.targets.GOARCH }}${{ matrix.targets.EXT }}
+            core/aiproxy-${{ matrix.targets.GOOS }}-${{ matrix.targets.GOARCH }}${{ matrix.targets.EXT }}
 
   build-docker-images:
     name: Build Docker Images
@@ -165,6 +168,7 @@ jobs:
           outputs: type=image,"name=${{ env.GHCR_REPO }}${{ env.DOCKERHUB_REPO && format(',{0}', env.DOCKERHUB_REPO) }}${{ env.ALIYUN_REPO && format(',{0}', env.ALIYUN_REPO) }}",name-canonical=true,push-by-digest=${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }},push=${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }}
 
       - name: Export digest
+        working-directory: core
         run: |
           mkdir -p ${{ runner.temp }}/digests
           digest="${{ steps.build.outputs.digest }}"

+ 5 - 5
Dockerfile

@@ -1,10 +1,10 @@
-FROM golang:1.23-alpine AS builder
+FROM golang:1.24-alpine AS builder
 
-WORKDIR /aiproxy
+RUN apk add --no-cache curl
 
-COPY ./ ./
+WORKDIR /aiproxy/core
 
-RUN apk add --no-cache curl
+COPY ./ /aiproxy
 
 RUN sh scripts/tiktoken.sh
 
@@ -25,7 +25,7 @@ VOLUME /aiproxy
 RUN apk add --no-cache ca-certificates tzdata ffmpeg curl && \
     rm -rf /var/cache/apk/*
 
-COPY --from=builder /aiproxy/aiproxy /usr/local/bin/aiproxy
+COPY --from=builder /aiproxy/core/aiproxy /usr/local/bin/aiproxy
 
 ENV PUID=0 PGID=0 UMASK=022
 

+ 0 - 0
.gitignore → core/.gitignore


+ 0 - 1
.golangci.yml → core/.golangci.yml

@@ -56,7 +56,6 @@ linters:
     - testpackage
     - tparallel
     - goheader
-    - gomoddirectives
     - gomodguard
     - musttag
     - tagalign

+ 1 - 1
common/audio/audio.go → core/common/audio/audio.go

@@ -7,7 +7,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/labring/aiproxy/common/config"
+	"github.com/labring/aiproxy/core/common/config"
 )
 
 var ErrAudioDurationNAN = errors.New("audio duration is N/A")

+ 1 - 1
common/balance/balance.go → core/common/balance/balance.go

@@ -3,7 +3,7 @@ package balance
 import (
 	"context"
 
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/model"
 )
 
 type GroupBalance interface {

+ 1 - 1
common/balance/mock.go → core/common/balance/mock.go

@@ -3,7 +3,7 @@ package balance
 import (
 	"context"
 
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/model"
 )
 
 var _ GroupBalance = (*MockGroupBalance)(nil)

+ 4 - 4
common/balance/sealos.go → core/common/balance/sealos.go

@@ -11,10 +11,10 @@ import (
 
 	"github.com/bytedance/sonic"
 	"github.com/golang-jwt/jwt/v5"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/common/conv"
-	"github.com/labring/aiproxy/common/env"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/conv"
+	"github.com/labring/aiproxy/core/common/env"
+	"github.com/labring/aiproxy/core/model"
 	"github.com/redis/go-redis/v9"
 	"github.com/shopspring/decimal"
 	log "github.com/sirupsen/logrus"

+ 0 - 0
common/color.go → core/common/color.go


+ 1 - 1
common/config/config.go → core/common/config/config.go

@@ -7,7 +7,7 @@ import (
 	"strconv"
 	"sync/atomic"
 
-	"github.com/labring/aiproxy/common/env"
+	"github.com/labring/aiproxy/core/common/env"
 )
 
 var (

+ 0 - 0
common/constants.go → core/common/constants.go


+ 22 - 6
common/consume/consume.go → core/common/consume/consume.go

@@ -2,13 +2,15 @@ package consume
 
 import (
 	"context"
+	"net/http"
 	"sync"
 	"time"
 
-	"github.com/labring/aiproxy/common/balance"
-	"github.com/labring/aiproxy/common/notify"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/relay/meta"
+	"github.com/labring/aiproxy/core/common/balance"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/notify"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/meta"
 	"github.com/shopspring/decimal"
 	log "github.com/sirupsen/logrus"
 )
@@ -70,9 +72,19 @@ func Consume(
 	requestDetail *model.RequestDetail,
 	downstreamResult bool,
 ) {
-	amount := CalculateAmount(usage, modelPrice)
+	var amount float64
+	if code == http.StatusOK {
+		amount = CalculateAmount(usage, modelPrice)
+		amount = consumeAmount(ctx, amount, postGroupConsumer, meta)
+	}
+
+	if requestDetail != nil && config.GetLogContentStorageHours() < 0 {
+		requestDetail = nil
+	}
 
-	amount = consumeAmount(ctx, amount, postGroupConsumer, meta)
+	if requestDetail == nil && config.GetLogStorageHours() < 0 {
+		return
+	}
 
 	err := recordConsume(
 		meta,
@@ -109,6 +121,10 @@ func CalculateAmount(
 	usage model.Usage,
 	modelPrice model.Price,
 ) float64 {
+	if modelPrice.PerRequestPrice != 0 {
+		return modelPrice.PerRequestPrice
+	}
+
 	inputTokens := usage.InputTokens
 	outputTokens := usage.OutputTokens
 

+ 2 - 2
common/consume/record.go → core/common/consume/record.go

@@ -3,8 +3,8 @@ package consume
 import (
 	"time"
 
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/relay/meta"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/meta"
 )
 
 func recordConsume(

+ 0 - 0
common/conv/any.go → core/common/conv/any.go


+ 1 - 1
common/database.go → core/common/database.go

@@ -1,7 +1,7 @@
 package common
 
 import (
-	"github.com/labring/aiproxy/common/env"
+	"github.com/labring/aiproxy/core/common/env"
 )
 
 var (

+ 1 - 1
common/env/helper.go → core/common/env/helper.go

@@ -5,7 +5,7 @@ import (
 	"strconv"
 
 	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/common/conv"
+	"github.com/labring/aiproxy/core/common/conv"
 	log "github.com/sirupsen/logrus"
 )
 

+ 1 - 1
common/fastJSONSerializer/fastJSONSerializer.go → core/common/fastJSONSerializer/fastJSONSerializer.go

@@ -6,7 +6,7 @@ import (
 	"reflect"
 
 	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/common/conv"
+	"github.com/labring/aiproxy/core/common/conv"
 	"gorm.io/gorm/schema"
 )
 

+ 0 - 9
common/gin.go → core/common/gin.go

@@ -11,7 +11,6 @@ import (
 
 	"github.com/bytedance/sonic"
 	"github.com/bytedance/sonic/ast"
-	"github.com/gin-gonic/gin"
 )
 
 type RequestBodyKey struct{}
@@ -100,11 +99,3 @@ func UnmarshalBody2Node(req *http.Request) (ast.Node, error) {
 	}
 	return sonic.Get(requestBody)
 }
-
-func SetEventStreamHeaders(c *gin.Context) {
-	c.Writer.Header().Set("Content-Type", "text/event-stream")
-	c.Writer.Header().Set("Cache-Control", "no-cache")
-	c.Writer.Header().Set("Connection", "keep-alive")
-	c.Writer.Header().Set("Transfer-Encoding", "chunked")
-	c.Writer.Header().Set("X-Accel-Buffering", "no")
-}

+ 1 - 1
common/image/image.go → core/common/image/image.go

@@ -19,7 +19,7 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/labring/aiproxy/common"
+	"github.com/labring/aiproxy/core/common"
 	// import webp decoder
 	_ "golang.org/x/image/webp"
 )

+ 0 - 0
common/image/svg.go → core/common/image/svg.go


+ 1 - 1
common/ipblack/main.go → core/common/ipblack/main.go

@@ -4,7 +4,7 @@ import (
 	"context"
 	"time"
 
-	"github.com/labring/aiproxy/common"
+	"github.com/labring/aiproxy/core/common"
 	log "github.com/sirupsen/logrus"
 )
 

+ 0 - 0
common/ipblack/mem.go → core/common/ipblack/mem.go


+ 1 - 1
common/ipblack/redis.go → core/common/ipblack/redis.go

@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/labring/aiproxy/common"
+	"github.com/labring/aiproxy/core/common"
 )
 
 const (

+ 48 - 0
core/common/mcpproxy/session.go

@@ -0,0 +1,48 @@
+package mcpproxy
+
+import "sync"
+
+// SessionManager defines the interface for managing session information
+type SessionManager interface {
+	// Set stores a sessionID and its corresponding backend endpoint
+	Set(sessionID, endpoint string)
+	// Get retrieves the backend endpoint for a sessionID
+	Get(sessionID string) (string, bool)
+	// Delete removes a sessionID from the store
+	Delete(sessionID string)
+}
+
+// MemStore implements the SessionManager interface
+type MemStore struct {
+	mu       sync.RWMutex
+	sessions map[string]string // sessionID -> host+endpoint
+}
+
+// NewMemStore creates a new session store
+func NewMemStore() *MemStore {
+	return &MemStore{
+		sessions: make(map[string]string),
+	}
+}
+
+// Set stores a sessionID and its corresponding backend endpoint
+func (s *MemStore) Set(sessionID, endpoint string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.sessions[sessionID] = endpoint
+}
+
+// Get retrieves the backend endpoint for a sessionID
+func (s *MemStore) Get(sessionID string) (string, bool) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	endpoint, ok := s.sessions[sessionID]
+	return endpoint, ok
+}
+
+// Delete removes a sessionID from the store
+func (s *MemStore) Delete(sessionID string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	delete(s.sessions, sessionID)
+}

+ 212 - 0
core/common/mcpproxy/sse.go

@@ -0,0 +1,212 @@
+package mcpproxy
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+)
+
+type EndpointProvider interface {
+	NewEndpoint() (newSession string, newEndpoint string)
+	LoadEndpoint(endpoint string) (session string)
+}
+
+// Proxy represents the proxy object that handles SSE and HTTP requests
+type Proxy struct {
+	store    SessionManager
+	endpoint EndpointProvider
+	backend  string
+	headers  map[string]string
+}
+
+// NewProxy creates a new proxy with the given backend and endpoint handler
+func NewProxy(backend string, headers map[string]string, store SessionManager, endpoint EndpointProvider) *Proxy {
+	return &Proxy{
+		store:    store,
+		endpoint: endpoint,
+		backend:  backend,
+		headers:  headers,
+	}
+}
+
+func (p *Proxy) SSEHandler(w http.ResponseWriter, r *http.Request) {
+	SSEHandler(w, r, p.store, p.endpoint, p.backend, p.headers)
+}
+
+func SSEHandler(
+	w http.ResponseWriter,
+	r *http.Request,
+	store SessionManager,
+	endpoint EndpointProvider,
+	backend string,
+	headers map[string]string,
+) {
+	// Create a request to the backend SSE endpoint
+	req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, backend, nil)
+	if err != nil {
+		http.Error(w, "Failed to create backend request", http.StatusInternalServerError)
+		return
+	}
+
+	// Copy headers from original request
+	for name, value := range headers {
+		req.Header.Set(name, value)
+	}
+
+	// Set necessary headers for SSE
+	req.Header.Set("Accept", "text/event-stream")
+	req.Header.Set("Cache-Control", "no-cache")
+	req.Header.Set("Connection", "keep-alive")
+
+	// Make the request to the backend
+	//nolint:bodyclose
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		http.Error(w, "Failed to connect to backend", http.StatusInternalServerError)
+		return
+	}
+	defer resp.Body.Close()
+
+	// Set SSE headers for the client response
+	w.Header().Set("Content-Type", "text/event-stream")
+	w.Header().Set("Cache-Control", "no-cache")
+	w.Header().Set("Connection", "keep-alive")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	// Create a context that cancels when the client disconnects
+	ctx, cancel := context.WithCancel(r.Context())
+	defer cancel()
+
+	// Monitor client disconnection
+	go func() {
+		<-ctx.Done()
+		resp.Body.Close()
+	}()
+
+	// Parse the SSE stream and extract sessionID
+	reader := bufio.NewReader(resp.Body)
+	flusher, ok := w.(http.Flusher)
+	if !ok {
+		http.Error(w, "Streaming not supported", http.StatusInternalServerError)
+		return
+	}
+
+	for {
+		line, err := reader.ReadString('\n')
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			return
+		}
+
+		// Write the line to the client
+		fmt.Fprint(w, line)
+		flusher.Flush()
+
+		// Check if this is an endpoint event with sessionID
+		if strings.HasPrefix(line, "event: endpoint") {
+			// Next line should contain the data
+			dataLine, err := reader.ReadString('\n')
+			if err != nil {
+				return
+			}
+
+			newSession, newEndpoint := endpoint.NewEndpoint()
+			defer func() {
+				store.Delete(newSession)
+			}()
+
+			// Extract sessionID from data line
+			// Example: data: /message?sessionId=3088a771-7961-44e8-9bdf-21953889f694
+			if strings.HasPrefix(dataLine, "data: ") {
+				endpoint := strings.TrimSpace(strings.TrimPrefix(dataLine, "data: "))
+				copyURL := *req.URL
+				backendHostURL := &copyURL
+				backendHostURL.Path = ""
+				backendHostURL.RawQuery = ""
+				store.Set(newSession, backendHostURL.String()+endpoint)
+			} else {
+				break
+			}
+
+			// Write the data line to the client
+			fmt.Fprintf(w, "data: %s\n", newEndpoint)
+			flusher.Flush()
+		}
+	}
+}
+
+func (p *Proxy) ProxyHandler(w http.ResponseWriter, r *http.Request) {
+	ProxyHandler(w, r, p.store, p.endpoint)
+}
+
+func ProxyHandler(
+	w http.ResponseWriter,
+	r *http.Request,
+	store SessionManager,
+	endpoint EndpointProvider,
+) {
+	// Extract sessionID from the request
+	sessionID := endpoint.LoadEndpoint(r.URL.String())
+	if sessionID == "" {
+		http.Error(w, "Missing sessionId", http.StatusBadRequest)
+		return
+	}
+
+	// Look up the backend endpoint
+	backendEndpoint, ok := store.Get(sessionID)
+	if !ok {
+		http.Error(w, "Invalid or expired sessionId", http.StatusNotFound)
+		return
+	}
+
+	u, err := url.Parse(backendEndpoint)
+	if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
+		http.Error(w, "Invalid backend", http.StatusBadRequest)
+		return
+	}
+
+	// Create a request to the backend
+	req, err := http.NewRequestWithContext(r.Context(), r.Method, backendEndpoint, r.Body)
+	if err != nil {
+		http.Error(w, "Failed to create backend request", http.StatusInternalServerError)
+		return
+	}
+
+	// Copy headers from original request
+	for name, values := range r.Header {
+		for _, value := range values {
+			req.Header.Add(name, value)
+		}
+	}
+
+	// Make the request to the backend
+	client := &http.Client{
+		Timeout: time.Second * 30,
+	}
+	resp, err := client.Do(req)
+	if err != nil {
+		http.Error(w, "Failed to connect to backend", http.StatusInternalServerError)
+		return
+	}
+	defer resp.Body.Close()
+
+	// Copy response headers
+	for name, values := range resp.Header {
+		for _, value := range values {
+			w.Header().Add(name, value)
+		}
+	}
+
+	// Set response status code
+	w.WriteHeader(resp.StatusCode)
+
+	// Copy response body
+	_, _ = io.Copy(w, resp.Body)
+}

+ 81 - 0
core/common/mcpproxy/sse_test.go

@@ -0,0 +1,81 @@
+package mcpproxy_test
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/labring/aiproxy/core/common/mcpproxy"
+)
+
+type TestEndpointHandler struct{}
+
+func (h *TestEndpointHandler) NewEndpoint() (string, string) {
+	return "test-session-id", "/message?sessionId=test-session-id"
+}
+
+func (h *TestEndpointHandler) LoadEndpoint(endpoint string) string {
+	if strings.Contains(endpoint, "test-session-id") {
+		return "test-session-id"
+	}
+	return ""
+}
+
+func TestProxySSEEndpoint(t *testing.T) {
+	// Setup a mock backend server
+	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		w.Header().Set("Content-Type", "text/event-stream")
+		w.Header().Set("Cache-Control", "no-cache")
+		w.Header().Set("Connection", "keep-alive")
+
+		flusher, ok := w.(http.Flusher)
+		if !ok {
+			t.Fatal("Expected ResponseWriter to be a Flusher")
+		}
+
+		// Send an endpoint event
+		fmt.Fprintf(w, "event: endpoint\n")
+		fmt.Fprintf(w, "data: /message?sessionId=original-session-id\n\n")
+		flusher.Flush()
+
+		// Keep the connection open for a bit
+		time.Sleep(100 * time.Millisecond)
+	}))
+	defer backendServer.Close()
+
+	// Create the proxy
+	store := mcpproxy.NewMemStore()
+	handler := &TestEndpointHandler{}
+	proxy := mcpproxy.NewProxy(backendServer.URL+"/sse", nil, store, handler)
+
+	// Setup the proxy server
+	proxyServer := httptest.NewServer(http.HandlerFunc(proxy.SSEHandler))
+	defer proxyServer.Close()
+
+	// Make a request to the proxy
+	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, proxyServer.URL, nil)
+	if err != nil {
+		t.Fatalf("Error making request to proxy: %v", err)
+	}
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		t.Fatalf("Error making request to proxy: %v", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Expected status code %d, got %d", http.StatusOK, resp.StatusCode)
+	}
+
+	// Verify the session was stored
+	endpoint, ok := store.Get("test-session-id")
+	if !ok {
+		t.Error("Session was not stored")
+	}
+	if !strings.Contains(endpoint, "/message?sessionId=original-session-id") {
+		t.Errorf("Endpoint does not contain expected path, got: %s", endpoint)
+	}
+}

+ 0 - 0
common/network/ip.go → core/common/network/ip.go


+ 1 - 1
common/network/ip_test.go → core/common/network/ip_test.go

@@ -3,7 +3,7 @@ package network_test
 import (
 	"testing"
 
-	"github.com/labring/aiproxy/common/network"
+	"github.com/labring/aiproxy/core/common/network"
 	"github.com/smartystreets/goconvey/convey"
 )
 

+ 2 - 2
common/notify/feishu.go → core/common/notify/feishu.go

@@ -8,8 +8,8 @@ import (
 	"time"
 
 	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/common/trylock"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/trylock"
 )
 
 type FeishuNotifier struct {

+ 1 - 1
common/notify/feishu_test.go → core/common/notify/feishu_test.go

@@ -5,7 +5,7 @@ import (
 	"os"
 	"testing"
 
-	"github.com/labring/aiproxy/common/notify"
+	"github.com/labring/aiproxy/core/common/notify"
 )
 
 func TestPostToFeiShuv2(t *testing.T) {

+ 0 - 0
common/notify/notify.go → core/common/notify/notify.go


+ 2 - 2
common/notify/std.go → core/common/notify/std.go

@@ -3,8 +3,8 @@ package notify
 import (
 	"time"
 
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/common/trylock"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/trylock"
 	log "github.com/sirupsen/logrus"
 )
 

+ 0 - 0
common/redis.go → core/common/redis.go


+ 6 - 12
common/render/event.go → core/common/render/event.go

@@ -3,12 +3,7 @@ package render
 import (
 	"net/http"
 
-	"github.com/labring/aiproxy/common/conv"
-)
-
-var (
-	contentType = []string{"text/event-stream"}
-	noCache     = []string{"no-cache"}
+	"github.com/labring/aiproxy/core/common/conv"
 )
 
 type OpenAISSE struct {
@@ -42,10 +37,9 @@ func (r *OpenAISSE) Render(w http.ResponseWriter) error {
 }
 
 func (r *OpenAISSE) WriteContentType(w http.ResponseWriter) {
-	header := w.Header()
-	header["Content-Type"] = contentType
-
-	if _, exist := header["Cache-Control"]; !exist {
-		header["Cache-Control"] = noCache
-	}
+	w.Header().Set("Content-Type", "text/event-stream")
+	w.Header().Set("Cache-Control", "no-cache")
+	w.Header().Set("Connection", "keep-alive")
+	w.Header().Set("Transfer-Encoding", "chunked")
+	w.Header().Set("X-Accel-Buffering", "no")
 }

+ 1 - 1
common/render/render.go → core/common/render/render.go

@@ -6,7 +6,7 @@ import (
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/conv"
+	"github.com/labring/aiproxy/core/common/conv"
 )
 
 func StringData(c *gin.Context, str string) {

+ 1 - 1
common/rpmlimit/main.go → core/common/rpmlimit/main.go

@@ -4,7 +4,7 @@ import (
 	"context"
 	"time"
 
-	"github.com/labring/aiproxy/common"
+	"github.com/labring/aiproxy/core/common"
 	log "github.com/sirupsen/logrus"
 )
 

+ 0 - 0
common/rpmlimit/mem.go → core/common/rpmlimit/mem.go


+ 1 - 1
common/rpmlimit/redis.go → core/common/rpmlimit/redis.go

@@ -8,7 +8,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/labring/aiproxy/common"
+	"github.com/labring/aiproxy/core/common"
 	"github.com/redis/go-redis/v9"
 )
 

+ 0 - 0
common/splitter/splitter.go → core/common/splitter/splitter.go


+ 1 - 1
common/splitter/think.go → core/common/splitter/think.go

@@ -1,6 +1,6 @@
 package splitter
 
-import "github.com/labring/aiproxy/common/conv"
+import "github.com/labring/aiproxy/core/common/conv"
 
 const (
 	ThinkHead = "<think>\n"

+ 1 - 1
common/tiktoken/assest.go → core/common/tiktoken/assest.go

@@ -9,7 +9,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/labring/aiproxy/common/conv"
+	"github.com/labring/aiproxy/core/common/conv"
 	"github.com/pkoukk/tiktoken-go"
 )
 

+ 0 - 0
common/tiktoken/assets/.gitkeep → core/common/tiktoken/assets/.gitkeep


+ 0 - 0
common/tiktoken/tiktoken.go → core/common/tiktoken/tiktoken.go


+ 1 - 1
common/trunc.go → core/common/trunc.go

@@ -3,7 +3,7 @@ package common
 import (
 	"unicode/utf8"
 
-	"github.com/labring/aiproxy/common/conv"
+	"github.com/labring/aiproxy/core/common/conv"
 )
 
 func TruncateByRune[T ~string](s T, length int) T {

+ 1 - 1
common/trylock/lock.go → core/common/trylock/lock.go

@@ -5,7 +5,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/labring/aiproxy/common"
+	"github.com/labring/aiproxy/core/common"
 	log "github.com/sirupsen/logrus"
 )
 

+ 1 - 1
common/trylock/lock_test.go → core/common/trylock/lock_test.go

@@ -4,7 +4,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/labring/aiproxy/common/trylock"
+	"github.com/labring/aiproxy/core/common/trylock"
 )
 
 func TestMemLock(t *testing.T) {

+ 6 - 6
controller/channel-billing.go → core/controller/channel-billing.go

@@ -9,14 +9,14 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/notify"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/relay/adaptor"
-	"github.com/labring/aiproxy/relay/channeltype"
+	"github.com/labring/aiproxy/core/common/notify"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
+	"github.com/labring/aiproxy/core/relay/channeltype"
 )
 
-// https://github.com/labring/aiproxy/issues/79
+// https://github.com/labring/aiproxy/core/issues/79
 
 func updateChannelBalance(channel *model.Channel) (float64, error) {
 	adaptorI, ok := channeltype.GetAdaptor(channel.Type)

+ 10 - 19
controller/channel-test.go → core/controller/channel-test.go

@@ -16,17 +16,16 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/common/notify"
-	"github.com/labring/aiproxy/common/render"
-	"github.com/labring/aiproxy/common/trylock"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/monitor"
-	"github.com/labring/aiproxy/relay/channeltype"
-	"github.com/labring/aiproxy/relay/meta"
-	"github.com/labring/aiproxy/relay/mode"
-	"github.com/labring/aiproxy/relay/utils"
+	"github.com/labring/aiproxy/core/common/notify"
+	"github.com/labring/aiproxy/core/common/render"
+	"github.com/labring/aiproxy/core/common/trylock"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/monitor"
+	"github.com/labring/aiproxy/core/relay/channeltype"
+	"github.com/labring/aiproxy/core/relay/meta"
+	"github.com/labring/aiproxy/core/relay/mode"
+	"github.com/labring/aiproxy/core/relay/utils"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -276,10 +275,6 @@ func TestChannelModels(c *gin.Context) {
 	successResponseBody := c.Query("success_body") == "true"
 	isStream := c.Query("stream") == "true"
 
-	if isStream {
-		common.SetEventStreamHeaders(c)
-	}
-
 	results := make([]*TestResult, 0)
 	resultsMutex := sync.Mutex{}
 	hasError := atomic.Bool{}
@@ -373,10 +368,6 @@ func TestAllChannels(c *gin.Context) {
 	successResponseBody := c.Query("success_body") == "true"
 	isStream := c.Query("stream") == "true"
 
-	if isStream {
-		common.SetEventStreamHeaders(c)
-	}
-
 	results := make([]*TestResult, 0)
 	resultsMutex := sync.Mutex{}
 	hasErrorMap := make(map[int]*atomic.Bool)

+ 5 - 5
controller/channel.go → core/controller/channel.go

@@ -9,11 +9,11 @@ import (
 	"strings"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/monitor"
-	"github.com/labring/aiproxy/relay/adaptor"
-	"github.com/labring/aiproxy/relay/channeltype"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/monitor"
+	"github.com/labring/aiproxy/core/relay/adaptor"
+	"github.com/labring/aiproxy/core/relay/channeltype"
 	log "github.com/sirupsen/logrus"
 )
 

+ 3 - 3
controller/dashboard.go → core/controller/dashboard.go

@@ -9,9 +9,9 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/rpmlimit"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/common/rpmlimit"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
 	"gorm.io/gorm"
 )
 

+ 10 - 3
controller/group.go → core/controller/group.go

@@ -7,8 +7,8 @@ import (
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
 )
 
 type GroupResponse struct {
@@ -482,13 +482,18 @@ func SaveGroupModelConfigs(c *gin.Context) {
 //	@Param			group	path		string						true	"Group name"
 //	@Param			data	body		SaveGroupModelConfigRequest	true	"Group model config information"
 //	@Success		200		{object}	middleware.APIResponse
-//	@Router			/api/group/{group}/model_config/ [post]
+//	@Router			/api/group/{group}/model_config/{model} [post]
 func SaveGroupModelConfig(c *gin.Context) {
 	group := c.Param("group")
 	if group == "" {
 		middleware.ErrorResponse(c, http.StatusOK, "invalid parameter")
 		return
 	}
+	modelName := c.Param("model")
+	if modelName == "" {
+		middleware.ErrorResponse(c, http.StatusOK, "invalid parameter")
+		return
+	}
 
 	req := SaveGroupModelConfigRequest{}
 	err := c.ShouldBindJSON(&req)
@@ -497,6 +502,7 @@ func SaveGroupModelConfig(c *gin.Context) {
 		return
 	}
 	modelConfig := req.ToGroupModelConfig(group)
+	modelConfig.Model = modelName
 	err = model.SaveGroupModelConfig(modelConfig)
 	if err != nil {
 		middleware.ErrorResponse(c, http.StatusOK, err.Error())
@@ -652,6 +658,7 @@ func UpdateGroupModelConfig(c *gin.Context) {
 		return
 	}
 	modelConfig := req.ToGroupModelConfig(group)
+	modelConfig.Model = modelName
 	err = model.UpdateGroupModelConfig(modelConfig)
 	if err != nil {
 		middleware.ErrorResponse(c, http.StatusOK, err.Error())

+ 2 - 2
controller/import.go → core/controller/import.go

@@ -5,8 +5,8 @@ import (
 	"strings"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
 	"gorm.io/gorm"
 )
 

+ 2 - 2
controller/log.go → core/controller/log.go

@@ -6,8 +6,8 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
 )
 
 func parseTimeRange(c *gin.Context) (startTime, endTime time.Time) {

+ 175 - 0
core/controller/mcpopenapi.go

@@ -0,0 +1,175 @@
+package controller
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+)
+
+// SSEServer implements a Server-Sent Events (SSE) based MCP server.
+// It provides real-time communication capabilities over HTTP using the SSE protocol.
+type SSEServer struct {
+	server          *server.MCPServer
+	messageEndpoint string
+	eventQueue      chan string
+
+	keepAlive         bool
+	keepAliveInterval time.Duration
+}
+
+// SSEOption defines a function type for configuring SSEServer
+type SSEOption func(*SSEServer)
+
+// WithMessageEndpoint sets the message endpoint path
+func WithMessageEndpoint(endpoint string) SSEOption {
+	return func(s *SSEServer) {
+		s.messageEndpoint = endpoint
+	}
+}
+
+func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption {
+	return func(s *SSEServer) {
+		s.keepAlive = true
+		s.keepAliveInterval = keepAliveInterval
+	}
+}
+
+func WithKeepAlive(keepAlive bool) SSEOption {
+	return func(s *SSEServer) {
+		s.keepAlive = keepAlive
+	}
+}
+
+// NewSSEServer creates a new SSE server instance with the given MCP server and options.
+func NewSSEServer(server *server.MCPServer, opts ...SSEOption) *SSEServer {
+	s := &SSEServer{
+		server:            server,
+		messageEndpoint:   "/message",
+		keepAlive:         false,
+		keepAliveInterval: 10 * time.Second,
+		eventQueue:        make(chan string, 100),
+	}
+
+	// Apply all options
+	for _, opt := range opts {
+		opt(s)
+	}
+
+	return s
+}
+
+// handleSSE handles incoming SSE connection requests.
+// It sets up appropriate headers and creates a new session for the client.
+func (s *SSEServer) HandleSSE(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	w.Header().Set("Content-Type", "text/event-stream")
+	w.Header().Set("Cache-Control", "no-cache")
+	w.Header().Set("Connection", "keep-alive")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	flusher, ok := w.(http.Flusher)
+	if !ok {
+		http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
+		return
+	}
+
+	// Start keep alive : ping
+	if s.keepAlive {
+		go func() {
+			ticker := time.NewTicker(s.keepAliveInterval)
+			defer ticker.Stop()
+			for {
+				select {
+				case <-ticker.C:
+					s.eventQueue <- fmt.Sprintf(":ping - %s\n\n", time.Now().Format(time.RFC3339))
+				case <-r.Context().Done():
+					return
+				}
+			}
+		}()
+	}
+
+	// Send the initial endpoint event
+	fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", s.messageEndpoint)
+	flusher.Flush()
+
+	// Main event loop - this runs in the HTTP handler goroutine
+	for {
+		select {
+		case event := <-s.eventQueue:
+			// Write the event to the response
+			fmt.Fprint(w, event)
+			flusher.Flush()
+		case <-r.Context().Done():
+			return
+		}
+	}
+}
+
+// handleMessage processes incoming JSON-RPC messages from clients and sends responses
+// back through both the SSE connection and HTTP response.
+func (s *SSEServer) HandleMessage(req []byte) error {
+	// Parse message as raw JSON
+	var rawMessage json.RawMessage
+	if err := json.Unmarshal(req, &rawMessage); err != nil {
+		return errors.New("parse error")
+	}
+
+	// Process message through MCPServer
+	response := s.server.HandleMessage(context.Background(), rawMessage)
+
+	// Only send response if there is one (not for notifications)
+	if response != nil {
+		eventData, err := json.Marshal(response)
+		if err != nil {
+			return err
+		}
+
+		// Queue the event for sending via SSE
+		select {
+		case s.eventQueue <- fmt.Sprintf("event: message\ndata: %s\n\n", eventData):
+			// Event queued successfully
+		default:
+			// Queue is full, could log this
+		}
+	}
+
+	return nil
+}
+
+func JSONRPCError(
+	id interface{},
+	code int,
+	message string,
+) ([]byte, error) {
+	return json.Marshal(createErrorResponse(id, code, message))
+}
+
+func createErrorResponse(
+	id interface{},
+	code int,
+	message string,
+) mcp.JSONRPCMessage {
+	return mcp.JSONRPCError{
+		JSONRPC: mcp.JSONRPC_VERSION,
+		ID:      id,
+		Error: struct {
+			Code    int         `json:"code"`
+			Message string      `json:"message"`
+			Data    interface{} `json:"data,omitempty"`
+		}{
+			Code:    code,
+			Message: message,
+		},
+	}
+}

+ 502 - 0
core/controller/mcpproxy.go

@@ -0,0 +1,502 @@
+package controller
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/mcpproxy"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/openapi-mcp/convert"
+	"github.com/redis/go-redis/v9"
+)
+
+// mcpEndpointProvider implements the EndpointProvider interface for MCP
+type mcpEndpointProvider struct {
+	key string
+	t   model.PublicMCPType
+}
+
+func newEndpoint(key string, t model.PublicMCPType) mcpproxy.EndpointProvider {
+	return &mcpEndpointProvider{
+		key: key,
+		t:   t,
+	}
+}
+
+func (m *mcpEndpointProvider) NewEndpoint() (newSession string, newEndpoint string) {
+	session := uuid.NewString()
+	endpoint := fmt.Sprintf("/mcp/message?sessionId=%s&key=%s&type=%s", session, m.key, m.t)
+	return session, endpoint
+}
+
+func (m *mcpEndpointProvider) LoadEndpoint(endpoint string) (session string) {
+	parsedURL, err := url.Parse(endpoint)
+	if err != nil {
+		return ""
+	}
+	return parsedURL.Query().Get("sessionId")
+}
+
+// Global variables for session management
+var (
+	memStore       mcpproxy.SessionManager = mcpproxy.NewMemStore()
+	redisStore     mcpproxy.SessionManager
+	redisStoreOnce = &sync.Once{}
+)
+
+func getStore() mcpproxy.SessionManager {
+	if common.RedisEnabled {
+		redisStoreOnce.Do(func() {
+			redisStore = newRedisStoreManager(common.RDB)
+		})
+		return redisStore
+	}
+	return memStore
+}
+
+// Redis-based session manager
+type redisStoreManager struct {
+	rdb *redis.Client
+}
+
+func newRedisStoreManager(rdb *redis.Client) mcpproxy.SessionManager {
+	return &redisStoreManager{
+		rdb: rdb,
+	}
+}
+
+var redisStoreManagerScript = redis.NewScript(`
+local key = KEYS[1]
+local value = redis.call('GET', key)
+if not value then
+	return nil
+end
+redis.call('EXPIRE', key, 300)
+return value
+`)
+
+func (r *redisStoreManager) Get(sessionID string) (string, bool) {
+	ctx := context.Background()
+
+	result, err := redisStoreManagerScript.Run(ctx, r.rdb, []string{"mcp:session:" + sessionID}).Result()
+	if err != nil || result == nil {
+		return "", false
+	}
+
+	return result.(string), true
+}
+
+func (r *redisStoreManager) Set(sessionID, endpoint string) {
+	ctx := context.Background()
+	r.rdb.Set(ctx, "mcp:session:"+sessionID, endpoint, time.Minute*5)
+}
+
+func (r *redisStoreManager) Delete(session string) {
+	ctx := context.Background()
+	r.rdb.Del(ctx, "mcp:session:"+session)
+}
+
+// MCPSseProxy godoc
+//
+//	@Summary	MCP SSE Proxy
+//	@Router		/mcp/public/{id}/sse [get]
+func MCPSseProxy(c *gin.Context) {
+	mcpID := c.Param("id")
+
+	publicMcp, err := model.GetPublicMCPByID(mcpID)
+	if err != nil {
+		middleware.AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	switch publicMcp.Type {
+	case model.PublicMCPTypeProxySSE:
+		handleProxySSE(c, publicMcp)
+	case model.PublicMCPTypeOpenAPI:
+		handleOpenAPI(c, publicMcp)
+	default:
+		middleware.AbortLogWithMessage(c, http.StatusBadRequest, "unknow mcp type")
+		return
+	}
+}
+
+// handleProxySSE processes SSE proxy requests
+func handleProxySSE(c *gin.Context, publicMcp *model.PublicMCP) {
+	config := publicMcp.ProxySSEConfig
+	if config == nil || config.URL == "" {
+		return
+	}
+
+	backendURL, err := url.Parse(config.URL)
+	if err != nil {
+		middleware.AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	headers := make(map[string]string)
+	backendQuery := &url.Values{}
+	group := middleware.GetGroup(c)
+	token := middleware.GetToken(c)
+
+	// Process reusing parameters if any
+	if err := processReusingParams(config.ReusingParams, publicMcp.ID, group.ID, headers, backendQuery); err != nil {
+		middleware.AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	backendURL.RawQuery = backendQuery.Encode()
+	mcpproxy.SSEHandler(
+		c.Writer,
+		c.Request,
+		getStore(),
+		newEndpoint(token.Key, publicMcp.Type),
+		backendURL.String(),
+		headers,
+	)
+}
+
+// handleOpenAPI processes OpenAPI requests
+func handleOpenAPI(c *gin.Context, publicMcp *model.PublicMCP) {
+	config := publicMcp.OpenAPIConfig
+	if config == nil || (config.OpenAPISpec == "" && config.OpenAPIContent == "") {
+		return
+	}
+
+	// Parse OpenAPI specification
+	parser := convert.NewParser()
+	var err error
+	var openAPIFrom string
+
+	if config.OpenAPISpec != "" {
+		openAPIFrom, err = parseOpenAPIFromURL(config, parser)
+	} else {
+		err = parseOpenAPIFromContent(config, parser)
+	}
+
+	if err != nil {
+		return
+	}
+
+	// Convert to MCP server
+	converter := convert.NewConverter(parser, convert.Options{
+		OpenAPIFrom: openAPIFrom,
+	})
+	s, err := converter.Convert()
+	if err != nil {
+		return
+	}
+
+	token := middleware.GetToken(c)
+
+	// Setup SSE server
+	newSession, newEndpoint := newEndpoint(token.Key, publicMcp.Type).NewEndpoint()
+	store := getStore()
+	store.Set(newSession, "openapi")
+	defer func() {
+		store.Delete(newSession)
+	}()
+
+	server := NewSSEServer(
+		s,
+		WithMessageEndpoint(newEndpoint),
+	)
+
+	ctx, cancel := context.WithCancel(c.Request.Context())
+	defer cancel()
+
+	// Start message processing goroutine
+	go processOpenAPIMessages(ctx, newSession, server)
+
+	// Handle SSE connection
+	server.HandleSSE(c.Writer, c.Request)
+}
+
+// parseOpenAPIFromURL parses OpenAPI spec from a URL
+func parseOpenAPIFromURL(config *model.MCPOpenAPIConfig, parser *convert.Parser) (string, error) {
+	spec, err := url.Parse(config.OpenAPISpec)
+	if err != nil || (spec.Scheme != "http" && spec.Scheme != "https") {
+		return "", errors.New("invalid OpenAPI spec URL")
+	}
+
+	openAPIFrom := spec.String()
+	if config.V2 {
+		err = parser.ParseFileV2(openAPIFrom)
+	} else {
+		err = parser.ParseFile(openAPIFrom)
+	}
+
+	return openAPIFrom, err
+}
+
+// parseOpenAPIFromContent parses OpenAPI spec from content string
+func parseOpenAPIFromContent(config *model.MCPOpenAPIConfig, parser *convert.Parser) error {
+	if config.V2 {
+		return parser.ParseV2([]byte(config.OpenAPIContent))
+	}
+	return parser.Parse([]byte(config.OpenAPIContent))
+}
+
+// processOpenAPIMessages handles message processing for OpenAPI
+func processOpenAPIMessages(ctx context.Context, sessionID string, server *SSEServer) {
+	mpscInstance := getMpsc()
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		default:
+			data, err := mpscInstance.recv(ctx, sessionID)
+			if err != nil {
+				return
+			}
+			if err := server.HandleMessage(data); err != nil {
+				return
+			}
+		}
+	}
+}
+
+// processReusingParams handles the reusing parameters for MCP proxy
+func processReusingParams(reusingParams map[string]model.ReusingParam, mcpID string, groupID string, headers map[string]string, backendQuery *url.Values) error {
+	if len(reusingParams) == 0 {
+		return nil
+	}
+
+	param, err := model.GetGroupPublicMCPReusingParam(mcpID, groupID)
+	if err != nil {
+		return err
+	}
+
+	for k, v := range reusingParams {
+		paramValue, ok := param.ReusingParams[k]
+		if !ok {
+			if v.Required {
+				return fmt.Errorf("%s required", k)
+			}
+			continue
+		}
+
+		switch v.Type {
+		case model.ParamTypeHeader:
+			headers[k] = paramValue
+		case model.ParamTypeQuery:
+			backendQuery.Set(k, paramValue)
+		default:
+			return errors.New("unknow param type")
+		}
+	}
+
+	return nil
+}
+
+// MCPMessage godoc
+//
+//	@Summary	MCP SSE Proxy
+//	@Router		/mcp/message [post]
+func MCPMessage(c *gin.Context) {
+	token := middleware.GetToken(c)
+	mcpTypeStr, _ := c.GetQuery("type")
+	if mcpTypeStr == "" {
+		return
+	}
+	mcpType := model.PublicMCPType(mcpTypeStr)
+	sessionID, _ := c.GetQuery("sessionId")
+	if sessionID == "" {
+		return
+	}
+
+	switch mcpType {
+	case model.PublicMCPTypeProxySSE:
+		mcpproxy.ProxyHandler(
+			c.Writer,
+			c.Request,
+			getStore(),
+			newEndpoint(token.Key, mcpType),
+		)
+	case model.PublicMCPTypeOpenAPI:
+		backend, ok := getStore().Get(sessionID)
+		if !ok || backend != "openapi" {
+			return
+		}
+		mpscInstance := getMpsc()
+		body, err := io.ReadAll(c.Request.Body)
+		if err != nil {
+			_ = c.AbortWithError(http.StatusInternalServerError, err)
+			return
+		}
+		err = mpscInstance.send(c.Request.Context(), sessionID, body)
+		if err != nil {
+			_ = c.AbortWithError(http.StatusInternalServerError, err)
+			return
+		}
+		c.Writer.WriteHeader(http.StatusAccepted)
+	}
+}
+
+// Interface for multi-producer, single-consumer message passing
+type mpsc interface {
+	recv(ctx context.Context, id string) ([]byte, error)
+	send(ctx context.Context, id string, data []byte) error
+}
+
+// Global MPSC instances
+var (
+	memMpsc       mpsc = newChannelMpsc()
+	redisMpsc     mpsc
+	redisMpscOnce = &sync.Once{}
+)
+
+func getMpsc() mpsc {
+	if common.RedisEnabled {
+		redisMpscOnce.Do(func() {
+			redisMpsc = newRedisMPSC(common.RDB)
+		})
+		return redisMpsc
+	}
+	return memMpsc
+}
+
+// In-memory channel-based MPSC implementation
+type channelMpsc struct {
+	channels     map[string]chan []byte
+	lastAccess   map[string]time.Time
+	channelMutex sync.RWMutex
+}
+
+// newChannelMpsc creates a new channel-based mpsc implementation
+func newChannelMpsc() *channelMpsc {
+	c := &channelMpsc{
+		channels:   make(map[string]chan []byte),
+		lastAccess: make(map[string]time.Time),
+	}
+
+	// Start a goroutine to clean up expired channels
+	go c.cleanupExpiredChannels()
+
+	return c
+}
+
+// cleanupExpiredChannels periodically checks for and removes channels that haven't been accessed in 5 minutes
+func (c *channelMpsc) cleanupExpiredChannels() {
+	ticker := time.NewTicker(1 * time.Minute)
+	defer ticker.Stop()
+
+	for range ticker.C {
+		c.channelMutex.Lock()
+		now := time.Now()
+		for id, lastAccess := range c.lastAccess {
+			if now.Sub(lastAccess) > 5*time.Minute {
+				// Close and delete the channel
+				if ch, exists := c.channels[id]; exists {
+					close(ch)
+					delete(c.channels, id)
+				}
+				delete(c.lastAccess, id)
+			}
+		}
+		c.channelMutex.Unlock()
+	}
+}
+
+// getOrCreateChannel gets an existing channel or creates a new one for the session
+func (c *channelMpsc) getOrCreateChannel(id string) chan []byte {
+	c.channelMutex.RLock()
+	ch, exists := c.channels[id]
+	c.channelMutex.RUnlock()
+
+	if !exists {
+		c.channelMutex.Lock()
+		if ch, exists = c.channels[id]; !exists {
+			ch = make(chan []byte, 10)
+			c.channels[id] = ch
+		}
+		c.lastAccess[id] = time.Now()
+		c.channelMutex.Unlock()
+	} else {
+		c.channelMutex.Lock()
+		c.lastAccess[id] = time.Now()
+		c.channelMutex.Unlock()
+	}
+
+	return ch
+}
+
+// recv receives data for the specified session
+func (c *channelMpsc) recv(ctx context.Context, id string) ([]byte, error) {
+	ch := c.getOrCreateChannel(id)
+
+	select {
+	case data, ok := <-ch:
+		if !ok {
+			return nil, fmt.Errorf("channel closed for session %s", id)
+		}
+		return data, nil
+	case <-ctx.Done():
+		return nil, ctx.Err()
+	}
+}
+
+// send sends data to the specified session
+func (c *channelMpsc) send(ctx context.Context, id string, data []byte) error {
+	ch := c.getOrCreateChannel(id)
+
+	select {
+	case ch <- data:
+		return nil
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		return fmt.Errorf("channel buffer full for session %s", id)
+	}
+}
+
+// Redis-based MPSC implementation
+type redisMPSC struct {
+	rdb *redis.Client
+}
+
+// newRedisMPSC creates a new Redis MPSC instance
+func newRedisMPSC(rdb *redis.Client) *redisMPSC {
+	return &redisMPSC{rdb: rdb}
+}
+
+func (r *redisMPSC) send(ctx context.Context, id string, data []byte) error {
+	// Set expiration to 5 minutes when sending data
+	pipe := r.rdb.Pipeline()
+	pipe.LPush(ctx, id, data)
+	pipe.Expire(ctx, id, 5*time.Minute)
+	_, err := pipe.Exec(ctx)
+	return err
+}
+
+func (r *redisMPSC) recv(ctx context.Context, id string) ([]byte, error) {
+	for {
+		select {
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		default:
+			result, err := r.rdb.BRPop(ctx, time.Second, id).Result()
+			if err != nil {
+				if errors.Is(err, redis.Nil) {
+					runtime.Gosched()
+					continue
+				}
+				return nil, err
+			}
+			if len(result) != 2 {
+				return nil, errors.New("invalid BRPop result")
+			}
+			return []byte(result[1]), nil
+		}
+	}
+}

+ 2 - 2
controller/misc.go → core/controller/misc.go

@@ -2,8 +2,8 @@ package controller
 
 import (
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/middleware"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/middleware"
 )
 
 type StatusData struct {

+ 4 - 4
controller/model.go → core/controller/model.go

@@ -8,10 +8,10 @@ import (
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/relay/channeltype"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/channeltype"
 	log "github.com/sirupsen/logrus"
 )
 

+ 2 - 2
controller/modelconfig.go → core/controller/modelconfig.go

@@ -4,8 +4,8 @@ import (
 	"net/http"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
 )
 
 // GetModelConfigs godoc

+ 2 - 2
controller/monitor.go → core/controller/monitor.go

@@ -5,8 +5,8 @@ import (
 	"strconv"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/monitor"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/monitor"
 )
 
 // GetAllChannelModelErrorRates godoc

+ 2 - 2
controller/option.go → core/controller/option.go

@@ -5,8 +5,8 @@ import (
 	"net/http"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
 )
 
 // GetOptions godoc

+ 224 - 0
core/controller/publicmcp.go

@@ -0,0 +1,224 @@
+package controller
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+)
+
+// GetPublicMCPs godoc
+//
+//	@Summary		Get MCPs
+//	@Description	Get a list of MCPs with pagination and filtering
+//	@Tags			mcp
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			page		query		int		false	"Page number"
+//	@Param			per_page	query		int		false	"Items per page"
+//	@Param			type		query		string	false	"MCP type"
+//	@Param			keyword		query		string	false	"Search keyword"
+//	@Success		200			{object}	middleware.APIResponse{data=[]model.PublicMCP}
+//	@Router			/api/mcp/public/ [get]
+func GetPublicMCPs(c *gin.Context) {
+	page, perPage := parsePageParams(c)
+	mcpType := model.PublicMCPType(c.Query("type"))
+	keyword := c.Query("keyword")
+
+	mcps, total, err := model.GetPublicMCPs(page, perPage, mcpType, keyword)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, gin.H{
+		"mcps":  mcps,
+		"total": total,
+	})
+}
+
+// GetMCPByID godoc
+//
+//	@Summary		Get MCP by ID
+//	@Description	Get a specific MCP by its ID
+//	@Tags			mcp
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			id	path		string	true	"MCP ID"
+//	@Success		200	{object}	middleware.APIResponse{data=model.PublicMCP}
+//	@Router			/api/mcp/public/{id} [get]
+func GetPublicMCPByIDHandler(c *gin.Context) {
+	id := c.Param("id")
+	if id == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
+		return
+	}
+
+	mcp, err := model.GetPublicMCPByID(id)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, mcp)
+}
+
+// CreatePublicMCP godoc
+//
+//	@Summary		Create MCP
+//	@Description	Create a new MCP
+//	@Tags			mcp
+//	@Accept			json
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			mcp	body		model.PublicMCP	true	"MCP object"
+//	@Success		200	{object}	middleware.APIResponse
+//	@Router			/api/mcp/public/ [post]
+func CreatePublicMCP(c *gin.Context) {
+	var mcp model.PublicMCP
+	if err := c.ShouldBindJSON(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	mcp.CreatedAt = time.Now()
+	mcp.UpdateAt = time.Now()
+
+	if err := model.CreatePublicMCP(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, mcp)
+}
+
+// UpdatePublicMCP godoc
+//
+//	@Summary		Update MCP
+//	@Description	Update an existing MCP
+//	@Tags			mcp
+//	@Accept			json
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			id	path		string			true	"MCP ID"
+//	@Param			mcp	body		model.PublicMCP	true	"MCP object"
+//	@Success		200	{object}	middleware.APIResponse
+//	@Router			/api/mcp/public/{id} [put]
+func UpdatePublicMCP(c *gin.Context) {
+	id := c.Param("id")
+	if id == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
+		return
+	}
+
+	var mcp model.PublicMCP
+	if err := c.ShouldBindJSON(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	mcp.ID = id
+	mcp.UpdateAt = time.Now()
+
+	if err := model.UpdatePublicMCP(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, mcp)
+}
+
+// DeletePublicMCP godoc
+//
+//	@Summary		Delete MCP
+//	@Description	Delete an MCP by ID
+//	@Tags			mcp
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			id	path		string	true	"MCP ID"
+//	@Success		200	{object}	middleware.APIResponse
+//	@Router			/api/mcp/public/{id} [delete]
+func DeletePublicMCP(c *gin.Context) {
+	id := c.Param("id")
+	if id == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
+		return
+	}
+
+	if err := model.DeletePublicMCP(id); err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, gin.H{"id": id})
+}
+
+// GetGroupPublicMCPReusingParam godoc
+//
+//	@Summary		Get group MCP reusing parameters
+//	@Description	Get reusing parameters for a specific group and MCP
+//	@Tags			mcp
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			id		path		string	true	"MCP ID"
+//	@Param			group	path		string	true	"Group ID"
+//	@Success		200		{object}	middleware.APIResponse{data=model.GroupPublicMCPReusingParam}
+//	@Router			/api/mcp/public/{id}/group/{group}/params [get]
+func GetGroupPublicMCPReusingParam(c *gin.Context) {
+	mcpID := c.Param("id")
+	groupID := c.Param("group")
+
+	if mcpID == "" || groupID == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
+		return
+	}
+
+	param, err := model.GetGroupPublicMCPReusingParam(mcpID, groupID)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, param)
+}
+
+// SaveGroupPublicMCPReusingParam godoc
+//
+//	@Summary		Create or update group MCP reusing parameters
+//	@Description	Create or update reusing parameters for a specific group and MCP
+//	@Tags			mcp
+//	@Accept			json
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			id		path		string								true	"MCP ID"
+//	@Param			group	path		string								true	"Group ID"
+//	@Param			params	body		model.GroupPublicMCPReusingParam	true	"Reusing parameters"
+//	@Success		200		{object}	middleware.APIResponse
+//	@Router			/api/mcp/public/{id}/group/{group}/params [post]
+func SaveGroupPublicMCPReusingParam(c *gin.Context) {
+	mcpID := c.Param("id")
+	groupID := c.Param("group")
+
+	if mcpID == "" || groupID == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
+		return
+	}
+
+	var param model.GroupPublicMCPReusingParam
+	if err := c.ShouldBindJSON(&param); err != nil {
+		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	param.MCPID = mcpID
+	param.GroupID = groupID
+
+	if err := model.SaveGroupPublicMCPReusingParam(&param); err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, param)
+}

+ 29 - 33
controller/relay-controller.go → core/controller/relay-controller.go

@@ -13,20 +13,19 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/common/consume"
-	"github.com/labring/aiproxy/common/notify"
-	"github.com/labring/aiproxy/common/trylock"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/monitor"
-	"github.com/labring/aiproxy/relay/channeltype"
-	"github.com/labring/aiproxy/relay/controller"
-	"github.com/labring/aiproxy/relay/meta"
-	"github.com/labring/aiproxy/relay/mode"
-	relaymodel "github.com/labring/aiproxy/relay/model"
-	"github.com/shopspring/decimal"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/consume"
+	"github.com/labring/aiproxy/core/common/notify"
+	"github.com/labring/aiproxy/core/common/trylock"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/monitor"
+	"github.com/labring/aiproxy/core/relay/channeltype"
+	"github.com/labring/aiproxy/core/relay/controller"
+	"github.com/labring/aiproxy/core/relay/meta"
+	"github.com/labring/aiproxy/core/relay/mode"
+	relaymodel "github.com/labring/aiproxy/core/relay/model"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -70,6 +69,9 @@ func relayController(m mode.Mode) RelayController {
 	case mode.Rerank:
 		c.GetRequestPrice = controller.GetRerankRequestPrice
 		c.GetRequestUsage = controller.GetRerankRequestUsage
+	case mode.Anthropic:
+		c.GetRequestPrice = controller.GetAnthropicRequestPrice
+		c.GetRequestUsage = controller.GetAnthropicRequestUsage
 	case mode.ChatCompletions:
 		c.GetRequestPrice = controller.GetChatRequestPrice
 		c.GetRequestUsage = controller.GetChatRequestUsage
@@ -333,7 +335,7 @@ func relay(c *gin.Context, mode mode.Mode, relayController RelayController) {
 			return
 		}
 		gbc := middleware.GetGroupBalanceConsumerFromContext(c)
-		if !gbc.CheckBalance(getPreConsumedAmount(requestUsage, price)) {
+		if !gbc.CheckBalance(consume.CalculateAmount(requestUsage, price)) {
 			middleware.AbortLogWithMessage(c,
 				http.StatusForbidden,
 				fmt.Sprintf("group (%s) balance not enough", gbc.Group),
@@ -351,6 +353,9 @@ func relay(c *gin.Context, mode mode.Mode, relayController RelayController) {
 	result, retry := RelayHelper(meta, c, relayController.Handler)
 
 	retryTimes := int(config.GetRetryTimes())
+	if mc.RetryTimes > 0 {
+		retryTimes = int(mc.RetryTimes)
+	}
 	if handleRelayResult(c, result.Error, retry, retryTimes) {
 		recordResult(c, meta, price, result, 0, true)
 		return
@@ -369,17 +374,6 @@ func relay(c *gin.Context, mode mode.Mode, relayController RelayController) {
 	retryLoop(c, mode, retryState, relayController.Handler, log)
 }
 
-func getPreConsumedAmount(usage model.Usage, price model.Price) float64 {
-	if usage.InputTokens == 0 || price.InputPrice == 0 {
-		return 0
-	}
-	return decimal.
-		NewFromInt(usage.InputTokens).
-		Mul(decimal.NewFromFloat(price.InputPrice)).
-		Div(decimal.NewFromInt(price.GetInputPriceUnit())).
-		InexactFloat64()
-}
-
 // recordResult records the consumption for the final result
 func recordResult(c *gin.Context, meta *meta.Meta, price model.Price, result *controller.HandleResult, retryTimes int, downstreamResult bool) {
 	code := http.StatusOK
@@ -402,13 +396,15 @@ func recordResult(c *gin.Context, meta *meta.Meta, price model.Price, result *co
 
 	gbc := middleware.GetGroupBalanceConsumerFromContext(c)
 
-	amount := consume.CalculateAmount(
-		result.Usage,
-		price,
-	)
-	if amount > 0 {
-		log := middleware.GetLogger(c)
-		log.Data["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
+	if code == http.StatusOK {
+		amount := consume.CalculateAmount(
+			result.Usage,
+			price,
+		)
+		if amount > 0 {
+			log := middleware.GetLogger(c)
+			log.Data["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
+		}
 	}
 
 	consume.AsyncConsume(

+ 5 - 5
controller/relay-dashboard.go → core/controller/relay-dashboard.go

@@ -6,9 +6,9 @@ import (
 	"net/http"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/balance"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/relay/adaptor/openai"
+	"github.com/labring/aiproxy/core/common/balance"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -20,7 +20,7 @@ import (
 //	@Produce		json
 //	@Security		ApiKeyAuth
 //	@Success		200	{object}	openai.SubscriptionResponse
-//	@Router			/v1/dashboard/subscription [get]
+//	@Router			/v1/dashboard/billing/subscription [get]
 func GetSubscription(c *gin.Context) {
 	group := middleware.GetGroup(c)
 	b, _, err := balance.GetGroupRemainBalance(c, *group)
@@ -53,7 +53,7 @@ func GetSubscription(c *gin.Context) {
 //	@Produce		json
 //	@Security		ApiKeyAuth
 //	@Success		200	{object}	openai.UsageResponse
-//	@Router			/v1/dashboard/usage [get]
+//	@Router			/v1/dashboard/billing/usage [get]
 func GetUsage(c *gin.Context) {
 	token := middleware.GetToken(c)
 	c.JSON(http.StatusOK, openai.UsageResponse{TotalUsage: token.UsedAmount * 100})

+ 2 - 2
controller/relay-model.go → core/controller/relay-model.go

@@ -5,8 +5,8 @@ import (
 	"net/http"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	model "github.com/labring/aiproxy/relay/model"
+	"github.com/labring/aiproxy/core/middleware"
+	model "github.com/labring/aiproxy/core/relay/model"
 )
 
 // ListModels godoc

+ 27 - 3
controller/relay.go → core/controller/relay.go

@@ -2,11 +2,11 @@ package controller
 
 import (
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/relay/mode"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/relay/mode"
 
 	// relay model used by swagger
-	_ "github.com/labring/aiproxy/relay/model"
+	_ "github.com/labring/aiproxy/core/relay/model"
 )
 
 // Completions godoc
@@ -33,6 +33,30 @@ func Completions() []gin.HandlerFunc {
 	}
 }
 
+// Anthropic godoc
+//
+//	@Summary		Anthropic
+//	@Description	Anthropic
+//	@Tags			relay
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			request			body		model.AnthropicMessageRequest	true	"Request"
+//	@Param			Aiproxy-Channel	header		string							false	"Optional Aiproxy-Channel header"
+//	@Success		200				{object}	model.TextResponse
+//	@Header			all				{integer}	X-RateLimit-Limit-Requests		"X-RateLimit-Limit-Requests"
+//	@Header			all				{integer}	X-RateLimit-Limit-Tokens		"X-RateLimit-Limit-Tokens"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Requests	"X-RateLimit-Remaining-Requests"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Tokens	"X-RateLimit-Remaining-Tokens"
+//	@Header			all				{string}	X-RateLimit-Reset-Requests		"X-RateLimit-Reset-Requests"
+//	@Header			all				{string}	X-RateLimit-Reset-Tokens		"X-RateLimit-Reset-Tokens"
+//	@Router			/v1/message [post]
+func Anthropic() []gin.HandlerFunc {
+	return []gin.HandlerFunc{
+		middleware.NewDistribute(mode.Anthropic),
+		NewRelay(mode.Anthropic),
+	}
+}
+
 // ChatCompletions godoc
 //
 //	@Summary		ChatCompletions

+ 3 - 3
controller/token.go → core/controller/token.go

@@ -9,9 +9,9 @@ import (
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/network"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
+	"github.com/labring/aiproxy/core/common/network"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
 )
 
 // TokenResponse represents the response structure for token endpoints

+ 0 - 0
controller/utils.go → core/controller/utils.go


+ 0 - 0
deploy/Kubefile → core/deploy/Kubefile


+ 0 - 0
deploy/manifests/aiproxy-config.yaml.tmpl → core/deploy/manifests/aiproxy-config.yaml.tmpl


+ 0 - 0
deploy/manifests/deploy.yaml.tmpl → core/deploy/manifests/deploy.yaml.tmpl


+ 0 - 0
deploy/manifests/ingress.yaml.tmpl → core/deploy/manifests/ingress.yaml.tmpl


+ 0 - 0
deploy/manifests/pgsql-log.yaml → core/deploy/manifests/pgsql-log.yaml


+ 0 - 0
deploy/manifests/pgsql.yaml → core/deploy/manifests/pgsql.yaml


+ 0 - 0
deploy/manifests/redis.yaml → core/deploy/manifests/redis.yaml


+ 0 - 0
deploy/scripts/init.sh → core/deploy/scripts/init.sh


+ 0 - 0
docker-compose.yaml → core/docker-compose.yaml


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 694 - 69
core/docs/docs.go


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 697 - 70
core/docs/swagger.json


+ 461 - 47
docs/swagger.yaml → core/docs/swagger.yaml

@@ -73,6 +73,8 @@ definitions:
         $ref: '#/definitions/model.ModelOwner'
       price:
         $ref: '#/definitions/model.Price'
+      retry_times:
+        type: integer
       rpm:
         type: integer
       tpm:
@@ -220,6 +222,8 @@ definitions:
         $ref: '#/definitions/model.ModelOwner'
       price:
         $ref: '#/definitions/model.Price'
+      retry_times:
+        type: integer
       rpm:
         type: integer
       tpm:
@@ -315,7 +319,7 @@ definitions:
       status:
         type: integer
     type: object
-  github_com_labring_aiproxy_model.Usage:
+  github_com_labring_aiproxy_core_model.Usage:
     properties:
       cache_creation_tokens:
         type: integer
@@ -330,10 +334,12 @@ definitions:
       web_search_count:
         type: integer
     type: object
-  github_com_labring_aiproxy_relay_model.Usage:
+  github_com_labring_aiproxy_core_relay_model.Usage:
     properties:
       completion_tokens:
         type: integer
+      completion_tokens_details:
+        $ref: '#/definitions/model.CompletionTokensDetails'
       prompt_tokens:
         type: integer
       prompt_tokens_details:
@@ -343,14 +349,6 @@ definitions:
       web_search_count:
         type: integer
     type: object
-  gorm.DeletedAt:
-    properties:
-      time:
-        type: string
-      valid:
-        description: Valid is true if Time is not NULL
-        type: boolean
-    type: object
   middleware.APIResponse:
     properties:
       data: {}
@@ -373,6 +371,7 @@ definitions:
     - 9
     - 10
     - 11
+    - 12
     type: integer
     x-enum-varnames:
     - Unknown
@@ -387,6 +386,16 @@ definitions:
     - AudioTranslation
     - Rerank
     - ParsePdf
+    - Anthropic
+  model.AnthropicMessageRequest:
+    properties:
+      messages:
+        items:
+          $ref: '#/definitions/model.Message'
+        type: array
+      model:
+        type: string
+    type: object
   model.Audio:
     properties:
       format:
@@ -412,8 +421,6 @@ definitions:
         $ref: '#/definitions/model.ChannelConfig'
       created_at:
         type: string
-      deletedAt:
-        $ref: '#/definitions/gorm.DeletedAt'
       enabled_auto_balance_check:
         type: boolean
       id:
@@ -500,6 +507,17 @@ definitions:
       web_search_count:
         type: integer
     type: object
+  model.CompletionTokensDetails:
+    properties:
+      accepted_prediction_tokens:
+        type: integer
+      audio_tokens:
+        type: integer
+      reasoning_tokens:
+        type: integer
+      rejected_prediction_tokens:
+        type: integer
+    type: object
   model.DashboardResponse:
     properties:
       cache_creation_tokens:
@@ -560,7 +578,7 @@ definitions:
       object:
         type: string
       usage:
-        $ref: '#/definitions/github_com_labring_aiproxy_relay_model.Usage'
+        $ref: '#/definitions/github_com_labring_aiproxy_core_relay_model.Usage'
     type: object
   model.EmbeddingResponseItem:
     properties:
@@ -573,6 +591,20 @@ definitions:
       object:
         type: string
     type: object
+  model.FinishReason:
+    enum:
+    - stop
+    - length
+    - content_filter
+    - tool_calls
+    - function_call
+    type: string
+    x-enum-varnames:
+    - FinishReasonStop
+    - FinishReasonLength
+    - FinishReasonContentFilter
+    - FinishReasonToolCalls
+    - FinishReasonFunctionCall
   model.Function:
     properties:
       arguments:
@@ -775,13 +807,28 @@ definitions:
         type: boolean
       override_price:
         type: boolean
+      override_retry_times:
+        type: boolean
       price:
         $ref: '#/definitions/model.Price'
+      retry_times:
+        type: integer
       rpm:
         type: integer
       tpm:
         type: integer
     type: object
+  model.GroupPublicMCPReusingParam:
+    properties:
+      group_id:
+        type: string
+      mcp_id:
+        type: string
+      reusing_params:
+        additionalProperties:
+          type: string
+        type: object
+    type: object
   model.ImageData:
     properties:
       b64_json:
@@ -876,10 +923,34 @@ definitions:
       ttfb_milliseconds:
         type: integer
       usage:
-        $ref: '#/definitions/github_com_labring_aiproxy_model.Usage'
+        $ref: '#/definitions/github_com_labring_aiproxy_core_model.Usage'
       used_amount:
         type: number
     type: object
+  model.MCPOpenAPIConfig:
+    properties:
+      authorization:
+        type: string
+      openapi_content:
+        type: string
+      openapi_spec:
+        type: string
+      price:
+        $ref: '#/definitions/model.MCPPrice'
+      server:
+        type: string
+      v2:
+        type: boolean
+    type: object
+  model.MCPPrice:
+    properties:
+      defaultToolsCallPrice:
+        type: number
+      toolsCallPrices:
+        additionalProperties:
+          type: number
+        type: object
+    type: object
   model.Message:
     properties:
       content: {}
@@ -915,6 +986,8 @@ definitions:
         $ref: '#/definitions/model.ModelOwner'
       price:
         $ref: '#/definitions/model.Price'
+      retry_times:
+        type: integer
       rpm:
         type: integer
       tpm:
@@ -978,6 +1051,7 @@ definitions:
     - stepfun
     - xai
     - doc2x
+    - jina
     type: string
     x-enum-varnames:
     - ModelOwnerOpenAI
@@ -1011,6 +1085,7 @@ definitions:
     - ModelOwnerStepFun
     - ModelOwnerXAI
     - ModelOwnerDoc2x
+    - ModelOwnerJina
   model.Option:
     properties:
       key:
@@ -1018,6 +1093,14 @@ definitions:
       value:
         type: string
     type: object
+  model.ParamType:
+    enum:
+    - header
+    - query
+    type: string
+    x-enum-varnames:
+    - ParamTypeHeader
+    - ParamTypeQuery
   model.ParsePdfResponse:
     properties:
       markdown:
@@ -1043,6 +1126,8 @@ definitions:
         type: number
       output_price_unit:
         type: integer
+      per_request_price:
+        type: number
       web_search_price:
         type: number
       web_search_price_unit:
@@ -1050,11 +1135,75 @@ definitions:
     type: object
   model.PromptTokensDetails:
     properties:
+      audio_tokens:
+        type: integer
       cache_creation_tokens:
         type: integer
       cached_tokens:
         type: integer
     type: object
+  model.PublicMCP:
+    properties:
+      author:
+        type: string
+      created_at:
+        type: string
+      id:
+        type: string
+      logo_url:
+        type: string
+      name:
+        type: string
+      openapi_config:
+        $ref: '#/definitions/model.MCPOpenAPIConfig'
+      proxy_sse_config:
+        $ref: '#/definitions/model.PublicMCPProxySSEConfig'
+      readme:
+        type: string
+      readme_url:
+        type: string
+      repo_url:
+        type: string
+      tags:
+        items:
+          type: string
+        type: array
+      type:
+        $ref: '#/definitions/model.PublicMCPType'
+      update_at:
+        type: string
+    type: object
+  model.PublicMCPProxySSEConfig:
+    properties:
+      headers:
+        additionalProperties:
+          type: string
+        type: object
+      price:
+        $ref: '#/definitions/model.MCPPrice'
+      querys:
+        additionalProperties:
+          type: string
+        type: object
+      reusing_params:
+        additionalProperties:
+          $ref: '#/definitions/model.ReusingParam'
+        type: object
+      url:
+        type: string
+    type: object
+  model.PublicMCPType:
+    enum:
+    - mcp_proxy_sse
+    - mcp_git_repo
+    - mcp_openapi
+    type: string
+    x-enum-comments:
+      PublicMCPTypeGitRepo: read only
+    x-enum-varnames:
+    - PublicMCPTypeProxySSE
+    - PublicMCPTypeGitRepo
+    - PublicMCPTypeOpenAPI
   model.RequestDetail:
     properties:
       id:
@@ -1130,6 +1279,17 @@ definitions:
       type:
         type: string
     type: object
+  model.ReusingParam:
+    properties:
+      description:
+        type: string
+      name:
+        type: string
+      required:
+        type: boolean
+      type:
+        $ref: '#/definitions/model.ParamType'
+    type: object
   model.StreamOptions:
     properties:
       include_usage:
@@ -1155,12 +1315,12 @@ definitions:
       object:
         type: string
       usage:
-        $ref: '#/definitions/github_com_labring_aiproxy_relay_model.Usage'
+        $ref: '#/definitions/github_com_labring_aiproxy_core_relay_model.Usage'
     type: object
   model.TextResponseChoice:
     properties:
       finish_reason:
-        type: string
+        $ref: '#/definitions/model.FinishReason'
       index:
         type: integer
       message:
@@ -1219,6 +1379,8 @@ definitions:
     type: object
 info:
   contact: {}
+  title: AI Proxy Swagger API
+  version: "1.0"
 paths:
   /api/channel:
     post:
@@ -2045,35 +2207,6 @@ paths:
       summary: Update a group
       tags:
       - group
-  /api/group/{group}/model_config/:
-    post:
-      consumes:
-      - application/json
-      description: Save group model config
-      parameters:
-      - description: Group name
-        in: path
-        name: group
-        required: true
-        type: string
-      - description: Group model config information
-        in: body
-        name: data
-        required: true
-        schema:
-          $ref: '#/definitions/controller.SaveGroupModelConfigRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            $ref: '#/definitions/middleware.APIResponse'
-      security:
-      - ApiKeyAuth: []
-      summary: Save group model config
-      tags:
-      - group
   /api/group/{group}/model_config/{model}:
     delete:
       description: Delete group model config
@@ -2130,6 +2263,34 @@ paths:
       summary: Get group model config
       tags:
       - group
+    post:
+      consumes:
+      - application/json
+      description: Save group model config
+      parameters:
+      - description: Group name
+        in: path
+        name: group
+        required: true
+        type: string
+      - description: Group model config information
+        in: body
+        name: data
+        required: true
+        schema:
+          $ref: '#/definitions/controller.SaveGroupModelConfigRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/middleware.APIResponse'
+      security:
+      - ApiKeyAuth: []
+      summary: Save group model config
+      tags:
+      - group
     put:
       consumes:
       - application/json
@@ -3156,6 +3317,206 @@ paths:
       summary: Get used token names
       tags:
       - logs
+  /api/mcp/public/:
+    get:
+      description: Get a list of MCPs with pagination and filtering
+      parameters:
+      - description: Page number
+        in: query
+        name: page
+        type: integer
+      - description: Items per page
+        in: query
+        name: per_page
+        type: integer
+      - description: MCP type
+        in: query
+        name: type
+        type: string
+      - description: Search keyword
+        in: query
+        name: keyword
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  items:
+                    $ref: '#/definitions/model.PublicMCP'
+                  type: array
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get MCPs
+      tags:
+      - mcp
+    post:
+      consumes:
+      - application/json
+      description: Create a new MCP
+      parameters:
+      - description: MCP object
+        in: body
+        name: mcp
+        required: true
+        schema:
+          $ref: '#/definitions/model.PublicMCP'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/middleware.APIResponse'
+      security:
+      - ApiKeyAuth: []
+      summary: Create MCP
+      tags:
+      - mcp
+  /api/mcp/public/{id}:
+    delete:
+      description: Delete an MCP by ID
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/middleware.APIResponse'
+      security:
+      - ApiKeyAuth: []
+      summary: Delete MCP
+      tags:
+      - mcp
+    get:
+      description: Get a specific MCP by its ID
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  $ref: '#/definitions/model.PublicMCP'
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get MCP by ID
+      tags:
+      - mcp
+    put:
+      consumes:
+      - application/json
+      description: Update an existing MCP
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: MCP object
+        in: body
+        name: mcp
+        required: true
+        schema:
+          $ref: '#/definitions/model.PublicMCP'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/middleware.APIResponse'
+      security:
+      - ApiKeyAuth: []
+      summary: Update MCP
+      tags:
+      - mcp
+  /api/mcp/public/{id}/group/{group}/params:
+    get:
+      description: Get reusing parameters for a specific group and MCP
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  $ref: '#/definitions/model.GroupPublicMCPReusingParam'
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get group MCP reusing parameters
+      tags:
+      - mcp
+    post:
+      consumes:
+      - application/json
+      description: Create or update reusing parameters for a specific group and MCP
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      - description: Reusing parameters
+        in: body
+        name: params
+        required: true
+        schema:
+          $ref: '#/definitions/model.GroupPublicMCPReusingParam'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/middleware.APIResponse'
+      security:
+      - ApiKeyAuth: []
+      summary: Create or update group MCP reusing parameters
+      tags:
+      - mcp
   /api/model_config:
     post:
       description: Saves a model config
@@ -4621,6 +4982,14 @@ paths:
       summary: Search tokens
       tags:
       - tokens
+  /mcp/message:
+    post:
+      responses: {}
+      summary: MCP SSE Proxy
+  /mcp/public/{id}/sse:
+    get:
+      responses: {}
+      summary: MCP SSE Proxy
   /v1/audio/speech:
     post:
       description: AudioSpeech
@@ -4854,7 +5223,7 @@ paths:
       summary: Completions
       tags:
       - relay
-  /v1/dashboard/subscription:
+  /v1/dashboard/billing/subscription:
     get:
       description: Get subscription
       produces:
@@ -4869,7 +5238,7 @@ paths:
       summary: Get subscription
       tags:
       - relay
-  /v1/dashboard/usage:
+  /v1/dashboard/billing/usage:
     get:
       description: Get usage
       produces:
@@ -4974,6 +5343,51 @@ paths:
       summary: ImagesGenerations
       tags:
       - relay
+  /v1/message:
+    post:
+      description: Anthropic
+      parameters:
+      - description: Request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/model.AnthropicMessageRequest'
+      - description: Optional Aiproxy-Channel header
+        in: header
+        name: Aiproxy-Channel
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          headers:
+            X-RateLimit-Limit-Requests:
+              description: X-RateLimit-Limit-Requests
+              type: integer
+            X-RateLimit-Limit-Tokens:
+              description: X-RateLimit-Limit-Tokens
+              type: integer
+            X-RateLimit-Remaining-Requests:
+              description: X-RateLimit-Remaining-Requests
+              type: integer
+            X-RateLimit-Remaining-Tokens:
+              description: X-RateLimit-Remaining-Tokens
+              type: integer
+            X-RateLimit-Reset-Requests:
+              description: X-RateLimit-Reset-Requests
+              type: string
+            X-RateLimit-Reset-Tokens:
+              description: X-RateLimit-Reset-Tokens
+              type: string
+          schema:
+            $ref: '#/definitions/model.TextResponse'
+      security:
+      - ApiKeyAuth: []
+      summary: Anthropic
+      tags:
+      - relay
   /v1/models:
     get:
       description: List all models

+ 21 - 13
go.mod → core/go.mod

@@ -1,13 +1,11 @@
-module github.com/labring/aiproxy
+module github.com/labring/aiproxy/core
 
-go 1.23.0
-
-toolchain go1.24.1
+go 1.23.8
 
 require (
-	cloud.google.com/go/iam v1.5.0
+	cloud.google.com/go/iam v1.5.2
 	github.com/aws/aws-sdk-go-v2 v1.36.3
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.66
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.67
 	github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.29.0
 	github.com/bytedance/sonic v1.13.2
 	github.com/gin-contrib/cors v1.7.5
@@ -20,6 +18,8 @@ require (
 	github.com/jinzhu/copier v0.4.0
 	github.com/joho/godotenv v1.5.1
 	github.com/json-iterator/go v1.1.12
+	github.com/labring/aiproxy/openapi-mcp v0.0.0-00010101000000-000000000000
+	github.com/mark3labs/mcp-go v0.21.1
 	github.com/maruel/natural v1.1.1
 	github.com/mattn/go-isatty v0.0.20
 	github.com/patrickmn/go-cache v2.1.0+incompatible
@@ -37,14 +37,14 @@ require (
 	github.com/swaggo/swag v1.16.4
 	golang.org/x/image v0.26.0
 	golang.org/x/sync v0.13.0
-	google.golang.org/api v0.228.0
+	google.golang.org/api v0.229.0
 	gorm.io/driver/mysql v1.5.7
 	gorm.io/driver/postgres v1.5.11
 	gorm.io/gorm v1.25.12
 )
 
 require (
-	cloud.google.com/go/auth v0.15.0 // indirect
+	cloud.google.com/go/auth v0.16.0 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/compute/metadata v0.6.0 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
@@ -61,7 +61,8 @@ require (
 	github.com/dlclark/regexp2 v1.11.5 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+	github.com/getkin/kin-openapi v0.131.0 // indirect
 	github.com/gin-contrib/sse v1.1.0 // indirect
 	github.com/glebarez/go-sqlite v1.22.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
@@ -92,13 +93,18 @@ require (
 	github.com/mailru/easyjson v0.9.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
+	github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+	github.com/perimeterx/marshmallow v1.1.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/smarty/assertions v1.15.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
+	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
@@ -114,13 +120,15 @@ require (
 	golang.org/x/text v0.24.0 // indirect
 	golang.org/x/time v0.11.0 // indirect
 	golang.org/x/tools v0.32.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
 	google.golang.org/grpc v1.71.1 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	modernc.org/libc v1.62.1 // indirect
+	modernc.org/libc v1.63.0 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
-	modernc.org/memory v1.9.1 // indirect
+	modernc.org/memory v1.10.0 // indirect
 	modernc.org/sqlite v1.37.0 // indirect
 )
+
+replace github.com/labring/aiproxy/openapi-mcp => ../openapi-mcp

+ 38 - 22
go.sum → core/go.sum

@@ -1,11 +1,11 @@
-cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
-cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
+cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
+cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
 cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
-cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs=
-cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo=
+cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
+cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
@@ -14,8 +14,8 @@ github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38y
 github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.66 h1:aKpEKaTy6n4CEJeYI1MNj97oSDLi4xro3UzQfwf5RWE=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.66/go.mod h1:xQ5SusDmHb/fy55wU0QqTy0yNfLqxzec59YcsRZB+rI=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
@@ -49,8 +49,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
-github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
+github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
 github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
 github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
 github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
@@ -87,6 +89,8 @@ github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
 github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
+github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
@@ -144,6 +148,8 @@ 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/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
 github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
+github.com/mark3labs/mcp-go v0.21.1 h1:7Ek6KPIIbMhEYHRiRIg6K6UAgNZCJaHKQp926MNr6V0=
+github.com/mark3labs/mcp-go v0.21.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
 github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
 github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -153,12 +159,20 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 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/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
+github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
+github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
@@ -203,6 +217,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 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=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
@@ -272,12 +288,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
 golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
-google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
-google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 h1:AMLTAunltONNuzWgVPZXrjLWtXpsG6A3yLLPEoJ/IjU=
-google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 h1:TwXJCGVREgQ/cl18iY0Z4wJCTL/GmW+Um2oSwZiZPnc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
+google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
 google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
@@ -295,20 +311,20 @@ gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSk
 gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
 gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
 gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
-modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
-modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
-modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
-modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
+modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
+modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
+modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
 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.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
 modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
-modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
-modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
+modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
+modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
 modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
 modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
-modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
-modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
+modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
 modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
 modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
 modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=

+ 20 - 16
main.go → core/main.go

@@ -20,18 +20,18 @@ import (
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
 	_ "github.com/joho/godotenv/autoload"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/common/balance"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/common/consume"
-	"github.com/labring/aiproxy/common/conv"
-	"github.com/labring/aiproxy/common/ipblack"
-	"github.com/labring/aiproxy/common/notify"
-	"github.com/labring/aiproxy/common/trylock"
-	"github.com/labring/aiproxy/controller"
-	"github.com/labring/aiproxy/middleware"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/router"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/balance"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/consume"
+	"github.com/labring/aiproxy/core/common/conv"
+	"github.com/labring/aiproxy/core/common/ipblack"
+	"github.com/labring/aiproxy/core/common/notify"
+	"github.com/labring/aiproxy/core/common/trylock"
+	"github.com/labring/aiproxy/core/controller"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/router"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -77,7 +77,7 @@ func initializeNotifier() {
 }
 
 var logCallerIgnoreFuncs = map[string]struct{}{
-	"github.com/labring/aiproxy/middleware.logColor": {},
+	"github.com/labring/aiproxy/core/middleware.logColor": {},
 }
 
 func setLog(l *log.Logger) {
@@ -268,9 +268,13 @@ func cleanLog(ctx context.Context) {
 	}
 }
 
-// @securityDefinitions.apikey	ApiKeyAuth
-// @in							header
-// @name						Authorization
+// Swagger godoc
+//
+//	@title						AI Proxy Swagger API
+//	@version					1.0
+//	@securityDefinitions.apikey	ApiKeyAuth
+//	@in							header
+//	@name						Authorization
 func main() {
 	flag.Parse()
 

+ 11 - 6
middleware/auth.go → core/middleware/auth.go

@@ -8,11 +8,11 @@ import (
 	"strings"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/common/network"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/relay/meta"
-	"github.com/labring/aiproxy/relay/mode"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/network"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/meta"
+	"github.com/labring/aiproxy/core/relay/mode"
 	"github.com/sirupsen/logrus"
 )
 
@@ -56,6 +56,9 @@ func AdminAuth(c *gin.Context) {
 func TokenAuth(c *gin.Context) {
 	log := GetLogger(c)
 	key := c.Request.Header.Get("Authorization")
+	if key == "" {
+		key = c.Request.Header.Get("X-Api-Key")
+	}
 	key = strings.TrimPrefix(
 		strings.TrimPrefix(key, "Bearer "),
 		"sk-",
@@ -65,7 +68,9 @@ func TokenAuth(c *gin.Context) {
 	var useInternalToken bool
 	if config.AdminKey != "" && config.AdminKey == key ||
 		config.GetInternalToken() != "" && config.GetInternalToken() == key {
-		token = &model.TokenCache{}
+		token = &model.TokenCache{
+			Key: key,
+		}
 		useInternalToken = true
 	} else {
 		var err error

+ 0 - 0
middleware/cors.go → core/middleware/cors.go


+ 0 - 0
middleware/ctxkey.go → core/middleware/ctxkey.go


+ 17 - 13
middleware/distributor.go → core/middleware/distributor.go

@@ -11,15 +11,15 @@ import (
 	"github.com/bytedance/sonic"
 	"github.com/bytedance/sonic/ast"
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/common/balance"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/common/consume"
-	"github.com/labring/aiproxy/common/notify"
-	"github.com/labring/aiproxy/common/rpmlimit"
-	"github.com/labring/aiproxy/model"
-	"github.com/labring/aiproxy/relay/meta"
-	"github.com/labring/aiproxy/relay/mode"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/balance"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/consume"
+	"github.com/labring/aiproxy/core/common/notify"
+	"github.com/labring/aiproxy/core/common/rpmlimit"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/meta"
+	"github.com/labring/aiproxy/core/relay/mode"
 )
 
 func calculateGroupConsumeLevelRatio(usedAmount float64) float64 {
@@ -103,7 +103,8 @@ func checkGroupModelRPMAndTPM(c *gin.Context, group *model.GroupCache, mc *model
 
 	count, overLimitCount := rpmlimit.PushRequestAnyWay(c.Request.Context(), group.ID, mc.Model, adjustedModelConfig.RPM, time.Minute)
 	log.Data["rpm"] = strconv.FormatInt(count+overLimitCount, 10)
-	if adjustedModelConfig.RPM > 0 {
+	if group.Status != model.GroupStatusInternal &&
+		adjustedModelConfig.RPM > 0 {
 		log.Data["rpm_limit"] = strconv.FormatInt(adjustedModelConfig.RPM, 10)
 		if count > adjustedModelConfig.RPM {
 			setRpmHeaders(c, adjustedModelConfig.RPM, 0)
@@ -112,7 +113,8 @@ func checkGroupModelRPMAndTPM(c *gin.Context, group *model.GroupCache, mc *model
 		setRpmHeaders(c, adjustedModelConfig.RPM, adjustedModelConfig.RPM-count)
 	}
 
-	if adjustedModelConfig.TPM > 0 {
+	if group.Status != model.GroupStatusInternal &&
+		adjustedModelConfig.TPM > 0 {
 		tpm, err := model.CacheGetGroupModelTPM(group.ID, mc.Model)
 		if err != nil {
 			log.Errorf("get group model tpm (%s:%s) error: %s", group.ID, mc.Model, err.Error())
@@ -274,8 +276,10 @@ func CheckRelayMode(requestMode mode.Mode, modelMode mode.Mode) bool {
 		return true
 	}
 	switch requestMode {
-	case mode.ChatCompletions, mode.Completions:
-		return modelMode == mode.ChatCompletions || modelMode == mode.Completions
+	case mode.ChatCompletions, mode.Completions, mode.Anthropic:
+		return modelMode == mode.ChatCompletions ||
+			modelMode == mode.Completions ||
+			modelMode == mode.Anthropic
 	default:
 		return requestMode == modelMode
 	}

+ 1 - 1
middleware/distributor_test.go → core/middleware/distributor_test.go

@@ -6,7 +6,7 @@ import (
 	"testing"
 
 	jsoniter "github.com/json-iterator/go"
-	"github.com/labring/aiproxy/middleware"
+	"github.com/labring/aiproxy/core/middleware"
 )
 
 func StdGetModelFromJSON(body []byte) (string, error) {

+ 1 - 1
middleware/ipblack.go → core/middleware/ipblack.go

@@ -4,7 +4,7 @@ import (
 	"net/http"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/common/ipblack"
+	"github.com/labring/aiproxy/core/common/ipblack"
 )
 
 func IPBlock(c *gin.Context) {

+ 0 - 0
middleware/log.go → core/middleware/log.go


+ 86 - 0
core/middleware/mcp.go

@@ -0,0 +1,86 @@
+package middleware
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/network"
+	"github.com/labring/aiproxy/core/model"
+)
+
+func MCPAuth(c *gin.Context) {
+	log := GetLogger(c)
+	key := c.Request.Header.Get("Authorization")
+	if key == "" {
+		key, _ = c.GetQuery("key")
+	}
+	key = strings.TrimPrefix(
+		strings.TrimPrefix(key, "Bearer "),
+		"sk-",
+	)
+
+	var token *model.TokenCache
+	var useInternalToken bool
+	if config.AdminKey != "" && config.AdminKey == key ||
+		config.GetInternalToken() != "" && config.GetInternalToken() == key {
+		token = &model.TokenCache{
+			Key: key,
+		}
+		useInternalToken = true
+	} else {
+		var err error
+		token, err = model.ValidateAndGetToken(key)
+		if err != nil {
+			AbortLogWithMessage(c, http.StatusUnauthorized, err.Error(), &ErrorField{
+				Code: "invalid_token",
+			})
+			return
+		}
+	}
+
+	SetLogTokenFields(log.Data, token, useInternalToken)
+
+	if len(token.Subnets) > 0 {
+		if ok, err := network.IsIPInSubnets(c.ClientIP(), token.Subnets); err != nil {
+			AbortLogWithMessage(c, http.StatusInternalServerError, err.Error())
+			return
+		} else if !ok {
+			AbortLogWithMessage(c, http.StatusForbidden,
+				fmt.Sprintf("token (%s[%d]) can only be used in the specified subnets: %v, current ip: %s",
+					token.Name,
+					token.ID,
+					token.Subnets,
+					c.ClientIP(),
+				),
+			)
+			return
+		}
+	}
+
+	var group *model.GroupCache
+	if useInternalToken {
+		group = &model.GroupCache{
+			Status: model.GroupStatusInternal,
+		}
+	} else {
+		var err error
+		group, err = model.CacheGetGroup(token.Group)
+		if err != nil {
+			AbortLogWithMessage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get group: %v", err))
+			return
+		}
+	}
+	SetLogGroupFields(log.Data, group)
+	if group.Status != model.GroupStatusEnabled && group.Status != model.GroupStatusInternal {
+		AbortLogWithMessage(c, http.StatusForbidden, "group is disabled")
+		return
+	}
+
+	c.Set(Group, group)
+	c.Set(Token, token)
+
+	c.Next()
+}

+ 0 - 0
middleware/reqid.go → core/middleware/reqid.go


+ 1 - 1
middleware/utils.go → core/middleware/utils.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/relay/model"
+	"github.com/labring/aiproxy/core/relay/model"
 )
 
 const (

+ 1 - 1
model/batch.go → core/model/batch.go

@@ -7,7 +7,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/labring/aiproxy/common/notify"
+	"github.com/labring/aiproxy/core/common/notify"
 	"github.com/shopspring/decimal"
 )
 

+ 4 - 4
model/cache.go → core/model/cache.go

@@ -13,10 +13,10 @@ import (
 	"time"
 
 	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/common/conv"
-	"github.com/labring/aiproxy/common/notify"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/conv"
+	"github.com/labring/aiproxy/core/common/notify"
 	"github.com/maruel/natural"
 	"github.com/redis/go-redis/v9"
 	log "github.com/sirupsen/logrus"

+ 5 - 5
model/channel.go → core/model/channel.go

@@ -8,10 +8,10 @@ import (
 	"time"
 
 	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/common"
-	"github.com/labring/aiproxy/common/config"
-	"github.com/labring/aiproxy/monitor"
-	"github.com/labring/aiproxy/relay/mode"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/monitor"
+	"github.com/labring/aiproxy/core/relay/mode"
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 )
@@ -35,7 +35,7 @@ type ChannelConfig struct {
 }
 
 type Channel struct {
-	DeletedAt               gorm.DeletedAt    `gorm:"index"`
+	DeletedAt               gorm.DeletedAt    `gorm:"index"                              json:"-"`
 	CreatedAt               time.Time         `gorm:"index"                              json:"created_at"`
 	LastTestErrorAt         time.Time         `json:"last_test_error_at"`
 	ChannelTests            []*ChannelTest    `gorm:"foreignKey:ChannelID;references:ID" json:"channel_tests,omitempty"`

+ 1 - 1
model/channeltest.go → core/model/channeltest.go

@@ -4,7 +4,7 @@ import (
 	"time"
 
 	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/relay/mode"
+	"github.com/labring/aiproxy/core/relay/mode"
 )
 
 type ChannelTest struct {

+ 0 - 0
model/configkey.go → core/model/configkey.go


+ 1 - 1
model/consumeerr.go → core/model/consumeerr.go

@@ -6,7 +6,7 @@ import (
 	"time"
 
 	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/common"
+	"github.com/labring/aiproxy/core/common"
 )
 
 type ConsumeError struct {

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä