mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 13:53:41 +08:00 
			
		
		
		
	Compare commits
	
		
			120 Commits
		
	
	
		
			v0.6.5-alp
			...
			v0.6.8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c936198ac8 | ||
|  | 296ab013b8 | ||
|  | 5f03c856b4 | ||
|  | 39383e5532 | ||
|  | 2a892c1937 | ||
|  | adba54acd3 | ||
|  | 6209ff9ea9 | ||
|  | 1c44d7e1cd | ||
|  | a3eefb7af0 | ||
|  | b65bee46fb | ||
|  | 422a4e8ee5 | ||
|  | cf9b5f0b92 | ||
|  | 65acb94f45 | ||
|  | 6ad169975f | ||
|  | f636c50c84 | ||
|  | 720fe2dfeb | ||
|  | e090e76c86 | ||
|  | 6a941748f8 | ||
|  | 46a0773580 | ||
|  | ffdb0b0c81 | ||
|  | efd30a40b3 | ||
|  | d7a78f3397 | ||
|  | 273be55797 | ||
|  | ec6ad24810 | ||
|  | c4fe57c165 | ||
|  | 274fcf3d76 | ||
|  | 0fc07ea558 | ||
|  | 1ce1e529ee | ||
|  | d936817de9 | ||
|  | fecaece71b | ||
|  | c135d74f13 | ||
|  | d0369b114f | ||
|  | b21b3b5b46 | ||
|  | ae1cd29f94 | ||
|  | f25aaf7752 | ||
|  | b70a07e814 | ||
|  | 34cb147a74 | ||
|  | 8cc1ee6360 | ||
|  | 5a58426859 | ||
|  | 254b9777c0 | ||
|  | 114c44c6e7 | ||
|  | a3c7e15aed | ||
|  | 3777517f64 | ||
|  | 9fc5f427dc | ||
|  | 864a467886 | ||
|  | ed78b5340b | ||
|  | fee69e7c20 | ||
|  | 9d23a44dbf | ||
|  | 6e4cfb20d5 | ||
|  | ff196b75a7 | ||
|  | 279caf82dc | ||
|  | b1520b308b | ||
|  | ed717211aa | ||
|  | 6ccf3f3cfc | ||
|  | f74577141c | ||
|  | 6aafb7a99e | ||
|  | c1971870fa | ||
|  | f83894c83f | ||
|  | e9981fff36 | ||
|  | 98669d5d48 | ||
|  | 9321427c6e | ||
|  | ceea4c6d4a | ||
|  | b53e00a9b3 | ||
|  | 332c8db0b3 | ||
|  | 3be28da57b | ||
|  | fa74ba0eaa | ||
|  | a9211d66f6 | ||
|  | 07b2fd58d6 | ||
|  | 0acee9a065 | ||
|  | f965469e8a | ||
|  | 03ea60532a | ||
|  | 2457d00afb | ||
|  | 91b80ae879 | ||
|  | 2720e1a358 | ||
|  | 71f4403fd5 | ||
|  | 1f76c80553 | ||
|  | 7e027d2bd0 | ||
|  | 30f373b623 | ||
|  | 1c2654320e | ||
|  | 6cffb116b7 | ||
|  | a84c7b38b7 | ||
|  | 1bd14af47b | ||
|  | 6170b91d1c | ||
|  | 04b49aa0ec | ||
|  | ef88497f25 | ||
|  | 007906216d | ||
|  | e64e7707a0 | ||
|  | ea210b6ed7 | ||
|  | 9026ec7510 | ||
|  | c317872097 | ||
|  | da0842272c | ||
|  | 0a650b85b4 | ||
|  | 24f026d18e | ||
|  | cb33e8aad5 | ||
|  | 779b747e9e | ||
|  | 3d149fedf4 | ||
|  | 83517f687c | ||
|  | e30ebda0fe | ||
|  | d87c55f542 | ||
|  | e5b3e37c46 | ||
|  | 8de489cf06 | ||
|  | d14e4aa01b | ||
|  | 541182102e | ||
|  | b2679cca65 | ||
|  | 8572fac7a2 | ||
|  | a2a00dfbc3 | ||
|  | 129282f4a9 | ||
|  | a873cbd392 | ||
|  | 35ba1da984 | ||
|  | 2369025842 | ||
|  | f452bd481e | ||
|  | ddee58df36 | ||
|  | 520a62e704 | ||
|  | fc9a784950 | ||
|  | 1a0b039bcf | ||
|  | 7bf61f9165 | ||
|  | a10232f43a | ||
|  | af543ab8ec | ||
|  | e086da05b1 | ||
|  | 3af4649b52 | 
							
								
								
									
										3
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | PORT=3000 | ||||||
|  | DEBUG=false | ||||||
|  | HTTPS_PROXY=http://localhost:7890 | ||||||
							
								
								
									
										47
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | name: CI | ||||||
|  |  | ||||||
|  | # This setup assumes that you run the unit tests with code coverage in the same | ||||||
|  | # workflow that will also print the coverage report as comment to the pull request.  | ||||||
|  | # Therefore, you need to trigger this workflow when a pull request is (re)opened or | ||||||
|  | # when new code is pushed to the branch of the pull request. In addition, you also | ||||||
|  | # need to trigger this workflow when new code is pushed to the main branch because  | ||||||
|  | # we need to upload the code coverage results as artifact for the main branch as | ||||||
|  | # well since it will be the baseline code coverage. | ||||||
|  | #  | ||||||
|  | # We do not want to trigger the workflow for pushes to *any* branch because this | ||||||
|  | # would trigger our jobs twice on pull requests (once from "push" event and once | ||||||
|  | # from "pull_request->synchronize") | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     types: [opened, reopened, synchronize] | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - 'main' | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   unit_tests: | ||||||
|  |     name: "Unit tests" | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout repository | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|  |       - name: Setup Go | ||||||
|  |         uses: actions/setup-go@v4 | ||||||
|  |         with: | ||||||
|  |           go-version: ^1.22 | ||||||
|  |  | ||||||
|  |       # When you execute your unit tests, make sure to use the "-coverprofile" flag to write a  | ||||||
|  |       # coverage profile to a file. You will need the name of the file (e.g. "coverage.txt") | ||||||
|  |       # in the next step as well as the next job. | ||||||
|  |       - name: Test | ||||||
|  |         run: go test -cover -coverprofile=coverage.txt ./... | ||||||
|  |       - uses: codecov/codecov-action@v4 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|  |  | ||||||
|  |   commit_lint: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v3 | ||||||
|  |       - uses: wagoid/commitlint-github-action@v6 | ||||||
							
								
								
									
										61
									
								
								.github/workflows/docker-image-amd64.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										61
									
								
								.github/workflows/docker-image-amd64.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,61 +0,0 @@ | |||||||
| name: Publish Docker image (amd64) |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     tags: |  | ||||||
|       - '*' |  | ||||||
|   workflow_dispatch: |  | ||||||
|     inputs: |  | ||||||
|       name: |  | ||||||
|         description: 'reason' |  | ||||||
|         required: false |  | ||||||
| jobs: |  | ||||||
|   push_to_registries: |  | ||||||
|     name: Push Docker image to multiple registries |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     permissions: |  | ||||||
|       packages: write |  | ||||||
|       contents: read |  | ||||||
|     steps: |  | ||||||
|       - name: Check out the repo |  | ||||||
|         uses: actions/checkout@v3 |  | ||||||
|  |  | ||||||
|       - name: Check repository URL |  | ||||||
|         run: | |  | ||||||
|           REPO_URL=$(git config --get remote.origin.url) |  | ||||||
|           if [[ $REPO_URL == *"pro" ]]; then |  | ||||||
|             exit 1 |  | ||||||
|           fi         |  | ||||||
|  |  | ||||||
|       - name: Save version info |  | ||||||
|         run: | |  | ||||||
|           git describe --tags > VERSION  |  | ||||||
|  |  | ||||||
|       - name: Log in to Docker Hub |  | ||||||
|         uses: docker/login-action@v2 |  | ||||||
|         with: |  | ||||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} |  | ||||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} |  | ||||||
|  |  | ||||||
|       - name: Log in to the Container registry |  | ||||||
|         uses: docker/login-action@v2 |  | ||||||
|         with: |  | ||||||
|           registry: ghcr.io |  | ||||||
|           username: ${{ github.actor }} |  | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|  |  | ||||||
|       - name: Extract metadata (tags, labels) for Docker |  | ||||||
|         id: meta |  | ||||||
|         uses: docker/metadata-action@v4 |  | ||||||
|         with: |  | ||||||
|           images: | |  | ||||||
|             justsong/one-api |  | ||||||
|             ghcr.io/${{ github.repository }} |  | ||||||
|  |  | ||||||
|       - name: Build and push Docker images |  | ||||||
|         uses: docker/build-push-action@v3 |  | ||||||
|         with: |  | ||||||
|           context: . |  | ||||||
|           push: true |  | ||||||
|           tags: ${{ steps.meta.outputs.tags }} |  | ||||||
|           labels: ${{ steps.meta.outputs.labels }} |  | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| name: Publish Docker image (amd64, English) | name: Publish Docker image (English) | ||||||
| 
 | 
 | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     tags: |     tags: | ||||||
|       - '*' |       - 'v*.*.*' | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|     inputs: |     inputs: | ||||||
|       name: |       name: | ||||||
| @@ -34,6 +34,13 @@ jobs: | |||||||
|       - name: Translate |       - name: Translate | ||||||
|         run: | |         run: | | ||||||
|           python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json |           python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json | ||||||
|  | 
 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v2 | ||||||
|  | 
 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v2 | ||||||
|  | 
 | ||||||
|       - name: Log in to Docker Hub |       - name: Log in to Docker Hub | ||||||
|         uses: docker/login-action@v2 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
| @@ -51,6 +58,7 @@ jobs: | |||||||
|         uses: docker/build-push-action@v3 |         uses: docker/build-push-action@v3 | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|  |           platforms: linux/amd64,linux/arm64 | ||||||
|           push: true |           push: true | ||||||
|           tags: ${{ steps.meta.outputs.tags }} |           tags: ${{ steps.meta.outputs.tags }} | ||||||
|           labels: ${{ steps.meta.outputs.labels }} |           labels: ${{ steps.meta.outputs.labels }} | ||||||
| @@ -1,10 +1,9 @@ | |||||||
| name: Publish Docker image (arm64) | name: Publish Docker image | ||||||
| 
 | 
 | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     tags: |     tags: | ||||||
|       - '*' |       - 'v*.*.*' | ||||||
|       - '!*-alpha*' |  | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|     inputs: |     inputs: | ||||||
|       name: |       name: | ||||||
							
								
								
									
										2
									
								
								.github/workflows/linux-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/linux-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -5,7 +5,7 @@ permissions: | |||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     tags: |     tags: | ||||||
|       - '*' |       - 'v*.*.*' | ||||||
|       - '!*-alpha*' |       - '!*-alpha*' | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|     inputs: |     inputs: | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/macos-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/macos-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -5,7 +5,7 @@ permissions: | |||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     tags: |     tags: | ||||||
|       - '*' |       - 'v*.*.*' | ||||||
|       - '!*-alpha*' |       - '!*-alpha*' | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|     inputs: |     inputs: | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/windows-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/windows-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -5,7 +5,7 @@ permissions: | |||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     tags: |     tags: | ||||||
|       - '*' |       - 'v*.*.*' | ||||||
|       - '!*-alpha*' |       - '!*-alpha*' | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|     inputs: |     inputs: | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -9,3 +9,4 @@ logs | |||||||
| data | data | ||||||
| /web/node_modules | /web/node_modules | ||||||
| cmd.md | cmd.md | ||||||
|  | .env | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| FROM node:16 as builder | FROM --platform=$BUILDPLATFORM node:16 AS builder | ||||||
|  |  | ||||||
| WORKDIR /web | WORKDIR /web | ||||||
| COPY ./VERSION . | COPY ./VERSION . | ||||||
| @@ -16,7 +16,9 @@ WORKDIR /web/air | |||||||
| RUN npm install | RUN npm install | ||||||
| RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build | RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build | ||||||
|  |  | ||||||
| FROM golang AS builder2 | FROM golang:alpine AS builder2 | ||||||
|  |  | ||||||
|  | RUN apk add --no-cache g++ | ||||||
|  |  | ||||||
| ENV GO111MODULE=on \ | ENV GO111MODULE=on \ | ||||||
|     CGO_ENABLED=1 \ |     CGO_ENABLED=1 \ | ||||||
| @@ -27,7 +29,7 @@ ADD go.mod go.sum ./ | |||||||
| RUN go mod download | RUN go mod download | ||||||
| COPY . . | COPY . . | ||||||
| COPY --from=builder /web/build ./web/build | COPY --from=builder /web/build ./web/build | ||||||
| RUN go build -ldflags "-s -w -X 'github.com/songquanpeng/one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api | RUN go build -trimpath -ldflags "-s -w -X 'github.com/songquanpeng/one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api | ||||||
|  |  | ||||||
| FROM alpine | FROM alpine | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										39
									
								
								README.en.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								README.en.md
									
									
									
									
									
								
							| @@ -245,16 +245,41 @@ If the channel ID is not provided, load balancing will be used to distribute the | |||||||
|     + Example: `LOG_SQL_DSN=root:123456@tcp(localhost:3306)/oneapi-logs` |     + Example: `LOG_SQL_DSN=root:123456@tcp(localhost:3306)/oneapi-logs` | ||||||
| 5. `FRONTEND_BASE_URL`: When set, the specified frontend address will be used instead of the backend address. | 5. `FRONTEND_BASE_URL`: When set, the specified frontend address will be used instead of the backend address. | ||||||
|     + Example: `FRONTEND_BASE_URL=https://openai.justsong.cn` |     + Example: `FRONTEND_BASE_URL=https://openai.justsong.cn` | ||||||
| 6. `SYNC_FREQUENCY`: When set, the system will periodically sync configurations from the database, with the unit in seconds. If not set, no sync will happen. | 6. 'MEMORY_CACHE_ENABLED': Enabling memory caching can cause a certain delay in updating user quotas, with optional values of 'true' and 'false'. If not set, it defaults to 'false'. | ||||||
|  | 7. `SYNC_FREQUENCY`: When set, the system will periodically sync configurations from the database, with the unit in seconds. If not set, no sync will happen. | ||||||
|     + Example: `SYNC_FREQUENCY=60` |     + Example: `SYNC_FREQUENCY=60` | ||||||
| 7. `NODE_TYPE`: When set, specifies the node type. Valid values are `master` and `slave`. If not set, it defaults to `master`. | 8. `NODE_TYPE`: When set, specifies the node type. Valid values are `master` and `slave`. If not set, it defaults to `master`. | ||||||
|     + Example: `NODE_TYPE=slave` |     + Example: `NODE_TYPE=slave` | ||||||
| 8. `CHANNEL_UPDATE_FREQUENCY`: When set, it periodically updates the channel balances, with the unit in minutes. If not set, no update will happen. | 9. `CHANNEL_UPDATE_FREQUENCY`: When set, it periodically updates the channel balances, with the unit in minutes. If not set, no update will happen. | ||||||
|     + Example: `CHANNEL_UPDATE_FREQUENCY=1440` |     + Example: `CHANNEL_UPDATE_FREQUENCY=1440` | ||||||
| 9. `CHANNEL_TEST_FREQUENCY`: When set, it periodically tests the channels, with the unit in minutes. If not set, no test will happen. | 10. `CHANNEL_TEST_FREQUENCY`: When set, it periodically tests the channels, with the unit in minutes. If not set, no test will happen. | ||||||
|     + Example: `CHANNEL_TEST_FREQUENCY=1440` |     + Example: `CHANNEL_TEST_FREQUENCY=1440` | ||||||
| 10. `POLLING_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval. | 11. `POLLING_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval. | ||||||
|     + Example: `POLLING_INTERVAL=5` |     + Example: `POLLING_INTERVAL=5` | ||||||
|  | 12. `BATCH_UPDATE_ENABLED`: Enabling batch database update aggregation can cause a certain delay in updating user quotas. The optional values are 'true' and 'false', but if not set, it defaults to 'false'. | ||||||
|  |     +Example: ` BATCH_UPDATE_ENABLED=true` | ||||||
|  |     +If you encounter an issue with too many database connections, you can try enabling this option. | ||||||
|  | 13. `BATCH_UPDATE_INTERVAL=5`: The time interval for batch updating aggregates, measured in seconds, defaults to '5'. | ||||||
|  |     +Example: ` BATCH_UPDATE_INTERVAL=5` | ||||||
|  | 14. Request frequency limit: | ||||||
|  |     + `GLOBAL_API_RATE_LIMIT`: Global API rate limit (excluding relay requests), the maximum number of requests within three minutes per IP, default to 180. | ||||||
|  |     + `GLOBAL_WEL_RATE_LIMIT`: Global web speed limit, the maximum number of requests within three minutes per IP, default to 60. | ||||||
|  | 15. Encoder cache settings: | ||||||
|  |     +`TIKTOKEN_CACHE_DIR`: By default, when the program starts, it will download the encoding of some common word elements online, such as' gpt-3.5 turbo '. In some unstable network environments or offline situations, it may cause startup problems. This directory can be configured to cache data and can be migrated to an offline environment. | ||||||
|  |     +`DATA_GYM_CACHE_DIR`: Currently, this configuration has the same function as' TIKTOKEN-CACHE-DIR ', but its priority is not as high as it. | ||||||
|  | 16. `RELAY_TIMEOUT`: Relay timeout setting, measured in seconds, with no default timeout time set. | ||||||
|  | 17. `RELAY_PROXY`: After setting up, use this proxy to request APIs. | ||||||
|  | 18. `USER_CONTENT_REQUEST_TIMEOUT`: The timeout period for users to upload and download content, measured in seconds. | ||||||
|  | 19. `USER_CONTENT_REQUEST_PROXY`: After setting up, use this agent to request content uploaded by users, such as images. | ||||||
|  | 20. `SQLITE_BUSY_TIMEOUT`: SQLite lock wait timeout setting, measured in milliseconds, default to '3000'. | ||||||
|  | 21. `GEMINI_SAFETY_SETTING`: Gemini's security settings are set to 'BLOCK-NONE' by default. | ||||||
|  | 22. `GEMINI_VERSION`: The Gemini version used by the One API, which defaults to 'v1'. | ||||||
|  | 23. `THE`: The system's theme setting, default to 'default', specific optional values refer to [here] (./web/README. md). | ||||||
|  | 24. `ENABLE_METRIC`: Whether to disable channels based on request success rate, default not enabled, optional values are 'true' and 'false'. | ||||||
|  | 25. `METRIC_QUEUE_SIZE`: Request success rate statistics queue size, default to '10'. | ||||||
|  | 26. `METRIC_SUCCESS_RATE_THRESHOLD`: Request success rate threshold, default to '0.8'. | ||||||
|  | 27. `INITIAL_ROOT_TOKEN`: If this value is set, a root user token with the value of the environment variable will be automatically created when the system starts for the first time. | ||||||
|  | 28. `INITIAL_ROOT_ACCESS_TOKEN`: If this value is set, a system management token will be automatically created for the root user with a value of the environment variable when the system starts for the first time. | ||||||
|  |  | ||||||
| ### Command Line Parameters | ### Command Line Parameters | ||||||
| 1. `--port <port_number>`: Specifies the port number on which the server listens. Defaults to `3000`. | 1. `--port <port_number>`: Specifies the port number on which the server listens. Defaults to `3000`. | ||||||
| @@ -287,7 +312,9 @@ If the channel ID is not provided, load balancing will be used to distribute the | |||||||
|     + Double-check that your interface address and API Key are correct. |     + Double-check that your interface address and API Key are correct. | ||||||
|  |  | ||||||
| ## Related Projects | ## Related Projects | ||||||
| [FastGPT](https://github.com/labring/FastGPT): Knowledge question answering system based on the LLM | * [FastGPT](https://github.com/labring/FastGPT): Knowledge question answering system based on the LLM | ||||||
|  | * [VChart](https://github.com/VisActor/VChart):  More than just a cross-platform charting library, but also an expressive data storyteller. | ||||||
|  | * [VMind](https://github.com/VisActor/VMind):  Not just automatic, but also fantastic. Open-source solution for intelligent visualization. | ||||||
|  |  | ||||||
| ## Note | ## Note | ||||||
| This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes. | This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes. | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								README.md
									
									
									
									
									
								
							| @@ -65,9 +65,10 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用  | |||||||
| ## 功能 | ## 功能 | ||||||
| 1. 支持多种大模型: | 1. 支持多种大模型: | ||||||
|    + [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference)) |    + [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference)) | ||||||
|    + [x] [Anthropic Claude 系列模型](https://anthropic.com) |    + [x] [Anthropic Claude 系列模型](https://anthropic.com) (支持 AWS Claude) | ||||||
|    + [x] [Google PaLM2/Gemini 系列模型](https://developers.generativeai.google) |    + [x] [Google PaLM2/Gemini 系列模型](https://developers.generativeai.google) | ||||||
|    + [x] [Mistral 系列模型](https://mistral.ai/) |    + [x] [Mistral 系列模型](https://mistral.ai/) | ||||||
|  |    + [x] [字节跳动豆包大模型](https://console.volcengine.com/ark/region:ark+cn-beijing/model) | ||||||
|    + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) |    + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) | ||||||
|    + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html) |    + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html) | ||||||
|    + [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html) |    + [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html) | ||||||
| @@ -76,12 +77,18 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用  | |||||||
|    + [x] [腾讯混元大模型](https://cloud.tencent.com/document/product/1729) |    + [x] [腾讯混元大模型](https://cloud.tencent.com/document/product/1729) | ||||||
|    + [x] [Moonshot AI](https://platform.moonshot.cn/) |    + [x] [Moonshot AI](https://platform.moonshot.cn/) | ||||||
|    + [x] [百川大模型](https://platform.baichuan-ai.com) |    + [x] [百川大模型](https://platform.baichuan-ai.com) | ||||||
|    + [ ] [字节云雀大模型](https://www.volcengine.com/product/ark) (WIP) |  | ||||||
|    + [x] [MINIMAX](https://api.minimax.chat/) |    + [x] [MINIMAX](https://api.minimax.chat/) | ||||||
|    + [x] [Groq](https://wow.groq.com/) |    + [x] [Groq](https://wow.groq.com/) | ||||||
|    + [x] [Ollama](https://github.com/ollama/ollama) |    + [x] [Ollama](https://github.com/ollama/ollama) | ||||||
|    + [x] [零一万物](https://platform.lingyiwanwu.com/) |    + [x] [零一万物](https://platform.lingyiwanwu.com/) | ||||||
|    + [x] [阶跃星辰](https://platform.stepfun.com/) |    + [x] [阶跃星辰](https://platform.stepfun.com/) | ||||||
|  |    + [x] [Coze](https://www.coze.com/) | ||||||
|  |    + [x] [Cohere](https://cohere.com/) | ||||||
|  |    + [x] [DeepSeek](https://www.deepseek.com/) | ||||||
|  |    + [x] [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/) | ||||||
|  |    + [x] [DeepL](https://www.deepl.com/) | ||||||
|  |    + [x] [together.ai](https://www.together.ai/) | ||||||
|  |    + [x] [novita.ai](https://www.novita.ai/) | ||||||
| 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 | 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 | ||||||
| 3. 支持通过**负载均衡**的方式访问多个渠道。 | 3. 支持通过**负载均衡**的方式访问多个渠道。 | ||||||
| 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | ||||||
| @@ -334,6 +341,7 @@ graph LR | |||||||
| 不加的话将会使用负载均衡的方式使用多个渠道。 | 不加的话将会使用负载均衡的方式使用多个渠道。 | ||||||
|  |  | ||||||
| ### 环境变量 | ### 环境变量 | ||||||
|  | > One API 支持从 `.env` 文件中读取环境变量,请参照 `.env.example` 文件,使用时请将其重命名为 `.env`。 | ||||||
| 1. `REDIS_CONN_STRING`:设置之后将使用 Redis 作为缓存使用。 | 1. `REDIS_CONN_STRING`:设置之后将使用 Redis 作为缓存使用。 | ||||||
|    + 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` |    + 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` | ||||||
|    + 如果数据库访问延迟很低,没有必要启用 Redis,启用后反而会出现数据滞后的问题。 |    + 如果数据库访问延迟很低,没有必要启用 Redis,启用后反而会出现数据滞后的问题。 | ||||||
| @@ -363,7 +371,7 @@ graph LR | |||||||
| 9. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。 | 9. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。 | ||||||
|    + 例子:`CHANNEL_UPDATE_FREQUENCY=1440` |    + 例子:`CHANNEL_UPDATE_FREQUENCY=1440` | ||||||
| 10. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。  | 10. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。  | ||||||
|    + 例子:`CHANNEL_TEST_FREQUENCY=1440` |    +例子:`CHANNEL_TEST_FREQUENCY=1440` | ||||||
| 11. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。 | 11. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。 | ||||||
|     + 例子:`POLLING_INTERVAL=5` |     + 例子:`POLLING_INTERVAL=5` | ||||||
| 12. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。 | 12. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。 | ||||||
| @@ -378,13 +386,18 @@ graph LR | |||||||
|     + `TIKTOKEN_CACHE_DIR`:默认程序启动时会联网下载一些通用的词元的编码,如:`gpt-3.5-turbo`,在一些网络环境不稳定,或者离线情况,可能会导致启动有问题,可以配置此目录缓存数据,可迁移到离线环境。 |     + `TIKTOKEN_CACHE_DIR`:默认程序启动时会联网下载一些通用的词元的编码,如:`gpt-3.5-turbo`,在一些网络环境不稳定,或者离线情况,可能会导致启动有问题,可以配置此目录缓存数据,可迁移到离线环境。 | ||||||
|     + `DATA_GYM_CACHE_DIR`:目前该配置作用与 `TIKTOKEN_CACHE_DIR` 一致,但是优先级没有它高。 |     + `DATA_GYM_CACHE_DIR`:目前该配置作用与 `TIKTOKEN_CACHE_DIR` 一致,但是优先级没有它高。 | ||||||
| 16. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。 | 16. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。 | ||||||
| 17. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。 | 17. `RELAY_PROXY`:设置后使用该代理来请求 API。 | ||||||
| 18. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。 | 18. `USER_CONTENT_REQUEST_TIMEOUT`:用户上传内容下载超时时间,单位为秒。 | ||||||
| 19. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。 | 19. `USER_CONTENT_REQUEST_PROXY`:设置后使用该代理来请求用户上传的内容,例如图片。 | ||||||
| 20. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。 | 20. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。 | ||||||
| 21. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。 | 21. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。 | ||||||
| 22. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。 | 22. `GEMINI_VERSION`:One API 所使用的 Gemini 版本,默认为 `v1`。 | ||||||
| 23. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。 | 23. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。 | ||||||
|  | 24. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。 | ||||||
|  | 25. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。 | ||||||
|  | 26. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。 | ||||||
|  | 27. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。 | ||||||
|  | 28. `INITIAL_ROOT_ACCESS_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量的 root 用户创建系统管理令牌。 | ||||||
|  |  | ||||||
| ### 命令行参数 | ### 命令行参数 | ||||||
| 1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。 | 1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。 | ||||||
| @@ -437,6 +450,8 @@ https://openai.justsong.cn | |||||||
| ## 相关项目 | ## 相关项目 | ||||||
| * [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统 | * [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统 | ||||||
| * [ChatGPT Next Web](https://github.com/Yidadaa/ChatGPT-Next-Web):  一键拥有你自己的跨平台 ChatGPT 应用 | * [ChatGPT Next Web](https://github.com/Yidadaa/ChatGPT-Next-Web):  一键拥有你自己的跨平台 ChatGPT 应用 | ||||||
|  | * [VChart](https://github.com/VisActor/VChart):  不只是开箱即用的多端图表库,更是生动灵活的数据故事讲述者。 | ||||||
|  | * [VMind](https://github.com/VisActor/VMind):  不仅自动,还很智能。开源智能可视化解决方案。 | ||||||
|  |  | ||||||
| ## 注意 | ## 注意 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										60
									
								
								common/client/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								common/client/init.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | package client | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var HTTPClient *http.Client | ||||||
|  | var ImpatientHTTPClient *http.Client | ||||||
|  | var UserContentRequestHTTPClient *http.Client | ||||||
|  |  | ||||||
|  | func Init() { | ||||||
|  | 	if config.UserContentRequestProxy != "" { | ||||||
|  | 		logger.SysLog(fmt.Sprintf("using %s as proxy to fetch user content", config.UserContentRequestProxy)) | ||||||
|  | 		proxyURL, err := url.Parse(config.UserContentRequestProxy) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.FatalLog(fmt.Sprintf("USER_CONTENT_REQUEST_PROXY set but invalid: %s", config.UserContentRequestProxy)) | ||||||
|  | 		} | ||||||
|  | 		transport := &http.Transport{ | ||||||
|  | 			Proxy: http.ProxyURL(proxyURL), | ||||||
|  | 		} | ||||||
|  | 		UserContentRequestHTTPClient = &http.Client{ | ||||||
|  | 			Transport: transport, | ||||||
|  | 			Timeout:   time.Second * time.Duration(config.UserContentRequestTimeout), | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		UserContentRequestHTTPClient = &http.Client{} | ||||||
|  | 	} | ||||||
|  | 	var transport http.RoundTripper | ||||||
|  | 	if config.RelayProxy != "" { | ||||||
|  | 		logger.SysLog(fmt.Sprintf("using %s as api relay proxy", config.RelayProxy)) | ||||||
|  | 		proxyURL, err := url.Parse(config.RelayProxy) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.FatalLog(fmt.Sprintf("USER_CONTENT_REQUEST_PROXY set but invalid: %s", config.UserContentRequestProxy)) | ||||||
|  | 		} | ||||||
|  | 		transport = &http.Transport{ | ||||||
|  | 			Proxy: http.ProxyURL(proxyURL), | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if config.RelayTimeout == 0 { | ||||||
|  | 		HTTPClient = &http.Client{ | ||||||
|  | 			Transport: transport, | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		HTTPClient = &http.Client{ | ||||||
|  | 			Timeout:   time.Duration(config.RelayTimeout) * time.Second, | ||||||
|  | 			Transport: transport, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ImpatientHTTPClient = &http.Client{ | ||||||
|  | 		Timeout:   5 * time.Second, | ||||||
|  | 		Transport: transport, | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/common/env" | 	"github.com/songquanpeng/one-api/common/env" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -51,9 +52,9 @@ var EmailDomainWhitelist = []string{ | |||||||
| 	"foxmail.com", | 	"foxmail.com", | ||||||
| } | } | ||||||
|  |  | ||||||
| var DebugEnabled = os.Getenv("DEBUG") == "true" | var DebugEnabled = strings.ToLower(os.Getenv("DEBUG")) == "true" | ||||||
| var DebugSQLEnabled = os.Getenv("DEBUG_SQL") == "true" | var DebugSQLEnabled = strings.ToLower(os.Getenv("DEBUG_SQL")) == "true" | ||||||
| var MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true" | var MemoryCacheEnabled = strings.ToLower(os.Getenv("MEMORY_CACHE_ENABLED")) == "true" | ||||||
|  |  | ||||||
| var LogConsumeEnabled = true | var LogConsumeEnabled = true | ||||||
|  |  | ||||||
| @@ -116,10 +117,10 @@ var ValidThemes = map[string]bool{ | |||||||
| // All duration's unit is seconds | // All duration's unit is seconds | ||||||
| // Shouldn't larger then RateLimitKeyExpirationDuration | // Shouldn't larger then RateLimitKeyExpirationDuration | ||||||
| var ( | var ( | ||||||
| 	GlobalApiRateLimitNum            = env.Int("GLOBAL_API_RATE_LIMIT", 180) | 	GlobalApiRateLimitNum            = env.Int("GLOBAL_API_RATE_LIMIT", 240) | ||||||
| 	GlobalApiRateLimitDuration int64 = 3 * 60 | 	GlobalApiRateLimitDuration int64 = 3 * 60 | ||||||
|  |  | ||||||
| 	GlobalWebRateLimitNum            = env.Int("GLOBAL_WEB_RATE_LIMIT", 60) | 	GlobalWebRateLimitNum            = env.Int("GLOBAL_WEB_RATE_LIMIT", 120) | ||||||
| 	GlobalWebRateLimitDuration int64 = 3 * 60 | 	GlobalWebRateLimitDuration int64 = 3 * 60 | ||||||
|  |  | ||||||
| 	UploadRateLimitNum            = 10 | 	UploadRateLimitNum            = 10 | ||||||
| @@ -141,3 +142,13 @@ var MetricSuccessChanSize = env.Int("METRIC_SUCCESS_CHAN_SIZE", 1024) | |||||||
| var MetricFailChanSize = env.Int("METRIC_FAIL_CHAN_SIZE", 128) | var MetricFailChanSize = env.Int("METRIC_FAIL_CHAN_SIZE", 128) | ||||||
|  |  | ||||||
| var InitialRootToken = os.Getenv("INITIAL_ROOT_TOKEN") | var InitialRootToken = os.Getenv("INITIAL_ROOT_TOKEN") | ||||||
|  |  | ||||||
|  | var InitialRootAccessToken = os.Getenv("INITIAL_ROOT_ACCESS_TOKEN") | ||||||
|  |  | ||||||
|  | var GeminiVersion = env.String("GEMINI_VERSION", "v1") | ||||||
|  |  | ||||||
|  | var OnlyOneLogFile = env.Bool("ONLY_ONE_LOG_FILE", false) | ||||||
|  |  | ||||||
|  | var RelayProxy = env.String("RELAY_PROXY", "") | ||||||
|  | var UserContentRequestProxy = env.String("USER_CONTENT_REQUEST_PROXY", "") | ||||||
|  | var UserContentRequestTimeout = env.Int("USER_CONTENT_REQUEST_TIMEOUT", 30) | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| package config |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	KeyPrefix = "cfg_" |  | ||||||
|  |  | ||||||
| 	KeyAPIVersion = KeyPrefix + "api_version" |  | ||||||
| 	KeyLibraryID  = KeyPrefix + "library_id" |  | ||||||
| 	KeyPlugin     = KeyPrefix + "plugin" |  | ||||||
| ) |  | ||||||
							
								
								
									
										23
									
								
								common/ctxkey/key.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								common/ctxkey/key.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | package ctxkey | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	Config            = "config" | ||||||
|  | 	Id                = "id" | ||||||
|  | 	Username          = "username" | ||||||
|  | 	Role              = "role" | ||||||
|  | 	Status            = "status" | ||||||
|  | 	Channel           = "channel" | ||||||
|  | 	ChannelId         = "channel_id" | ||||||
|  | 	SpecificChannelId = "specific_channel_id" | ||||||
|  | 	RequestModel      = "request_model" | ||||||
|  | 	ConvertedRequest  = "converted_request" | ||||||
|  | 	OriginalModel     = "original_model" | ||||||
|  | 	Group             = "group" | ||||||
|  | 	ModelMapping      = "model_mapping" | ||||||
|  | 	ChannelName       = "channel_name" | ||||||
|  | 	TokenId           = "token_id" | ||||||
|  | 	TokenName         = "token_name" | ||||||
|  | 	BaseURL           = "base_url" | ||||||
|  | 	AvailableModels   = "available_models" | ||||||
|  | 	KeyRequestBody    = "key_request_body" | ||||||
|  | ) | ||||||
| @@ -4,14 +4,13 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"io" | 	"io" | ||||||
| 	"strings" | 	"strings" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const KeyRequestBody = "key_request_body" |  | ||||||
|  |  | ||||||
| func GetRequestBody(c *gin.Context) ([]byte, error) { | func GetRequestBody(c *gin.Context) ([]byte, error) { | ||||||
| 	requestBody, _ := c.Get(KeyRequestBody) | 	requestBody, _ := c.Get(ctxkey.KeyRequestBody) | ||||||
| 	if requestBody != nil { | 	if requestBody != nil { | ||||||
| 		return requestBody.([]byte), nil | 		return requestBody.([]byte), nil | ||||||
| 	} | 	} | ||||||
| @@ -20,7 +19,7 @@ func GetRequestBody(c *gin.Context) ([]byte, error) { | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	_ = c.Request.Body.Close() | 	_ = c.Request.Body.Close() | ||||||
| 	c.Set(KeyRequestBody, requestBody) | 	c.Set(ctxkey.KeyRequestBody, requestBody) | ||||||
| 	return requestBody.([]byte), nil | 	return requestBody.([]byte), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package helper | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/random" | 	"github.com/songquanpeng/one-api/common/random" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"log" | 	"log" | ||||||
| @@ -105,6 +106,11 @@ func GenRequestID() string { | |||||||
| 	return GetTimeString() + random.GetRandomNumberString(8) | 	return GetTimeString() + random.GetRandomNumberString(8) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func GetResponseID(c *gin.Context) string { | ||||||
|  | 	logID := c.GetString(RequestIdKey) | ||||||
|  | 	return fmt.Sprintf("chatcmpl-%s", logID) | ||||||
|  | } | ||||||
|  |  | ||||||
| func Max(a int, b int) int { | func Max(a int, b int) int { | ||||||
| 	if a >= b { | 	if a >= b { | ||||||
| 		return a | 		return a | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								common/helper/key.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								common/helper/key.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | package helper | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	RequestIdKey = "X-Oneapi-Request-Id" | ||||||
|  | ) | ||||||
| @@ -3,6 +3,7 @@ package image | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/client" | ||||||
| 	"image" | 	"image" | ||||||
| 	_ "image/gif" | 	_ "image/gif" | ||||||
| 	_ "image/jpeg" | 	_ "image/jpeg" | ||||||
| @@ -19,7 +20,7 @@ import ( | |||||||
| var dataURLPattern = regexp.MustCompile(`data:image/([^;]+);base64,(.*)`) | var dataURLPattern = regexp.MustCompile(`data:image/([^;]+);base64,(.*)`) | ||||||
|  |  | ||||||
| func IsImageUrl(url string) (bool, error) { | func IsImageUrl(url string) (bool, error) { | ||||||
| 	resp, err := http.Head(url) | 	resp, err := client.UserContentRequestHTTPClient.Head(url) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
| @@ -34,7 +35,7 @@ func GetImageSizeFromUrl(url string) (width int, height int, err error) { | |||||||
| 	if !isImage { | 	if !isImage { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	resp, err := http.Get(url) | 	resp, err := client.UserContentRequestHTTPClient.Get(url) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package image_test | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/client" | ||||||
| 	"image" | 	"image" | ||||||
| 	_ "image/gif" | 	_ "image/gif" | ||||||
| 	_ "image/jpeg" | 	_ "image/jpeg" | ||||||
| @@ -44,6 +45,11 @@ var ( | |||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | func TestMain(m *testing.M) { | ||||||
|  | 	client.Init() | ||||||
|  | 	m.Run() | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestDecode(t *testing.T) { | func TestDecode(t *testing.T) { | ||||||
| 	// Bytes read: varies sometimes | 	// Bytes read: varies sometimes | ||||||
| 	// jpeg: 1063892 | 	// jpeg: 1063892 | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ func printHelp() { | |||||||
| 	fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]") | 	fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]") | ||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func Init() { | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
|  |  | ||||||
| 	if *PrintVersion { | 	if *PrintVersion { | ||||||
|   | |||||||
| @@ -1,7 +1,3 @@ | |||||||
| package logger | package logger | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	RequestIdKey = "X-Oneapi-Request-Id" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var LogDir string | var LogDir string | ||||||
|   | |||||||
| @@ -3,15 +3,16 @@ package logger | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"github.com/songquanpeng/one-api/common/config" |  | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -21,21 +22,17 @@ const ( | |||||||
| 	loggerError = "ERR" | 	loggerError = "ERR" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var setupLogLock sync.Mutex | var setupLogOnce sync.Once | ||||||
| var setupLogWorking bool |  | ||||||
|  |  | ||||||
| func SetupLogger() { | func SetupLogger() { | ||||||
|  | 	setupLogOnce.Do(func() { | ||||||
| 		if LogDir != "" { | 		if LogDir != "" { | ||||||
| 		ok := setupLogLock.TryLock() | 			var logPath string | ||||||
| 		if !ok { | 			if config.OnlyOneLogFile { | ||||||
| 			log.Println("setup log is already working") | 				logPath = filepath.Join(LogDir, "oneapi.log") | ||||||
| 			return | 			} else { | ||||||
|  | 				logPath = filepath.Join(LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102"))) | ||||||
| 			} | 			} | ||||||
| 		defer func() { |  | ||||||
| 			setupLogLock.Unlock() |  | ||||||
| 			setupLogWorking = false |  | ||||||
| 		}() |  | ||||||
| 		logPath := filepath.Join(LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102"))) |  | ||||||
| 			fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | 			fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal("failed to open log file") | 				log.Fatal("failed to open log file") | ||||||
| @@ -43,6 +40,7 @@ func SetupLogger() { | |||||||
| 			gin.DefaultWriter = io.MultiWriter(os.Stdout, fd) | 			gin.DefaultWriter = io.MultiWriter(os.Stdout, fd) | ||||||
| 			gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd) | 			gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd) | ||||||
| 		} | 		} | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func SysLog(s string) { | func SysLog(s string) { | ||||||
| @@ -50,11 +48,19 @@ func SysLog(s string) { | |||||||
| 	_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) | 	_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func SysLogf(format string, a ...any) { | ||||||
|  | 	SysLog(fmt.Sprintf(format, a...)) | ||||||
|  | } | ||||||
|  |  | ||||||
| func SysError(s string) { | func SysError(s string) { | ||||||
| 	t := time.Now() | 	t := time.Now() | ||||||
| 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) | 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func SysErrorf(format string, a ...any) { | ||||||
|  | 	SysError(fmt.Sprintf(format, a...)) | ||||||
|  | } | ||||||
|  |  | ||||||
| func Debug(ctx context.Context, msg string) { | func Debug(ctx context.Context, msg string) { | ||||||
| 	if config.DebugEnabled { | 	if config.DebugEnabled { | ||||||
| 		logHelper(ctx, loggerDEBUG, msg) | 		logHelper(ctx, loggerDEBUG, msg) | ||||||
| @@ -94,18 +100,13 @@ func logHelper(ctx context.Context, level string, msg string) { | |||||||
| 	if level == loggerINFO { | 	if level == loggerINFO { | ||||||
| 		writer = gin.DefaultWriter | 		writer = gin.DefaultWriter | ||||||
| 	} | 	} | ||||||
| 	id := ctx.Value(RequestIdKey) | 	id := ctx.Value(helper.RequestIdKey) | ||||||
| 	if id == nil { | 	if id == nil { | ||||||
| 		id = helper.GenRequestID() | 		id = helper.GenRequestID() | ||||||
| 	} | 	} | ||||||
| 	now := time.Now() | 	now := time.Now() | ||||||
| 	_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg) | 	_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg) | ||||||
| 	if !setupLogWorking { |  | ||||||
| 		setupLogWorking = true |  | ||||||
| 		go func() { |  | ||||||
| 	SetupLogger() | 	SetupLogger() | ||||||
| 		}() |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func FatalLog(v ...any) { | func FatalLog(v ...any) { | ||||||
|   | |||||||
| @@ -6,11 +6,16 @@ import ( | |||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"net" | ||||||
| 	"net/smtp" | 	"net/smtp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | func shouldAuth() bool { | ||||||
|  | 	return config.SMTPAccount != "" || config.SMTPToken != "" | ||||||
|  | } | ||||||
|  |  | ||||||
| func SendEmail(subject string, receiver string, content string) error { | func SendEmail(subject string, receiver string, content string) error { | ||||||
| 	if receiver == "" { | 	if receiver == "" { | ||||||
| 		return fmt.Errorf("receiver is empty") | 		return fmt.Errorf("receiver is empty") | ||||||
| @@ -41,16 +46,24 @@ func SendEmail(subject string, receiver string, content string) error { | |||||||
| 		"Date: %s\r\n"+ | 		"Date: %s\r\n"+ | ||||||
| 		"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n", | 		"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n", | ||||||
| 		receiver, config.SystemName, config.SMTPFrom, encodedSubject, messageId, time.Now().Format(time.RFC1123Z), content)) | 		receiver, config.SystemName, config.SMTPFrom, encodedSubject, messageId, time.Now().Format(time.RFC1123Z), content)) | ||||||
|  |  | ||||||
| 	auth := smtp.PlainAuth("", config.SMTPAccount, config.SMTPToken, config.SMTPServer) | 	auth := smtp.PlainAuth("", config.SMTPAccount, config.SMTPToken, config.SMTPServer) | ||||||
| 	addr := fmt.Sprintf("%s:%d", config.SMTPServer, config.SMTPPort) | 	addr := fmt.Sprintf("%s:%d", config.SMTPServer, config.SMTPPort) | ||||||
| 	to := strings.Split(receiver, ";") | 	to := strings.Split(receiver, ";") | ||||||
|  |  | ||||||
|  | 	if config.SMTPPort == 465 || !shouldAuth() { | ||||||
|  | 		// need advanced client | ||||||
|  | 		var conn net.Conn | ||||||
|  | 		var err error | ||||||
| 		if config.SMTPPort == 465 { | 		if config.SMTPPort == 465 { | ||||||
| 			tlsConfig := &tls.Config{ | 			tlsConfig := &tls.Config{ | ||||||
| 				InsecureSkipVerify: true, | 				InsecureSkipVerify: true, | ||||||
| 				ServerName:         config.SMTPServer, | 				ServerName:         config.SMTPServer, | ||||||
| 			} | 			} | ||||||
| 		conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", config.SMTPServer, config.SMTPPort), tlsConfig) | 			conn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", config.SMTPServer, config.SMTPPort), tlsConfig) | ||||||
|  | 		} else { | ||||||
|  | 			conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", config.SMTPServer, config.SMTPPort)) | ||||||
|  | 		} | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @@ -59,9 +72,11 @@ func SendEmail(subject string, receiver string, content string) error { | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		defer client.Close() | 		defer client.Close() | ||||||
|  | 		if shouldAuth() { | ||||||
| 			if err = client.Auth(auth); err != nil { | 			if err = client.Auth(auth); err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
| 		if err = client.Mail(config.SMTPFrom); err != nil { | 		if err = client.Mail(config.SMTPFrom); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								common/render/render.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								common/render/render.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | package render | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func StringData(c *gin.Context, str string) { | ||||||
|  | 	str = strings.TrimPrefix(str, "data: ") | ||||||
|  | 	str = strings.TrimSuffix(str, "\r") | ||||||
|  | 	c.Render(-1, common.CustomEvent{Data: "data: " + str}) | ||||||
|  | 	c.Writer.Flush() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ObjectData(c *gin.Context, object interface{}) error { | ||||||
|  | 	jsonData, err := json.Marshal(object) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error marshalling object: %w", err) | ||||||
|  | 	} | ||||||
|  | 	StringData(c, string(jsonData)) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Done(c *gin.Context) { | ||||||
|  | 	StringData(c, "[DONE]") | ||||||
|  | } | ||||||
| @@ -6,6 +6,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/controller" | 	"github.com/songquanpeng/one-api/controller" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -136,7 +137,7 @@ func WeChatBind(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt(ctxkey.Id) | ||||||
| 	user := model.User{ | 	user := model.User{ | ||||||
| 		Id: id, | 		Id: id, | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	relaymodel "github.com/songquanpeng/one-api/relay/model" | 	relaymodel "github.com/songquanpeng/one-api/relay/model" | ||||||
| ) | ) | ||||||
| @@ -14,13 +15,13 @@ func GetSubscription(c *gin.Context) { | |||||||
| 	var token *model.Token | 	var token *model.Token | ||||||
| 	var expiredTime int64 | 	var expiredTime int64 | ||||||
| 	if config.DisplayTokenStatEnabled { | 	if config.DisplayTokenStatEnabled { | ||||||
| 		tokenId := c.GetInt("token_id") | 		tokenId := c.GetInt(ctxkey.TokenId) | ||||||
| 		token, err = model.GetTokenById(tokenId) | 		token, err = model.GetTokenById(tokenId) | ||||||
| 		expiredTime = token.ExpiredTime | 		expiredTime = token.ExpiredTime | ||||||
| 		remainQuota = token.RemainQuota | 		remainQuota = token.RemainQuota | ||||||
| 		usedQuota = token.UsedQuota | 		usedQuota = token.UsedQuota | ||||||
| 	} else { | 	} else { | ||||||
| 		userId := c.GetInt("id") | 		userId := c.GetInt(ctxkey.Id) | ||||||
| 		remainQuota, err = model.GetUserQuota(userId) | 		remainQuota, err = model.GetUserQuota(userId) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			usedQuota, err = model.GetUserUsedQuota(userId) | 			usedQuota, err = model.GetUserUsedQuota(userId) | ||||||
| @@ -64,11 +65,11 @@ func GetUsage(c *gin.Context) { | |||||||
| 	var err error | 	var err error | ||||||
| 	var token *model.Token | 	var token *model.Token | ||||||
| 	if config.DisplayTokenStatEnabled { | 	if config.DisplayTokenStatEnabled { | ||||||
| 		tokenId := c.GetInt("token_id") | 		tokenId := c.GetInt(ctxkey.TokenId) | ||||||
| 		token, err = model.GetTokenById(tokenId) | 		token, err = model.GetTokenById(tokenId) | ||||||
| 		quota = token.UsedQuota | 		quota = token.UsedQuota | ||||||
| 	} else { | 	} else { | ||||||
| 		userId := c.GetInt("id") | 		userId := c.GetInt(ctxkey.Id) | ||||||
| 		quota, err = model.GetUserUsedQuota(userId) | 		quota, err = model.GetUserUsedQuota(userId) | ||||||
| 	} | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -4,12 +4,12 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/client" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"github.com/songquanpeng/one-api/monitor" | 	"github.com/songquanpeng/one-api/monitor" | ||||||
| 	"github.com/songquanpeng/one-api/relay/channeltype" | 	"github.com/songquanpeng/one-api/relay/channeltype" | ||||||
| 	"github.com/songquanpeng/one-api/relay/client" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|   | |||||||
| @@ -5,7 +5,18 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/common/message" | 	"github.com/songquanpeng/one-api/common/message" | ||||||
| 	"github.com/songquanpeng/one-api/middleware" | 	"github.com/songquanpeng/one-api/middleware" | ||||||
| @@ -17,23 +28,15 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/relay/meta" | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
| 	relaymodel "github.com/songquanpeng/one-api/relay/model" | 	relaymodel "github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"github.com/songquanpeng/one-api/relay/relaymode" | 	"github.com/songquanpeng/one-api/relay/relaymode" | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"net/url" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 	"sync" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func buildTestRequest() *relaymodel.GeneralOpenAIRequest { | func buildTestRequest(model string) *relaymodel.GeneralOpenAIRequest { | ||||||
|  | 	if model == "" { | ||||||
|  | 		model = "gpt-3.5-turbo" | ||||||
|  | 	} | ||||||
| 	testRequest := &relaymodel.GeneralOpenAIRequest{ | 	testRequest := &relaymodel.GeneralOpenAIRequest{ | ||||||
| 		MaxTokens: 2, | 		MaxTokens: 2, | ||||||
| 		Stream:    false, | 		Model:     model, | ||||||
| 		Model:     "gpt-3.5-turbo", |  | ||||||
| 	} | 	} | ||||||
| 	testMessage := relaymodel.Message{ | 	testMessage := relaymodel.Message{ | ||||||
| 		Role:    "user", | 		Role:    "user", | ||||||
| @@ -43,7 +46,7 @@ func buildTestRequest() *relaymodel.GeneralOpenAIRequest { | |||||||
| 	return testRequest | 	return testRequest | ||||||
| } | } | ||||||
|  |  | ||||||
| func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error) { | func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIRequest) (err error, openaiErr *relaymodel.Error) { | ||||||
| 	w := httptest.NewRecorder() | 	w := httptest.NewRecorder() | ||||||
| 	c, _ := gin.CreateTestContext(w) | 	c, _ := gin.CreateTestContext(w) | ||||||
| 	c.Request = &http.Request{ | 	c.Request = &http.Request{ | ||||||
| @@ -54,8 +57,10 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error | |||||||
| 	} | 	} | ||||||
| 	c.Request.Header.Set("Authorization", "Bearer "+channel.Key) | 	c.Request.Header.Set("Authorization", "Bearer "+channel.Key) | ||||||
| 	c.Request.Header.Set("Content-Type", "application/json") | 	c.Request.Header.Set("Content-Type", "application/json") | ||||||
| 	c.Set("channel", channel.Type) | 	c.Set(ctxkey.Channel, channel.Type) | ||||||
| 	c.Set("base_url", channel.GetBaseURL()) | 	c.Set(ctxkey.BaseURL, channel.GetBaseURL()) | ||||||
|  | 	cfg, _ := channel.LoadConfig() | ||||||
|  | 	c.Set(ctxkey.Config, cfg) | ||||||
| 	middleware.SetupContextForSelectedChannel(c, channel, "") | 	middleware.SetupContextForSelectedChannel(c, channel, "") | ||||||
| 	meta := meta.GetByContext(c) | 	meta := meta.GetByContext(c) | ||||||
| 	apiType := channeltype.ToAPIType(channel.Type) | 	apiType := channeltype.ToAPIType(channel.Type) | ||||||
| @@ -64,16 +69,19 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error | |||||||
| 		return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil | 		return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil | ||||||
| 	} | 	} | ||||||
| 	adaptor.Init(meta) | 	adaptor.Init(meta) | ||||||
| 	modelName := adaptor.GetModelList()[0] | 	modelName := request.Model | ||||||
| 	if !strings.Contains(channel.Models, modelName) { | 	modelMap := channel.GetModelMapping() | ||||||
|  | 	if modelName == "" || !strings.Contains(channel.Models, modelName) { | ||||||
| 		modelNames := strings.Split(channel.Models, ",") | 		modelNames := strings.Split(channel.Models, ",") | ||||||
| 		if len(modelNames) > 0 { | 		if len(modelNames) > 0 { | ||||||
| 			modelName = modelNames[0] | 			modelName = modelNames[0] | ||||||
| 		} | 		} | ||||||
|  | 		if modelMap != nil && modelMap[modelName] != "" { | ||||||
|  | 			modelName = modelMap[modelName] | ||||||
| 		} | 		} | ||||||
| 	request := buildTestRequest() | 	} | ||||||
|  | 	meta.OriginModelName, meta.ActualModelName = request.Model, modelName | ||||||
| 	request.Model = modelName | 	request.Model = modelName | ||||||
| 	meta.OriginModelName, meta.ActualModelName = modelName, modelName |  | ||||||
| 	convertedRequest, err := adaptor.ConvertRequest(c, relaymode.ChatCompletions, request) | 	convertedRequest, err := adaptor.ConvertRequest(c, relaymode.ChatCompletions, request) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err, nil | 		return err, nil | ||||||
| @@ -82,13 +90,14 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err, nil | 		return err, nil | ||||||
| 	} | 	} | ||||||
|  | 	logger.SysLog(string(jsonData)) | ||||||
| 	requestBody := bytes.NewBuffer(jsonData) | 	requestBody := bytes.NewBuffer(jsonData) | ||||||
| 	c.Request.Body = io.NopCloser(requestBody) | 	c.Request.Body = io.NopCloser(requestBody) | ||||||
| 	resp, err := adaptor.DoRequest(c, meta, requestBody) | 	resp, err := adaptor.DoRequest(c, meta, requestBody) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err, nil | 		return err, nil | ||||||
| 	} | 	} | ||||||
| 	if resp.StatusCode != http.StatusOK { | 	if resp != nil && resp.StatusCode != http.StatusOK { | ||||||
| 		err := controller.RelayErrorHandler(resp) | 		err := controller.RelayErrorHandler(resp) | ||||||
| 		return fmt.Errorf("status code %d: %s", resp.StatusCode, err.Error.Message), &err.Error | 		return fmt.Errorf("status code %d: %s", resp.StatusCode, err.Error.Message), &err.Error | ||||||
| 	} | 	} | ||||||
| @@ -126,10 +135,15 @@ func TestChannel(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 	model := c.Query("model") | ||||||
|  | 	testRequest := buildTestRequest(model) | ||||||
| 	tik := time.Now() | 	tik := time.Now() | ||||||
| 	err, _ = testChannel(channel) | 	err, _ = testChannel(channel, testRequest) | ||||||
| 	tok := time.Now() | 	tok := time.Now() | ||||||
| 	milliseconds := tok.Sub(tik).Milliseconds() | 	milliseconds := tok.Sub(tik).Milliseconds() | ||||||
|  | 	if err != nil { | ||||||
|  | 		milliseconds = 0 | ||||||
|  | 	} | ||||||
| 	go channel.UpdateResponseTime(milliseconds) | 	go channel.UpdateResponseTime(milliseconds) | ||||||
| 	consumedTime := float64(milliseconds) / 1000.0 | 	consumedTime := float64(milliseconds) / 1000.0 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -137,6 +151,7 @@ func TestChannel(c *gin.Context) { | |||||||
| 			"success": false, | 			"success": false, | ||||||
| 			"message": err.Error(), | 			"message": err.Error(), | ||||||
| 			"time":    consumedTime, | 			"time":    consumedTime, | ||||||
|  | 			"model":   model, | ||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -144,6 +159,7 @@ func TestChannel(c *gin.Context) { | |||||||
| 		"success": true, | 		"success": true, | ||||||
| 		"message": "", | 		"message": "", | ||||||
| 		"time":    consumedTime, | 		"time":    consumedTime, | ||||||
|  | 		"model":   model, | ||||||
| 	}) | 	}) | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
| @@ -174,11 +190,12 @@ func testChannels(notify bool, scope string) error { | |||||||
| 		for _, channel := range channels { | 		for _, channel := range channels { | ||||||
| 			isChannelEnabled := channel.Status == model.ChannelStatusEnabled | 			isChannelEnabled := channel.Status == model.ChannelStatusEnabled | ||||||
| 			tik := time.Now() | 			tik := time.Now() | ||||||
| 			err, openaiErr := testChannel(channel) | 			testRequest := buildTestRequest("") | ||||||
|  | 			err, openaiErr := testChannel(channel, testRequest) | ||||||
| 			tok := time.Now() | 			tok := time.Now() | ||||||
| 			milliseconds := tok.Sub(tik).Milliseconds() | 			milliseconds := tok.Sub(tik).Milliseconds() | ||||||
| 			if isChannelEnabled && milliseconds > disableThreshold { | 			if isChannelEnabled && milliseconds > disableThreshold { | ||||||
| 				err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)) | 				err = fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0) | ||||||
| 				if config.AutomaticDisableChannelEnabled { | 				if config.AutomaticDisableChannelEnabled { | ||||||
| 					monitor.DisableChannel(channel.Id, channel.Name, err.Error()) | 					monitor.DisableChannel(channel.Id, channel.Name, err.Error()) | ||||||
| 				} else { | 				} else { | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -41,7 +42,7 @@ func GetUserLogs(c *gin.Context) { | |||||||
| 	if p < 0 { | 	if p < 0 { | ||||||
| 		p = 0 | 		p = 0 | ||||||
| 	} | 	} | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	logType, _ := strconv.Atoi(c.Query("type")) | 	logType, _ := strconv.Atoi(c.Query("type")) | ||||||
| 	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) | 	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) | ||||||
| 	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) | 	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) | ||||||
| @@ -83,7 +84,7 @@ func SearchAllLogs(c *gin.Context) { | |||||||
|  |  | ||||||
| func SearchUserLogs(c *gin.Context) { | func SearchUserLogs(c *gin.Context) { | ||||||
| 	keyword := c.Query("keyword") | 	keyword := c.Query("keyword") | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	logs, err := model.SearchUserLogs(userId, keyword) | 	logs, err := model.SearchUserLogs(userId, keyword) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| @@ -122,7 +123,7 @@ func GetLogsStat(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetLogsSelfStat(c *gin.Context) { | func GetLogsSelfStat(c *gin.Context) { | ||||||
| 	username := c.GetString("username") | 	username := c.GetString(ctxkey.Username) | ||||||
| 	logType, _ := strconv.Atoi(c.Query("type")) | 	logType, _ := strconv.Atoi(c.Query("type")) | ||||||
| 	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) | 	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) | ||||||
| 	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) | 	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	relay "github.com/songquanpeng/one-api/relay" | 	relay "github.com/songquanpeng/one-api/relay" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
| @@ -131,10 +132,10 @@ func ListAllModels(c *gin.Context) { | |||||||
| func ListModels(c *gin.Context) { | func ListModels(c *gin.Context) { | ||||||
| 	ctx := c.Request.Context() | 	ctx := c.Request.Context() | ||||||
| 	var availableModels []string | 	var availableModels []string | ||||||
| 	if c.GetString("available_models") != "" { | 	if c.GetString(ctxkey.AvailableModels) != "" { | ||||||
| 		availableModels = strings.Split(c.GetString("available_models"), ",") | 		availableModels = strings.Split(c.GetString(ctxkey.AvailableModels), ",") | ||||||
| 	} else { | 	} else { | ||||||
| 		userId := c.GetInt("id") | 		userId := c.GetInt(ctxkey.Id) | ||||||
| 		userGroup, _ := model.CacheGetUserGroup(userId) | 		userGroup, _ := model.CacheGetUserGroup(userId) | ||||||
| 		availableModels, _ = model.CacheGetGroupModels(ctx, userGroup) | 		availableModels, _ = model.CacheGetGroupModels(ctx, userGroup) | ||||||
| 	} | 	} | ||||||
| @@ -186,7 +187,7 @@ func RetrieveModel(c *gin.Context) { | |||||||
|  |  | ||||||
| func GetUserAvailableModels(c *gin.Context) { | func GetUserAvailableModels(c *gin.Context) { | ||||||
| 	ctx := c.Request.Context() | 	ctx := c.Request.Context() | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt(ctxkey.Id) | ||||||
| 	userGroup, err := model.CacheGetUserGroup(id) | 	userGroup, err := model.CacheGetUserGroup(id) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/random" | 	"github.com/songquanpeng/one-api/common/random" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| @@ -109,7 +110,7 @@ func AddRedemption(c *gin.Context) { | |||||||
| 	for i := 0; i < redemption.Count; i++ { | 	for i := 0; i < redemption.Count; i++ { | ||||||
| 		key := random.GetUUID() | 		key := random.GetUUID() | ||||||
| 		cleanRedemption := model.Redemption{ | 		cleanRedemption := model.Redemption{ | ||||||
| 			UserId:      c.GetInt("id"), | 			UserId:      c.GetInt(ctxkey.Id), | ||||||
| 			Name:        redemption.Name, | 			Name:        redemption.Name, | ||||||
| 			Key:         key, | 			Key:         key, | ||||||
| 			CreatedTime: helper.GetTimestamp(), | 			CreatedTime: helper.GetTimestamp(), | ||||||
|   | |||||||
| @@ -4,9 +4,13 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/middleware" | 	"github.com/songquanpeng/one-api/middleware" | ||||||
| @@ -15,8 +19,6 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/relay/controller" | 	"github.com/songquanpeng/one-api/relay/controller" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"github.com/songquanpeng/one-api/relay/relaymode" | 	"github.com/songquanpeng/one-api/relay/relaymode" | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // https://platform.openai.com/docs/api-reference/chat | // https://platform.openai.com/docs/api-reference/chat | ||||||
| @@ -32,6 +34,8 @@ func relayHelper(c *gin.Context, relayMode int) *model.ErrorWithStatusCode { | |||||||
| 		fallthrough | 		fallthrough | ||||||
| 	case relaymode.AudioTranscription: | 	case relaymode.AudioTranscription: | ||||||
| 		err = controller.RelayAudioHelper(c, relayMode) | 		err = controller.RelayAudioHelper(c, relayMode) | ||||||
|  | 	case relaymode.Proxy: | ||||||
|  | 		err = controller.RelayProxyHelper(c, relayMode) | ||||||
| 	default: | 	default: | ||||||
| 		err = controller.RelayTextHelper(c) | 		err = controller.RelayTextHelper(c) | ||||||
| 	} | 	} | ||||||
| @@ -45,18 +49,19 @@ func Relay(c *gin.Context) { | |||||||
| 		requestBody, _ := common.GetRequestBody(c) | 		requestBody, _ := common.GetRequestBody(c) | ||||||
| 		logger.Debugf(ctx, "request body: %s", string(requestBody)) | 		logger.Debugf(ctx, "request body: %s", string(requestBody)) | ||||||
| 	} | 	} | ||||||
| 	channelId := c.GetInt("channel_id") | 	channelId := c.GetInt(ctxkey.ChannelId) | ||||||
|  | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	bizErr := relayHelper(c, relayMode) | 	bizErr := relayHelper(c, relayMode) | ||||||
| 	if bizErr == nil { | 	if bizErr == nil { | ||||||
| 		monitor.Emit(channelId, true) | 		monitor.Emit(channelId, true) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	lastFailedChannelId := channelId | 	lastFailedChannelId := channelId | ||||||
| 	channelName := c.GetString("channel_name") | 	channelName := c.GetString(ctxkey.ChannelName) | ||||||
| 	group := c.GetString("group") | 	group := c.GetString(ctxkey.Group) | ||||||
| 	originalModel := c.GetString("original_model") | 	originalModel := c.GetString(ctxkey.OriginalModel) | ||||||
| 	go processChannelRelayError(ctx, channelId, channelName, bizErr) | 	go processChannelRelayError(ctx, userId, channelId, channelName, bizErr) | ||||||
| 	requestId := c.GetString(logger.RequestIdKey) | 	requestId := c.GetString(helper.RequestIdKey) | ||||||
| 	retryTimes := config.RetryTimes | 	retryTimes := config.RetryTimes | ||||||
| 	if !shouldRetry(c, bizErr.StatusCode) { | 	if !shouldRetry(c, bizErr.StatusCode) { | ||||||
| 		logger.Errorf(ctx, "relay error happen, status code is %d, won't retry in this case", bizErr.StatusCode) | 		logger.Errorf(ctx, "relay error happen, status code is %d, won't retry in this case", bizErr.StatusCode) | ||||||
| @@ -65,7 +70,7 @@ func Relay(c *gin.Context) { | |||||||
| 	for i := retryTimes; i > 0; i-- { | 	for i := retryTimes; i > 0; i-- { | ||||||
| 		channel, err := dbmodel.CacheGetRandomSatisfiedChannel(group, originalModel, i != retryTimes) | 		channel, err := dbmodel.CacheGetRandomSatisfiedChannel(group, originalModel, i != retryTimes) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logger.Errorf(ctx, "CacheGetRandomSatisfiedChannel failed: %w", err) | 			logger.Errorf(ctx, "CacheGetRandomSatisfiedChannel failed: %+v", err) | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 		logger.Infof(ctx, "using channel #%d to retry (remain times %d)", channel.Id, i) | 		logger.Infof(ctx, "using channel #%d to retry (remain times %d)", channel.Id, i) | ||||||
| @@ -79,15 +84,18 @@ func Relay(c *gin.Context) { | |||||||
| 		if bizErr == nil { | 		if bizErr == nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		channelId := c.GetInt("channel_id") | 		channelId := c.GetInt(ctxkey.ChannelId) | ||||||
| 		lastFailedChannelId = channelId | 		lastFailedChannelId = channelId | ||||||
| 		channelName := c.GetString("channel_name") | 		channelName := c.GetString(ctxkey.ChannelName) | ||||||
| 		go processChannelRelayError(ctx, channelId, channelName, bizErr) | 		// BUG: bizErr is in race condition | ||||||
|  | 		go processChannelRelayError(ctx, userId, channelId, channelName, bizErr) | ||||||
| 	} | 	} | ||||||
| 	if bizErr != nil { | 	if bizErr != nil { | ||||||
| 		if bizErr.StatusCode == http.StatusTooManyRequests { | 		if bizErr.StatusCode == http.StatusTooManyRequests { | ||||||
| 			bizErr.Error.Message = "当前分组上游负载已饱和,请稍后再试" | 			bizErr.Error.Message = "当前分组上游负载已饱和,请稍后再试" | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// BUG: bizErr is in race condition | ||||||
| 		bizErr.Error.Message = helper.MessageWithRequestId(bizErr.Error.Message, requestId) | 		bizErr.Error.Message = helper.MessageWithRequestId(bizErr.Error.Message, requestId) | ||||||
| 		c.JSON(bizErr.StatusCode, gin.H{ | 		c.JSON(bizErr.StatusCode, gin.H{ | ||||||
| 			"error": bizErr.Error, | 			"error": bizErr.Error, | ||||||
| @@ -96,7 +104,7 @@ func Relay(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func shouldRetry(c *gin.Context, statusCode int) bool { | func shouldRetry(c *gin.Context, statusCode int) bool { | ||||||
| 	if _, ok := c.Get("specific_channel_id"); ok { | 	if _, ok := c.Get(ctxkey.SpecificChannelId); ok { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	if statusCode == http.StatusTooManyRequests { | 	if statusCode == http.StatusTooManyRequests { | ||||||
| @@ -114,8 +122,8 @@ func shouldRetry(c *gin.Context, statusCode int) bool { | |||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  |  | ||||||
| func processChannelRelayError(ctx context.Context, channelId int, channelName string, err *model.ErrorWithStatusCode) { | func processChannelRelayError(ctx context.Context, userId int, channelId int, channelName string, err *model.ErrorWithStatusCode) { | ||||||
| 	logger.Errorf(ctx, "relay error (channel #%d): %s", channelId, err.Message) | 	logger.Errorf(ctx, "relay error (channel id %d, user id: %d): %s", channelId, userId, err.Message) | ||||||
| 	// https://platform.openai.com/docs/guides/error-codes/api-errors | 	// https://platform.openai.com/docs/guides/error-codes/api-errors | ||||||
| 	if monitor.ShouldDisableChannel(&err.Error, err.StatusCode) { | 	if monitor.ShouldDisableChannel(&err.Error, err.StatusCode) { | ||||||
| 		monitor.DisableChannel(channelId, channelName, err.Message) | 		monitor.DisableChannel(channelId, channelName, err.Message) | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/network" | 	"github.com/songquanpeng/one-api/common/network" | ||||||
| 	"github.com/songquanpeng/one-api/common/random" | 	"github.com/songquanpeng/one-api/common/random" | ||||||
| @@ -13,7 +14,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetAllTokens(c *gin.Context) { | func GetAllTokens(c *gin.Context) { | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	p, _ := strconv.Atoi(c.Query("p")) | 	p, _ := strconv.Atoi(c.Query("p")) | ||||||
| 	if p < 0 { | 	if p < 0 { | ||||||
| 		p = 0 | 		p = 0 | ||||||
| @@ -38,7 +39,7 @@ func GetAllTokens(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func SearchTokens(c *gin.Context) { | func SearchTokens(c *gin.Context) { | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	keyword := c.Query("keyword") | 	keyword := c.Query("keyword") | ||||||
| 	tokens, err := model.SearchUserTokens(userId, keyword) | 	tokens, err := model.SearchUserTokens(userId, keyword) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -58,7 +59,7 @@ func SearchTokens(c *gin.Context) { | |||||||
|  |  | ||||||
| func GetToken(c *gin.Context) { | func GetToken(c *gin.Context) { | ||||||
| 	id, err := strconv.Atoi(c.Param("id")) | 	id, err := strconv.Atoi(c.Param("id")) | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
| @@ -83,8 +84,8 @@ func GetToken(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetTokenStatus(c *gin.Context) { | func GetTokenStatus(c *gin.Context) { | ||||||
| 	tokenId := c.GetInt("token_id") | 	tokenId := c.GetInt(ctxkey.TokenId) | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	token, err := model.GetTokenByIds(tokenId, userId) | 	token, err := model.GetTokenByIds(tokenId, userId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| @@ -139,7 +140,7 @@ func AddToken(c *gin.Context) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cleanToken := model.Token{ | 	cleanToken := model.Token{ | ||||||
| 		UserId:         c.GetInt("id"), | 		UserId:         c.GetInt(ctxkey.Id), | ||||||
| 		Name:           token.Name, | 		Name:           token.Name, | ||||||
| 		Key:            random.GenerateKey(), | 		Key:            random.GenerateKey(), | ||||||
| 		CreatedTime:    helper.GetTimestamp(), | 		CreatedTime:    helper.GetTimestamp(), | ||||||
| @@ -168,7 +169,7 @@ func AddToken(c *gin.Context) { | |||||||
|  |  | ||||||
| func DeleteToken(c *gin.Context) { | func DeleteToken(c *gin.Context) { | ||||||
| 	id, _ := strconv.Atoi(c.Param("id")) | 	id, _ := strconv.Atoi(c.Param("id")) | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	err := model.DeleteTokenById(id, userId) | 	err := model.DeleteTokenById(id, userId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| @@ -185,7 +186,7 @@ func DeleteToken(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func UpdateToken(c *gin.Context) { | func UpdateToken(c *gin.Context) { | ||||||
| 	userId := c.GetInt("id") | 	userId := c.GetInt(ctxkey.Id) | ||||||
| 	statusOnly := c.Query("status_only") | 	statusOnly := c.Query("status_only") | ||||||
| 	token := model.Token{} | 	token := model.Token{} | ||||||
| 	err := c.ShouldBindJSON(&token) | 	err := c.ShouldBindJSON(&token) | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/common/random" | 	"github.com/songquanpeng/one-api/common/random" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -172,6 +173,7 @@ func Register(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.JSON(http.StatusOK, gin.H{ | 	c.JSON(http.StatusOK, gin.H{ | ||||||
| 		"success": true, | 		"success": true, | ||||||
| 		"message": "", | 		"message": "", | ||||||
| @@ -238,7 +240,7 @@ func GetUser(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	myRole := c.GetInt("role") | 	myRole := c.GetInt(ctxkey.Role) | ||||||
| 	if myRole <= user.Role && myRole != model.RoleRootUser { | 	if myRole <= user.Role && myRole != model.RoleRootUser { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
| @@ -255,7 +257,7 @@ func GetUser(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetUserDashboard(c *gin.Context) { | func GetUserDashboard(c *gin.Context) { | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt(ctxkey.Id) | ||||||
| 	now := time.Now() | 	now := time.Now() | ||||||
| 	startOfDay := now.Truncate(24*time.Hour).AddDate(0, 0, -6).Unix() | 	startOfDay := now.Truncate(24*time.Hour).AddDate(0, 0, -6).Unix() | ||||||
| 	endOfDay := now.Truncate(24 * time.Hour).Add(24*time.Hour - time.Second).Unix() | 	endOfDay := now.Truncate(24 * time.Hour).Add(24*time.Hour - time.Second).Unix() | ||||||
| @@ -278,7 +280,7 @@ func GetUserDashboard(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GenerateAccessToken(c *gin.Context) { | func GenerateAccessToken(c *gin.Context) { | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt(ctxkey.Id) | ||||||
| 	user, err := model.GetUserById(id, true) | 	user, err := model.GetUserById(id, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| @@ -314,7 +316,7 @@ func GenerateAccessToken(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetAffCode(c *gin.Context) { | func GetAffCode(c *gin.Context) { | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt(ctxkey.Id) | ||||||
| 	user, err := model.GetUserById(id, true) | 	user, err := model.GetUserById(id, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| @@ -342,7 +344,7 @@ func GetAffCode(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetSelf(c *gin.Context) { | func GetSelf(c *gin.Context) { | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt(ctxkey.Id) | ||||||
| 	user, err := model.GetUserById(id, false) | 	user, err := model.GetUserById(id, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| @@ -387,7 +389,7 @@ func UpdateUser(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	myRole := c.GetInt("role") | 	myRole := c.GetInt(ctxkey.Role) | ||||||
| 	if myRole <= originUser.Role && myRole != model.RoleRootUser { | 	if myRole <= originUser.Role && myRole != model.RoleRootUser { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
| @@ -445,7 +447,7 @@ func UpdateSelf(c *gin.Context) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cleanUser := model.User{ | 	cleanUser := model.User{ | ||||||
| 		Id:          c.GetInt("id"), | 		Id:          c.GetInt(ctxkey.Id), | ||||||
| 		Username:    user.Username, | 		Username:    user.Username, | ||||||
| 		Password:    user.Password, | 		Password:    user.Password, | ||||||
| 		DisplayName: user.DisplayName, | 		DisplayName: user.DisplayName, | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,71 +1,111 @@ | |||||||
| module github.com/songquanpeng/one-api | module github.com/songquanpeng/one-api | ||||||
|  |  | ||||||
| // +heroku goVersion go1.18 | // +heroku goVersion go1.18 | ||||||
| go 1.18 | go 1.20 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/gin-contrib/cors v1.4.0 | 	cloud.google.com/go/iam v1.1.10 | ||||||
| 	github.com/gin-contrib/gzip v0.0.6 | 	github.com/aws/aws-sdk-go-v2 v1.27.0 | ||||||
| 	github.com/gin-contrib/sessions v0.0.5 | 	github.com/aws/aws-sdk-go-v2/credentials v1.17.15 | ||||||
| 	github.com/gin-contrib/static v0.0.1 | 	github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.3 | ||||||
| 	github.com/gin-gonic/gin v1.9.1 | 	github.com/gin-contrib/cors v1.7.2 | ||||||
| 	github.com/go-playground/validator/v10 v10.14.0 | 	github.com/gin-contrib/gzip v1.0.1 | ||||||
|  | 	github.com/gin-contrib/sessions v1.0.1 | ||||||
|  | 	github.com/gin-contrib/static v1.1.2 | ||||||
|  | 	github.com/gin-gonic/gin v1.10.0 | ||||||
|  | 	github.com/go-playground/validator/v10 v10.20.0 | ||||||
| 	github.com/go-redis/redis/v8 v8.11.5 | 	github.com/go-redis/redis/v8 v8.11.5 | ||||||
| 	github.com/golang-jwt/jwt v3.2.2+incompatible | 	github.com/golang-jwt/jwt v3.2.2+incompatible | ||||||
| 	github.com/google/uuid v1.3.0 | 	github.com/google/uuid v1.6.0 | ||||||
| 	github.com/gorilla/websocket v1.5.0 | 	github.com/gorilla/websocket v1.5.1 | ||||||
| 	github.com/pkoukk/tiktoken-go v0.1.5 | 	github.com/jinzhu/copier v0.4.0 | ||||||
|  | 	github.com/joho/godotenv v1.5.1 | ||||||
|  | 	github.com/patrickmn/go-cache v2.1.0+incompatible | ||||||
|  | 	github.com/pkg/errors v0.9.1 | ||||||
|  | 	github.com/pkoukk/tiktoken-go v0.1.7 | ||||||
| 	github.com/smartystreets/goconvey v1.8.1 | 	github.com/smartystreets/goconvey v1.8.1 | ||||||
| 	github.com/stretchr/testify v1.8.3 | 	github.com/stretchr/testify v1.9.0 | ||||||
| 	golang.org/x/crypto v0.17.0 | 	golang.org/x/crypto v0.24.0 | ||||||
| 	golang.org/x/image v0.14.0 | 	golang.org/x/image v0.18.0 | ||||||
| 	gorm.io/driver/mysql v1.4.3 | 	google.golang.org/api v0.187.0 | ||||||
| 	gorm.io/driver/postgres v1.5.2 | 	gorm.io/driver/mysql v1.5.6 | ||||||
| 	gorm.io/driver/sqlite v1.4.3 | 	gorm.io/driver/postgres v1.5.7 | ||||||
| 	gorm.io/gorm v1.25.0 | 	gorm.io/driver/sqlite v1.5.5 | ||||||
|  | 	gorm.io/gorm v1.25.10 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/bytedance/sonic v1.9.1 // indirect | 	cloud.google.com/go/auth v0.6.1 // indirect | ||||||
| 	github.com/cespare/xxhash/v2 v2.1.2 // indirect | 	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect | ||||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect | 	cloud.google.com/go/compute/metadata v0.3.0 // indirect | ||||||
|  | 	filippo.io/edwards25519 v1.1.0 // indirect | ||||||
|  | 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect | ||||||
|  | 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 // indirect | ||||||
|  | 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 // indirect | ||||||
|  | 	github.com/aws/smithy-go v1.20.2 // indirect | ||||||
|  | 	github.com/bytedance/sonic v1.11.6 // indirect | ||||||
|  | 	github.com/bytedance/sonic/loader v0.1.1 // indirect | ||||||
|  | 	github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||||||
|  | 	github.com/cloudwego/base64x v0.1.4 // indirect | ||||||
|  | 	github.com/cloudwego/iasm v0.2.0 // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||||
| 	github.com/dlclark/regexp2 v1.10.0 // indirect | 	github.com/dlclark/regexp2 v1.11.0 // indirect | ||||||
| 	github.com/gabriel-vasile/mimetype v1.4.2 // indirect | 	github.com/felixge/httpsnoop v1.0.4 // indirect | ||||||
|  | 	github.com/fsnotify/fsnotify v1.7.0 // indirect | ||||||
|  | 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect | ||||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||||
|  | 	github.com/go-logr/logr v1.4.1 // indirect | ||||||
|  | 	github.com/go-logr/stdr v1.2.2 // indirect | ||||||
| 	github.com/go-playground/locales v0.14.1 // indirect | 	github.com/go-playground/locales v0.14.1 // indirect | ||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||||
| 	github.com/go-sql-driver/mysql v1.6.0 // indirect | 	github.com/go-sql-driver/mysql v1.8.1 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.2 // indirect | 	github.com/goccy/go-json v0.10.3 // indirect | ||||||
|  | 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | ||||||
|  | 	github.com/golang/protobuf v1.5.4 // indirect | ||||||
|  | 	github.com/google/s2a-go v0.1.7 // indirect | ||||||
|  | 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect | ||||||
|  | 	github.com/googleapis/gax-go/v2 v2.12.5 // indirect | ||||||
| 	github.com/gopherjs/gopherjs v1.17.2 // indirect | 	github.com/gopherjs/gopherjs v1.17.2 // indirect | ||||||
| 	github.com/gorilla/context v1.1.1 // indirect | 	github.com/gorilla/context v1.1.2 // indirect | ||||||
| 	github.com/gorilla/securecookie v1.1.1 // indirect | 	github.com/gorilla/securecookie v1.1.2 // indirect | ||||||
| 	github.com/gorilla/sessions v1.2.1 // indirect | 	github.com/gorilla/sessions v1.2.2 // indirect | ||||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||||
| 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect | 	github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect | ||||||
| 	github.com/jackc/pgx/v5 v5.5.4 // indirect | 	github.com/jackc/pgx/v5 v5.5.5 // indirect | ||||||
| 	github.com/jackc/puddle/v2 v2.2.1 // indirect | 	github.com/jackc/puddle/v2 v2.2.1 // indirect | ||||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||||
| 	github.com/jinzhu/now v1.1.5 // indirect | 	github.com/jinzhu/now v1.1.5 // indirect | ||||||
| 	github.com/json-iterator/go v1.1.12 // indirect | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
| 	github.com/jtolds/gls v4.20.0+incompatible // indirect | 	github.com/jtolds/gls v4.20.0+incompatible // indirect | ||||||
| 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect | 	github.com/klauspost/cpuid/v2 v2.2.7 // indirect | ||||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | 	github.com/kr/text v0.2.0 // indirect | ||||||
| 	github.com/mattn/go-isatty v0.0.19 // indirect | 	github.com/leodido/go-urn v1.4.0 // indirect | ||||||
| 	github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect | 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||||
|  | 	github.com/mattn/go-sqlite3 v1.14.22 // indirect | ||||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||||
| 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect | 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
| 	github.com/smarty/assertions v1.15.0 // indirect | 	github.com/smarty/assertions v1.15.0 // indirect | ||||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||||
| 	github.com/ugorji/go/codec v1.2.11 // indirect | 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||||
| 	golang.org/x/arch v0.3.0 // indirect | 	go.opencensus.io v0.24.0 // indirect | ||||||
| 	golang.org/x/net v0.17.0 // indirect | 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect | ||||||
| 	golang.org/x/sync v0.1.0 // indirect | 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect | ||||||
| 	golang.org/x/sys v0.15.0 // indirect | 	go.opentelemetry.io/otel v1.24.0 // indirect | ||||||
| 	golang.org/x/text v0.14.0 // indirect | 	go.opentelemetry.io/otel/metric v1.24.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.33.0 // indirect | 	go.opentelemetry.io/otel/trace v1.24.0 // indirect | ||||||
|  | 	golang.org/x/arch v0.8.0 // indirect | ||||||
|  | 	golang.org/x/net v0.26.0 // indirect | ||||||
|  | 	golang.org/x/oauth2 v0.21.0 // indirect | ||||||
|  | 	golang.org/x/sync v0.7.0 // indirect | ||||||
|  | 	golang.org/x/sys v0.21.0 // indirect | ||||||
|  | 	golang.org/x/text v0.16.0 // indirect | ||||||
|  | 	golang.org/x/time v0.5.0 // indirect | ||||||
|  | 	google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect | ||||||
|  | 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect | ||||||
|  | 	google.golang.org/grpc v1.64.1 // indirect | ||||||
|  | 	google.golang.org/protobuf v1.34.2 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										387
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										387
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,136 +1,190 @@ | |||||||
| github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||||
| github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= | cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38= | ||||||
| github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= | ||||||
| github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= | cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= | ||||||
| github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= | ||||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI= | ||||||
|  | cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps= | ||||||
|  | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | ||||||
|  | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | ||||||
|  | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||||
|  | github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo= | ||||||
|  | github.com/aws/aws-sdk-go-v2 v1.27.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= | ||||||
|  | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= | ||||||
|  | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= | ||||||
|  | github.com/aws/aws-sdk-go-v2/credentials v1.17.15 h1:YDexlvDRCA8ems2T5IP1xkMtOZ1uLJOCJdTr0igs5zo= | ||||||
|  | github.com/aws/aws-sdk-go-v2/credentials v1.17.15/go.mod h1:vxHggqW6hFNaeNC0WyXS3VdyjcV0a4KMUY4dKJ96buU= | ||||||
|  | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 h1:lf/8VTF2cM+N4SLzaYJERKEWAXq8MOMpZfU6wEPWsPk= | ||||||
|  | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7/go.mod h1:4SjkU7QiqK2M9oozyMzfZ/23LmUY+h3oFqhdeP5OMiI= | ||||||
|  | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 h1:4OYVp0705xu8yjdyoWix0r9wPIRXnIzzOoUpQVHIJ/g= | ||||||
|  | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7/go.mod h1:vd7ESTEvI76T2Na050gODNmNU7+OyKrIKroYTu4ABiI= | ||||||
|  | github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.3 h1:Fihjyd6DeNjcawBEGLH9dkIEUi6AdhucDKPE9nJ4QiY= | ||||||
|  | github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.3/go.mod h1:opvUj3ismqSCxYc+m4WIjPL0ewZGtvp0ess7cKvBPOQ= | ||||||
|  | github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= | ||||||
|  | github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= | ||||||
|  | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= | ||||||
|  | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= | ||||||
|  | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= | ||||||
|  | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= | ||||||
|  | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
|  | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | ||||||
|  | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||||
|  | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | ||||||
|  | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= | ||||||
|  | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= | ||||||
|  | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= | ||||||
|  | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= | ||||||
|  | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= | ||||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | ||||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||||||
| github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= | ||||||
| github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | ||||||
| github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= | ||||||
| github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||||
| github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= | ||||||
| github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | ||||||
| github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= | ||||||
| github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | ||||||
| github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= | ||||||
|  | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= | ||||||
|  | github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= | ||||||
|  | github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= | ||||||
|  | github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE= | ||||||
|  | github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4= | ||||||
|  | github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI= | ||||||
|  | github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM= | ||||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||||
| github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= | github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4= | ||||||
| github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= | github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw= | ||||||
| github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= | ||||||
| github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | ||||||
| github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||||||
| github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= | ||||||
| github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||||||
|  | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||||||
|  | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | ||||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||||
| github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= |  | ||||||
| github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= |  | ||||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||||
| github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= |  | ||||||
| github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= |  | ||||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||||
| github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= | ||||||
| github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= | ||||||
| github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= |  | ||||||
| github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= |  | ||||||
| github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | ||||||
| github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | ||||||
| github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||||
| github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= | ||||||
| github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= | ||||||
| github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= | ||||||
| github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||||
| github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= | ||||||
| github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= | ||||||
| github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= | ||||||
| github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||||
|  | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | ||||||
|  | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
|  | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= | ||||||
|  | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= | ||||||
|  | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= | ||||||
|  | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | ||||||
|  | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | ||||||
|  | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= | ||||||
|  | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | ||||||
|  | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
|  | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
|  | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | ||||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= | ||||||
|  | github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= | ||||||
|  | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||||
|  | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= | ||||||
|  | github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= | ||||||
|  | github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= | ||||||
|  | github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= | ||||||
| github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= | ||||||
| github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= | ||||||
| github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= | ||||||
| github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= | ||||||
| github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | ||||||
| github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= | ||||||
| github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= | ||||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= | ||||||
| github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= | ||||||
| github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= | ||||||
| github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= | ||||||
| github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= | ||||||
| github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= | ||||||
| github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= | ||||||
| github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= | ||||||
| github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= | ||||||
| github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= | ||||||
|  | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= | ||||||
|  | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= | ||||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |  | ||||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||||
|  | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||||
| github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||||||
| github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= | ||||||
| github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= |  | ||||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||||
| github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= |  | ||||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |  | ||||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |  | ||||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||||
| github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= | ||||||
| github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | ||||||
| github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||||
| github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= | ||||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= | ||||||
| github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= |  | ||||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= |  | ||||||
| github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= |  | ||||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= |  | ||||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= |  | ||||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | ||||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= |  | ||||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | 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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||||
| github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= | ||||||
| github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | ||||||
| github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= | ||||||
| github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= | ||||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= | ||||||
| github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4= | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||||
| github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= | ||||||
|  | github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | ||||||
| github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= |  | ||||||
| github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= | ||||||
| github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= | ||||||
| github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= | ||||||
| @@ -138,81 +192,126 @@ github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9 | |||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
|  | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | ||||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |  | ||||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |  | ||||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
| github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||||||
| github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||||
| github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||||||
| github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||||
| github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= | ||||||
| github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= | ||||||
| github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= | ||||||
| github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= | ||||||
|  | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= | ||||||
|  | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= | ||||||
|  | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= | ||||||
|  | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= | ||||||
|  | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= | ||||||
|  | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= | ||||||
|  | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= | ||||||
|  | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
| golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= | ||||||
| golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | ||||||
| golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= | ||||||
| golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= | golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= | ||||||
| golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= | ||||||
| golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= | ||||||
| golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||||
| golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= | ||||||
| golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
| golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
| golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | ||||||
|  | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= | ||||||
|  | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= | ||||||
|  | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
|  | golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= | ||||||
|  | golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||||||
|  | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= | ||||||
|  | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
|  | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= | ||||||
| golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= |  | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= | ||||||
| golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= | ||||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= | ||||||
|  | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= | ||||||
|  | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||||
|  | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= | google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo= | ||||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk= | ||||||
| google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||||
| google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||||
| google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||||
|  | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | ||||||
|  | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= | ||||||
|  | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= | ||||||
|  | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= | ||||||
|  | google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= | ||||||
|  | google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= | ||||||
|  | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | ||||||
|  | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | ||||||
|  | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= | ||||||
|  | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= | ||||||
|  | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= | ||||||
|  | google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= | ||||||
|  | google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= | ||||||
|  | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= | ||||||
|  | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= | ||||||
|  | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= | ||||||
|  | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= | ||||||
|  | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= | ||||||
|  | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||||
|  | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||||
|  | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||||
|  | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= | ||||||
|  | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | ||||||
|  | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |  | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= |  | ||||||
| gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= |  | ||||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |  | ||||||
| gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |  | ||||||
| gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||||||
| gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |  | ||||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |  | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= | gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= | ||||||
| gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= | gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= | ||||||
| gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= | gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= | ||||||
| gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= | gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= | ||||||
| gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= | gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= | ||||||
| gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= | gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= | ||||||
| gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= | ||||||
| gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= | gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= | ||||||
| gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= | gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= | ||||||
| gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||||
|  | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||||
|  | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | ||||||
| rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								main.go
									
									
									
									
									
								
							| @@ -6,7 +6,9 @@ import ( | |||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-contrib/sessions/cookie" | 	"github.com/gin-contrib/sessions/cookie" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	_ "github.com/joho/godotenv/autoload" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/client" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/controller" | 	"github.com/songquanpeng/one-api/controller" | ||||||
| @@ -22,29 +24,22 @@ import ( | |||||||
| var buildFS embed.FS | var buildFS embed.FS | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
|  | 	common.Init() | ||||||
| 	logger.SetupLogger() | 	logger.SetupLogger() | ||||||
| 	logger.SysLog(fmt.Sprintf("One API %s started", common.Version)) | 	logger.SysLogf("One API %s started", common.Version) | ||||||
| 	if os.Getenv("GIN_MODE") != "debug" { |  | ||||||
|  | 	if os.Getenv("GIN_MODE") != gin.DebugMode { | ||||||
| 		gin.SetMode(gin.ReleaseMode) | 		gin.SetMode(gin.ReleaseMode) | ||||||
| 	} | 	} | ||||||
| 	if config.DebugEnabled { | 	if config.DebugEnabled { | ||||||
| 		logger.SysLog("running in debug mode") | 		logger.SysLog("running in debug mode") | ||||||
| 	} | 	} | ||||||
| 	var err error |  | ||||||
| 	// Initialize SQL Database | 	// Initialize SQL Database | ||||||
| 	model.DB, err = model.InitDB("SQL_DSN") | 	model.InitDB() | ||||||
| 	if err != nil { | 	model.InitLogDB() | ||||||
| 		logger.FatalLog("failed to initialize database: " + err.Error()) |  | ||||||
| 	} | 	var err error | ||||||
| 	if os.Getenv("LOG_SQL_DSN") != "" { |  | ||||||
| 		logger.SysLog("using secondary database for table logs") |  | ||||||
| 		model.LOG_DB, err = model.InitDB("LOG_SQL_DSN") |  | ||||||
| 		if err != nil { |  | ||||||
| 			logger.FatalLog("failed to initialize secondary database: " + err.Error()) |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		model.LOG_DB = model.DB |  | ||||||
| 	} |  | ||||||
| 	err = model.CreateRootAccountIfNeed() | 	err = model.CreateRootAccountIfNeed() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logger.FatalLog("database init error: " + err.Error()) | 		logger.FatalLog("database init error: " + err.Error()) | ||||||
| @@ -71,7 +66,7 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 	if config.MemoryCacheEnabled { | 	if config.MemoryCacheEnabled { | ||||||
| 		logger.SysLog("memory cache enabled") | 		logger.SysLog("memory cache enabled") | ||||||
| 		logger.SysError(fmt.Sprintf("sync frequency: %d seconds", config.SyncFrequency)) | 		logger.SysLog(fmt.Sprintf("sync frequency: %d seconds", config.SyncFrequency)) | ||||||
| 		model.InitChannelCache() | 		model.InitChannelCache() | ||||||
| 	} | 	} | ||||||
| 	if config.MemoryCacheEnabled { | 	if config.MemoryCacheEnabled { | ||||||
| @@ -94,6 +89,7 @@ func main() { | |||||||
| 		logger.SysLog("metric enabled, will disable channel if too much request failed") | 		logger.SysLog("metric enabled, will disable channel if too much request failed") | ||||||
| 	} | 	} | ||||||
| 	openai.InitTokenEncoders() | 	openai.InitTokenEncoders() | ||||||
|  | 	client.Init() | ||||||
|  |  | ||||||
| 	// Initialize HTTP server | 	// Initialize HTTP server | ||||||
| 	server := gin.New() | 	server := gin.New() | ||||||
| @@ -111,6 +107,7 @@ func main() { | |||||||
| 	if port == "" { | 	if port == "" { | ||||||
| 		port = strconv.Itoa(*common.Port) | 		port = strconv.Itoa(*common.Port) | ||||||
| 	} | 	} | ||||||
|  | 	logger.SysLogf("server started on http://localhost:%s", port) | ||||||
| 	err = server.Run(":" + port) | 	err = server.Run(":" + port) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logger.FatalLog("failed to start HTTP server: " + err.Error()) | 		logger.FatalLog("failed to start HTTP server: " + err.Error()) | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import ( | |||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/blacklist" | 	"github.com/songquanpeng/one-api/common/blacklist" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/common/network" | 	"github.com/songquanpeng/one-api/common/network" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -116,29 +117,51 @@ func TokenAuth() func(c *gin.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		requestModel, err := getRequestModel(c) | 		requestModel, err := getRequestModel(c) | ||||||
| 		if err != nil && !strings.HasPrefix(c.Request.URL.Path, "/v1/models") { | 		if err != nil && shouldCheckModel(c) { | ||||||
| 			abortWithMessage(c, http.StatusBadRequest, err.Error()) | 			abortWithMessage(c, http.StatusBadRequest, err.Error()) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		c.Set("request_model", requestModel) | 		c.Set(ctxkey.RequestModel, requestModel) | ||||||
| 		if token.Models != nil && *token.Models != "" { | 		if token.Models != nil && *token.Models != "" { | ||||||
| 			c.Set("available_models", *token.Models) | 			c.Set(ctxkey.AvailableModels, *token.Models) | ||||||
| 			if requestModel != "" && !isModelInList(requestModel, *token.Models) { | 			if requestModel != "" && !isModelInList(requestModel, *token.Models) { | ||||||
| 				abortWithMessage(c, http.StatusForbidden, fmt.Sprintf("该令牌无权使用模型:%s", requestModel)) | 				abortWithMessage(c, http.StatusForbidden, fmt.Sprintf("该令牌无权使用模型:%s", requestModel)) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		c.Set("id", token.UserId) | 		c.Set(ctxkey.Id, token.UserId) | ||||||
| 		c.Set("token_id", token.Id) | 		c.Set(ctxkey.TokenId, token.Id) | ||||||
| 		c.Set("token_name", token.Name) | 		c.Set(ctxkey.TokenName, token.Name) | ||||||
| 		if len(parts) > 1 { | 		if len(parts) > 1 { | ||||||
| 			if model.IsAdmin(token.UserId) { | 			if model.IsAdmin(token.UserId) { | ||||||
| 				c.Set("specific_channel_id", parts[1]) | 				c.Set(ctxkey.SpecificChannelId, parts[1]) | ||||||
| 			} else { | 			} else { | ||||||
| 				abortWithMessage(c, http.StatusForbidden, "普通用户不支持指定渠道") | 				abortWithMessage(c, http.StatusForbidden, "普通用户不支持指定渠道") | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// set channel id for proxy relay | ||||||
|  | 		if channelId := c.Param("channelid"); channelId != "" { | ||||||
|  | 			c.Set(ctxkey.SpecificChannelId, channelId) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		c.Next() | 		c.Next() | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func shouldCheckModel(c *gin.Context) bool { | ||||||
|  | 	if strings.HasPrefix(c.Request.URL.Path, "/v1/completions") { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if strings.HasPrefix(c.Request.URL.Path, "/v1/images") { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ package middleware | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"github.com/songquanpeng/one-api/relay/channeltype" | 	"github.com/songquanpeng/one-api/relay/channeltype" | ||||||
| @@ -17,12 +17,12 @@ type ModelRequest struct { | |||||||
|  |  | ||||||
| func Distribute() func(c *gin.Context) { | func Distribute() func(c *gin.Context) { | ||||||
| 	return func(c *gin.Context) { | 	return func(c *gin.Context) { | ||||||
| 		userId := c.GetInt("id") | 		userId := c.GetInt(ctxkey.Id) | ||||||
| 		userGroup, _ := model.CacheGetUserGroup(userId) | 		userGroup, _ := model.CacheGetUserGroup(userId) | ||||||
| 		c.Set("group", userGroup) | 		c.Set(ctxkey.Group, userGroup) | ||||||
| 		var requestModel string | 		var requestModel string | ||||||
| 		var channel *model.Channel | 		var channel *model.Channel | ||||||
| 		channelId, ok := c.Get("specific_channel_id") | 		channelId, ok := c.Get(ctxkey.SpecificChannelId) | ||||||
| 		if ok { | 		if ok { | ||||||
| 			id, err := strconv.Atoi(channelId.(string)) | 			id, err := strconv.Atoi(channelId.(string)) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -39,7 +39,7 @@ func Distribute() func(c *gin.Context) { | |||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			requestModel := c.GetString("request_model") | 			requestModel = c.GetString(ctxkey.RequestModel) | ||||||
| 			var err error | 			var err error | ||||||
| 			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, requestModel, false) | 			channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, requestModel, false) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -58,28 +58,38 @@ func Distribute() func(c *gin.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) { | func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) { | ||||||
| 	c.Set("channel", channel.Type) | 	c.Set(ctxkey.Channel, channel.Type) | ||||||
| 	c.Set("channel_id", channel.Id) | 	c.Set(ctxkey.ChannelId, channel.Id) | ||||||
| 	c.Set("channel_name", channel.Name) | 	c.Set(ctxkey.ChannelName, channel.Name) | ||||||
| 	c.Set("model_mapping", channel.GetModelMapping()) | 	c.Set(ctxkey.ModelMapping, channel.GetModelMapping()) | ||||||
| 	c.Set("original_model", modelName) // for retry | 	c.Set(ctxkey.OriginalModel, modelName) // for retry | ||||||
| 	c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) | 	c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) | ||||||
| 	c.Set("base_url", channel.GetBaseURL()) | 	c.Set(ctxkey.BaseURL, channel.GetBaseURL()) | ||||||
|  | 	cfg, _ := channel.LoadConfig() | ||||||
| 	// this is for backward compatibility | 	// this is for backward compatibility | ||||||
|  | 	if channel.Other != nil { | ||||||
| 		switch channel.Type { | 		switch channel.Type { | ||||||
| 		case channeltype.Azure: | 		case channeltype.Azure: | ||||||
| 		c.Set(config.KeyAPIVersion, channel.Other) | 			if cfg.APIVersion == "" { | ||||||
|  | 				cfg.APIVersion = *channel.Other | ||||||
|  | 			} | ||||||
| 		case channeltype.Xunfei: | 		case channeltype.Xunfei: | ||||||
| 		c.Set(config.KeyAPIVersion, channel.Other) | 			if cfg.APIVersion == "" { | ||||||
|  | 				cfg.APIVersion = *channel.Other | ||||||
|  | 			} | ||||||
| 		case channeltype.Gemini: | 		case channeltype.Gemini: | ||||||
| 		c.Set(config.KeyAPIVersion, channel.Other) | 			if cfg.APIVersion == "" { | ||||||
|  | 				cfg.APIVersion = *channel.Other | ||||||
|  | 			} | ||||||
| 		case channeltype.AIProxyLibrary: | 		case channeltype.AIProxyLibrary: | ||||||
| 		c.Set(config.KeyLibraryID, channel.Other) | 			if cfg.LibraryID == "" { | ||||||
|  | 				cfg.LibraryID = *channel.Other | ||||||
|  | 			} | ||||||
| 		case channeltype.Ali: | 		case channeltype.Ali: | ||||||
| 		c.Set(config.KeyPlugin, channel.Other) | 			if cfg.Plugin == "" { | ||||||
|  | 				cfg.Plugin = *channel.Other | ||||||
| 			} | 			} | ||||||
| 	cfg, _ := channel.LoadConfig() |  | ||||||
| 	for k, v := range cfg { |  | ||||||
| 		c.Set(config.KeyPrefix+k, v) |  | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  | 	c.Set(ctxkey.Config, cfg) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,14 +3,14 @@ package middleware | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func SetUpLogger(server *gin.Engine) { | func SetUpLogger(server *gin.Engine) { | ||||||
| 	server.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { | 	server.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { | ||||||
| 		var requestID string | 		var requestID string | ||||||
| 		if param.Keys != nil { | 		if param.Keys != nil { | ||||||
| 			requestID = param.Keys[logger.RequestIdKey].(string) | 			requestID = param.Keys[helper.RequestIdKey].(string) | ||||||
| 		} | 		} | ||||||
| 		return fmt.Sprintf("[GIN] %s | %s | %3d | %13v | %15s | %7s %s\n", | 		return fmt.Sprintf("[GIN] %s | %s | %3d | %13v | %15s | %7s %s\n", | ||||||
| 			param.TimeStamp.Format("2006/01/02 - 15:04:05"), | 			param.TimeStamp.Format("2006/01/02 - 15:04:05"), | ||||||
|   | |||||||
| @@ -3,11 +3,12 @@ package middleware | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"net/http" |  | ||||||
| 	"time" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var timeFormat = "2006-01-02T15:04:05.000Z" | var timeFormat = "2006-01-02T15:04:05.000Z" | ||||||
| @@ -70,6 +71,11 @@ func memoryRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark s | |||||||
| } | } | ||||||
|  |  | ||||||
| func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) { | func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) { | ||||||
|  | 	if maxRequestNum == 0 { | ||||||
|  | 		return func(c *gin.Context) { | ||||||
|  | 			c.Next() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	if common.RedisEnabled { | 	if common.RedisEnabled { | ||||||
| 		return func(c *gin.Context) { | 		return func(c *gin.Context) { | ||||||
| 			redisRateLimiter(c, maxRequestNum, duration, mark) | 			redisRateLimiter(c, maxRequestNum, duration, mark) | ||||||
|   | |||||||
| @@ -4,16 +4,15 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func RequestId() func(c *gin.Context) { | func RequestId() func(c *gin.Context) { | ||||||
| 	return func(c *gin.Context) { | 	return func(c *gin.Context) { | ||||||
| 		id := helper.GenRequestID() | 		id := helper.GenRequestID() | ||||||
| 		c.Set(logger.RequestIdKey, id) | 		c.Set(helper.RequestIdKey, id) | ||||||
| 		ctx := context.WithValue(c.Request.Context(), logger.RequestIdKey, id) | 		ctx := context.WithValue(c.Request.Context(), helper.RequestIdKey, id) | ||||||
| 		c.Request = c.Request.WithContext(ctx) | 		c.Request = c.Request.WithContext(ctx) | ||||||
| 		c.Header(logger.RequestIdKey, id) | 		c.Header(helper.RequestIdKey, id) | ||||||
| 		c.Next() | 		c.Next() | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import ( | |||||||
| func abortWithMessage(c *gin.Context, statusCode int, message string) { | func abortWithMessage(c *gin.Context, statusCode int, message string) { | ||||||
| 	c.JSON(statusCode, gin.H{ | 	c.JSON(statusCode, gin.H{ | ||||||
| 		"error": gin.H{ | 		"error": gin.H{ | ||||||
| 			"message": helper.MessageWithRequestId(message, c.GetString(logger.RequestIdKey)), | 			"message": helper.MessageWithRequestId(message, c.GetString(helper.RequestIdKey)), | ||||||
| 			"type":    "one_api_error", | 			"type":    "one_api_error", | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package model | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| @@ -27,7 +28,7 @@ type Channel struct { | |||||||
| 	TestTime           int64   `json:"test_time" gorm:"bigint"` | 	TestTime           int64   `json:"test_time" gorm:"bigint"` | ||||||
| 	ResponseTime       int     `json:"response_time"` // in milliseconds | 	ResponseTime       int     `json:"response_time"` // in milliseconds | ||||||
| 	BaseURL            *string `json:"base_url" gorm:"column:base_url;default:''"` | 	BaseURL            *string `json:"base_url" gorm:"column:base_url;default:''"` | ||||||
| 	Other              string  `json:"other"`   // DEPRECATED: please save config to field Config | 	Other              *string `json:"other"`   // DEPRECATED: please save config to field Config | ||||||
| 	Balance            float64 `json:"balance"` // in USD | 	Balance            float64 `json:"balance"` // in USD | ||||||
| 	BalanceUpdatedTime int64   `json:"balance_updated_time" gorm:"bigint"` | 	BalanceUpdatedTime int64   `json:"balance_updated_time" gorm:"bigint"` | ||||||
| 	Models             string  `json:"models"` | 	Models             string  `json:"models"` | ||||||
| @@ -38,6 +39,18 @@ type Channel struct { | |||||||
| 	Config             string  `json:"config"` | 	Config             string  `json:"config"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type ChannelConfig struct { | ||||||
|  | 	Region            string `json:"region,omitempty"` | ||||||
|  | 	SK                string `json:"sk,omitempty"` | ||||||
|  | 	AK                string `json:"ak,omitempty"` | ||||||
|  | 	UserID            string `json:"user_id,omitempty"` | ||||||
|  | 	APIVersion        string `json:"api_version,omitempty"` | ||||||
|  | 	LibraryID         string `json:"library_id,omitempty"` | ||||||
|  | 	Plugin            string `json:"plugin,omitempty"` | ||||||
|  | 	VertexAIProjectID string `json:"vertex_ai_project_id,omitempty"` | ||||||
|  | 	VertexAIADC       string `json:"vertex_ai_adc,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
| func GetAllChannels(startIdx int, num int, scope string) ([]*Channel, error) { | func GetAllChannels(startIdx int, num int, scope string) ([]*Channel, error) { | ||||||
| 	var channels []*Channel | 	var channels []*Channel | ||||||
| 	var err error | 	var err error | ||||||
| @@ -161,14 +174,14 @@ func (channel *Channel) Delete() error { | |||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| func (channel *Channel) LoadConfig() (map[string]string, error) { | func (channel *Channel) LoadConfig() (ChannelConfig, error) { | ||||||
|  | 	var cfg ChannelConfig | ||||||
| 	if channel.Config == "" { | 	if channel.Config == "" { | ||||||
| 		return nil, nil | 		return cfg, nil | ||||||
| 	} | 	} | ||||||
| 	cfg := make(map[string]string) |  | ||||||
| 	err := json.Unmarshal([]byte(channel.Config), &cfg) | 	err := json.Unmarshal([]byte(channel.Config), &cfg) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return cfg, err | ||||||
| 	} | 	} | ||||||
| 	return cfg, nil | 	return cfg, nil | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										175
									
								
								model/main.go
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								model/main.go
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| package model | package model | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"database/sql" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| @@ -29,13 +30,17 @@ func CreateRootAccountIfNeed() error { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | 		accessToken := random.GetUUID() | ||||||
|  | 		if config.InitialRootAccessToken != "" { | ||||||
|  | 			accessToken = config.InitialRootAccessToken | ||||||
|  | 		} | ||||||
| 		rootUser := User{ | 		rootUser := User{ | ||||||
| 			Username:    "root", | 			Username:    "root", | ||||||
| 			Password:    hashedPassword, | 			Password:    hashedPassword, | ||||||
| 			Role:        RoleRootUser, | 			Role:        RoleRootUser, | ||||||
| 			Status:      UserStatusEnabled, | 			Status:      UserStatusEnabled, | ||||||
| 			DisplayName: "Root User", | 			DisplayName: "Root User", | ||||||
| 			AccessToken: random.GetUUID(), | 			AccessToken: accessToken, | ||||||
| 			Quota:       500000000000000, | 			Quota:       500000000000000, | ||||||
| 		} | 		} | ||||||
| 		DB.Create(&rootUser) | 		DB.Create(&rootUser) | ||||||
| @@ -60,10 +65,22 @@ func CreateRootAccountIfNeed() error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func chooseDB(envName string) (*gorm.DB, error) { | func chooseDB(envName string) (*gorm.DB, error) { | ||||||
| 	if os.Getenv(envName) != "" { |  | ||||||
| 	dsn := os.Getenv(envName) | 	dsn := os.Getenv(envName) | ||||||
| 		if strings.HasPrefix(dsn, "postgres://") { |  | ||||||
|  | 	switch { | ||||||
|  | 	case strings.HasPrefix(dsn, "postgres://"): | ||||||
| 		// Use PostgreSQL | 		// Use PostgreSQL | ||||||
|  | 		return openPostgreSQL(dsn) | ||||||
|  | 	case dsn != "": | ||||||
|  | 		// Use MySQL | ||||||
|  | 		return openMySQL(dsn) | ||||||
|  | 	default: | ||||||
|  | 		// Use SQLite | ||||||
|  | 		return openSQLite() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func openPostgreSQL(dsn string) (*gorm.DB, error) { | ||||||
| 	logger.SysLog("using PostgreSQL as database") | 	logger.SysLog("using PostgreSQL as database") | ||||||
| 	common.UsingPostgreSQL = true | 	common.UsingPostgreSQL = true | ||||||
| 	return gorm.Open(postgres.New(postgres.Config{ | 	return gorm.Open(postgres.New(postgres.Config{ | ||||||
| @@ -72,78 +89,132 @@ func chooseDB(envName string) (*gorm.DB, error) { | |||||||
| 	}), &gorm.Config{ | 	}), &gorm.Config{ | ||||||
| 		PrepareStmt: true, // precompile SQL | 		PrepareStmt: true, // precompile SQL | ||||||
| 	}) | 	}) | ||||||
| 		} | } | ||||||
| 		// Use MySQL |  | ||||||
|  | func openMySQL(dsn string) (*gorm.DB, error) { | ||||||
| 	logger.SysLog("using MySQL as database") | 	logger.SysLog("using MySQL as database") | ||||||
| 	common.UsingMySQL = true | 	common.UsingMySQL = true | ||||||
| 	return gorm.Open(mysql.Open(dsn), &gorm.Config{ | 	return gorm.Open(mysql.Open(dsn), &gorm.Config{ | ||||||
| 		PrepareStmt: true, // precompile SQL | 		PrepareStmt: true, // precompile SQL | ||||||
| 	}) | 	}) | ||||||
| 	} | } | ||||||
| 	// Use SQLite |  | ||||||
|  | func openSQLite() (*gorm.DB, error) { | ||||||
| 	logger.SysLog("SQL_DSN not set, using SQLite as database") | 	logger.SysLog("SQL_DSN not set, using SQLite as database") | ||||||
| 	common.UsingSQLite = true | 	common.UsingSQLite = true | ||||||
| 	config := fmt.Sprintf("?_busy_timeout=%d", common.SQLiteBusyTimeout) | 	dsn := fmt.Sprintf("%s?_busy_timeout=%d", common.SQLitePath, common.SQLiteBusyTimeout) | ||||||
| 	return gorm.Open(sqlite.Open(common.SQLitePath+config), &gorm.Config{ | 	return gorm.Open(sqlite.Open(dsn), &gorm.Config{ | ||||||
| 		PrepareStmt: true, // precompile SQL | 		PrepareStmt: true, // precompile SQL | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func InitDB(envName string) (db *gorm.DB, err error) { | func InitDB() { | ||||||
| 	db, err = chooseDB(envName) | 	var err error | ||||||
| 	if err == nil { | 	DB, err = chooseDB("SQL_DSN") | ||||||
| 		if config.DebugSQLEnabled { |  | ||||||
| 			db = db.Debug() |  | ||||||
| 		} |  | ||||||
| 		sqlDB, err := db.DB() |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 			return nil, err | 		logger.FatalLog("failed to initialize database: " + err.Error()) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 		sqlDB.SetMaxIdleConns(env.Int("SQL_MAX_IDLE_CONNS", 100)) |  | ||||||
| 		sqlDB.SetMaxOpenConns(env.Int("SQL_MAX_OPEN_CONNS", 1000)) | 	sqlDB := setDBConns(DB) | ||||||
| 		sqlDB.SetConnMaxLifetime(time.Second * time.Duration(env.Int("SQL_MAX_LIFETIME", 60))) |  | ||||||
|  |  | ||||||
| 	if !config.IsMasterNode { | 	if !config.IsMasterNode { | ||||||
| 			return db, err | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if common.UsingMySQL { | 	if common.UsingMySQL { | ||||||
| 		_, _ = sqlDB.Exec("DROP INDEX idx_channels_key ON channels;") // TODO: delete this line when most users have upgraded | 		_, _ = sqlDB.Exec("DROP INDEX idx_channels_key ON channels;") // TODO: delete this line when most users have upgraded | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logger.SysLog("database migration started") | 	logger.SysLog("database migration started") | ||||||
| 		err = db.AutoMigrate(&Channel{}) | 	if err = migrateDB(); err != nil { | ||||||
| 		if err != nil { | 		logger.FatalLog("failed to migrate database: " + err.Error()) | ||||||
| 			return nil, err | 		return | ||||||
| 		} |  | ||||||
| 		err = db.AutoMigrate(&Token{}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		err = db.AutoMigrate(&User{}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		err = db.AutoMigrate(&Option{}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		err = db.AutoMigrate(&Redemption{}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		err = db.AutoMigrate(&Ability{}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		err = db.AutoMigrate(&Log{}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 	} | 	} | ||||||
| 	logger.SysLog("database migrated") | 	logger.SysLog("database migrated") | ||||||
| 		return db, err | } | ||||||
| 	} else { |  | ||||||
| 		logger.FatalLog(err) | func migrateDB() error { | ||||||
|  | 	var err error | ||||||
|  | 	if err = DB.AutoMigrate(&Channel{}); err != nil { | ||||||
|  | 		return err | ||||||
| 	} | 	} | ||||||
| 	return db, err | 	if err = DB.AutoMigrate(&Token{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = DB.AutoMigrate(&User{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = DB.AutoMigrate(&Option{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = DB.AutoMigrate(&Redemption{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = DB.AutoMigrate(&Ability{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = DB.AutoMigrate(&Log{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = DB.AutoMigrate(&Channel{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func InitLogDB() { | ||||||
|  | 	if os.Getenv("LOG_SQL_DSN") == "" { | ||||||
|  | 		LOG_DB = DB | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.SysLog("using secondary database for table logs") | ||||||
|  | 	var err error | ||||||
|  | 	LOG_DB, err = chooseDB("LOG_SQL_DSN") | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.FatalLog("failed to initialize secondary database: " + err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	setDBConns(LOG_DB) | ||||||
|  |  | ||||||
|  | 	if !config.IsMasterNode { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.SysLog("secondary database migration started") | ||||||
|  | 	err = migrateLOGDB() | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.FatalLog("failed to migrate secondary database: " + err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	logger.SysLog("secondary database migrated") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func migrateLOGDB() error { | ||||||
|  | 	var err error | ||||||
|  | 	if err = LOG_DB.AutoMigrate(&Log{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setDBConns(db *gorm.DB) *sql.DB { | ||||||
|  | 	if config.DebugSQLEnabled { | ||||||
|  | 		db = db.Debug() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sqlDB, err := db.DB() | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.FatalLog("failed to connect database: " + err.Error()) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sqlDB.SetMaxIdleConns(env.Int("SQL_MAX_IDLE_CONNS", 100)) | ||||||
|  | 	sqlDB.SetMaxOpenConns(env.Int("SQL_MAX_OPEN_CONNS", 1000)) | ||||||
|  | 	sqlDB.SetConnMaxLifetime(time.Second * time.Duration(env.Int("SQL_MAX_LIFETIME", 60))) | ||||||
|  | 	return sqlDB | ||||||
| } | } | ||||||
|  |  | ||||||
| func closeDB(db *gorm.DB) error { | func closeDB(db *gorm.DB) error { | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/blacklist" | 	"github.com/songquanpeng/one-api/common/blacklist" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/common/random" | 	"github.com/songquanpeng/one-api/common/random" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| @@ -140,6 +141,22 @@ func (user *User) Insert(inviterId int) error { | |||||||
| 			RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter))) | 			RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter))) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	// create default token | ||||||
|  | 	cleanToken := Token{ | ||||||
|  | 		UserId:         user.Id, | ||||||
|  | 		Name:           "default", | ||||||
|  | 		Key:            random.GenerateKey(), | ||||||
|  | 		CreatedTime:    helper.GetTimestamp(), | ||||||
|  | 		AccessedTime:   helper.GetTimestamp(), | ||||||
|  | 		ExpiredTime:    -1, | ||||||
|  | 		RemainQuota:    -1, | ||||||
|  | 		UnlimitedQuota: true, | ||||||
|  | 	} | ||||||
|  | 	result.Error = cleanToken.Insert() | ||||||
|  | 	if result.Error != nil { | ||||||
|  | 		// do not block | ||||||
|  | 		logger.SysError(fmt.Sprintf("create default token for user %d failed: %s", user.Id, result.Error.Error())) | ||||||
|  | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,12 +5,19 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/aiproxy" | 	"github.com/songquanpeng/one-api/relay/adaptor/aiproxy" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/ali" | 	"github.com/songquanpeng/one-api/relay/adaptor/ali" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/anthropic" | 	"github.com/songquanpeng/one-api/relay/adaptor/anthropic" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/aws" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/baidu" | 	"github.com/songquanpeng/one-api/relay/adaptor/baidu" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/cloudflare" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/cohere" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/coze" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/deepl" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/gemini" | 	"github.com/songquanpeng/one-api/relay/adaptor/gemini" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/ollama" | 	"github.com/songquanpeng/one-api/relay/adaptor/ollama" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/palm" | 	"github.com/songquanpeng/one-api/relay/adaptor/palm" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/proxy" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/tencent" | 	"github.com/songquanpeng/one-api/relay/adaptor/tencent" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/vertexai" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/xunfei" | 	"github.com/songquanpeng/one-api/relay/adaptor/xunfei" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/zhipu" | 	"github.com/songquanpeng/one-api/relay/adaptor/zhipu" | ||||||
| 	"github.com/songquanpeng/one-api/relay/apitype" | 	"github.com/songquanpeng/one-api/relay/apitype" | ||||||
| @@ -24,6 +31,8 @@ func GetAdaptor(apiType int) adaptor.Adaptor { | |||||||
| 		return &ali.Adaptor{} | 		return &ali.Adaptor{} | ||||||
| 	case apitype.Anthropic: | 	case apitype.Anthropic: | ||||||
| 		return &anthropic.Adaptor{} | 		return &anthropic.Adaptor{} | ||||||
|  | 	case apitype.AwsClaude: | ||||||
|  | 		return &aws.Adaptor{} | ||||||
| 	case apitype.Baidu: | 	case apitype.Baidu: | ||||||
| 		return &baidu.Adaptor{} | 		return &baidu.Adaptor{} | ||||||
| 	case apitype.Gemini: | 	case apitype.Gemini: | ||||||
| @@ -40,6 +49,18 @@ func GetAdaptor(apiType int) adaptor.Adaptor { | |||||||
| 		return &zhipu.Adaptor{} | 		return &zhipu.Adaptor{} | ||||||
| 	case apitype.Ollama: | 	case apitype.Ollama: | ||||||
| 		return &ollama.Adaptor{} | 		return &ollama.Adaptor{} | ||||||
|  | 	case apitype.Coze: | ||||||
|  | 		return &coze.Adaptor{} | ||||||
|  | 	case apitype.Cohere: | ||||||
|  | 		return &cohere.Adaptor{} | ||||||
|  | 	case apitype.Cloudflare: | ||||||
|  | 		return &cloudflare.Adaptor{} | ||||||
|  | 	case apitype.DeepL: | ||||||
|  | 		return &deepl.Adaptor{} | ||||||
|  | 	case apitype.VertexAI: | ||||||
|  | 		return &vertexai.Adaptor{} | ||||||
|  | 	case apitype.Proxy: | ||||||
|  | 		return &proxy.Adaptor{} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" |  | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor" | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
| 	"github.com/songquanpeng/one-api/relay/meta" | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| @@ -13,10 +12,11 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type Adaptor struct { | type Adaptor struct { | ||||||
|  | 	meta *meta.Meta | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Adaptor) Init(meta *meta.Meta) { | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  | 	a.meta = meta | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
| @@ -34,7 +34,7 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G | |||||||
| 		return nil, errors.New("request is nil") | 		return nil, errors.New("request is nil") | ||||||
| 	} | 	} | ||||||
| 	aiProxyLibraryRequest := ConvertRequest(*request) | 	aiProxyLibraryRequest := ConvertRequest(*request) | ||||||
| 	aiProxyLibraryRequest.LibraryId = c.GetString(config.KeyLibraryID) | 	aiProxyLibraryRequest.LibraryId = a.meta.Config.LibraryID | ||||||
| 	return aiProxyLibraryRequest, nil | 	return aiProxyLibraryRequest, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,12 @@ import ( | |||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/render" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| @@ -12,10 +18,6 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
| 	"github.com/songquanpeng/one-api/relay/constant" | 	"github.com/songquanpeng/one-api/relay/constant" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // https://docs.aiproxy.io/dev/library#使用已经定制好的知识库进行对话问答 | // https://docs.aiproxy.io/dev/library#使用已经定制好的知识库进行对话问答 | ||||||
| @@ -89,6 +91,7 @@ func streamResponseAIProxyLibrary2OpenAI(response *LibraryStreamResponse) *opena | |||||||
|  |  | ||||||
| func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { | func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { | ||||||
| 	var usage model.Usage | 	var usage model.Usage | ||||||
|  | 	var documents []LibraryDocument | ||||||
| 	scanner := bufio.NewScanner(resp.Body) | 	scanner := bufio.NewScanner(resp.Body) | ||||||
| 	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | 	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | ||||||
| 		if atEOF && len(data) == 0 { | 		if atEOF && len(data) == 0 { | ||||||
| @@ -102,60 +105,48 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC | |||||||
| 		} | 		} | ||||||
| 		return 0, nil, nil | 		return 0, nil, nil | ||||||
| 	}) | 	}) | ||||||
| 	dataChan := make(chan string) |  | ||||||
| 	stopChan := make(chan bool) | 	common.SetEventStreamHeaders(c) | ||||||
| 	go func() { |  | ||||||
| 	for scanner.Scan() { | 	for scanner.Scan() { | ||||||
| 		data := scanner.Text() | 		data := scanner.Text() | ||||||
| 			if len(data) < 5 { // ignore blank line or wrong format | 		if len(data) < 5 || data[:5] != "data:" { | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			if data[:5] != "data:" { |  | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		data = data[5:] | 		data = data[5:] | ||||||
| 			dataChan <- data |  | ||||||
| 		} |  | ||||||
| 		stopChan <- true |  | ||||||
| 	}() |  | ||||||
| 	common.SetEventStreamHeaders(c) |  | ||||||
| 	var documents []LibraryDocument |  | ||||||
| 	c.Stream(func(w io.Writer) bool { |  | ||||||
| 		select { |  | ||||||
| 		case data := <-dataChan: |  | ||||||
| 		var AIProxyLibraryResponse LibraryStreamResponse | 		var AIProxyLibraryResponse LibraryStreamResponse | ||||||
| 		err := json.Unmarshal([]byte(data), &AIProxyLibraryResponse) | 		err := json.Unmarshal([]byte(data), &AIProxyLibraryResponse) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logger.SysError("error unmarshalling stream response: " + err.Error()) | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
| 				return true | 			continue | ||||||
| 		} | 		} | ||||||
| 		if len(AIProxyLibraryResponse.Documents) != 0 { | 		if len(AIProxyLibraryResponse.Documents) != 0 { | ||||||
| 			documents = AIProxyLibraryResponse.Documents | 			documents = AIProxyLibraryResponse.Documents | ||||||
| 		} | 		} | ||||||
| 		response := streamResponseAIProxyLibrary2OpenAI(&AIProxyLibraryResponse) | 		response := streamResponseAIProxyLibrary2OpenAI(&AIProxyLibraryResponse) | ||||||
| 			jsonResponse, err := json.Marshal(response) | 		err = render.ObjectData(c, response) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				logger.SysError("error marshalling stream response: " + err.Error()) | 			logger.SysError(err.Error()) | ||||||
| 				return true |  | ||||||
| 		} | 		} | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) | 	} | ||||||
| 			return true |  | ||||||
| 		case <-stopChan: | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	response := documentsAIProxyLibrary(documents) | 	response := documentsAIProxyLibrary(documents) | ||||||
| 			jsonResponse, err := json.Marshal(response) | 	err := render.ObjectData(c, response) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 				logger.SysError("error marshalling stream response: " + err.Error()) | 		logger.SysError(err.Error()) | ||||||
| 				return true |  | ||||||
| 	} | 	} | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) | 	render.Done(c) | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) |  | ||||||
| 			return false | 	err = resp.Body.Close() | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| 	err := resp.Body.Close() |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil, &usage | 	return nil, &usage | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" |  | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor" | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
| 	"github.com/songquanpeng/one-api/relay/meta" | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| @@ -16,10 +15,11 @@ import ( | |||||||
| // https://help.aliyun.com/zh/dashscope/developer-reference/api-details | // https://help.aliyun.com/zh/dashscope/developer-reference/api-details | ||||||
|  |  | ||||||
| type Adaptor struct { | type Adaptor struct { | ||||||
|  | 	meta *meta.Meta | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Adaptor) Init(meta *meta.Meta) { | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  | 	a.meta = meta | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
| @@ -47,8 +47,8 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *me | |||||||
| 	if meta.Mode == relaymode.ImagesGenerations { | 	if meta.Mode == relaymode.ImagesGenerations { | ||||||
| 		req.Header.Set("X-DashScope-Async", "enable") | 		req.Header.Set("X-DashScope-Async", "enable") | ||||||
| 	} | 	} | ||||||
| 	if c.GetString(config.KeyPlugin) != "" { | 	if a.meta.Config.Plugin != "" { | ||||||
| 		req.Header.Set("X-DashScope-Plugin", c.GetString(config.KeyPlugin)) | 		req.Header.Set("X-DashScope-Plugin", a.meta.Config.Plugin) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,15 +3,17 @@ package ali | |||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/render" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r | // https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r | ||||||
| @@ -181,32 +183,21 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC | |||||||
| 		} | 		} | ||||||
| 		return 0, nil, nil | 		return 0, nil, nil | ||||||
| 	}) | 	}) | ||||||
| 	dataChan := make(chan string) |  | ||||||
| 	stopChan := make(chan bool) | 	common.SetEventStreamHeaders(c) | ||||||
| 	go func() { |  | ||||||
| 	for scanner.Scan() { | 	for scanner.Scan() { | ||||||
| 		data := scanner.Text() | 		data := scanner.Text() | ||||||
| 			if len(data) < 5 { // ignore blank line or wrong format | 		if len(data) < 5 || data[:5] != "data:" { | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			if data[:5] != "data:" { |  | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		data = data[5:] | 		data = data[5:] | ||||||
| 			dataChan <- data |  | ||||||
| 		} |  | ||||||
| 		stopChan <- true |  | ||||||
| 	}() |  | ||||||
| 	common.SetEventStreamHeaders(c) |  | ||||||
| 	//lastResponseText := "" |  | ||||||
| 	c.Stream(func(w io.Writer) bool { |  | ||||||
| 		select { |  | ||||||
| 		case data := <-dataChan: |  | ||||||
| 		var aliResponse ChatResponse | 		var aliResponse ChatResponse | ||||||
| 		err := json.Unmarshal([]byte(data), &aliResponse) | 		err := json.Unmarshal([]byte(data), &aliResponse) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logger.SysError("error unmarshalling stream response: " + err.Error()) | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
| 				return true | 			continue | ||||||
| 		} | 		} | ||||||
| 		if aliResponse.Usage.OutputTokens != 0 { | 		if aliResponse.Usage.OutputTokens != 0 { | ||||||
| 			usage.PromptTokens = aliResponse.Usage.InputTokens | 			usage.PromptTokens = aliResponse.Usage.InputTokens | ||||||
| @@ -215,22 +206,20 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC | |||||||
| 		} | 		} | ||||||
| 		response := streamResponseAli2OpenAI(&aliResponse) | 		response := streamResponseAli2OpenAI(&aliResponse) | ||||||
| 		if response == nil { | 		if response == nil { | ||||||
| 				return true | 			continue | ||||||
| 		} | 		} | ||||||
| 			//response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText) | 		err = render.ObjectData(c, response) | ||||||
| 			//lastResponseText = aliResponse.Output.Text |  | ||||||
| 			jsonResponse, err := json.Marshal(response) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				logger.SysError("error marshalling stream response: " + err.Error()) | 			logger.SysError(err.Error()) | ||||||
| 				return true |  | ||||||
| 		} | 		} | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) |  | ||||||
| 			return true |  | ||||||
| 		case <-stopChan: |  | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) |  | ||||||
| 			return false |  | ||||||
| 	} | 	} | ||||||
| 	}) |  | ||||||
|  | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render.Done(c) | ||||||
|  |  | ||||||
| 	err := resp.Body.Close() | 	err := resp.Body.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|   | |||||||
| @@ -3,12 +3,14 @@ package anthropic | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor" | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
| 	"github.com/songquanpeng/one-api/relay/meta" | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Adaptor struct { | type Adaptor struct { | ||||||
| @@ -31,6 +33,13 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *me | |||||||
| 	} | 	} | ||||||
| 	req.Header.Set("anthropic-version", anthropicVersion) | 	req.Header.Set("anthropic-version", anthropicVersion) | ||||||
| 	req.Header.Set("anthropic-beta", "messages-2023-12-15") | 	req.Header.Set("anthropic-beta", "messages-2023-12-15") | ||||||
|  |  | ||||||
|  | 	// https://x.com/alexalbert__/status/1812921642143900036 | ||||||
|  | 	// claude-3-5-sonnet can support 8k context | ||||||
|  | 	if strings.HasPrefix(meta.ActualModelName, "claude-3-5-sonnet") { | ||||||
|  | 		req.Header.Set("anthropic-beta", "max-tokens-3-5-sonnet-2024-07-15") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,4 +5,5 @@ var ModelList = []string{ | |||||||
| 	"claude-3-haiku-20240307", | 	"claude-3-haiku-20240307", | ||||||
| 	"claude-3-sonnet-20240229", | 	"claude-3-sonnet-20240229", | ||||||
| 	"claude-3-opus-20240229", | 	"claude-3-opus-20240229", | ||||||
|  | 	"claude-3-5-sonnet-20240620", | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,11 @@ import ( | |||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/render" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| @@ -11,9 +16,6 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func stopReasonClaude2OpenAI(reason *string) string { | func stopReasonClaude2OpenAI(reason *string) string { | ||||||
| @@ -27,12 +29,30 @@ func stopReasonClaude2OpenAI(reason *string) string { | |||||||
| 		return "stop" | 		return "stop" | ||||||
| 	case "max_tokens": | 	case "max_tokens": | ||||||
| 		return "length" | 		return "length" | ||||||
|  | 	case "tool_use": | ||||||
|  | 		return "tool_calls" | ||||||
| 	default: | 	default: | ||||||
| 		return *reason | 		return *reason | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { | func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { | ||||||
|  | 	claudeTools := make([]Tool, 0, len(textRequest.Tools)) | ||||||
|  |  | ||||||
|  | 	for _, tool := range textRequest.Tools { | ||||||
|  | 		if params, ok := tool.Function.Parameters.(map[string]any); ok { | ||||||
|  | 			claudeTools = append(claudeTools, Tool{ | ||||||
|  | 				Name:        tool.Function.Name, | ||||||
|  | 				Description: tool.Function.Description, | ||||||
|  | 				InputSchema: InputSchema{ | ||||||
|  | 					Type:       params["type"].(string), | ||||||
|  | 					Properties: params["properties"], | ||||||
|  | 					Required:   params["required"], | ||||||
|  | 				}, | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	claudeRequest := Request{ | 	claudeRequest := Request{ | ||||||
| 		Model:       textRequest.Model, | 		Model:       textRequest.Model, | ||||||
| 		MaxTokens:   textRequest.MaxTokens, | 		MaxTokens:   textRequest.MaxTokens, | ||||||
| @@ -40,6 +60,24 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { | |||||||
| 		TopP:        textRequest.TopP, | 		TopP:        textRequest.TopP, | ||||||
| 		TopK:        textRequest.TopK, | 		TopK:        textRequest.TopK, | ||||||
| 		Stream:      textRequest.Stream, | 		Stream:      textRequest.Stream, | ||||||
|  | 		Tools:       claudeTools, | ||||||
|  | 	} | ||||||
|  | 	if len(claudeTools) > 0 { | ||||||
|  | 		claudeToolChoice := struct { | ||||||
|  | 			Type string `json:"type"` | ||||||
|  | 			Name string `json:"name,omitempty"` | ||||||
|  | 		}{Type: "auto"} // default value https://docs.anthropic.com/en/docs/build-with-claude/tool-use#controlling-claudes-output | ||||||
|  | 		if choice, ok := textRequest.ToolChoice.(map[string]any); ok { | ||||||
|  | 			if function, ok := choice["function"].(map[string]any); ok { | ||||||
|  | 				claudeToolChoice.Type = "tool" | ||||||
|  | 				claudeToolChoice.Name = function["name"].(string) | ||||||
|  | 			} | ||||||
|  | 		} else if toolChoiceType, ok := textRequest.ToolChoice.(string); ok { | ||||||
|  | 			if toolChoiceType == "any" { | ||||||
|  | 				claudeToolChoice.Type = toolChoiceType | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		claudeRequest.ToolChoice = claudeToolChoice | ||||||
| 	} | 	} | ||||||
| 	if claudeRequest.MaxTokens == 0 { | 	if claudeRequest.MaxTokens == 0 { | ||||||
| 		claudeRequest.MaxTokens = 4096 | 		claudeRequest.MaxTokens = 4096 | ||||||
| @@ -62,7 +100,24 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { | |||||||
| 		if message.IsStringContent() { | 		if message.IsStringContent() { | ||||||
| 			content.Type = "text" | 			content.Type = "text" | ||||||
| 			content.Text = message.StringContent() | 			content.Text = message.StringContent() | ||||||
|  | 			if message.Role == "tool" { | ||||||
|  | 				claudeMessage.Role = "user" | ||||||
|  | 				content.Type = "tool_result" | ||||||
|  | 				content.Content = content.Text | ||||||
|  | 				content.Text = "" | ||||||
|  | 				content.ToolUseId = message.ToolCallId | ||||||
|  | 			} | ||||||
| 			claudeMessage.Content = append(claudeMessage.Content, content) | 			claudeMessage.Content = append(claudeMessage.Content, content) | ||||||
|  | 			for i := range message.ToolCalls { | ||||||
|  | 				inputParam := make(map[string]any) | ||||||
|  | 				_ = json.Unmarshal([]byte(message.ToolCalls[i].Function.Arguments.(string)), &inputParam) | ||||||
|  | 				claudeMessage.Content = append(claudeMessage.Content, Content{ | ||||||
|  | 					Type:  "tool_use", | ||||||
|  | 					Id:    message.ToolCalls[i].Id, | ||||||
|  | 					Name:  message.ToolCalls[i].Function.Name, | ||||||
|  | 					Input: inputParam, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
| 			claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage) | 			claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| @@ -91,20 +146,39 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { | |||||||
| } | } | ||||||
|  |  | ||||||
| // https://docs.anthropic.com/claude/reference/messages-streaming | // https://docs.anthropic.com/claude/reference/messages-streaming | ||||||
| func streamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { | func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { | ||||||
| 	var response *Response | 	var response *Response | ||||||
| 	var responseText string | 	var responseText string | ||||||
| 	var stopReason string | 	var stopReason string | ||||||
|  | 	tools := make([]model.Tool, 0) | ||||||
|  |  | ||||||
| 	switch claudeResponse.Type { | 	switch claudeResponse.Type { | ||||||
| 	case "message_start": | 	case "message_start": | ||||||
| 		return nil, claudeResponse.Message | 		return nil, claudeResponse.Message | ||||||
| 	case "content_block_start": | 	case "content_block_start": | ||||||
| 		if claudeResponse.ContentBlock != nil { | 		if claudeResponse.ContentBlock != nil { | ||||||
| 			responseText = claudeResponse.ContentBlock.Text | 			responseText = claudeResponse.ContentBlock.Text | ||||||
|  | 			if claudeResponse.ContentBlock.Type == "tool_use" { | ||||||
|  | 				tools = append(tools, model.Tool{ | ||||||
|  | 					Id:   claudeResponse.ContentBlock.Id, | ||||||
|  | 					Type: "function", | ||||||
|  | 					Function: model.Function{ | ||||||
|  | 						Name:      claudeResponse.ContentBlock.Name, | ||||||
|  | 						Arguments: "", | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	case "content_block_delta": | 	case "content_block_delta": | ||||||
| 		if claudeResponse.Delta != nil { | 		if claudeResponse.Delta != nil { | ||||||
| 			responseText = claudeResponse.Delta.Text | 			responseText = claudeResponse.Delta.Text | ||||||
|  | 			if claudeResponse.Delta.Type == "input_json_delta" { | ||||||
|  | 				tools = append(tools, model.Tool{ | ||||||
|  | 					Function: model.Function{ | ||||||
|  | 						Arguments: claudeResponse.Delta.PartialJson, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	case "message_delta": | 	case "message_delta": | ||||||
| 		if claudeResponse.Usage != nil { | 		if claudeResponse.Usage != nil { | ||||||
| @@ -118,6 +192,10 @@ func streamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo | |||||||
| 	} | 	} | ||||||
| 	var choice openai.ChatCompletionsStreamResponseChoice | 	var choice openai.ChatCompletionsStreamResponseChoice | ||||||
| 	choice.Delta.Content = responseText | 	choice.Delta.Content = responseText | ||||||
|  | 	if len(tools) > 0 { | ||||||
|  | 		choice.Delta.Content = nil // compatible with other OpenAI derivative applications, like LobeOpenAICompatibleFactory ... | ||||||
|  | 		choice.Delta.ToolCalls = tools | ||||||
|  | 	} | ||||||
| 	choice.Delta.Role = "assistant" | 	choice.Delta.Role = "assistant" | ||||||
| 	finishReason := stopReasonClaude2OpenAI(&stopReason) | 	finishReason := stopReasonClaude2OpenAI(&stopReason) | ||||||
| 	if finishReason != "null" { | 	if finishReason != "null" { | ||||||
| @@ -129,17 +207,32 @@ func streamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo | |||||||
| 	return &openaiResponse, response | 	return &openaiResponse, response | ||||||
| } | } | ||||||
|  |  | ||||||
| func responseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { | func ResponseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { | ||||||
| 	var responseText string | 	var responseText string | ||||||
| 	if len(claudeResponse.Content) > 0 { | 	if len(claudeResponse.Content) > 0 { | ||||||
| 		responseText = claudeResponse.Content[0].Text | 		responseText = claudeResponse.Content[0].Text | ||||||
| 	} | 	} | ||||||
|  | 	tools := make([]model.Tool, 0) | ||||||
|  | 	for _, v := range claudeResponse.Content { | ||||||
|  | 		if v.Type == "tool_use" { | ||||||
|  | 			args, _ := json.Marshal(v.Input) | ||||||
|  | 			tools = append(tools, model.Tool{ | ||||||
|  | 				Id:   v.Id, | ||||||
|  | 				Type: "function", // compatible with other OpenAI derivative applications | ||||||
|  | 				Function: model.Function{ | ||||||
|  | 					Name:      v.Name, | ||||||
|  | 					Arguments: string(args), | ||||||
|  | 				}, | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	choice := openai.TextResponseChoice{ | 	choice := openai.TextResponseChoice{ | ||||||
| 		Index: 0, | 		Index: 0, | ||||||
| 		Message: model.Message{ | 		Message: model.Message{ | ||||||
| 			Role:      "assistant", | 			Role:      "assistant", | ||||||
| 			Content:   responseText, | 			Content:   responseText, | ||||||
| 			Name:      nil, | 			Name:      nil, | ||||||
|  | 			ToolCalls: tools, | ||||||
| 		}, | 		}, | ||||||
| 		FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason), | 		FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason), | ||||||
| 	} | 	} | ||||||
| @@ -168,64 +261,77 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC | |||||||
| 		} | 		} | ||||||
| 		return 0, nil, nil | 		return 0, nil, nil | ||||||
| 	}) | 	}) | ||||||
| 	dataChan := make(chan string) |  | ||||||
| 	stopChan := make(chan bool) |  | ||||||
| 	go func() { |  | ||||||
| 		for scanner.Scan() { |  | ||||||
| 			data := scanner.Text() |  | ||||||
| 			if len(data) < 6 { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			if !strings.HasPrefix(data, "data: ") { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			data = strings.TrimPrefix(data, "data: ") |  | ||||||
| 			dataChan <- data |  | ||||||
| 		} |  | ||||||
| 		stopChan <- true |  | ||||||
| 	}() |  | ||||||
| 	common.SetEventStreamHeaders(c) | 	common.SetEventStreamHeaders(c) | ||||||
|  |  | ||||||
| 	var usage model.Usage | 	var usage model.Usage | ||||||
| 	var modelName string | 	var modelName string | ||||||
| 	var id string | 	var id string | ||||||
| 	c.Stream(func(w io.Writer) bool { | 	var lastToolCallChoice openai.ChatCompletionsStreamResponseChoice | ||||||
| 		select { |  | ||||||
| 		case data := <-dataChan: | 	for scanner.Scan() { | ||||||
| 			// some implementations may add \r at the end of data | 		data := scanner.Text() | ||||||
| 			data = strings.TrimSuffix(data, "\r") | 		if len(data) < 6 || !strings.HasPrefix(data, "data:") { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		data = strings.TrimPrefix(data, "data:") | ||||||
|  | 		data = strings.TrimSpace(data) | ||||||
|  |  | ||||||
| 		var claudeResponse StreamResponse | 		var claudeResponse StreamResponse | ||||||
| 		err := json.Unmarshal([]byte(data), &claudeResponse) | 		err := json.Unmarshal([]byte(data), &claudeResponse) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logger.SysError("error unmarshalling stream response: " + err.Error()) | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
| 				return true | 			continue | ||||||
| 		} | 		} | ||||||
| 			response, meta := streamResponseClaude2OpenAI(&claudeResponse) |  | ||||||
|  | 		response, meta := StreamResponseClaude2OpenAI(&claudeResponse) | ||||||
| 		if meta != nil { | 		if meta != nil { | ||||||
| 			usage.PromptTokens += meta.Usage.InputTokens | 			usage.PromptTokens += meta.Usage.InputTokens | ||||||
| 			usage.CompletionTokens += meta.Usage.OutputTokens | 			usage.CompletionTokens += meta.Usage.OutputTokens | ||||||
|  | 			if len(meta.Id) > 0 { // only message_start has an id, otherwise it's a finish_reason event. | ||||||
| 				modelName = meta.Model | 				modelName = meta.Model | ||||||
| 				id = fmt.Sprintf("chatcmpl-%s", meta.Id) | 				id = fmt.Sprintf("chatcmpl-%s", meta.Id) | ||||||
| 				return true | 				continue | ||||||
|  | 			} else { // finish_reason case | ||||||
|  | 				if len(lastToolCallChoice.Delta.ToolCalls) > 0 { | ||||||
|  | 					lastArgs := &lastToolCallChoice.Delta.ToolCalls[len(lastToolCallChoice.Delta.ToolCalls)-1].Function | ||||||
|  | 					if len(lastArgs.Arguments.(string)) == 0 { // compatible with OpenAI sending an empty object `{}` when no arguments. | ||||||
|  | 						lastArgs.Arguments = "{}" | ||||||
|  | 						response.Choices[len(response.Choices)-1].Delta.Content = nil | ||||||
|  | 						response.Choices[len(response.Choices)-1].Delta.ToolCalls = lastToolCallChoice.Delta.ToolCalls | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		if response == nil { | 		if response == nil { | ||||||
| 				return true | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		response.Id = id | 		response.Id = id | ||||||
| 		response.Model = modelName | 		response.Model = modelName | ||||||
| 		response.Created = createdTime | 		response.Created = createdTime | ||||||
| 			jsonStr, err := json.Marshal(response) |  | ||||||
|  | 		for _, choice := range response.Choices { | ||||||
|  | 			if len(choice.Delta.ToolCalls) > 0 { | ||||||
|  | 				lastToolCallChoice = choice | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		err = render.ObjectData(c, response) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				logger.SysError("error marshalling stream response: " + err.Error()) | 			logger.SysError(err.Error()) | ||||||
| 				return true |  | ||||||
| 		} | 		} | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) |  | ||||||
| 			return true |  | ||||||
| 		case <-stopChan: |  | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) |  | ||||||
| 			return false |  | ||||||
| 	} | 	} | ||||||
| 	}) |  | ||||||
| 	_ = resp.Body.Close() | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render.Done(c) | ||||||
|  |  | ||||||
|  | 	err := resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
| 	return nil, &usage | 	return nil, &usage | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -254,7 +360,7 @@ func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName st | |||||||
| 			StatusCode: resp.StatusCode, | 			StatusCode: resp.StatusCode, | ||||||
| 		}, nil | 		}, nil | ||||||
| 	} | 	} | ||||||
| 	fullTextResponse := responseClaude2OpenAI(&claudeResponse) | 	fullTextResponse := ResponseClaude2OpenAI(&claudeResponse) | ||||||
| 	fullTextResponse.Model = modelName | 	fullTextResponse.Model = modelName | ||||||
| 	usage := model.Usage{ | 	usage := model.Usage{ | ||||||
| 		PromptTokens:     claudeResponse.Usage.InputTokens, | 		PromptTokens:     claudeResponse.Usage.InputTokens, | ||||||
|   | |||||||
| @@ -16,6 +16,12 @@ type Content struct { | |||||||
| 	Type   string       `json:"type"` | 	Type   string       `json:"type"` | ||||||
| 	Text   string       `json:"text,omitempty"` | 	Text   string       `json:"text,omitempty"` | ||||||
| 	Source *ImageSource `json:"source,omitempty"` | 	Source *ImageSource `json:"source,omitempty"` | ||||||
|  | 	// tool_calls | ||||||
|  | 	Id        string `json:"id,omitempty"` | ||||||
|  | 	Name      string `json:"name,omitempty"` | ||||||
|  | 	Input     any    `json:"input,omitempty"` | ||||||
|  | 	Content   string `json:"content,omitempty"` | ||||||
|  | 	ToolUseId string `json:"tool_use_id,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type Message struct { | type Message struct { | ||||||
| @@ -23,6 +29,18 @@ type Message struct { | |||||||
| 	Content []Content `json:"content"` | 	Content []Content `json:"content"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type Tool struct { | ||||||
|  | 	Name        string      `json:"name"` | ||||||
|  | 	Description string      `json:"description,omitempty"` | ||||||
|  | 	InputSchema InputSchema `json:"input_schema"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type InputSchema struct { | ||||||
|  | 	Type       string `json:"type"` | ||||||
|  | 	Properties any    `json:"properties,omitempty"` | ||||||
|  | 	Required   any    `json:"required,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
| type Request struct { | type Request struct { | ||||||
| 	Model         string    `json:"model"` | 	Model         string    `json:"model"` | ||||||
| 	Messages      []Message `json:"messages"` | 	Messages      []Message `json:"messages"` | ||||||
| @@ -33,6 +51,8 @@ type Request struct { | |||||||
| 	Temperature   float64   `json:"temperature,omitempty"` | 	Temperature   float64   `json:"temperature,omitempty"` | ||||||
| 	TopP          float64   `json:"top_p,omitempty"` | 	TopP          float64   `json:"top_p,omitempty"` | ||||||
| 	TopK          int       `json:"top_k,omitempty"` | 	TopK          int       `json:"top_k,omitempty"` | ||||||
|  | 	Tools         []Tool    `json:"tools,omitempty"` | ||||||
|  | 	ToolChoice    any       `json:"tool_choice,omitempty"` | ||||||
| 	//Metadata    `json:"metadata,omitempty"` | 	//Metadata    `json:"metadata,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -61,6 +81,7 @@ type Response struct { | |||||||
| type Delta struct { | type Delta struct { | ||||||
| 	Type         string  `json:"type"` | 	Type         string  `json:"type"` | ||||||
| 	Text         string  `json:"text"` | 	Text         string  `json:"text"` | ||||||
|  | 	PartialJson  string  `json:"partial_json,omitempty"` | ||||||
| 	StopReason   *string `json:"stop_reason"` | 	StopReason   *string `json:"stop_reason"` | ||||||
| 	StopSequence *string `json:"stop_sequence"` | 	StopSequence *string `json:"stop_sequence"` | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										84
									
								
								relay/adaptor/aws/adaptor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								relay/adaptor/aws/adaptor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/aws" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/credentials" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/aws/utils" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var _ adaptor.Adaptor = new(Adaptor) | ||||||
|  |  | ||||||
|  | type Adaptor struct { | ||||||
|  | 	awsAdapter utils.AwsAdapter | ||||||
|  |  | ||||||
|  | 	Meta      *meta.Meta | ||||||
|  | 	AwsClient *bedrockruntime.Client | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  | 	a.Meta = meta | ||||||
|  | 	a.AwsClient = bedrockruntime.New(bedrockruntime.Options{ | ||||||
|  | 		Region:      meta.Config.Region, | ||||||
|  | 		Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(meta.Config.AK, meta.Config.SK, "")), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	adaptor := GetAdaptor(request.Model) | ||||||
|  | 	if adaptor == nil { | ||||||
|  | 		return nil, errors.New("adaptor not found") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	a.awsAdapter = adaptor | ||||||
|  | 	return adaptor.ConvertRequest(c, relayMode, request) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { | ||||||
|  | 	if a.awsAdapter == nil { | ||||||
|  | 		return nil, utils.WrapErr(errors.New("awsAdapter is nil")) | ||||||
|  | 	} | ||||||
|  | 	return a.awsAdapter.DoResponse(c, a.AwsClient, meta) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetModelList() (models []string) { | ||||||
|  | 	for model := range adaptors { | ||||||
|  | 		models = append(models, model) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetChannelName() string { | ||||||
|  | 	return "aws" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
|  | 	return "", nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	return request, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								relay/adaptor/aws/claude/adapter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								relay/adaptor/aws/claude/adapter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/anthropic" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/aws/utils" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var _ utils.AwsAdapter = new(Adaptor) | ||||||
|  |  | ||||||
|  | type Adaptor struct { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	claudeReq := anthropic.ConvertRequest(*request) | ||||||
|  | 	c.Set(ctxkey.RequestModel, request.Model) | ||||||
|  | 	c.Set(ctxkey.ConvertedRequest, claudeReq) | ||||||
|  | 	return claudeReq, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { | ||||||
|  | 	if meta.IsStream { | ||||||
|  | 		err, usage = StreamHandler(c, awsCli) | ||||||
|  | 	} else { | ||||||
|  | 		err, usage = Handler(c, awsCli, meta.ActualModelName) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										204
									
								
								relay/adaptor/aws/claude/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								relay/adaptor/aws/claude/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,204 @@ | |||||||
|  | // Package aws provides the AWS adaptor for the relay service. | ||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/aws" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/jinzhu/copier" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/anthropic" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/aws/utils" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	relaymodel "github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html | ||||||
|  | var AwsModelIDMap = map[string]string{ | ||||||
|  | 	"claude-instant-1.2":         "anthropic.claude-instant-v1", | ||||||
|  | 	"claude-2.0":                 "anthropic.claude-v2", | ||||||
|  | 	"claude-2.1":                 "anthropic.claude-v2:1", | ||||||
|  | 	"claude-3-sonnet-20240229":   "anthropic.claude-3-sonnet-20240229-v1:0", | ||||||
|  | 	"claude-3-5-sonnet-20240620": "anthropic.claude-3-5-sonnet-20240620-v1:0", | ||||||
|  | 	"claude-3-opus-20240229":     "anthropic.claude-3-opus-20240229-v1:0", | ||||||
|  | 	"claude-3-haiku-20240307":    "anthropic.claude-3-haiku-20240307-v1:0", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func awsModelID(requestModel string) (string, error) { | ||||||
|  | 	if awsModelID, ok := AwsModelIDMap[requestModel]; ok { | ||||||
|  | 		return awsModelID, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", errors.Errorf("model %s not found", requestModel) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { | ||||||
|  | 	awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsReq := &bedrockruntime.InvokeModelInput{ | ||||||
|  | 		ModelId:     aws.String(awsModelId), | ||||||
|  | 		Accept:      aws.String("application/json"), | ||||||
|  | 		ContentType: aws.String("application/json"), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	claudeReq_, ok := c.Get(ctxkey.ConvertedRequest) | ||||||
|  | 	if !ok { | ||||||
|  | 		return utils.WrapErr(errors.New("request not found")), nil | ||||||
|  | 	} | ||||||
|  | 	claudeReq := claudeReq_.(*anthropic.Request) | ||||||
|  | 	awsClaudeReq := &Request{ | ||||||
|  | 		AnthropicVersion: "bedrock-2023-05-31", | ||||||
|  | 	} | ||||||
|  | 	if err = copier.Copy(awsClaudeReq, claudeReq); err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "copy request")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsReq.Body, err = json.Marshal(awsClaudeReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "marshal request")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "InvokeModel")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	claudeResponse := new(anthropic.Response) | ||||||
|  | 	err = json.Unmarshal(awsResp.Body, claudeResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "unmarshal response")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	openaiResp := anthropic.ResponseClaude2OpenAI(claudeResponse) | ||||||
|  | 	openaiResp.Model = modelName | ||||||
|  | 	usage := relaymodel.Usage{ | ||||||
|  | 		PromptTokens:     claudeResponse.Usage.InputTokens, | ||||||
|  | 		CompletionTokens: claudeResponse.Usage.OutputTokens, | ||||||
|  | 		TotalTokens:      claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens, | ||||||
|  | 	} | ||||||
|  | 	openaiResp.Usage = usage | ||||||
|  |  | ||||||
|  | 	c.JSON(http.StatusOK, openaiResp) | ||||||
|  | 	return nil, &usage | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { | ||||||
|  | 	createdTime := helper.GetTimestamp() | ||||||
|  | 	awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{ | ||||||
|  | 		ModelId:     aws.String(awsModelId), | ||||||
|  | 		Accept:      aws.String("application/json"), | ||||||
|  | 		ContentType: aws.String("application/json"), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	claudeReq_, ok := c.Get(ctxkey.ConvertedRequest) | ||||||
|  | 	if !ok { | ||||||
|  | 		return utils.WrapErr(errors.New("request not found")), nil | ||||||
|  | 	} | ||||||
|  | 	claudeReq := claudeReq_.(*anthropic.Request) | ||||||
|  |  | ||||||
|  | 	awsClaudeReq := &Request{ | ||||||
|  | 		AnthropicVersion: "bedrock-2023-05-31", | ||||||
|  | 	} | ||||||
|  | 	if err = copier.Copy(awsClaudeReq, claudeReq); err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "copy request")), nil | ||||||
|  | 	} | ||||||
|  | 	awsReq.Body, err = json.Marshal(awsClaudeReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "marshal request")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "InvokeModelWithResponseStream")), nil | ||||||
|  | 	} | ||||||
|  | 	stream := awsResp.GetStream() | ||||||
|  | 	defer stream.Close() | ||||||
|  |  | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "text/event-stream") | ||||||
|  | 	var usage relaymodel.Usage | ||||||
|  | 	var id string | ||||||
|  | 	var lastToolCallChoice openai.ChatCompletionsStreamResponseChoice | ||||||
|  |  | ||||||
|  | 	c.Stream(func(w io.Writer) bool { | ||||||
|  | 		event, ok := <-stream.Events() | ||||||
|  | 		if !ok { | ||||||
|  | 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		switch v := event.(type) { | ||||||
|  | 		case *types.ResponseStreamMemberChunk: | ||||||
|  | 			claudeResp := new(anthropic.StreamResponse) | ||||||
|  | 			err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(claudeResp) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			response, meta := anthropic.StreamResponseClaude2OpenAI(claudeResp) | ||||||
|  | 			if meta != nil { | ||||||
|  | 				usage.PromptTokens += meta.Usage.InputTokens | ||||||
|  | 				usage.CompletionTokens += meta.Usage.OutputTokens | ||||||
|  | 				if len(meta.Id) > 0 { // only message_start has an id, otherwise it's a finish_reason event. | ||||||
|  | 					id = fmt.Sprintf("chatcmpl-%s", meta.Id) | ||||||
|  | 					return true | ||||||
|  | 				} else { // finish_reason case | ||||||
|  | 					if len(lastToolCallChoice.Delta.ToolCalls) > 0 { | ||||||
|  | 						lastArgs := &lastToolCallChoice.Delta.ToolCalls[len(lastToolCallChoice.Delta.ToolCalls)-1].Function | ||||||
|  | 						if len(lastArgs.Arguments.(string)) == 0 { // compatible with OpenAI sending an empty object `{}` when no arguments. | ||||||
|  | 							lastArgs.Arguments = "{}" | ||||||
|  | 							response.Choices[len(response.Choices)-1].Delta.Content = nil | ||||||
|  | 							response.Choices[len(response.Choices)-1].Delta.ToolCalls = lastToolCallChoice.Delta.ToolCalls | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if response == nil { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 			response.Id = id | ||||||
|  | 			response.Model = c.GetString(ctxkey.OriginalModel) | ||||||
|  | 			response.Created = createdTime | ||||||
|  |  | ||||||
|  | 			for _, choice := range response.Choices { | ||||||
|  | 				if len(choice.Delta.ToolCalls) > 0 { | ||||||
|  | 					lastToolCallChoice = choice | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			jsonStr, err := json.Marshal(response) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.SysError("error marshalling stream response: " + err.Error()) | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) | ||||||
|  | 			return true | ||||||
|  | 		case *types.UnknownUnionMember: | ||||||
|  | 			fmt.Println("unknown tag:", v.Tag) | ||||||
|  | 			return false | ||||||
|  | 		default: | ||||||
|  | 			fmt.Println("union is nil or unknown type") | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return nil, &usage | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								relay/adaptor/aws/claude/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								relay/adaptor/aws/claude/model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import "github.com/songquanpeng/one-api/relay/adaptor/anthropic" | ||||||
|  |  | ||||||
|  | // Request is the request to AWS Claude | ||||||
|  | // | ||||||
|  | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html | ||||||
|  | type Request struct { | ||||||
|  | 	// AnthropicVersion should be "bedrock-2023-05-31" | ||||||
|  | 	AnthropicVersion string              `json:"anthropic_version"` | ||||||
|  | 	Messages         []anthropic.Message `json:"messages"` | ||||||
|  | 	System           string              `json:"system,omitempty"` | ||||||
|  | 	MaxTokens        int                 `json:"max_tokens,omitempty"` | ||||||
|  | 	Temperature      float64             `json:"temperature,omitempty"` | ||||||
|  | 	TopP             float64             `json:"top_p,omitempty"` | ||||||
|  | 	TopK             int                 `json:"top_k,omitempty"` | ||||||
|  | 	StopSequences    []string            `json:"stop_sequences,omitempty"` | ||||||
|  | 	Tools            []anthropic.Tool    `json:"tools,omitempty"` | ||||||
|  | 	ToolChoice       any                 `json:"tool_choice,omitempty"` | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								relay/adaptor/aws/llama3/adapter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								relay/adaptor/aws/llama3/adapter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/aws/utils" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var _ utils.AwsAdapter = new(Adaptor) | ||||||
|  |  | ||||||
|  | type Adaptor struct { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	llamaReq := ConvertRequest(*request) | ||||||
|  | 	c.Set(ctxkey.RequestModel, request.Model) | ||||||
|  | 	c.Set(ctxkey.ConvertedRequest, llamaReq) | ||||||
|  | 	return llamaReq, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { | ||||||
|  | 	if meta.IsStream { | ||||||
|  | 		err, usage = StreamHandler(c, awsCli) | ||||||
|  | 	} else { | ||||||
|  | 		err, usage = Handler(c, awsCli, meta.ActualModelName) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										231
									
								
								relay/adaptor/aws/llama3/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								relay/adaptor/aws/llama3/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | |||||||
|  | // Package aws provides the AWS adaptor for the relay service. | ||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"text/template" | ||||||
|  |  | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/random" | ||||||
|  |  | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/aws" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/aws/utils" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	relaymodel "github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Only support llama-3-8b and llama-3-70b instruction models | ||||||
|  | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html | ||||||
|  | var AwsModelIDMap = map[string]string{ | ||||||
|  | 	"llama3-8b-8192":  "meta.llama3-8b-instruct-v1:0", | ||||||
|  | 	"llama3-70b-8192": "meta.llama3-70b-instruct-v1:0", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func awsModelID(requestModel string) (string, error) { | ||||||
|  | 	if awsModelID, ok := AwsModelIDMap[requestModel]; ok { | ||||||
|  | 		return awsModelID, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", errors.Errorf("model %s not found", requestModel) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // promptTemplate with range | ||||||
|  | const promptTemplate = `<|begin_of_text|>{{range .Messages}}<|start_header_id|>{{.Role}}<|end_header_id|>{{.StringContent}}<|eot_id|>{{end}}<|start_header_id|>assistant<|end_header_id|> | ||||||
|  | ` | ||||||
|  |  | ||||||
|  | var promptTpl = template.Must(template.New("llama3-chat").Parse(promptTemplate)) | ||||||
|  |  | ||||||
|  | func RenderPrompt(messages []relaymodel.Message) string { | ||||||
|  | 	var buf bytes.Buffer | ||||||
|  | 	err := promptTpl.Execute(&buf, struct{ Messages []relaymodel.Message }{messages}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.SysError("error rendering prompt messages: " + err.Error()) | ||||||
|  | 	} | ||||||
|  | 	return buf.String() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ConvertRequest(textRequest relaymodel.GeneralOpenAIRequest) *Request { | ||||||
|  | 	llamaRequest := Request{ | ||||||
|  | 		MaxGenLen:   textRequest.MaxTokens, | ||||||
|  | 		Temperature: textRequest.Temperature, | ||||||
|  | 		TopP:        textRequest.TopP, | ||||||
|  | 	} | ||||||
|  | 	if llamaRequest.MaxGenLen == 0 { | ||||||
|  | 		llamaRequest.MaxGenLen = 2048 | ||||||
|  | 	} | ||||||
|  | 	prompt := RenderPrompt(textRequest.Messages) | ||||||
|  | 	llamaRequest.Prompt = prompt | ||||||
|  | 	return &llamaRequest | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { | ||||||
|  | 	awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsReq := &bedrockruntime.InvokeModelInput{ | ||||||
|  | 		ModelId:     aws.String(awsModelId), | ||||||
|  | 		Accept:      aws.String("application/json"), | ||||||
|  | 		ContentType: aws.String("application/json"), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	llamaReq, ok := c.Get(ctxkey.ConvertedRequest) | ||||||
|  | 	if !ok { | ||||||
|  | 		return utils.WrapErr(errors.New("request not found")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsReq.Body, err = json.Marshal(llamaReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "marshal request")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "InvokeModel")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var llamaResponse Response | ||||||
|  | 	err = json.Unmarshal(awsResp.Body, &llamaResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "unmarshal response")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	openaiResp := ResponseLlama2OpenAI(&llamaResponse) | ||||||
|  | 	openaiResp.Model = modelName | ||||||
|  | 	usage := relaymodel.Usage{ | ||||||
|  | 		PromptTokens:     llamaResponse.PromptTokenCount, | ||||||
|  | 		CompletionTokens: llamaResponse.GenerationTokenCount, | ||||||
|  | 		TotalTokens:      llamaResponse.PromptTokenCount + llamaResponse.GenerationTokenCount, | ||||||
|  | 	} | ||||||
|  | 	openaiResp.Usage = usage | ||||||
|  |  | ||||||
|  | 	c.JSON(http.StatusOK, openaiResp) | ||||||
|  | 	return nil, &usage | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ResponseLlama2OpenAI(llamaResponse *Response) *openai.TextResponse { | ||||||
|  | 	var responseText string | ||||||
|  | 	if len(llamaResponse.Generation) > 0 { | ||||||
|  | 		responseText = llamaResponse.Generation | ||||||
|  | 	} | ||||||
|  | 	choice := openai.TextResponseChoice{ | ||||||
|  | 		Index: 0, | ||||||
|  | 		Message: relaymodel.Message{ | ||||||
|  | 			Role:    "assistant", | ||||||
|  | 			Content: responseText, | ||||||
|  | 			Name:    nil, | ||||||
|  | 		}, | ||||||
|  | 		FinishReason: llamaResponse.StopReason, | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := openai.TextResponse{ | ||||||
|  | 		Id:      fmt.Sprintf("chatcmpl-%s", random.GetUUID()), | ||||||
|  | 		Object:  "chat.completion", | ||||||
|  | 		Created: helper.GetTimestamp(), | ||||||
|  | 		Choices: []openai.TextResponseChoice{choice}, | ||||||
|  | 	} | ||||||
|  | 	return &fullTextResponse | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { | ||||||
|  | 	createdTime := helper.GetTimestamp() | ||||||
|  | 	awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "awsModelID")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{ | ||||||
|  | 		ModelId:     aws.String(awsModelId), | ||||||
|  | 		Accept:      aws.String("application/json"), | ||||||
|  | 		ContentType: aws.String("application/json"), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	llamaReq, ok := c.Get(ctxkey.ConvertedRequest) | ||||||
|  | 	if !ok { | ||||||
|  | 		return utils.WrapErr(errors.New("request not found")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsReq.Body, err = json.Marshal(llamaReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "marshal request")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return utils.WrapErr(errors.Wrap(err, "InvokeModelWithResponseStream")), nil | ||||||
|  | 	} | ||||||
|  | 	stream := awsResp.GetStream() | ||||||
|  | 	defer stream.Close() | ||||||
|  |  | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "text/event-stream") | ||||||
|  | 	var usage relaymodel.Usage | ||||||
|  | 	c.Stream(func(w io.Writer) bool { | ||||||
|  | 		event, ok := <-stream.Events() | ||||||
|  | 		if !ok { | ||||||
|  | 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		switch v := event.(type) { | ||||||
|  | 		case *types.ResponseStreamMemberChunk: | ||||||
|  | 			var llamaResp StreamResponse | ||||||
|  | 			err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(&llamaResp) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if llamaResp.PromptTokenCount > 0 { | ||||||
|  | 				usage.PromptTokens = llamaResp.PromptTokenCount | ||||||
|  | 			} | ||||||
|  | 			if llamaResp.StopReason == "stop" { | ||||||
|  | 				usage.CompletionTokens = llamaResp.GenerationTokenCount | ||||||
|  | 				usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens | ||||||
|  | 			} | ||||||
|  | 			response := StreamResponseLlama2OpenAI(&llamaResp) | ||||||
|  | 			response.Id = fmt.Sprintf("chatcmpl-%s", random.GetUUID()) | ||||||
|  | 			response.Model = c.GetString(ctxkey.OriginalModel) | ||||||
|  | 			response.Created = createdTime | ||||||
|  | 			jsonStr, err := json.Marshal(response) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.SysError("error marshalling stream response: " + err.Error()) | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) | ||||||
|  | 			return true | ||||||
|  | 		case *types.UnknownUnionMember: | ||||||
|  | 			fmt.Println("unknown tag:", v.Tag) | ||||||
|  | 			return false | ||||||
|  | 		default: | ||||||
|  | 			fmt.Println("union is nil or unknown type") | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return nil, &usage | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamResponseLlama2OpenAI(llamaResponse *StreamResponse) *openai.ChatCompletionsStreamResponse { | ||||||
|  | 	var choice openai.ChatCompletionsStreamResponseChoice | ||||||
|  | 	choice.Delta.Content = llamaResponse.Generation | ||||||
|  | 	choice.Delta.Role = "assistant" | ||||||
|  | 	finishReason := llamaResponse.StopReason | ||||||
|  | 	if finishReason != "null" { | ||||||
|  | 		choice.FinishReason = &finishReason | ||||||
|  | 	} | ||||||
|  | 	var openaiResponse openai.ChatCompletionsStreamResponse | ||||||
|  | 	openaiResponse.Object = "chat.completion.chunk" | ||||||
|  | 	openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} | ||||||
|  | 	return &openaiResponse | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								relay/adaptor/aws/llama3/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								relay/adaptor/aws/llama3/main_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | package aws_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	aws "github.com/songquanpeng/one-api/relay/adaptor/aws/llama3" | ||||||
|  | 	relaymodel "github.com/songquanpeng/one-api/relay/model" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestRenderPrompt(t *testing.T) { | ||||||
|  | 	messages := []relaymodel.Message{ | ||||||
|  | 		{ | ||||||
|  | 			Role:    "user", | ||||||
|  | 			Content: "What's your name?", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	prompt := aws.RenderPrompt(messages) | ||||||
|  | 	expected := `<|begin_of_text|><|start_header_id|>user<|end_header_id|>What's your name?<|eot_id|><|start_header_id|>assistant<|end_header_id|> | ||||||
|  | ` | ||||||
|  | 	assert.Equal(t, expected, prompt) | ||||||
|  |  | ||||||
|  | 	messages = []relaymodel.Message{ | ||||||
|  | 		{ | ||||||
|  | 			Role:    "system", | ||||||
|  | 			Content: "Your name is Kat. You are a detective.", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Role:    "user", | ||||||
|  | 			Content: "What's your name?", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Role:    "assistant", | ||||||
|  | 			Content: "Kat", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Role:    "user", | ||||||
|  | 			Content: "What's your job?", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	prompt = aws.RenderPrompt(messages) | ||||||
|  | 	expected = `<|begin_of_text|><|start_header_id|>system<|end_header_id|>Your name is Kat. You are a detective.<|eot_id|><|start_header_id|>user<|end_header_id|>What's your name?<|eot_id|><|start_header_id|>assistant<|end_header_id|>Kat<|eot_id|><|start_header_id|>user<|end_header_id|>What's your job?<|eot_id|><|start_header_id|>assistant<|end_header_id|> | ||||||
|  | ` | ||||||
|  | 	assert.Equal(t, expected, prompt) | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								relay/adaptor/aws/llama3/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								relay/adaptor/aws/llama3/model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | package aws | ||||||
|  |  | ||||||
|  | // Request is the request to AWS Llama3 | ||||||
|  | // | ||||||
|  | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html | ||||||
|  | type Request struct { | ||||||
|  | 	Prompt      string  `json:"prompt"` | ||||||
|  | 	MaxGenLen   int     `json:"max_gen_len,omitempty"` | ||||||
|  | 	Temperature float64 `json:"temperature,omitempty"` | ||||||
|  | 	TopP        float64 `json:"top_p,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Response is the response from AWS Llama3 | ||||||
|  | // | ||||||
|  | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html | ||||||
|  | type Response struct { | ||||||
|  | 	Generation           string `json:"generation"` | ||||||
|  | 	PromptTokenCount     int    `json:"prompt_token_count"` | ||||||
|  | 	GenerationTokenCount int    `json:"generation_token_count"` | ||||||
|  | 	StopReason           string `json:"stop_reason"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // {'generation': 'Hi', 'prompt_token_count': 15, 'generation_token_count': 1, 'stop_reason': None} | ||||||
|  | type StreamResponse struct { | ||||||
|  | 	Generation           string `json:"generation"` | ||||||
|  | 	PromptTokenCount     int    `json:"prompt_token_count"` | ||||||
|  | 	GenerationTokenCount int    `json:"generation_token_count"` | ||||||
|  | 	StopReason           string `json:"stop_reason"` | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								relay/adaptor/aws/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								relay/adaptor/aws/registry.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	claude "github.com/songquanpeng/one-api/relay/adaptor/aws/claude" | ||||||
|  | 	llama3 "github.com/songquanpeng/one-api/relay/adaptor/aws/llama3" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/aws/utils" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type AwsModelType int | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	AwsClaude AwsModelType = iota + 1 | ||||||
|  | 	AwsLlama3 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	adaptors = map[string]AwsModelType{} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	for model := range claude.AwsModelIDMap { | ||||||
|  | 		adaptors[model] = AwsClaude | ||||||
|  | 	} | ||||||
|  | 	for model := range llama3.AwsModelIDMap { | ||||||
|  | 		adaptors[model] = AwsLlama3 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetAdaptor(model string) utils.AwsAdapter { | ||||||
|  | 	adaptorType := adaptors[model] | ||||||
|  | 	switch adaptorType { | ||||||
|  | 	case AwsClaude: | ||||||
|  | 		return &claude.Adaptor{} | ||||||
|  | 	case AwsLlama3: | ||||||
|  | 		return &llama3.Adaptor{} | ||||||
|  | 	default: | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								relay/adaptor/aws/utils/adaptor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								relay/adaptor/aws/utils/adaptor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/aws" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/credentials" | ||||||
|  | 	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type AwsAdapter interface { | ||||||
|  | 	ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) | ||||||
|  | 	DoResponse(c *gin.Context, awsCli *bedrockruntime.Client, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Adaptor struct { | ||||||
|  | 	Meta      *meta.Meta | ||||||
|  | 	AwsClient *bedrockruntime.Client | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  | 	a.Meta = meta | ||||||
|  | 	a.AwsClient = bedrockruntime.New(bedrockruntime.Options{ | ||||||
|  | 		Region:      meta.Config.Region, | ||||||
|  | 		Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(meta.Config.AK, meta.Config.SK, "")), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
|  | 	return "", nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	return request, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								relay/adaptor/aws/utils/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								relay/adaptor/aws/utils/utils.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	relaymodel "github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func WrapErr(err error) *relaymodel.ErrorWithStatusCode { | ||||||
|  | 	return &relaymodel.ErrorWithStatusCode{ | ||||||
|  | 		StatusCode: http.StatusInternalServerError, | ||||||
|  | 		Error: relaymodel.Error{ | ||||||
|  | 			Message: err.Error(), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| package azure |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"github.com/songquanpeng/one-api/common/config" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func GetAPIVersion(c *gin.Context) string { |  | ||||||
| 	query := c.Request.URL.Query() |  | ||||||
| 	apiVersion := query.Get("api-version") |  | ||||||
| 	if apiVersion == "" { |  | ||||||
| 		apiVersion = c.GetString(config.KeyAPIVersion) |  | ||||||
| 	} |  | ||||||
| 	return apiVersion |  | ||||||
| } |  | ||||||
| @@ -5,18 +5,20 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/songquanpeng/one-api/common/render" | ||||||
| 	"github.com/songquanpeng/one-api/common" |  | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" |  | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" |  | ||||||
| 	"github.com/songquanpeng/one-api/relay/client" |  | ||||||
| 	"github.com/songquanpeng/one-api/relay/constant" |  | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/client" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/constant" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2 | // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2 | ||||||
| @@ -137,40 +139,22 @@ func embeddingResponseBaidu2OpenAI(response *EmbeddingResponse) *openai.Embeddin | |||||||
| func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { | func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { | ||||||
| 	var usage model.Usage | 	var usage model.Usage | ||||||
| 	scanner := bufio.NewScanner(resp.Body) | 	scanner := bufio.NewScanner(resp.Body) | ||||||
| 	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | 	scanner.Split(bufio.ScanLines) | ||||||
| 		if atEOF && len(data) == 0 { |  | ||||||
| 			return 0, nil, nil | 	common.SetEventStreamHeaders(c) | ||||||
| 		} |  | ||||||
| 		if i := strings.Index(string(data), "\n"); i >= 0 { |  | ||||||
| 			return i + 1, data[0:i], nil |  | ||||||
| 		} |  | ||||||
| 		if atEOF { |  | ||||||
| 			return len(data), data, nil |  | ||||||
| 		} |  | ||||||
| 		return 0, nil, nil |  | ||||||
| 	}) |  | ||||||
| 	dataChan := make(chan string) |  | ||||||
| 	stopChan := make(chan bool) |  | ||||||
| 	go func() { |  | ||||||
| 	for scanner.Scan() { | 	for scanner.Scan() { | ||||||
| 		data := scanner.Text() | 		data := scanner.Text() | ||||||
| 			if len(data) < 6 { // ignore blank line or wrong format | 		if len(data) < 6 { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		data = data[6:] | 		data = data[6:] | ||||||
| 			dataChan <- data |  | ||||||
| 		} |  | ||||||
| 		stopChan <- true |  | ||||||
| 	}() |  | ||||||
| 	common.SetEventStreamHeaders(c) |  | ||||||
| 	c.Stream(func(w io.Writer) bool { |  | ||||||
| 		select { |  | ||||||
| 		case data := <-dataChan: |  | ||||||
| 		var baiduResponse ChatStreamResponse | 		var baiduResponse ChatStreamResponse | ||||||
| 		err := json.Unmarshal([]byte(data), &baiduResponse) | 		err := json.Unmarshal([]byte(data), &baiduResponse) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logger.SysError("error unmarshalling stream response: " + err.Error()) | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
| 				return true | 			continue | ||||||
| 		} | 		} | ||||||
| 		if baiduResponse.Usage.TotalTokens != 0 { | 		if baiduResponse.Usage.TotalTokens != 0 { | ||||||
| 			usage.TotalTokens = baiduResponse.Usage.TotalTokens | 			usage.TotalTokens = baiduResponse.Usage.TotalTokens | ||||||
| @@ -178,18 +162,18 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC | |||||||
| 			usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens | 			usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens | ||||||
| 		} | 		} | ||||||
| 		response := streamResponseBaidu2OpenAI(&baiduResponse) | 		response := streamResponseBaidu2OpenAI(&baiduResponse) | ||||||
| 			jsonResponse, err := json.Marshal(response) | 		err = render.ObjectData(c, response) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				logger.SysError("error marshalling stream response: " + err.Error()) | 			logger.SysError(err.Error()) | ||||||
| 				return true |  | ||||||
| 		} | 		} | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) |  | ||||||
| 			return true |  | ||||||
| 		case <-stopChan: |  | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) |  | ||||||
| 			return false |  | ||||||
| 	} | 	} | ||||||
| 	}) |  | ||||||
|  | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render.Done(c) | ||||||
|  |  | ||||||
| 	err := resp.Body.Close() | 	err := resp.Body.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|   | |||||||
							
								
								
									
										100
									
								
								relay/adaptor/cloudflare/adaptor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								relay/adaptor/cloudflare/adaptor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | package cloudflare | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/relaymode" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Adaptor struct { | ||||||
|  | 	meta *meta.Meta | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ConvertImageRequest implements adaptor.Adaptor. | ||||||
|  | func (*Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | ||||||
|  | 	return nil, errors.New("not implemented") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ConvertImageRequest implements adaptor.Adaptor. | ||||||
|  |  | ||||||
|  | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  | 	a.meta = meta | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WorkerAI cannot be used across accounts with AIGateWay | ||||||
|  | // https://developers.cloudflare.com/ai-gateway/providers/workersai/#openai-compatible-endpoints | ||||||
|  | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/workers-ai | ||||||
|  | func (a *Adaptor) isAIGateWay(baseURL string) bool { | ||||||
|  | 	return strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") && strings.HasSuffix(baseURL, "/workers-ai") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
|  | 	isAIGateWay := a.isAIGateWay(meta.BaseURL) | ||||||
|  | 	var urlPrefix string | ||||||
|  | 	if isAIGateWay { | ||||||
|  | 		urlPrefix = meta.BaseURL | ||||||
|  | 	} else { | ||||||
|  | 		urlPrefix = fmt.Sprintf("%s/client/v4/accounts/%s/ai", meta.BaseURL, meta.Config.UserID) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch meta.Mode { | ||||||
|  | 	case relaymode.ChatCompletions: | ||||||
|  | 		return fmt.Sprintf("%s/v1/chat/completions", urlPrefix), nil | ||||||
|  | 	case relaymode.Embeddings: | ||||||
|  | 		return fmt.Sprintf("%s/v1/embeddings", urlPrefix), nil | ||||||
|  | 	default: | ||||||
|  | 		if isAIGateWay { | ||||||
|  | 			return fmt.Sprintf("%s/%s", urlPrefix, meta.ActualModelName), nil | ||||||
|  | 		} | ||||||
|  | 		return fmt.Sprintf("%s/run/%s", urlPrefix, meta.ActualModelName), nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { | ||||||
|  | 	adaptor.SetupCommonRequestHeader(c, req, meta) | ||||||
|  | 	req.Header.Set("Authorization", "Bearer "+meta.APIKey) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	switch relayMode { | ||||||
|  | 	case relaymode.Completions: | ||||||
|  | 		return ConvertCompletionsRequest(*request), nil | ||||||
|  | 	case relaymode.ChatCompletions, relaymode.Embeddings: | ||||||
|  | 		return request, nil | ||||||
|  | 	default: | ||||||
|  | 		return nil, errors.New("not implemented") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { | ||||||
|  | 	return adaptor.DoRequestHelper(a, c, meta, requestBody) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { | ||||||
|  | 	if meta.IsStream { | ||||||
|  | 		err, usage = StreamHandler(c, resp, meta.PromptTokens, meta.ActualModelName) | ||||||
|  | 	} else { | ||||||
|  | 		err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetModelList() []string { | ||||||
|  | 	return ModelList | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetChannelName() string { | ||||||
|  | 	return "cloudflare" | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								relay/adaptor/cloudflare/constant.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								relay/adaptor/cloudflare/constant.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | package cloudflare | ||||||
|  |  | ||||||
|  | var ModelList = []string{ | ||||||
|  | 	"@cf/meta/llama-2-7b-chat-fp16", | ||||||
|  | 	"@cf/meta/llama-2-7b-chat-int8", | ||||||
|  | 	"@cf/mistral/mistral-7b-instruct-v0.1", | ||||||
|  | 	"@hf/thebloke/deepseek-coder-6.7b-base-awq", | ||||||
|  | 	"@hf/thebloke/deepseek-coder-6.7b-instruct-awq", | ||||||
|  | 	"@cf/deepseek-ai/deepseek-math-7b-base", | ||||||
|  | 	"@cf/deepseek-ai/deepseek-math-7b-instruct", | ||||||
|  | 	"@cf/thebloke/discolm-german-7b-v1-awq", | ||||||
|  | 	"@cf/tiiuae/falcon-7b-instruct", | ||||||
|  | 	"@cf/google/gemma-2b-it-lora", | ||||||
|  | 	"@hf/google/gemma-7b-it", | ||||||
|  | 	"@cf/google/gemma-7b-it-lora", | ||||||
|  | 	"@hf/nousresearch/hermes-2-pro-mistral-7b", | ||||||
|  | 	"@hf/thebloke/llama-2-13b-chat-awq", | ||||||
|  | 	"@cf/meta-llama/llama-2-7b-chat-hf-lora", | ||||||
|  | 	"@cf/meta/llama-3-8b-instruct", | ||||||
|  | 	"@hf/thebloke/llamaguard-7b-awq", | ||||||
|  | 	"@hf/thebloke/mistral-7b-instruct-v0.1-awq", | ||||||
|  | 	"@hf/mistralai/mistral-7b-instruct-v0.2", | ||||||
|  | 	"@cf/mistral/mistral-7b-instruct-v0.2-lora", | ||||||
|  | 	"@hf/thebloke/neural-chat-7b-v3-1-awq", | ||||||
|  | 	"@cf/openchat/openchat-3.5-0106", | ||||||
|  | 	"@hf/thebloke/openhermes-2.5-mistral-7b-awq", | ||||||
|  | 	"@cf/microsoft/phi-2", | ||||||
|  | 	"@cf/qwen/qwen1.5-0.5b-chat", | ||||||
|  | 	"@cf/qwen/qwen1.5-1.8b-chat", | ||||||
|  | 	"@cf/qwen/qwen1.5-14b-chat-awq", | ||||||
|  | 	"@cf/qwen/qwen1.5-7b-chat-awq", | ||||||
|  | 	"@cf/defog/sqlcoder-7b-2", | ||||||
|  | 	"@hf/nexusflow/starling-lm-7b-beta", | ||||||
|  | 	"@cf/tinyllama/tinyllama-1.1b-chat-v1.0", | ||||||
|  | 	"@hf/thebloke/zephyr-7b-beta-awq", | ||||||
|  | } | ||||||
							
								
								
									
										115
									
								
								relay/adaptor/cloudflare/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								relay/adaptor/cloudflare/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | package cloudflare | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/songquanpeng/one-api/common/ctxkey" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/render" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func ConvertCompletionsRequest(textRequest model.GeneralOpenAIRequest) *Request { | ||||||
|  | 	p, _ := textRequest.Prompt.(string) | ||||||
|  | 	return &Request{ | ||||||
|  | 		Prompt:      p, | ||||||
|  | 		MaxTokens:   textRequest.MaxTokens, | ||||||
|  | 		Stream:      textRequest.Stream, | ||||||
|  | 		Temperature: textRequest.Temperature, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamHandler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { | ||||||
|  | 	scanner := bufio.NewScanner(resp.Body) | ||||||
|  | 	scanner.Split(bufio.ScanLines) | ||||||
|  |  | ||||||
|  | 	common.SetEventStreamHeaders(c) | ||||||
|  | 	id := helper.GetResponseID(c) | ||||||
|  | 	responseModel := c.GetString(ctxkey.OriginalModel) | ||||||
|  | 	var responseText string | ||||||
|  |  | ||||||
|  | 	for scanner.Scan() { | ||||||
|  | 		data := scanner.Text() | ||||||
|  | 		if len(data) < len("data: ") { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		data = strings.TrimPrefix(data, "data: ") | ||||||
|  | 		data = strings.TrimSuffix(data, "\r") | ||||||
|  |  | ||||||
|  | 		if data == "[DONE]" { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var response openai.ChatCompletionsStreamResponse | ||||||
|  | 		err := json.Unmarshal([]byte(data), &response) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for _, v := range response.Choices { | ||||||
|  | 			v.Delta.Role = "assistant" | ||||||
|  | 			responseText += v.Delta.StringContent() | ||||||
|  | 		} | ||||||
|  | 		response.Id = id | ||||||
|  | 		response.Model = modelName | ||||||
|  | 		err = render.ObjectData(c, response) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.SysError(err.Error()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render.Done(c) | ||||||
|  |  | ||||||
|  | 	err := resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	usage := openai.ResponseText2Usage(responseText, responseModel, promptTokens) | ||||||
|  | 	return nil, usage | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { | ||||||
|  | 	responseBody, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	err = resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	var response openai.TextResponse | ||||||
|  | 	err = json.Unmarshal(responseBody, &response) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	response.Model = modelName | ||||||
|  | 	var responseText string | ||||||
|  | 	for _, v := range response.Choices { | ||||||
|  | 		responseText += v.Message.Content.(string) | ||||||
|  | 	} | ||||||
|  | 	usage := openai.ResponseText2Usage(responseText, modelName, promptTokens) | ||||||
|  | 	response.Usage = *usage | ||||||
|  | 	response.Id = helper.GetResponseID(c) | ||||||
|  | 	jsonResponse, err := json.Marshal(response) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "application/json") | ||||||
|  | 	c.Writer.WriteHeader(resp.StatusCode) | ||||||
|  | 	_, _ = c.Writer.Write(jsonResponse) | ||||||
|  | 	return nil, usage | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								relay/adaptor/cloudflare/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								relay/adaptor/cloudflare/model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | package cloudflare | ||||||
|  |  | ||||||
|  | import "github.com/songquanpeng/one-api/relay/model" | ||||||
|  |  | ||||||
|  | type Request struct { | ||||||
|  | 	Messages    []model.Message `json:"messages,omitempty"` | ||||||
|  | 	Lora        string          `json:"lora,omitempty"` | ||||||
|  | 	MaxTokens   int             `json:"max_tokens,omitempty"` | ||||||
|  | 	Prompt      string          `json:"prompt,omitempty"` | ||||||
|  | 	Raw         bool            `json:"raw,omitempty"` | ||||||
|  | 	Stream      bool            `json:"stream,omitempty"` | ||||||
|  | 	Temperature float64         `json:"temperature,omitempty"` | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								relay/adaptor/cohere/adaptor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								relay/adaptor/cohere/adaptor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | package cohere | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Adaptor struct{} | ||||||
|  |  | ||||||
|  | // ConvertImageRequest implements adaptor.Adaptor. | ||||||
|  | func (*Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | ||||||
|  | 	return nil, errors.New("not implemented") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ConvertImageRequest implements adaptor.Adaptor. | ||||||
|  |  | ||||||
|  | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
|  | 	return fmt.Sprintf("%s/v1/chat", meta.BaseURL), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { | ||||||
|  | 	adaptor.SetupCommonRequestHeader(c, req, meta) | ||||||
|  | 	req.Header.Set("Authorization", "Bearer "+meta.APIKey) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	return ConvertRequest(*request), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { | ||||||
|  | 	return adaptor.DoRequestHelper(a, c, meta, requestBody) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { | ||||||
|  | 	if meta.IsStream { | ||||||
|  | 		err, usage = StreamHandler(c, resp) | ||||||
|  | 	} else { | ||||||
|  | 		err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetModelList() []string { | ||||||
|  | 	return ModelList | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetChannelName() string { | ||||||
|  | 	return "Cohere" | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								relay/adaptor/cohere/constant.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								relay/adaptor/cohere/constant.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | package cohere | ||||||
|  |  | ||||||
|  | var ModelList = []string{ | ||||||
|  | 	"command", "command-nightly", | ||||||
|  | 	"command-light", "command-light-nightly", | ||||||
|  | 	"command-r", "command-r-plus", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	num := len(ModelList) | ||||||
|  | 	for i := 0; i < num; i++ { | ||||||
|  | 		ModelList = append(ModelList, ModelList[i]+"-internet") | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										228
									
								
								relay/adaptor/cohere/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								relay/adaptor/cohere/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,228 @@ | |||||||
|  | package cohere | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/render" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	WebSearchConnector = Connector{ID: "web-search"} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func stopReasonCohere2OpenAI(reason *string) string { | ||||||
|  | 	if reason == nil { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	switch *reason { | ||||||
|  | 	case "COMPLETE": | ||||||
|  | 		return "stop" | ||||||
|  | 	default: | ||||||
|  | 		return *reason | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { | ||||||
|  | 	cohereRequest := Request{ | ||||||
|  | 		Model:            textRequest.Model, | ||||||
|  | 		Message:          "", | ||||||
|  | 		MaxTokens:        textRequest.MaxTokens, | ||||||
|  | 		Temperature:      textRequest.Temperature, | ||||||
|  | 		P:                textRequest.TopP, | ||||||
|  | 		K:                textRequest.TopK, | ||||||
|  | 		Stream:           textRequest.Stream, | ||||||
|  | 		FrequencyPenalty: textRequest.FrequencyPenalty, | ||||||
|  | 		PresencePenalty:  textRequest.FrequencyPenalty, | ||||||
|  | 		Seed:             int(textRequest.Seed), | ||||||
|  | 	} | ||||||
|  | 	if cohereRequest.Model == "" { | ||||||
|  | 		cohereRequest.Model = "command-r" | ||||||
|  | 	} | ||||||
|  | 	if strings.HasSuffix(cohereRequest.Model, "-internet") { | ||||||
|  | 		cohereRequest.Model = strings.TrimSuffix(cohereRequest.Model, "-internet") | ||||||
|  | 		cohereRequest.Connectors = append(cohereRequest.Connectors, WebSearchConnector) | ||||||
|  | 	} | ||||||
|  | 	for _, message := range textRequest.Messages { | ||||||
|  | 		if message.Role == "user" { | ||||||
|  | 			cohereRequest.Message = message.Content.(string) | ||||||
|  | 		} else { | ||||||
|  | 			var role string | ||||||
|  | 			if message.Role == "assistant" { | ||||||
|  | 				role = "CHATBOT" | ||||||
|  | 			} else if message.Role == "system" { | ||||||
|  | 				role = "SYSTEM" | ||||||
|  | 			} else { | ||||||
|  | 				role = "USER" | ||||||
|  | 			} | ||||||
|  | 			cohereRequest.ChatHistory = append(cohereRequest.ChatHistory, ChatMessage{ | ||||||
|  | 				Role:    role, | ||||||
|  | 				Message: message.Content.(string), | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return &cohereRequest | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamResponseCohere2OpenAI(cohereResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { | ||||||
|  | 	var response *Response | ||||||
|  | 	var responseText string | ||||||
|  | 	var finishReason string | ||||||
|  |  | ||||||
|  | 	switch cohereResponse.EventType { | ||||||
|  | 	case "stream-start": | ||||||
|  | 		return nil, nil | ||||||
|  | 	case "text-generation": | ||||||
|  | 		responseText += cohereResponse.Text | ||||||
|  | 	case "stream-end": | ||||||
|  | 		usage := cohereResponse.Response.Meta.Tokens | ||||||
|  | 		response = &Response{ | ||||||
|  | 			Meta: Meta{ | ||||||
|  | 				Tokens: Usage{ | ||||||
|  | 					InputTokens:  usage.InputTokens, | ||||||
|  | 					OutputTokens: usage.OutputTokens, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 		finishReason = *cohereResponse.Response.FinishReason | ||||||
|  | 	default: | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var choice openai.ChatCompletionsStreamResponseChoice | ||||||
|  | 	choice.Delta.Content = responseText | ||||||
|  | 	choice.Delta.Role = "assistant" | ||||||
|  | 	if finishReason != "" { | ||||||
|  | 		choice.FinishReason = &finishReason | ||||||
|  | 	} | ||||||
|  | 	var openaiResponse openai.ChatCompletionsStreamResponse | ||||||
|  | 	openaiResponse.Object = "chat.completion.chunk" | ||||||
|  | 	openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} | ||||||
|  | 	return &openaiResponse, response | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ResponseCohere2OpenAI(cohereResponse *Response) *openai.TextResponse { | ||||||
|  | 	choice := openai.TextResponseChoice{ | ||||||
|  | 		Index: 0, | ||||||
|  | 		Message: model.Message{ | ||||||
|  | 			Role:    "assistant", | ||||||
|  | 			Content: cohereResponse.Text, | ||||||
|  | 			Name:    nil, | ||||||
|  | 		}, | ||||||
|  | 		FinishReason: stopReasonCohere2OpenAI(cohereResponse.FinishReason), | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := openai.TextResponse{ | ||||||
|  | 		Id:      fmt.Sprintf("chatcmpl-%s", cohereResponse.ResponseID), | ||||||
|  | 		Model:   "model", | ||||||
|  | 		Object:  "chat.completion", | ||||||
|  | 		Created: helper.GetTimestamp(), | ||||||
|  | 		Choices: []openai.TextResponseChoice{choice}, | ||||||
|  | 	} | ||||||
|  | 	return &fullTextResponse | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { | ||||||
|  | 	createdTime := helper.GetTimestamp() | ||||||
|  | 	scanner := bufio.NewScanner(resp.Body) | ||||||
|  | 	scanner.Split(bufio.ScanLines) | ||||||
|  |  | ||||||
|  | 	common.SetEventStreamHeaders(c) | ||||||
|  | 	var usage model.Usage | ||||||
|  |  | ||||||
|  | 	for scanner.Scan() { | ||||||
|  | 		data := scanner.Text() | ||||||
|  | 		data = strings.TrimSuffix(data, "\r") | ||||||
|  |  | ||||||
|  | 		var cohereResponse StreamResponse | ||||||
|  | 		err := json.Unmarshal([]byte(data), &cohereResponse) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		response, meta := StreamResponseCohere2OpenAI(&cohereResponse) | ||||||
|  | 		if meta != nil { | ||||||
|  | 			usage.PromptTokens += meta.Meta.Tokens.InputTokens | ||||||
|  | 			usage.CompletionTokens += meta.Meta.Tokens.OutputTokens | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if response == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		response.Id = fmt.Sprintf("chatcmpl-%d", createdTime) | ||||||
|  | 		response.Model = c.GetString("original_model") | ||||||
|  | 		response.Created = createdTime | ||||||
|  |  | ||||||
|  | 		err = render.ObjectData(c, response) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.SysError(err.Error()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render.Done(c) | ||||||
|  |  | ||||||
|  | 	err := resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, &usage | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { | ||||||
|  | 	responseBody, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	err = resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	var cohereResponse Response | ||||||
|  | 	err = json.Unmarshal(responseBody, &cohereResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	if cohereResponse.ResponseID == "" { | ||||||
|  | 		return &model.ErrorWithStatusCode{ | ||||||
|  | 			Error: model.Error{ | ||||||
|  | 				Message: cohereResponse.Message, | ||||||
|  | 				Type:    cohereResponse.Message, | ||||||
|  | 				Param:   "", | ||||||
|  | 				Code:    resp.StatusCode, | ||||||
|  | 			}, | ||||||
|  | 			StatusCode: resp.StatusCode, | ||||||
|  | 		}, nil | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := ResponseCohere2OpenAI(&cohereResponse) | ||||||
|  | 	fullTextResponse.Model = modelName | ||||||
|  | 	usage := model.Usage{ | ||||||
|  | 		PromptTokens:     cohereResponse.Meta.Tokens.InputTokens, | ||||||
|  | 		CompletionTokens: cohereResponse.Meta.Tokens.OutputTokens, | ||||||
|  | 		TotalTokens:      cohereResponse.Meta.Tokens.InputTokens + cohereResponse.Meta.Tokens.OutputTokens, | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse.Usage = usage | ||||||
|  | 	jsonResponse, err := json.Marshal(fullTextResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "application/json") | ||||||
|  | 	c.Writer.WriteHeader(resp.StatusCode) | ||||||
|  | 	_, err = c.Writer.Write(jsonResponse) | ||||||
|  | 	return nil, &usage | ||||||
|  | } | ||||||
							
								
								
									
										147
									
								
								relay/adaptor/cohere/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								relay/adaptor/cohere/model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | package cohere | ||||||
|  |  | ||||||
|  | type Request struct { | ||||||
|  | 	Message          string        `json:"message" required:"true"` | ||||||
|  | 	Model            string        `json:"model,omitempty"`  // 默认值为"command-r" | ||||||
|  | 	Stream           bool          `json:"stream,omitempty"` // 默认值为false | ||||||
|  | 	Preamble         string        `json:"preamble,omitempty"` | ||||||
|  | 	ChatHistory      []ChatMessage `json:"chat_history,omitempty"` | ||||||
|  | 	ConversationID   string        `json:"conversation_id,omitempty"` | ||||||
|  | 	PromptTruncation string        `json:"prompt_truncation,omitempty"` // 默认值为"AUTO" | ||||||
|  | 	Connectors       []Connector   `json:"connectors,omitempty"` | ||||||
|  | 	Documents        []Document    `json:"documents,omitempty"` | ||||||
|  | 	Temperature      float64       `json:"temperature,omitempty"` // 默认值为0.3 | ||||||
|  | 	MaxTokens        int           `json:"max_tokens,omitempty"` | ||||||
|  | 	MaxInputTokens   int           `json:"max_input_tokens,omitempty"` | ||||||
|  | 	K                int           `json:"k,omitempty"` // 默认值为0 | ||||||
|  | 	P                float64       `json:"p,omitempty"` // 默认值为0.75 | ||||||
|  | 	Seed             int           `json:"seed,omitempty"` | ||||||
|  | 	StopSequences    []string      `json:"stop_sequences,omitempty"` | ||||||
|  | 	FrequencyPenalty float64       `json:"frequency_penalty,omitempty"` // 默认值为0.0 | ||||||
|  | 	PresencePenalty  float64       `json:"presence_penalty,omitempty"`  // 默认值为0.0 | ||||||
|  | 	Tools            []Tool        `json:"tools,omitempty"` | ||||||
|  | 	ToolResults      []ToolResult  `json:"tool_results,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ChatMessage struct { | ||||||
|  | 	Role    string `json:"role" required:"true"` | ||||||
|  | 	Message string `json:"message" required:"true"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Tool struct { | ||||||
|  | 	Name                 string                   `json:"name" required:"true"` | ||||||
|  | 	Description          string                   `json:"description" required:"true"` | ||||||
|  | 	ParameterDefinitions map[string]ParameterSpec `json:"parameter_definitions"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ParameterSpec struct { | ||||||
|  | 	Description string `json:"description"` | ||||||
|  | 	Type        string `json:"type" required:"true"` | ||||||
|  | 	Required    bool   `json:"required"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ToolResult struct { | ||||||
|  | 	Call    ToolCall                 `json:"call"` | ||||||
|  | 	Outputs []map[string]interface{} `json:"outputs"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ToolCall struct { | ||||||
|  | 	Name       string                 `json:"name" required:"true"` | ||||||
|  | 	Parameters map[string]interface{} `json:"parameters" required:"true"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type StreamResponse struct { | ||||||
|  | 	IsFinished    bool            `json:"is_finished"` | ||||||
|  | 	EventType     string          `json:"event_type"` | ||||||
|  | 	GenerationID  string          `json:"generation_id,omitempty"` | ||||||
|  | 	SearchQueries []*SearchQuery  `json:"search_queries,omitempty"` | ||||||
|  | 	SearchResults []*SearchResult `json:"search_results,omitempty"` | ||||||
|  | 	Documents     []*Document     `json:"documents,omitempty"` | ||||||
|  | 	Text          string          `json:"text,omitempty"` | ||||||
|  | 	Citations     []*Citation     `json:"citations,omitempty"` | ||||||
|  | 	Response      *Response       `json:"response,omitempty"` | ||||||
|  | 	FinishReason  string          `json:"finish_reason,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type SearchQuery struct { | ||||||
|  | 	Text         string `json:"text"` | ||||||
|  | 	GenerationID string `json:"generation_id"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type SearchResult struct { | ||||||
|  | 	SearchQuery *SearchQuery `json:"search_query"` | ||||||
|  | 	DocumentIDs []string     `json:"document_ids"` | ||||||
|  | 	Connector   *Connector   `json:"connector"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Connector struct { | ||||||
|  | 	ID string `json:"id"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Document struct { | ||||||
|  | 	ID        string `json:"id"` | ||||||
|  | 	Snippet   string `json:"snippet"` | ||||||
|  | 	Timestamp string `json:"timestamp"` | ||||||
|  | 	Title     string `json:"title"` | ||||||
|  | 	URL       string `json:"url"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Citation struct { | ||||||
|  | 	Start       int      `json:"start"` | ||||||
|  | 	End         int      `json:"end"` | ||||||
|  | 	Text        string   `json:"text"` | ||||||
|  | 	DocumentIDs []string `json:"document_ids"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Response struct { | ||||||
|  | 	ResponseID    string          `json:"response_id"` | ||||||
|  | 	Text          string          `json:"text"` | ||||||
|  | 	GenerationID  string          `json:"generation_id"` | ||||||
|  | 	ChatHistory   []*Message      `json:"chat_history"` | ||||||
|  | 	FinishReason  *string         `json:"finish_reason"` | ||||||
|  | 	Meta          Meta            `json:"meta"` | ||||||
|  | 	Citations     []*Citation     `json:"citations"` | ||||||
|  | 	Documents     []*Document     `json:"documents"` | ||||||
|  | 	SearchResults []*SearchResult `json:"search_results"` | ||||||
|  | 	SearchQueries []*SearchQuery  `json:"search_queries"` | ||||||
|  | 	Message       string          `json:"message"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Message struct { | ||||||
|  | 	Role    string `json:"role"` | ||||||
|  | 	Message string `json:"message"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Version struct { | ||||||
|  | 	Version string `json:"version"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Units struct { | ||||||
|  | 	InputTokens  int `json:"input_tokens"` | ||||||
|  | 	OutputTokens int `json:"output_tokens"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ChatEntry struct { | ||||||
|  | 	Role    string `json:"role"` | ||||||
|  | 	Message string `json:"message"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Meta struct { | ||||||
|  | 	APIVersion  APIVersion  `json:"api_version"` | ||||||
|  | 	BilledUnits BilledUnits `json:"billed_units"` | ||||||
|  | 	Tokens      Usage       `json:"tokens"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type APIVersion struct { | ||||||
|  | 	Version string `json:"version"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type BilledUnits struct { | ||||||
|  | 	InputTokens  int `json:"input_tokens"` | ||||||
|  | 	OutputTokens int `json:"output_tokens"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Usage struct { | ||||||
|  | 	InputTokens  int `json:"input_tokens"` | ||||||
|  | 	OutputTokens int `json:"output_tokens"` | ||||||
|  | } | ||||||
| @@ -4,7 +4,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/relay/client" | 	"github.com/songquanpeng/one-api/common/client" | ||||||
| 	"github.com/songquanpeng/one-api/relay/meta" | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|   | |||||||
							
								
								
									
										75
									
								
								relay/adaptor/coze/adaptor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								relay/adaptor/coze/adaptor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | package coze | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Adaptor struct { | ||||||
|  | 	meta *meta.Meta | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  | 	a.meta = meta | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
|  | 	return fmt.Sprintf("%s/open_api/v2/chat", meta.BaseURL), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { | ||||||
|  | 	adaptor.SetupCommonRequestHeader(c, req, meta) | ||||||
|  | 	req.Header.Set("Authorization", "Bearer "+meta.APIKey) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	request.User = a.meta.Config.UserID | ||||||
|  | 	return ConvertRequest(*request), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	return request, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { | ||||||
|  | 	return adaptor.DoRequestHelper(a, c, meta, requestBody) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { | ||||||
|  | 	var responseText *string | ||||||
|  | 	if meta.IsStream { | ||||||
|  | 		err, responseText = StreamHandler(c, resp) | ||||||
|  | 	} else { | ||||||
|  | 		err, responseText = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) | ||||||
|  | 	} | ||||||
|  | 	if responseText != nil { | ||||||
|  | 		usage = openai.ResponseText2Usage(*responseText, meta.ActualModelName, meta.PromptTokens) | ||||||
|  | 	} else { | ||||||
|  | 		usage = &model.Usage{} | ||||||
|  | 	} | ||||||
|  | 	usage.PromptTokens = meta.PromptTokens | ||||||
|  | 	usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetModelList() []string { | ||||||
|  | 	return ModelList | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetChannelName() string { | ||||||
|  | 	return "coze" | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								relay/adaptor/coze/constant/contenttype/define.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								relay/adaptor/coze/constant/contenttype/define.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | package contenttype | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	Text = "text" | ||||||
|  | ) | ||||||
							
								
								
									
										7
									
								
								relay/adaptor/coze/constant/event/define.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								relay/adaptor/coze/constant/event/define.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | package event | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	Message = "message" | ||||||
|  | 	Done    = "done" | ||||||
|  | 	Error   = "error" | ||||||
|  | ) | ||||||
							
								
								
									
										6
									
								
								relay/adaptor/coze/constant/messagetype/define.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								relay/adaptor/coze/constant/messagetype/define.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | package messagetype | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	Answer   = "answer" | ||||||
|  | 	FollowUp = "follow_up" | ||||||
|  | ) | ||||||
							
								
								
									
										3
									
								
								relay/adaptor/coze/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								relay/adaptor/coze/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | package coze | ||||||
|  |  | ||||||
|  | var ModelList = []string{} | ||||||
							
								
								
									
										10
									
								
								relay/adaptor/coze/helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								relay/adaptor/coze/helper.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | package coze | ||||||
|  |  | ||||||
|  | import "github.com/songquanpeng/one-api/relay/adaptor/coze/constant/event" | ||||||
|  |  | ||||||
|  | func event2StopReason(e *string) string { | ||||||
|  | 	if e == nil || *e == event.Message { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	return "stop" | ||||||
|  | } | ||||||
							
								
								
									
										202
									
								
								relay/adaptor/coze/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								relay/adaptor/coze/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | |||||||
|  | package coze | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/render" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/conv" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/coze/constant/messagetype" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // https://www.coze.com/open | ||||||
|  |  | ||||||
|  | func stopReasonCoze2OpenAI(reason *string) string { | ||||||
|  | 	if reason == nil { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	switch *reason { | ||||||
|  | 	case "end_turn": | ||||||
|  | 		return "stop" | ||||||
|  | 	case "stop_sequence": | ||||||
|  | 		return "stop" | ||||||
|  | 	case "max_tokens": | ||||||
|  | 		return "length" | ||||||
|  | 	default: | ||||||
|  | 		return *reason | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { | ||||||
|  | 	cozeRequest := Request{ | ||||||
|  | 		Stream: textRequest.Stream, | ||||||
|  | 		User:   textRequest.User, | ||||||
|  | 		BotId:  strings.TrimPrefix(textRequest.Model, "bot-"), | ||||||
|  | 	} | ||||||
|  | 	for i, message := range textRequest.Messages { | ||||||
|  | 		if i == len(textRequest.Messages)-1 { | ||||||
|  | 			cozeRequest.Query = message.StringContent() | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		cozeMessage := Message{ | ||||||
|  | 			Role:    message.Role, | ||||||
|  | 			Content: message.StringContent(), | ||||||
|  | 		} | ||||||
|  | 		cozeRequest.ChatHistory = append(cozeRequest.ChatHistory, cozeMessage) | ||||||
|  | 	} | ||||||
|  | 	return &cozeRequest | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamResponseCoze2OpenAI(cozeResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { | ||||||
|  | 	var response *Response | ||||||
|  | 	var stopReason string | ||||||
|  | 	var choice openai.ChatCompletionsStreamResponseChoice | ||||||
|  |  | ||||||
|  | 	if cozeResponse.Message != nil { | ||||||
|  | 		if cozeResponse.Message.Type != messagetype.Answer { | ||||||
|  | 			return nil, nil | ||||||
|  | 		} | ||||||
|  | 		choice.Delta.Content = cozeResponse.Message.Content | ||||||
|  | 	} | ||||||
|  | 	choice.Delta.Role = "assistant" | ||||||
|  | 	finishReason := stopReasonCoze2OpenAI(&stopReason) | ||||||
|  | 	if finishReason != "null" { | ||||||
|  | 		choice.FinishReason = &finishReason | ||||||
|  | 	} | ||||||
|  | 	var openaiResponse openai.ChatCompletionsStreamResponse | ||||||
|  | 	openaiResponse.Object = "chat.completion.chunk" | ||||||
|  | 	openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} | ||||||
|  | 	openaiResponse.Id = cozeResponse.ConversationId | ||||||
|  | 	return &openaiResponse, response | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ResponseCoze2OpenAI(cozeResponse *Response) *openai.TextResponse { | ||||||
|  | 	var responseText string | ||||||
|  | 	for _, message := range cozeResponse.Messages { | ||||||
|  | 		if message.Type == messagetype.Answer { | ||||||
|  | 			responseText = message.Content | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	choice := openai.TextResponseChoice{ | ||||||
|  | 		Index: 0, | ||||||
|  | 		Message: model.Message{ | ||||||
|  | 			Role:    "assistant", | ||||||
|  | 			Content: responseText, | ||||||
|  | 			Name:    nil, | ||||||
|  | 		}, | ||||||
|  | 		FinishReason: "stop", | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := openai.TextResponse{ | ||||||
|  | 		Id:      fmt.Sprintf("chatcmpl-%s", cozeResponse.ConversationId), | ||||||
|  | 		Model:   "coze-bot", | ||||||
|  | 		Object:  "chat.completion", | ||||||
|  | 		Created: helper.GetTimestamp(), | ||||||
|  | 		Choices: []openai.TextResponseChoice{choice}, | ||||||
|  | 	} | ||||||
|  | 	return &fullTextResponse | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *string) { | ||||||
|  | 	var responseText string | ||||||
|  | 	createdTime := helper.GetTimestamp() | ||||||
|  | 	scanner := bufio.NewScanner(resp.Body) | ||||||
|  | 	scanner.Split(bufio.ScanLines) | ||||||
|  |  | ||||||
|  | 	common.SetEventStreamHeaders(c) | ||||||
|  | 	var modelName string | ||||||
|  |  | ||||||
|  | 	for scanner.Scan() { | ||||||
|  | 		data := scanner.Text() | ||||||
|  | 		if len(data) < 5 || !strings.HasPrefix(data, "data:") { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		data = strings.TrimPrefix(data, "data:") | ||||||
|  | 		data = strings.TrimSuffix(data, "\r") | ||||||
|  |  | ||||||
|  | 		var cozeResponse StreamResponse | ||||||
|  | 		err := json.Unmarshal([]byte(data), &cozeResponse) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		response, _ := StreamResponseCoze2OpenAI(&cozeResponse) | ||||||
|  | 		if response == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, choice := range response.Choices { | ||||||
|  | 			responseText += conv.AsString(choice.Delta.Content) | ||||||
|  | 		} | ||||||
|  | 		response.Model = modelName | ||||||
|  | 		response.Created = createdTime | ||||||
|  |  | ||||||
|  | 		err = render.ObjectData(c, response) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.SysError(err.Error()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render.Done(c) | ||||||
|  |  | ||||||
|  | 	err := resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, &responseText | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *string) { | ||||||
|  | 	responseBody, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	err = resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	var cozeResponse Response | ||||||
|  | 	err = json.Unmarshal(responseBody, &cozeResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	if cozeResponse.Code != 0 { | ||||||
|  | 		return &model.ErrorWithStatusCode{ | ||||||
|  | 			Error: model.Error{ | ||||||
|  | 				Message: cozeResponse.Msg, | ||||||
|  | 				Code:    cozeResponse.Code, | ||||||
|  | 			}, | ||||||
|  | 			StatusCode: resp.StatusCode, | ||||||
|  | 		}, nil | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := ResponseCoze2OpenAI(&cozeResponse) | ||||||
|  | 	fullTextResponse.Model = modelName | ||||||
|  | 	jsonResponse, err := json.Marshal(fullTextResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "application/json") | ||||||
|  | 	c.Writer.WriteHeader(resp.StatusCode) | ||||||
|  | 	_, err = c.Writer.Write(jsonResponse) | ||||||
|  | 	var responseText string | ||||||
|  | 	if len(fullTextResponse.Choices) > 0 { | ||||||
|  | 		responseText = fullTextResponse.Choices[0].Message.StringContent() | ||||||
|  | 	} | ||||||
|  | 	return nil, &responseText | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								relay/adaptor/coze/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								relay/adaptor/coze/model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | package coze | ||||||
|  |  | ||||||
|  | type Message struct { | ||||||
|  | 	Role        string `json:"role"` | ||||||
|  | 	Type        string `json:"type"` | ||||||
|  | 	Content     string `json:"content"` | ||||||
|  | 	ContentType string `json:"content_type"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ErrorInformation struct { | ||||||
|  | 	Code int    `json:"code"` | ||||||
|  | 	Msg  string `json:"msg"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Request struct { | ||||||
|  | 	ConversationId string    `json:"conversation_id,omitempty"` | ||||||
|  | 	BotId          string    `json:"bot_id"` | ||||||
|  | 	User           string    `json:"user"` | ||||||
|  | 	Query          string    `json:"query"` | ||||||
|  | 	ChatHistory    []Message `json:"chat_history,omitempty"` | ||||||
|  | 	Stream         bool      `json:"stream"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Response struct { | ||||||
|  | 	ConversationId string    `json:"conversation_id,omitempty"` | ||||||
|  | 	Messages       []Message `json:"messages,omitempty"` | ||||||
|  | 	Code           int       `json:"code,omitempty"` | ||||||
|  | 	Msg            string    `json:"msg,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type StreamResponse struct { | ||||||
|  | 	Event            string            `json:"event,omitempty"` | ||||||
|  | 	Message          *Message          `json:"message,omitempty"` | ||||||
|  | 	IsFinish         bool              `json:"is_finish,omitempty"` | ||||||
|  | 	Index            int               `json:"index,omitempty"` | ||||||
|  | 	ConversationId   string            `json:"conversation_id,omitempty"` | ||||||
|  | 	ErrorInformation *ErrorInformation `json:"error_information,omitempty"` | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								relay/adaptor/deepl/adaptor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								relay/adaptor/deepl/adaptor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | package deepl | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Adaptor struct { | ||||||
|  | 	meta       *meta.Meta | ||||||
|  | 	promptText string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) Init(meta *meta.Meta) { | ||||||
|  | 	a.meta = meta | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
|  | 	return fmt.Sprintf("%s/v2/translate", meta.BaseURL), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { | ||||||
|  | 	adaptor.SetupCommonRequestHeader(c, req, meta) | ||||||
|  | 	req.Header.Set("Authorization", "DeepL-Auth-Key "+meta.APIKey) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	convertedRequest, text := ConvertRequest(*request) | ||||||
|  | 	a.promptText = text | ||||||
|  | 	return convertedRequest, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | ||||||
|  | 	if request == nil { | ||||||
|  | 		return nil, errors.New("request is nil") | ||||||
|  | 	} | ||||||
|  | 	return request, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { | ||||||
|  | 	return adaptor.DoRequestHelper(a, c, meta, requestBody) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { | ||||||
|  | 	if meta.IsStream { | ||||||
|  | 		err = StreamHandler(c, resp, meta.ActualModelName) | ||||||
|  | 	} else { | ||||||
|  | 		err = Handler(c, resp, meta.ActualModelName) | ||||||
|  | 	} | ||||||
|  | 	promptTokens := len(a.promptText) | ||||||
|  | 	usage = &model.Usage{ | ||||||
|  | 		PromptTokens: promptTokens, | ||||||
|  | 		TotalTokens:  promptTokens, | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetModelList() []string { | ||||||
|  | 	return ModelList | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Adaptor) GetChannelName() string { | ||||||
|  | 	return "deepl" | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								relay/adaptor/deepl/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								relay/adaptor/deepl/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | package deepl | ||||||
|  |  | ||||||
|  | // https://developers.deepl.com/docs/api-reference/glossaries | ||||||
|  |  | ||||||
|  | var ModelList = []string{ | ||||||
|  | 	"deepl-zh", | ||||||
|  | 	"deepl-en", | ||||||
|  | 	"deepl-ja", | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								relay/adaptor/deepl/helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								relay/adaptor/deepl/helper.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | package deepl | ||||||
|  |  | ||||||
|  | import "strings" | ||||||
|  |  | ||||||
|  | func parseLangFromModelName(modelName string) string { | ||||||
|  | 	parts := strings.Split(modelName, "-") | ||||||
|  | 	if len(parts) == 1 { | ||||||
|  | 		return "ZH" | ||||||
|  | 	} | ||||||
|  | 	return parts[1] | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								relay/adaptor/deepl/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								relay/adaptor/deepl/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | |||||||
|  | package deepl | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/constant" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/constant/finishreason" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/constant/role" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // https://developers.deepl.com/docs/getting-started/your-first-api-request | ||||||
|  |  | ||||||
|  | func ConvertRequest(textRequest model.GeneralOpenAIRequest) (*Request, string) { | ||||||
|  | 	var text string | ||||||
|  | 	if len(textRequest.Messages) != 0 { | ||||||
|  | 		text = textRequest.Messages[len(textRequest.Messages)-1].StringContent() | ||||||
|  | 	} | ||||||
|  | 	deeplRequest := Request{ | ||||||
|  | 		TargetLang: parseLangFromModelName(textRequest.Model), | ||||||
|  | 		Text:       []string{text}, | ||||||
|  | 	} | ||||||
|  | 	return &deeplRequest, text | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamResponseDeepL2OpenAI(deeplResponse *Response) *openai.ChatCompletionsStreamResponse { | ||||||
|  | 	var choice openai.ChatCompletionsStreamResponseChoice | ||||||
|  | 	if len(deeplResponse.Translations) != 0 { | ||||||
|  | 		choice.Delta.Content = deeplResponse.Translations[0].Text | ||||||
|  | 	} | ||||||
|  | 	choice.Delta.Role = role.Assistant | ||||||
|  | 	choice.FinishReason = &constant.StopFinishReason | ||||||
|  | 	openaiResponse := openai.ChatCompletionsStreamResponse{ | ||||||
|  | 		Object:  constant.StreamObject, | ||||||
|  | 		Created: helper.GetTimestamp(), | ||||||
|  | 		Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, | ||||||
|  | 	} | ||||||
|  | 	return &openaiResponse | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ResponseDeepL2OpenAI(deeplResponse *Response) *openai.TextResponse { | ||||||
|  | 	var responseText string | ||||||
|  | 	if len(deeplResponse.Translations) != 0 { | ||||||
|  | 		responseText = deeplResponse.Translations[0].Text | ||||||
|  | 	} | ||||||
|  | 	choice := openai.TextResponseChoice{ | ||||||
|  | 		Index: 0, | ||||||
|  | 		Message: model.Message{ | ||||||
|  | 			Role:    role.Assistant, | ||||||
|  | 			Content: responseText, | ||||||
|  | 			Name:    nil, | ||||||
|  | 		}, | ||||||
|  | 		FinishReason: finishreason.Stop, | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := openai.TextResponse{ | ||||||
|  | 		Object:  constant.NonStreamObject, | ||||||
|  | 		Created: helper.GetTimestamp(), | ||||||
|  | 		Choices: []openai.TextResponseChoice{choice}, | ||||||
|  | 	} | ||||||
|  | 	return &fullTextResponse | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StreamHandler(c *gin.Context, resp *http.Response, modelName string) *model.ErrorWithStatusCode { | ||||||
|  | 	responseBody, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	err = resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	var deeplResponse Response | ||||||
|  | 	err = json.Unmarshal(responseBody, &deeplResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := StreamResponseDeepL2OpenAI(&deeplResponse) | ||||||
|  | 	fullTextResponse.Model = modelName | ||||||
|  | 	fullTextResponse.Id = helper.GetResponseID(c) | ||||||
|  | 	jsonData, err := json.Marshal(fullTextResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	common.SetEventStreamHeaders(c) | ||||||
|  | 	c.Stream(func(w io.Writer) bool { | ||||||
|  | 		if jsonData != nil { | ||||||
|  | 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonData)}) | ||||||
|  | 			jsonData = nil | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 		c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) | ||||||
|  | 		return false | ||||||
|  | 	}) | ||||||
|  | 	_ = resp.Body.Close() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Handler(c *gin.Context, resp *http.Response, modelName string) *model.ErrorWithStatusCode { | ||||||
|  | 	responseBody, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	err = resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	var deeplResponse Response | ||||||
|  | 	err = json.Unmarshal(responseBody, &deeplResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	if deeplResponse.Message != "" { | ||||||
|  | 		return &model.ErrorWithStatusCode{ | ||||||
|  | 			Error: model.Error{ | ||||||
|  | 				Message: deeplResponse.Message, | ||||||
|  | 				Code:    "deepl_error", | ||||||
|  | 			}, | ||||||
|  | 			StatusCode: resp.StatusCode, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := ResponseDeepL2OpenAI(&deeplResponse) | ||||||
|  | 	fullTextResponse.Model = modelName | ||||||
|  | 	fullTextResponse.Id = helper.GetResponseID(c) | ||||||
|  | 	jsonResponse, err := json.Marshal(fullTextResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "application/json") | ||||||
|  | 	c.Writer.WriteHeader(resp.StatusCode) | ||||||
|  | 	_, err = c.Writer.Write(jsonResponse) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								relay/adaptor/deepl/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								relay/adaptor/deepl/model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | package deepl | ||||||
|  |  | ||||||
|  | type Request struct { | ||||||
|  | 	Text       []string `json:"text"` | ||||||
|  | 	TargetLang string   `json:"target_lang"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Translation struct { | ||||||
|  | 	DetectedSourceLanguage string `json:"detected_source_language,omitempty"` | ||||||
|  | 	Text                   string `json:"text,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Response struct { | ||||||
|  | 	Translations []Translation `json:"translations,omitempty"` | ||||||
|  | 	Message      string        `json:"message,omitempty"` | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								relay/adaptor/deepseek/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								relay/adaptor/deepseek/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | package deepseek | ||||||
|  |  | ||||||
|  | var ModelList = []string{ | ||||||
|  | 	"deepseek-chat", | ||||||
|  | 	"deepseek-coder", | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								relay/adaptor/doubao/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								relay/adaptor/doubao/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | package doubao | ||||||
|  |  | ||||||
|  | // https://console.volcengine.com/ark/region:ark+cn-beijing/model | ||||||
|  |  | ||||||
|  | var ModelList = []string{ | ||||||
|  | 	"Doubao-pro-128k", | ||||||
|  | 	"Doubao-pro-32k", | ||||||
|  | 	"Doubao-pro-4k", | ||||||
|  | 	"Doubao-lite-128k", | ||||||
|  | 	"Doubao-lite-32k", | ||||||
|  | 	"Doubao-lite-4k", | ||||||
|  | 	"Doubao-embedding", | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								relay/adaptor/doubao/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								relay/adaptor/doubao/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | package doubao | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/relaymode" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
|  | 	switch meta.Mode { | ||||||
|  | 	case relaymode.ChatCompletions: | ||||||
|  | 		return fmt.Sprintf("%s/api/v3/chat/completions", meta.BaseURL), nil | ||||||
|  | 	case relaymode.Embeddings: | ||||||
|  | 		return fmt.Sprintf("%s/api/v3/embeddings", meta.BaseURL), nil | ||||||
|  | 	default: | ||||||
|  | 	} | ||||||
|  | 	return "", fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode) | ||||||
|  | } | ||||||
| @@ -3,14 +3,17 @@ package gemini | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	channelhelper "github.com/songquanpeng/one-api/relay/adaptor" | 	channelhelper "github.com/songquanpeng/one-api/relay/adaptor" | ||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
| 	"github.com/songquanpeng/one-api/relay/meta" | 	"github.com/songquanpeng/one-api/relay/meta" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"io" | 	"github.com/songquanpeng/one-api/relay/relaymode" | ||||||
| 	"net/http" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Adaptor struct { | type Adaptor struct { | ||||||
| @@ -21,10 +24,17 @@ func (a *Adaptor) Init(meta *meta.Meta) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { | ||||||
| 	version := helper.AssignOrDefault(meta.APIVersion, "v1") | 	version := helper.AssignOrDefault(meta.Config.APIVersion, config.GeminiVersion) | ||||||
| 	action := "generateContent" | 	action := "" | ||||||
|  | 	switch meta.Mode { | ||||||
|  | 	case relaymode.Embeddings: | ||||||
|  | 		action = "batchEmbedContents" | ||||||
|  | 	default: | ||||||
|  | 		action = "generateContent" | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if meta.IsStream { | 	if meta.IsStream { | ||||||
| 		action = "streamGenerateContent" | 		action = "streamGenerateContent?alt=sse" | ||||||
| 	} | 	} | ||||||
| 	return fmt.Sprintf("%s/%s/models/%s:%s", meta.BaseURL, version, meta.ActualModelName, action), nil | 	return fmt.Sprintf("%s/%s/models/%s:%s", meta.BaseURL, version, meta.ActualModelName, action), nil | ||||||
| } | } | ||||||
| @@ -39,7 +49,14 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G | |||||||
| 	if request == nil { | 	if request == nil { | ||||||
| 		return nil, errors.New("request is nil") | 		return nil, errors.New("request is nil") | ||||||
| 	} | 	} | ||||||
| 	return ConvertRequest(*request), nil | 	switch relayMode { | ||||||
|  | 	case relaymode.Embeddings: | ||||||
|  | 		geminiEmbeddingRequest := ConvertEmbeddingRequest(*request) | ||||||
|  | 		return geminiEmbeddingRequest, nil | ||||||
|  | 	default: | ||||||
|  | 		geminiRequest := ConvertRequest(*request) | ||||||
|  | 		return geminiRequest, nil | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { | ||||||
| @@ -59,8 +76,13 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met | |||||||
| 		err, responseText = StreamHandler(c, resp) | 		err, responseText = StreamHandler(c, resp) | ||||||
| 		usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) | 		usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) | ||||||
| 	} else { | 	} else { | ||||||
|  | 		switch meta.Mode { | ||||||
|  | 		case relaymode.Embeddings: | ||||||
|  | 			err, usage = EmbeddingHandler(c, resp) | ||||||
|  | 		default: | ||||||
| 			err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) | 			err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,5 +4,5 @@ package gemini | |||||||
|  |  | ||||||
| var ModelList = []string{ | var ModelList = []string{ | ||||||
| 	"gemini-pro", "gemini-1.0-pro-001", "gemini-1.5-pro", | 	"gemini-pro", "gemini-1.0-pro-001", "gemini-1.5-pro", | ||||||
| 	"gemini-pro-vision", "gemini-1.0-pro-vision-001", | 	"gemini-pro-vision", "gemini-1.0-pro-vision-001", "embedding-001", "text-embedding-004", | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,11 @@ import ( | |||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/render" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| @@ -13,9 +18,6 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | 	"github.com/songquanpeng/one-api/relay/adaptor/openai" | ||||||
| 	"github.com/songquanpeng/one-api/relay/constant" | 	"github.com/songquanpeng/one-api/relay/constant" | ||||||
| 	"github.com/songquanpeng/one-api/relay/model" | 	"github.com/songquanpeng/one-api/relay/model" | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
| @@ -54,7 +56,17 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *ChatRequest { | |||||||
| 			MaxOutputTokens: textRequest.MaxTokens, | 			MaxOutputTokens: textRequest.MaxTokens, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	if textRequest.Functions != nil { | 	if textRequest.Tools != nil { | ||||||
|  | 		functions := make([]model.Function, 0, len(textRequest.Tools)) | ||||||
|  | 		for _, tool := range textRequest.Tools { | ||||||
|  | 			functions = append(functions, tool.Function) | ||||||
|  | 		} | ||||||
|  | 		geminiRequest.Tools = []ChatTools{ | ||||||
|  | 			{ | ||||||
|  | 				FunctionDeclarations: functions, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} else if textRequest.Functions != nil { | ||||||
| 		geminiRequest.Tools = []ChatTools{ | 		geminiRequest.Tools = []ChatTools{ | ||||||
| 			{ | 			{ | ||||||
| 				FunctionDeclarations: textRequest.Functions, | 				FunctionDeclarations: textRequest.Functions, | ||||||
| @@ -123,6 +135,29 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *ChatRequest { | |||||||
| 	return &geminiRequest | 	return &geminiRequest | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *BatchEmbeddingRequest { | ||||||
|  | 	inputs := request.ParseInput() | ||||||
|  | 	requests := make([]EmbeddingRequest, len(inputs)) | ||||||
|  | 	model := fmt.Sprintf("models/%s", request.Model) | ||||||
|  |  | ||||||
|  | 	for i, input := range inputs { | ||||||
|  | 		requests[i] = EmbeddingRequest{ | ||||||
|  | 			Model: model, | ||||||
|  | 			Content: ChatContent{ | ||||||
|  | 				Parts: []Part{ | ||||||
|  | 					{ | ||||||
|  | 						Text: input, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &BatchEmbeddingRequest{ | ||||||
|  | 		Requests: requests, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| type ChatResponse struct { | type ChatResponse struct { | ||||||
| 	Candidates     []ChatCandidate    `json:"candidates"` | 	Candidates     []ChatCandidate    `json:"candidates"` | ||||||
| 	PromptFeedback ChatPromptFeedback `json:"promptFeedback"` | 	PromptFeedback ChatPromptFeedback `json:"promptFeedback"` | ||||||
| @@ -154,6 +189,30 @@ type ChatPromptFeedback struct { | |||||||
| 	SafetyRatings []ChatSafetyRating `json:"safetyRatings"` | 	SafetyRatings []ChatSafetyRating `json:"safetyRatings"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func getToolCalls(candidate *ChatCandidate) []model.Tool { | ||||||
|  | 	var toolCalls []model.Tool | ||||||
|  |  | ||||||
|  | 	item := candidate.Content.Parts[0] | ||||||
|  | 	if item.FunctionCall == nil { | ||||||
|  | 		return toolCalls | ||||||
|  | 	} | ||||||
|  | 	argsBytes, err := json.Marshal(item.FunctionCall.Arguments) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.FatalLog("getToolCalls failed: " + err.Error()) | ||||||
|  | 		return toolCalls | ||||||
|  | 	} | ||||||
|  | 	toolCall := model.Tool{ | ||||||
|  | 		Id:   fmt.Sprintf("call_%s", random.GetUUID()), | ||||||
|  | 		Type: "function", | ||||||
|  | 		Function: model.Function{ | ||||||
|  | 			Arguments: string(argsBytes), | ||||||
|  | 			Name:      item.FunctionCall.FunctionName, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	toolCalls = append(toolCalls, toolCall) | ||||||
|  | 	return toolCalls | ||||||
|  | } | ||||||
|  |  | ||||||
| func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { | func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { | ||||||
| 	fullTextResponse := openai.TextResponse{ | 	fullTextResponse := openai.TextResponse{ | ||||||
| 		Id:      fmt.Sprintf("chatcmpl-%s", random.GetUUID()), | 		Id:      fmt.Sprintf("chatcmpl-%s", random.GetUUID()), | ||||||
| @@ -166,13 +225,19 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { | |||||||
| 			Index: i, | 			Index: i, | ||||||
| 			Message: model.Message{ | 			Message: model.Message{ | ||||||
| 				Role: "assistant", | 				Role: "assistant", | ||||||
| 				Content: "", |  | ||||||
| 			}, | 			}, | ||||||
| 			FinishReason: constant.StopFinishReason, | 			FinishReason: constant.StopFinishReason, | ||||||
| 		} | 		} | ||||||
| 		if len(candidate.Content.Parts) > 0 { | 		if len(candidate.Content.Parts) > 0 { | ||||||
|  | 			if candidate.Content.Parts[0].FunctionCall != nil { | ||||||
|  | 				choice.Message.ToolCalls = getToolCalls(&candidate) | ||||||
|  | 			} else { | ||||||
| 				choice.Message.Content = candidate.Content.Parts[0].Text | 				choice.Message.Content = candidate.Content.Parts[0].Text | ||||||
| 			} | 			} | ||||||
|  | 		} else { | ||||||
|  | 			choice.Message.Content = "" | ||||||
|  | 			choice.FinishReason = candidate.FinishReason | ||||||
|  | 		} | ||||||
| 		fullTextResponse.Choices = append(fullTextResponse.Choices, choice) | 		fullTextResponse.Choices = append(fullTextResponse.Choices, choice) | ||||||
| 	} | 	} | ||||||
| 	return &fullTextResponse | 	return &fullTextResponse | ||||||
| @@ -181,81 +246,80 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { | |||||||
| func streamResponseGeminiChat2OpenAI(geminiResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { | func streamResponseGeminiChat2OpenAI(geminiResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { | ||||||
| 	var choice openai.ChatCompletionsStreamResponseChoice | 	var choice openai.ChatCompletionsStreamResponseChoice | ||||||
| 	choice.Delta.Content = geminiResponse.GetResponseText() | 	choice.Delta.Content = geminiResponse.GetResponseText() | ||||||
| 	choice.FinishReason = &constant.StopFinishReason | 	//choice.FinishReason = &constant.StopFinishReason | ||||||
| 	var response openai.ChatCompletionsStreamResponse | 	var response openai.ChatCompletionsStreamResponse | ||||||
|  | 	response.Id = fmt.Sprintf("chatcmpl-%s", random.GetUUID()) | ||||||
|  | 	response.Created = helper.GetTimestamp() | ||||||
| 	response.Object = "chat.completion.chunk" | 	response.Object = "chat.completion.chunk" | ||||||
| 	response.Model = "gemini" | 	response.Model = "gemini" | ||||||
| 	response.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} | 	response.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} | ||||||
| 	return &response | 	return &response | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func embeddingResponseGemini2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { | ||||||
|  | 	openAIEmbeddingResponse := openai.EmbeddingResponse{ | ||||||
|  | 		Object: "list", | ||||||
|  | 		Data:   make([]openai.EmbeddingResponseItem, 0, len(response.Embeddings)), | ||||||
|  | 		Model:  "gemini-embedding", | ||||||
|  | 		Usage:  model.Usage{TotalTokens: 0}, | ||||||
|  | 	} | ||||||
|  | 	for _, item := range response.Embeddings { | ||||||
|  | 		openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ | ||||||
|  | 			Object:    `embedding`, | ||||||
|  | 			Index:     0, | ||||||
|  | 			Embedding: item.Values, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	return &openAIEmbeddingResponse | ||||||
|  | } | ||||||
|  |  | ||||||
| func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, string) { | func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, string) { | ||||||
| 	responseText := "" | 	responseText := "" | ||||||
| 	dataChan := make(chan string) |  | ||||||
| 	stopChan := make(chan bool) |  | ||||||
| 	scanner := bufio.NewScanner(resp.Body) | 	scanner := bufio.NewScanner(resp.Body) | ||||||
| 	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { | 	scanner.Split(bufio.ScanLines) | ||||||
| 		if atEOF && len(data) == 0 { |  | ||||||
| 			return 0, nil, nil | 	common.SetEventStreamHeaders(c) | ||||||
| 		} |  | ||||||
| 		if i := strings.Index(string(data), "\n"); i >= 0 { |  | ||||||
| 			return i + 1, data[0:i], nil |  | ||||||
| 		} |  | ||||||
| 		if atEOF { |  | ||||||
| 			return len(data), data, nil |  | ||||||
| 		} |  | ||||||
| 		return 0, nil, nil |  | ||||||
| 	}) |  | ||||||
| 	go func() { |  | ||||||
| 	for scanner.Scan() { | 	for scanner.Scan() { | ||||||
| 		data := scanner.Text() | 		data := scanner.Text() | ||||||
| 		data = strings.TrimSpace(data) | 		data = strings.TrimSpace(data) | ||||||
| 			if !strings.HasPrefix(data, "\"text\": \"") { | 		if !strings.HasPrefix(data, "data: ") { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 			data = strings.TrimPrefix(data, "\"text\": \"") | 		data = strings.TrimPrefix(data, "data: ") | ||||||
| 		data = strings.TrimSuffix(data, "\"") | 		data = strings.TrimSuffix(data, "\"") | ||||||
| 			dataChan <- data |  | ||||||
| 		} | 		var geminiResponse ChatResponse | ||||||
| 		stopChan <- true | 		err := json.Unmarshal([]byte(data), &geminiResponse) | ||||||
| 	}() |  | ||||||
| 	common.SetEventStreamHeaders(c) |  | ||||||
| 	c.Stream(func(w io.Writer) bool { |  | ||||||
| 		select { |  | ||||||
| 		case data := <-dataChan: |  | ||||||
| 			// this is used to prevent annoying \ related format bug |  | ||||||
| 			data = fmt.Sprintf("{\"content\": \"%s\"}", data) |  | ||||||
| 			type dummyStruct struct { |  | ||||||
| 				Content string `json:"content"` |  | ||||||
| 			} |  | ||||||
| 			var dummy dummyStruct |  | ||||||
| 			err := json.Unmarshal([]byte(data), &dummy) |  | ||||||
| 			responseText += dummy.Content |  | ||||||
| 			var choice openai.ChatCompletionsStreamResponseChoice |  | ||||||
| 			choice.Delta.Content = dummy.Content |  | ||||||
| 			response := openai.ChatCompletionsStreamResponse{ |  | ||||||
| 				Id:      fmt.Sprintf("chatcmpl-%s", random.GetUUID()), |  | ||||||
| 				Object:  "chat.completion.chunk", |  | ||||||
| 				Created: helper.GetTimestamp(), |  | ||||||
| 				Model:   "gemini-pro", |  | ||||||
| 				Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, |  | ||||||
| 			} |  | ||||||
| 			jsonResponse, err := json.Marshal(response) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				logger.SysError("error marshalling stream response: " + err.Error()) | 			logger.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
| 				return true | 			continue | ||||||
| 		} | 		} | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) |  | ||||||
| 			return true | 		response := streamResponseGeminiChat2OpenAI(&geminiResponse) | ||||||
| 		case <-stopChan: | 		if response == nil { | ||||||
| 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) | 			continue | ||||||
| 			return false |  | ||||||
| 		} | 		} | ||||||
| 	}) |  | ||||||
|  | 		responseText += response.Choices[0].Delta.StringContent() | ||||||
|  |  | ||||||
|  | 		err = render.ObjectData(c, response) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.SysError(err.Error()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		logger.SysError("error reading stream: " + err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render.Done(c) | ||||||
|  |  | ||||||
| 	err := resp.Body.Close() | 	err := resp.Body.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), "" | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), "" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil, responseText | 	return nil, responseText | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -302,3 +366,39 @@ func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName st | |||||||
| 	_, err = c.Writer.Write(jsonResponse) | 	_, err = c.Writer.Write(jsonResponse) | ||||||
| 	return nil, &usage | 	return nil, &usage | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { | ||||||
|  | 	var geminiEmbeddingResponse EmbeddingResponse | ||||||
|  | 	responseBody, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	err = resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	err = json.Unmarshal(responseBody, &geminiEmbeddingResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	if geminiEmbeddingResponse.Error != nil { | ||||||
|  | 		return &model.ErrorWithStatusCode{ | ||||||
|  | 			Error: model.Error{ | ||||||
|  | 				Message: geminiEmbeddingResponse.Error.Message, | ||||||
|  | 				Type:    "gemini_error", | ||||||
|  | 				Param:   "", | ||||||
|  | 				Code:    geminiEmbeddingResponse.Error.Code, | ||||||
|  | 			}, | ||||||
|  | 			StatusCode: resp.StatusCode, | ||||||
|  | 		}, nil | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := embeddingResponseGemini2OpenAI(&geminiEmbeddingResponse) | ||||||
|  | 	jsonResponse, err := json.Marshal(fullTextResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "application/json") | ||||||
|  | 	c.Writer.WriteHeader(resp.StatusCode) | ||||||
|  | 	_, err = c.Writer.Write(jsonResponse) | ||||||
|  | 	return nil, &fullTextResponse.Usage | ||||||
|  | } | ||||||
|   | |||||||
| @@ -7,14 +7,47 @@ type ChatRequest struct { | |||||||
| 	Tools            []ChatTools          `json:"tools,omitempty"` | 	Tools            []ChatTools          `json:"tools,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type EmbeddingRequest struct { | ||||||
|  | 	Model                string      `json:"model"` | ||||||
|  | 	Content              ChatContent `json:"content"` | ||||||
|  | 	TaskType             string      `json:"taskType,omitempty"` | ||||||
|  | 	Title                string      `json:"title,omitempty"` | ||||||
|  | 	OutputDimensionality int         `json:"outputDimensionality,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type BatchEmbeddingRequest struct { | ||||||
|  | 	Requests []EmbeddingRequest `json:"requests"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type EmbeddingData struct { | ||||||
|  | 	Values []float64 `json:"values"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type EmbeddingResponse struct { | ||||||
|  | 	Embeddings []EmbeddingData `json:"embeddings"` | ||||||
|  | 	Error      *Error          `json:"error,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Error struct { | ||||||
|  | 	Code    int    `json:"code,omitempty"` | ||||||
|  | 	Message string `json:"message,omitempty"` | ||||||
|  | 	Status  string `json:"status,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
| type InlineData struct { | type InlineData struct { | ||||||
| 	MimeType string `json:"mimeType"` | 	MimeType string `json:"mimeType"` | ||||||
| 	Data     string `json:"data"` | 	Data     string `json:"data"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type FunctionCall struct { | ||||||
|  | 	FunctionName string `json:"name"` | ||||||
|  | 	Arguments    any    `json:"args"` | ||||||
|  | } | ||||||
|  |  | ||||||
| type Part struct { | type Part struct { | ||||||
| 	Text         string        `json:"text,omitempty"` | 	Text         string        `json:"text,omitempty"` | ||||||
| 	InlineData   *InlineData   `json:"inlineData,omitempty"` | 	InlineData   *InlineData   `json:"inlineData,omitempty"` | ||||||
|  | 	FunctionCall *FunctionCall `json:"functionCall,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type ChatContent struct { | type ChatContent struct { | ||||||
| @@ -28,7 +61,7 @@ type ChatSafetySettings struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type ChatTools struct { | type ChatTools struct { | ||||||
| 	FunctionDeclarations any `json:"functionDeclarations,omitempty"` | 	FunctionDeclarations any `json:"function_declarations,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type ChatGenerationConfig struct { | type ChatGenerationConfig struct { | ||||||
|   | |||||||
| @@ -7,4 +7,6 @@ var ModelList = []string{ | |||||||
| 	"llama2-7b-2048", | 	"llama2-7b-2048", | ||||||
| 	"llama2-70b-4096", | 	"llama2-70b-4096", | ||||||
| 	"mixtral-8x7b-32768", | 	"mixtral-8x7b-32768", | ||||||
|  | 	"llama3-8b-8192", | ||||||
|  | 	"llama3-70b-8192", | ||||||
| } | } | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user