├── .github └── workflows │ └── go-build.yml ├── LICENSE ├── api-pool-lite.go ├── api-pool.go ├── augment2api_auth.py ├── augment2api_server.py ├── auth2xapikey.py ├── claude-card.html ├── copilot-models.md ├── hfs ├── api-pool │ ├── Dockerfile │ ├── README.md │ └── entrypoint.sh ├── hunyuan2api │ ├── Dockerfile │ └── README.md └── qwen2api │ ├── Dockerfile │ └── README.md ├── hunyuan2api.go ├── qwen2api-cf.js ├── qwen2api-cf.md └── wo2api.go /.github/workflows/go-build.yml: -------------------------------------------------------------------------------- 1 | name: Go Build 2 | 3 | # 触发条件 4 | on: 5 | push: 6 | branches: [ main, master ] 7 | paths: 8 | - '**.go' # 当Go文件变更时触发 9 | - '.github/workflows/go-build.yml' # 当工作流文件本身变更时触发 10 | workflow_dispatch: # 支持手动触发 11 | 12 | jobs: 13 | build-api-pool: 14 | name: Build api-pool 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | os: [windows, linux, darwin] 19 | arch: [amd64, arm64] 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: '1.21' 28 | check-latest: true 29 | cache: true 30 | 31 | - name: Set version 32 | id: set-version 33 | run: | 34 | echo "version=$(date +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT 35 | 36 | - name: Build for ${{ matrix.os }}-${{ matrix.arch }} 37 | env: 38 | GOOS: ${{ matrix.os }} 39 | GOARCH: ${{ matrix.arch }} 40 | VERSION: ${{ steps.set-version.outputs.version }} 41 | run: | 42 | # 设置文件扩展名(Windows为.exe,其他无扩展名) 43 | if [ "${{ matrix.os }}" == "windows" ]; then 44 | EXT=".exe" 45 | else 46 | EXT="" 47 | fi 48 | 49 | BINARY_NAME="api-pool-${{ matrix.os }}-${{ matrix.arch }}${EXT}" 50 | go build -ldflags "-X main.AppVersion=$VERSION" -o "$BINARY_NAME" ./api-pool.go 51 | 52 | - name: Upload Artifact 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: api-pool-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.set-version.outputs.version }} 56 | path: api-pool-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} 57 | retention-days: 90 58 | 59 | build-wo2api: 60 | name: Build wo2api 61 | runs-on: ubuntu-latest 62 | strategy: 63 | matrix: 64 | os: [windows, linux, darwin] 65 | arch: [amd64, arm64] 66 | steps: 67 | - name: Check out code 68 | uses: actions/checkout@v4 69 | 70 | - name: Set up Go 71 | uses: actions/setup-go@v4 72 | with: 73 | go-version: '1.21' 74 | check-latest: true 75 | cache: true 76 | 77 | - name: Set version 78 | id: set-version 79 | run: | 80 | echo "version=$(date +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT 81 | 82 | - name: Build for ${{ matrix.os }}-${{ matrix.arch }} 83 | env: 84 | GOOS: ${{ matrix.os }} 85 | GOARCH: ${{ matrix.arch }} 86 | VERSION: ${{ steps.set-version.outputs.version }} 87 | run: | 88 | # 设置文件扩展名(Windows为.exe,其他无扩展名) 89 | if [ "${{ matrix.os }}" == "windows" ]; then 90 | EXT=".exe" 91 | else 92 | EXT="" 93 | fi 94 | 95 | BINARY_NAME="wo2api-${{ matrix.os }}-${{ matrix.arch }}${EXT}" 96 | go build -ldflags "-X main.AppVersion=$VERSION" -o "$BINARY_NAME" ./wo2api.go 97 | 98 | - name: Upload Artifact 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: wo2api-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.set-version.outputs.version }} 102 | path: wo2api-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} 103 | retention-days: 90 104 | 105 | build-hunyuan2api: 106 | name: Build hunyuan2api 107 | runs-on: ubuntu-latest 108 | strategy: 109 | matrix: 110 | os: [windows, linux, darwin] 111 | arch: [amd64, arm64] 112 | steps: 113 | - name: Check out code 114 | uses: actions/checkout@v4 115 | 116 | - name: Set up Go 117 | uses: actions/setup-go@v4 118 | with: 119 | go-version: '1.21' 120 | check-latest: true 121 | cache: true 122 | 123 | - name: Set version 124 | id: set-version 125 | run: | 126 | echo "version=$(date +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT 127 | 128 | - name: Build for ${{ matrix.os }}-${{ matrix.arch }} 129 | env: 130 | GOOS: ${{ matrix.os }} 131 | GOARCH: ${{ matrix.arch }} 132 | VERSION: ${{ steps.set-version.outputs.version }} 133 | run: | 134 | # 设置文件扩展名(Windows为.exe,其他无扩展名) 135 | if [ "${{ matrix.os }}" == "windows" ]; then 136 | EXT=".exe" 137 | else 138 | EXT="" 139 | fi 140 | 141 | BINARY_NAME="hunyuan2api-${{ matrix.os }}-${{ matrix.arch }}${EXT}" 142 | go build -ldflags "-X main.AppVersion=$VERSION" -o "$BINARY_NAME" ./hunyuan2api.go 143 | 144 | - name: Upload Artifact 145 | uses: actions/upload-artifact@v4 146 | with: 147 | name: hunyuan2api-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.set-version.outputs.version }} 148 | path: hunyuan2api-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} 149 | retention-days: 90 150 | 151 | build-api-pool-lite: 152 | name: Build api-pool-lite 153 | runs-on: ubuntu-latest 154 | strategy: 155 | matrix: 156 | os: [windows, linux, darwin] 157 | arch: [amd64, arm64] 158 | steps: 159 | - name: Check out code 160 | uses: actions/checkout@v4 161 | 162 | - name: Set up Go 163 | uses: actions/setup-go@v4 164 | with: 165 | go-version: '1.21' 166 | check-latest: true 167 | cache: true 168 | 169 | - name: Set version 170 | id: set-version 171 | run: | 172 | echo "version=$(date +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT 173 | 174 | - name: Build for ${{ matrix.os }}-${{ matrix.arch }} 175 | env: 176 | GOOS: ${{ matrix.os }} 177 | GOARCH: ${{ matrix.arch }} 178 | VERSION: ${{ steps.set-version.outputs.version }} 179 | run: | 180 | # 设置文件扩展名(Windows为.exe,其他无扩展名) 181 | if [ "${{ matrix.os }}" == "windows" ]; then 182 | EXT=".exe" 183 | else 184 | EXT="" 185 | fi 186 | 187 | BINARY_NAME="api-pool-lite-${{ matrix.os }}-${{ matrix.arch }}${EXT}" 188 | go build -ldflags "-X main.AppVersion=$VERSION" -o "$BINARY_NAME" ./api-pool.go 189 | 190 | - name: Upload Artifact 191 | uses: actions/upload-artifact@v4 192 | with: 193 | name: api-pool-lite-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.set-version.outputs.version }} 194 | path: api-pool-lite-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} 195 | retention-days: 90 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /api-pool-lite.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "flag" 8 | "io" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path" 14 | "strings" 15 | "sync" 16 | ) 17 | 18 | // Config 结构体用于存储命令行参数配置 19 | type Config struct { 20 | KeyFile string // API 密钥文件路径 21 | TargetURL string // 目标 API 基础 URL 22 | Port string // 代理服务器监听端口 23 | Address string // 代理服务器监听地址 24 | Password string // 客户端身份验证密码 25 | MaxWorkers int // 最大工作协程数 26 | MaxQueue int // 最大请求队列长度 27 | } 28 | 29 | // parseFlags 解析命令行参数并返回 Config 实例 30 | func parseFlags() *Config { 31 | cfg := &Config{} 32 | flag.StringVar(&cfg.KeyFile, "key-file", "", "Path to the API key file") 33 | flag.StringVar(&cfg.TargetURL, "target-url", "", "Target API base URL") 34 | flag.StringVar(&cfg.Port, "port", "8080", "Port to listen on") 35 | flag.StringVar(&cfg.Address, "address", "localhost", "Address to listen on") 36 | flag.StringVar(&cfg.Password, "password", "", "Password for client authentication") 37 | 38 | // 添加WorkerPool相关配置 39 | maxWorkers := flag.Int("max-workers", 50, "Maximum number of worker goroutines") 40 | maxQueue := flag.Int("max-queue", 500, "Maximum size of request queue") 41 | 42 | flag.Parse() 43 | 44 | // 将WorkerPool配置添加到Config结构体 45 | cfg.MaxWorkers = *maxWorkers 46 | cfg.MaxQueue = *maxQueue 47 | 48 | return cfg 49 | } 50 | 51 | // KeyPool 管理 API 密钥池 52 | type KeyPool struct { 53 | keys []string // 密钥列表 54 | mu sync.Mutex // 互斥锁,确保线程安全 55 | currentIndex int // 当前密钥索引,用于循环抽取 56 | } 57 | 58 | // NewKeyPool 从文件中加载密钥并创建 KeyPool 实例 59 | func NewKeyPool(filePath string) (*KeyPool, error) { 60 | file, err := os.Open(filePath) 61 | if err != nil { 62 | log.Printf("[ERROR] Failed to open key file %s: %v", filePath, err) 63 | return nil, err 64 | } 65 | defer file.Close() 66 | 67 | var keys []string 68 | scanner := bufio.NewScanner(file) 69 | for scanner.Scan() { 70 | key := strings.TrimSpace(scanner.Text()) 71 | if key != "" { 72 | keys = append(keys, key) 73 | } 74 | } 75 | if err := scanner.Err(); err != nil { 76 | log.Printf("[ERROR] Failed to read key file %s: %v", filePath, err) 77 | return nil, err 78 | } 79 | log.Printf("[INFO] Loaded %d keys from file %s", len(keys), filePath) 80 | return &KeyPool{keys: keys, currentIndex: 0}, nil 81 | } 82 | 83 | // GetRandomKey 按顺序循环返回一个密钥 84 | func (kp *KeyPool) GetRandomKey() string { 85 | kp.mu.Lock() 86 | defer kp.mu.Unlock() 87 | if len(kp.keys) == 0 { 88 | return "" 89 | } 90 | key := kp.keys[kp.currentIndex] 91 | kp.currentIndex = (kp.currentIndex + 1) % len(kp.keys) // 循环到下一个索引 92 | return key 93 | } 94 | 95 | // 定义请求结构体 96 | type ProxyRequest struct { 97 | Request *http.Request 98 | Response http.ResponseWriter 99 | Done chan bool // 用于通知请求处理完成 100 | } 101 | 102 | // Worker结构体,表示一个工作协程 103 | type Worker struct { 104 | ID int 105 | TaskQueue chan *ProxyRequest // 任务队列 106 | Quit chan bool // 退出信号 107 | WorkerPool *WorkerPool // 所属工作池 108 | } 109 | 110 | // 创建新的Worker 111 | func NewWorker(id int, workerPool *WorkerPool) *Worker { 112 | return &Worker{ 113 | ID: id, 114 | TaskQueue: make(chan *ProxyRequest), 115 | Quit: make(chan bool), 116 | WorkerPool: workerPool, 117 | } 118 | } 119 | 120 | // Worker开始工作 121 | func (w *Worker) Start() { 122 | go func() { 123 | for { 124 | // 将worker注册到工作池的空闲队列 125 | w.WorkerPool.WorkerQueue <- w.TaskQueue 126 | 127 | select { 128 | case task := <-w.TaskQueue: 129 | // 处理请求 130 | w.WorkerPool.HandleFunc(task.Response, task.Request) 131 | task.Done <- true 132 | case <-w.Quit: 133 | // 收到退出信号 134 | return 135 | } 136 | } 137 | }() 138 | } 139 | 140 | // Worker停止工作 141 | func (w *Worker) Stop() { 142 | go func() { 143 | w.Quit <- true 144 | }() 145 | } 146 | 147 | // WorkerPool结构体,管理工作协程池 148 | type WorkerPool struct { 149 | WorkerQueue chan chan *ProxyRequest // 空闲Worker队列 150 | TaskQueue chan *ProxyRequest // 任务队列 151 | MaxWorkers int // 最大Worker数量 152 | MaxQueue int // 最大队列长度 153 | HandleFunc func(http.ResponseWriter, *http.Request) // 请求处理函数 154 | } 155 | 156 | // 创建新的WorkerPool 157 | func NewWorkerPool(maxWorkers int, maxQueue int, handleFunc func(http.ResponseWriter, *http.Request)) *WorkerPool { 158 | pool := &WorkerPool{ 159 | WorkerQueue: make(chan chan *ProxyRequest, maxWorkers), 160 | TaskQueue: make(chan *ProxyRequest, maxQueue), 161 | MaxWorkers: maxWorkers, 162 | MaxQueue: maxQueue, 163 | HandleFunc: handleFunc, 164 | } 165 | return pool 166 | } 167 | 168 | // 启动WorkerPool 169 | func (wp *WorkerPool) Start() { 170 | // 创建并启动workers 171 | for i := 0; i < wp.MaxWorkers; i++ { 172 | worker := NewWorker(i, wp) 173 | worker.Start() 174 | log.Printf("[INFO] Started worker %d", i) 175 | } 176 | 177 | // 启动任务分发协程 178 | go wp.dispatch() 179 | } 180 | 181 | // 停止WorkerPool 182 | func (wp *WorkerPool) Stop() { 183 | // TODO: 实现停止逻辑 184 | } 185 | 186 | // 将任务分发给空闲worker 187 | func (wp *WorkerPool) dispatch() { 188 | for { 189 | select { 190 | case task := <-wp.TaskQueue: 191 | // 等待空闲worker 192 | workerTaskQueue := <-wp.WorkerQueue 193 | // 将任务发送给worker 194 | workerTaskQueue <- task 195 | } 196 | } 197 | } 198 | 199 | // 将请求提交到WorkerPool 200 | func (wp *WorkerPool) Submit(response http.ResponseWriter, request *http.Request) bool { 201 | task := &ProxyRequest{ 202 | Request: request, 203 | Response: response, 204 | Done: make(chan bool, 1), 205 | } 206 | 207 | select { 208 | case wp.TaskQueue <- task: 209 | // 请求成功加入队列 210 | <-task.Done // 等待任务完成 211 | return true 212 | default: 213 | // 队列已满,实现背压 214 | log.Println("[WARN] Task queue is full, rejecting request") 215 | http.Error(response, "Server is busy, please try again later", http.StatusServiceUnavailable) 216 | return false 217 | } 218 | } 219 | 220 | // ProxyHandler 处理 HTTP 代理请求 221 | type ProxyHandler struct { 222 | cfg *Config // 配置信息 223 | keyPool *KeyPool // 密钥池 224 | client *http.Client // HTTP 客户端 225 | workerPool *WorkerPool // 工作协程池 226 | } 227 | 228 | // NewProxyHandler 创建 ProxyHandler 实例 229 | func NewProxyHandler(cfg *Config, keyPool *KeyPool) *ProxyHandler { 230 | handler := &ProxyHandler{ 231 | cfg: cfg, 232 | keyPool: keyPool, 233 | client: &http.Client{}, 234 | } 235 | return handler 236 | } 237 | 238 | // InitWorkerPool 初始化工作协程池 239 | func (ph *ProxyHandler) InitWorkerPool(maxWorkers int, maxQueue int) { 240 | ph.workerPool = NewWorkerPool(maxWorkers, maxQueue, ph.HandleRequest) 241 | ph.workerPool.Start() 242 | log.Printf("[INFO] Started worker pool with %d workers and queue size %d", maxWorkers, maxQueue) 243 | } 244 | 245 | // ServeHTTP 实现 HTTP 处理逻辑 246 | func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 247 | // 记录接收到的请求 248 | log.Printf("[INFO] Received request: %s %s", r.Method, r.URL.String()) 249 | 250 | // 将请求提交到工作池处理 251 | ph.workerPool.Submit(w, r) 252 | } 253 | 254 | // 简化的模型提取函数 255 | func extractModelInfo(bodyData []byte) (string, error) { 256 | var data map[string]interface{} 257 | if err := json.Unmarshal(bodyData, &data); err != nil { 258 | return "", err 259 | } 260 | if model, ok := data["model"].(string); ok { 261 | return model, nil 262 | } 263 | return "", nil 264 | } 265 | 266 | // 检测是否为流式请求 267 | func isStreamRequest(r *http.Request, bodyData []byte) bool { 268 | // 检查URL参数 269 | if strings.Contains(r.URL.RawQuery, "stream=true") { 270 | return true 271 | } 272 | 273 | // 检查请求体 274 | var data map[string]interface{} 275 | if err := json.Unmarshal(bodyData, &data); err == nil { 276 | if stream, ok := data["stream"].(bool); ok && stream { 277 | return true 278 | } 279 | } 280 | return false 281 | } 282 | 283 | // HandleRequest 处理请求的方法,由Worker调用 284 | func (ph *ProxyHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { 285 | // 验证客户端身份 286 | if !ph.authenticate(r) { 287 | log.Println("[WARN] Unauthorized access attempt") 288 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 289 | return 290 | } 291 | log.Println("[INFO] Authentication successful") 292 | 293 | // 读取请求体 - 只读取一次 294 | var bodyBytes []byte 295 | var err error 296 | if r.Body != nil { 297 | bodyBytes, err = io.ReadAll(r.Body) 298 | if err != nil { 299 | log.Printf("[ERROR] Failed to read request body: %v", err) 300 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 301 | return 302 | } 303 | 304 | // 异步提取模型信息 305 | go func() { 306 | if model, err := extractModelInfo(bodyBytes); err == nil && model != "" { 307 | log.Printf("[INFO] Model specified in request: %s", model) 308 | } 309 | }() 310 | 311 | // 检测是否为流式请求 312 | if isStreamRequest(r, bodyBytes) { 313 | log.Println("[INFO] Streaming request detected") 314 | } 315 | } 316 | 317 | // 构建目标 URL 318 | targetURL, err := ph.buildTargetURL(r) 319 | if err != nil { 320 | log.Printf("[ERROR] Failed to build target URL: %v", err) 321 | http.Error(w, "Bad Request", http.StatusBadRequest) 322 | return 323 | } 324 | log.Printf("[INFO] Target URL: %s", targetURL) 325 | 326 | // 重试逻辑 327 | maxRetries := len(ph.keyPool.keys) 328 | attemptedKeys := make(map[string]bool) 329 | log.Printf("[INFO] Starting key selection process, total keys available: %d", maxRetries) 330 | 331 | for i := 0; i < maxRetries; i++ { 332 | key := ph.getUnusedKey(attemptedKeys) 333 | if key == "" { 334 | log.Printf("[ERROR] No unused keys remaining after %d attempts", i) 335 | break 336 | } 337 | attemptedKeys[key] = true 338 | maskedKey := maskKey(key) 339 | log.Printf("[INFO] Attempt %d/%d: Selecting key %s", i+1, maxRetries, maskedKey) 340 | 341 | // 创建请求 - 为每次尝试创建新的请求体副本 342 | req, err := ph.createRequest(r, targetURL, key, bodyBytes) 343 | if err != nil { 344 | log.Printf("[ERROR] Failed to create request with key %s: %v", maskedKey, err) 345 | log.Printf("[INFO] Switching to another key due to request creation failure") 346 | continue 347 | } 348 | 349 | // 发送请求 350 | log.Printf("[INFO] Sending request to target API with key %s", maskedKey) 351 | resp, err := ph.client.Do(req) 352 | if err != nil { 353 | log.Printf("[ERROR] Failed to send request with key %s: %v", maskedKey, err) 354 | log.Printf("[INFO] Switching to another key due to network error") 355 | continue 356 | } 357 | defer resp.Body.Close() 358 | 359 | // 处理响应 360 | log.Printf("[INFO] Received response with status code %d", resp.StatusCode) 361 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 362 | log.Println("[INFO] Request successful, forwarding response") 363 | ph.forwardResponse(w, resp) 364 | return 365 | } else if resp.StatusCode == 403 || resp.StatusCode == 429 { 366 | log.Printf("[WARN] Received %d status code with key %s", resp.StatusCode, maskedKey) 367 | log.Printf("[INFO] Switching to another key due to status code %d", resp.StatusCode) 368 | continue 369 | } else { 370 | log.Printf("[INFO] Forwarding response with status code %d", resp.StatusCode) 371 | ph.forwardResponse(w, resp) 372 | return 373 | } 374 | } 375 | 376 | // 所有密钥尝试后仍失败 377 | log.Printf("[ERROR] All %d keys failed after retries", maxRetries) 378 | http.Error(w, "Failed to get response from API after all retries", http.StatusBadGateway) 379 | } 380 | 381 | // getUnusedKey 获取一个未使用过的密钥 382 | func (ph *ProxyHandler) getUnusedKey(attempted map[string]bool) string { 383 | key := ph.keyPool.GetRandomKey() 384 | // 如果获取到的密钥已使用过,则尝试其他密钥 385 | for attempted[key] && len(attempted) < len(ph.keyPool.keys) { 386 | key = ph.keyPool.GetRandomKey() 387 | } 388 | // 如果所有密钥都已尝试过,返回空字符串 389 | if attempted[key] { 390 | return "" 391 | } 392 | return key 393 | } 394 | 395 | // authenticate 验证客户端身份 396 | func (ph *ProxyHandler) authenticate(r *http.Request) bool { 397 | authHeader := r.Header.Get("Authorization") 398 | if authHeader == "" { 399 | return false 400 | } 401 | parts := strings.Split(authHeader, " ") 402 | if len(parts) != 2 || parts[0] != "Bearer" { 403 | return false 404 | } 405 | return parts[1] == ph.cfg.Password 406 | } 407 | 408 | // buildTargetURL 构建目标 API 的完整 URL 409 | func (ph *ProxyHandler) buildTargetURL(r *http.Request) (string, error) { 410 | u, err := url.Parse(ph.cfg.TargetURL) 411 | if err != nil { 412 | return "", err 413 | } 414 | u.Path = path.Join(u.Path, r.URL.Path) 415 | u.RawQuery = r.URL.RawQuery 416 | return u.String(), nil 417 | } 418 | 419 | // createRequest 创建转发请求 - 只修改Authorization头,为每次请求创建新的请求体 420 | func (ph *ProxyHandler) createRequest(r *http.Request, targetURL, key string, bodyBytes []byte) (*http.Request, error) { 421 | // 为每次请求创建新的请求体 422 | var bodyReader io.Reader 423 | if len(bodyBytes) > 0 { 424 | bodyReader = bytes.NewReader(bodyBytes) 425 | } 426 | 427 | // 创建请求 428 | req, err := http.NewRequest(r.Method, targetURL, bodyReader) 429 | if err != nil { 430 | return nil, err 431 | } 432 | 433 | // 复制所有原始请求头 434 | for k, v := range r.Header { 435 | req.Header[k] = v 436 | } 437 | 438 | // 仅替换Authorization头 439 | req.Header.Set("Authorization", "Bearer "+key) 440 | return req, nil 441 | } 442 | 443 | // forwardResponse 将响应转发给客户端,支持流式和非流式 444 | func (ph *ProxyHandler) forwardResponse(w http.ResponseWriter, resp *http.Response) { 445 | // 设置响应头 446 | for k, v := range resp.Header { 447 | w.Header()[k] = v 448 | } 449 | w.WriteHeader(resp.StatusCode) 450 | 451 | // 处理流式响应 452 | if strings.Contains(resp.Header.Get("Content-Type"), "text/event-stream") || resp.Header.Get("Transfer-Encoding") == "chunked" { 453 | log.Println("[INFO] Handling streaming response") 454 | flusher, ok := w.(http.Flusher) 455 | if !ok { 456 | log.Println("[ERROR] Streaming unsupported by server") 457 | http.Error(w, "Streaming unsupported", http.StatusInternalServerError) 458 | return 459 | } 460 | reader := bufio.NewReader(resp.Body) 461 | for { 462 | line, err := reader.ReadBytes('\n') 463 | if err != nil { 464 | if err == io.EOF { 465 | log.Println("[INFO] Stream ended") 466 | break 467 | } 468 | log.Printf("[ERROR] Error reading stream: %v", err) 469 | http.Error(w, "Error reading stream", http.StatusInternalServerError) 470 | return 471 | } 472 | w.Write(line) 473 | flusher.Flush() 474 | } 475 | } else { 476 | // 非流式响应,直接复制 477 | _, err := io.Copy(w, resp.Body) 478 | if err != nil { 479 | log.Printf("[ERROR] Failed to forward response: %v", err) 480 | } 481 | } 482 | } 483 | 484 | // maskKey 直接返回原始密钥,不再进行掩码处理 485 | func maskKey(key string) string { 486 | return key 487 | } 488 | 489 | // main 函数,启动代理服务器 490 | func main() { 491 | // 解析配置 492 | cfg := parseFlags() 493 | if cfg.KeyFile == "" || cfg.TargetURL == "" || cfg.Password == "" { 494 | log.Println("[ERROR] Missing required flags: --key-file, --target-url, --password") 495 | os.Exit(1) 496 | } 497 | log.Printf("[INFO] Configuration loaded: KeyFile=%s, TargetURL=%s, Address=%s, Port=%s, MaxWorkers=%d, MaxQueue=%d", 498 | cfg.KeyFile, cfg.TargetURL, cfg.Address, cfg.Port, cfg.MaxWorkers, cfg.MaxQueue) 499 | 500 | // 初始化密钥池 501 | keyPool, err := NewKeyPool(cfg.KeyFile) 502 | if err != nil { 503 | log.Printf("[ERROR] Failed to initialize key pool: %v", err) 504 | os.Exit(1) 505 | } 506 | 507 | // 创建代理处理器 508 | proxyHandler := NewProxyHandler(cfg, keyPool) 509 | 510 | // 初始化并启动工作池 511 | proxyHandler.InitWorkerPool(cfg.MaxWorkers, cfg.MaxQueue) 512 | 513 | // 启动服务器 514 | addr := cfg.Address + ":" + cfg.Port 515 | log.Printf("[INFO] Starting proxy server on %s", addr) 516 | if err := http.ListenAndServe(addr, proxyHandler); err != nil { 517 | log.Printf("[ERROR] Failed to start server: %v", err) 518 | os.Exit(1) 519 | } 520 | } -------------------------------------------------------------------------------- /api-pool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path" 14 | "strings" 15 | "sync" 16 | ) 17 | 18 | // Config 结构体用于存储命令行参数配置 19 | type Config struct { 20 | KeyFile string // API 密钥文件路径 21 | TargetURL string // 目标 API 基础 URL 22 | Port string // 代理服务器监听端口 23 | Address string // 代理服务器监听地址 24 | Password string // 客户端身份验证密码 25 | MaxWorkers int // 最大工作协程数 26 | MaxQueue int // 最大请求队列长度 27 | } 28 | 29 | // parseFlags 解析命令行参数并返回 Config 实例 30 | func parseFlags() *Config { 31 | cfg := &Config{} 32 | 33 | // 基本配置 34 | flag.StringVar(&cfg.KeyFile, "key-file", "", "Path to the API key file") 35 | flag.StringVar(&cfg.TargetURL, "target-url", "", "Target API base URL") 36 | flag.StringVar(&cfg.Port, "port", "8080", "Port to listen on") 37 | flag.StringVar(&cfg.Address, "address", "localhost", "Address to listen on") 38 | flag.StringVar(&cfg.Password, "password", "", "Password for client authentication") 39 | 40 | // WorkerPool相关配置,直接存储到Config结构体字段中 41 | flag.IntVar(&cfg.MaxWorkers, "max-workers", 50, "Maximum number of worker goroutines") 42 | flag.IntVar(&cfg.MaxQueue, "max-queue", 500, "Maximum size of request queue") 43 | 44 | // 添加帮助信息 45 | flag.Usage = func() { 46 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 47 | flag.PrintDefaults() 48 | fmt.Fprintf(os.Stderr, "\nExample:\n %s --key-file=./keys.txt --target-url=https://api.example.com --password=mysecret --max-workers=100 --max-queue=1000\n", os.Args[0]) 49 | } 50 | 51 | flag.Parse() 52 | 53 | // 验证参数值范围 54 | if cfg.MaxWorkers <= 0 { 55 | log.Printf("[WARN] Invalid max-workers value %d, using default 50", cfg.MaxWorkers) 56 | cfg.MaxWorkers = 50 57 | } 58 | 59 | if cfg.MaxQueue <= 0 { 60 | log.Printf("[WARN] Invalid max-queue value %d, using default 500", cfg.MaxQueue) 61 | cfg.MaxQueue = 500 62 | } 63 | 64 | return cfg 65 | } 66 | 67 | // KeyPool 管理 API 密钥池 68 | type KeyPool struct { 69 | keys []string // 密钥列表 70 | mu sync.Mutex // 互斥锁,确保线程安全 71 | currentIndex int // 当前密钥索引,用于循环抽取 72 | } 73 | 74 | // NewKeyPool 从文件中加载密钥并创建 KeyPool 实例 75 | func NewKeyPool(filePath string) (*KeyPool, error) { 76 | file, err := os.Open(filePath) 77 | if err != nil { 78 | log.Printf("[ERROR] Failed to open key file %s: %v", filePath, err) 79 | return nil, err 80 | } 81 | defer file.Close() 82 | 83 | var keys []string 84 | scanner := bufio.NewScanner(file) 85 | for scanner.Scan() { 86 | key := strings.TrimSpace(scanner.Text()) 87 | if key != "" { 88 | keys = append(keys, key) 89 | } 90 | } 91 | if err := scanner.Err(); err != nil { 92 | log.Printf("[ERROR] Failed to read key file %s: %v", filePath, err) 93 | return nil, err 94 | } 95 | log.Printf("[INFO] Loaded %d keys from file %s", len(keys), filePath) 96 | return &KeyPool{keys: keys, currentIndex: 0}, nil 97 | } 98 | 99 | // GetRandomKey 按顺序循环返回一个密钥 100 | func (kp *KeyPool) GetRandomKey() string { 101 | kp.mu.Lock() 102 | defer kp.mu.Unlock() 103 | if len(kp.keys) == 0 { 104 | return "" 105 | } 106 | key := kp.keys[kp.currentIndex] 107 | kp.currentIndex = (kp.currentIndex + 1) % len(kp.keys) // 循环到下一个索引 108 | return key 109 | } 110 | 111 | // 定义请求结构体 112 | type ProxyRequest struct { 113 | Request *http.Request 114 | Response http.ResponseWriter 115 | Done chan bool // 用于通知请求处理完成 116 | } 117 | 118 | // Worker结构体,表示一个工作协程 119 | type Worker struct { 120 | ID int 121 | TaskQueue chan *ProxyRequest // 任务队列 122 | Quit chan bool // 退出信号 123 | WorkerPool *WorkerPool // 所属工作池 124 | } 125 | 126 | // 创建新的Worker 127 | func NewWorker(id int, workerPool *WorkerPool) *Worker { 128 | return &Worker{ 129 | ID: id, 130 | TaskQueue: make(chan *ProxyRequest), 131 | Quit: make(chan bool), 132 | WorkerPool: workerPool, 133 | } 134 | } 135 | 136 | // Worker开始工作 137 | func (w *Worker) Start() { 138 | go func() { 139 | for { 140 | // 将worker注册到工作池的空闲队列 141 | w.WorkerPool.WorkerQueue <- w.TaskQueue 142 | 143 | select { 144 | case task := <-w.TaskQueue: 145 | // 处理请求 146 | w.WorkerPool.HandleFunc(task.Response, task.Request) 147 | task.Done <- true 148 | case <-w.Quit: 149 | // 收到退出信号 150 | return 151 | } 152 | } 153 | }() 154 | } 155 | 156 | // Worker停止工作 157 | func (w *Worker) Stop() { 158 | go func() { 159 | w.Quit <- true 160 | }() 161 | } 162 | 163 | // WorkerPool结构体,管理工作协程池 164 | type WorkerPool struct { 165 | WorkerQueue chan chan *ProxyRequest // 空闲Worker队列 166 | TaskQueue chan *ProxyRequest // 任务队列 167 | MaxWorkers int // 最大Worker数量 168 | MaxQueue int // 最大队列长度 169 | HandleFunc func(http.ResponseWriter, *http.Request) // 请求处理函数 170 | } 171 | 172 | // 创建新的WorkerPool 173 | func NewWorkerPool(maxWorkers int, maxQueue int, handleFunc func(http.ResponseWriter, *http.Request)) *WorkerPool { 174 | pool := &WorkerPool{ 175 | WorkerQueue: make(chan chan *ProxyRequest, maxWorkers), 176 | TaskQueue: make(chan *ProxyRequest, maxQueue), 177 | MaxWorkers: maxWorkers, 178 | MaxQueue: maxQueue, 179 | HandleFunc: handleFunc, 180 | } 181 | return pool 182 | } 183 | 184 | // 启动WorkerPool 185 | func (wp *WorkerPool) Start() { 186 | // 创建并启动workers 187 | for i := 0; i < wp.MaxWorkers; i++ { 188 | worker := NewWorker(i, wp) 189 | worker.Start() 190 | log.Printf("[INFO] Started worker %d", i) 191 | } 192 | 193 | // 启动任务分发协程 194 | go wp.dispatch() 195 | } 196 | 197 | // 停止WorkerPool 198 | func (wp *WorkerPool) Stop() { 199 | // TODO: 实现停止逻辑 200 | } 201 | 202 | // 将任务分发给空闲worker 203 | func (wp *WorkerPool) dispatch() { 204 | for { 205 | select { 206 | case task := <-wp.TaskQueue: 207 | // 等待空闲worker 208 | workerTaskQueue := <-wp.WorkerQueue 209 | // 将任务发送给worker 210 | workerTaskQueue <- task 211 | } 212 | } 213 | } 214 | 215 | // 将请求提交到WorkerPool 216 | func (wp *WorkerPool) Submit(response http.ResponseWriter, request *http.Request) bool { 217 | task := &ProxyRequest{ 218 | Request: request, 219 | Response: response, 220 | Done: make(chan bool, 1), 221 | } 222 | 223 | select { 224 | case wp.TaskQueue <- task: 225 | // 请求成功加入队列 226 | <-task.Done // 等待任务完成 227 | return true 228 | default: 229 | // 队列已满,实现背压 230 | log.Println("[WARN] Task queue is full, rejecting request") 231 | http.Error(response, "Server is busy, please try again later", http.StatusServiceUnavailable) 232 | return false 233 | } 234 | } 235 | 236 | // ProxyHandler 处理 HTTP 代理请求 237 | type ProxyHandler struct { 238 | cfg *Config // 配置信息 239 | keyPool *KeyPool // 密钥池 240 | client *http.Client // HTTP 客户端 241 | workerPool *WorkerPool // 工作协程池 242 | } 243 | 244 | // NewProxyHandler 创建 ProxyHandler 实例 245 | func NewProxyHandler(cfg *Config, keyPool *KeyPool) *ProxyHandler { 246 | handler := &ProxyHandler{ 247 | cfg: cfg, 248 | keyPool: keyPool, 249 | client: &http.Client{}, 250 | } 251 | return handler 252 | } 253 | 254 | // InitWorkerPool 初始化工作协程池 255 | func (ph *ProxyHandler) InitWorkerPool(maxWorkers int, maxQueue int) { 256 | ph.workerPool = NewWorkerPool(maxWorkers, maxQueue, ph.HandleRequest) 257 | ph.workerPool.Start() 258 | log.Printf("[INFO] Started worker pool with %d workers and queue size %d", maxWorkers, maxQueue) 259 | } 260 | 261 | // ServeHTTP 实现 HTTP 处理逻辑 262 | func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 263 | // 记录接收到的请求 264 | log.Printf("[INFO] Received request: %s %s", r.Method, r.URL.String()) 265 | 266 | // 将请求提交到工作池处理 267 | ph.workerPool.Submit(w, r) 268 | } 269 | 270 | // HandleRequest 处理请求的方法,由Worker调用 271 | func (ph *ProxyHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { 272 | // 验证客户端身份 273 | if !ph.authenticate(r) { 274 | log.Println("[WARN] Unauthorized access attempt") 275 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 276 | return 277 | } 278 | log.Println("[INFO] Authentication successful") 279 | 280 | // 尝试解析请求体中的模型信息 281 | model, err := ph.extractModelFromRequest(r) 282 | if err != nil { 283 | log.Printf("[WARN] Failed to extract model from request: %v", err) 284 | } else if model != "" { 285 | log.Printf("[INFO] Model specified in request: %s", model) 286 | } 287 | 288 | // 构建目标 URL 289 | targetURL, err := ph.buildTargetURL(r) 290 | if err != nil { 291 | log.Printf("[ERROR] Failed to build target URL: %v", err) 292 | http.Error(w, "Bad Request", http.StatusBadRequest) 293 | return 294 | } 295 | log.Printf("[INFO] Target URL: %s", targetURL) 296 | 297 | // 重试逻辑 298 | maxRetries := len(ph.keyPool.keys) 299 | attemptedKeys := make(map[string]bool) 300 | log.Printf("[INFO] Starting key selection process, total keys available: %d", maxRetries) 301 | 302 | for i := 0; i < maxRetries; i++ { 303 | key := ph.getUnusedKey(attemptedKeys) 304 | if key == "" { 305 | log.Printf("[ERROR] No unused keys remaining after %d attempts", i) 306 | break 307 | } 308 | attemptedKeys[key] = true 309 | maskedKey := maskKey(key) 310 | log.Printf("[INFO] Attempt %d/%d: Selecting key %s", i+1, maxRetries, maskedKey) 311 | 312 | // 创建请求 313 | req, err := ph.createRequest(r, targetURL, key) 314 | if err != nil { 315 | log.Printf("[ERROR] Failed to create request with key %s: %v", maskedKey, err) 316 | log.Printf("[INFO] Switching to another key due to request creation failure") 317 | continue 318 | } 319 | 320 | // 发送请求 321 | log.Printf("[INFO] Sending request to target API with key %s", maskedKey) 322 | resp, err := ph.client.Do(req) 323 | if err != nil { 324 | log.Printf("[ERROR] Failed to send request with key %s: %v", maskedKey, err) 325 | log.Printf("[INFO] Switching to another key due to network error") 326 | continue 327 | } 328 | defer resp.Body.Close() 329 | 330 | // 处理响应 331 | log.Printf("[INFO] Received response with status code %d", resp.StatusCode) 332 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 333 | log.Println("[INFO] Request successful, forwarding response") 334 | ph.forwardResponse(w, resp) 335 | return 336 | } else if resp.StatusCode == 403 || resp.StatusCode == 429 { 337 | log.Printf("[WARN] Received %d status code with key %s", resp.StatusCode, maskedKey) 338 | log.Printf("[INFO] Switching to another key due to status code %d", resp.StatusCode) 339 | continue 340 | } else { 341 | log.Printf("[INFO] Forwarding response with status code %d", resp.StatusCode) 342 | ph.forwardResponse(w, resp) 343 | return 344 | } 345 | } 346 | 347 | // 所有密钥尝试后仍失败 348 | log.Printf("[ERROR] All %d keys failed after retries", maxRetries) 349 | http.Error(w, "Failed to get response from API after all retries", http.StatusBadGateway) 350 | } 351 | 352 | // getUnusedKey 获取一个未使用过的密钥 353 | func (ph *ProxyHandler) getUnusedKey(attempted map[string]bool) string { 354 | key := ph.keyPool.GetRandomKey() 355 | // 如果获取到的密钥已使用过,则尝试其他密钥 356 | for attempted[key] && len(attempted) < len(ph.keyPool.keys) { 357 | key = ph.keyPool.GetRandomKey() 358 | } 359 | // 如果所有密钥都已尝试过,返回空字符串 360 | if attempted[key] { 361 | return "" 362 | } 363 | return key 364 | } 365 | 366 | // authenticate 验证客户端身份 367 | func (ph *ProxyHandler) authenticate(r *http.Request) bool { 368 | authHeader := r.Header.Get("Authorization") 369 | if authHeader == "" { 370 | return false 371 | } 372 | parts := strings.Split(authHeader, " ") 373 | if len(parts) != 2 || parts[0] != "Bearer" { 374 | return false 375 | } 376 | return parts[1] == ph.cfg.Password 377 | } 378 | 379 | // buildTargetURL 构建目标 API 的完整 URL 380 | func (ph *ProxyHandler) buildTargetURL(r *http.Request) (string, error) { 381 | u, err := url.Parse(ph.cfg.TargetURL) 382 | if err != nil { 383 | return "", err 384 | } 385 | u.Path = path.Join(u.Path, r.URL.Path) 386 | u.RawQuery = r.URL.RawQuery 387 | return u.String(), nil 388 | } 389 | 390 | // createRequest 创建转发请求 391 | func (ph *ProxyHandler) createRequest(r *http.Request, targetURL, key string) (*http.Request, error) { 392 | req, err := http.NewRequest(r.Method, targetURL, r.Body) 393 | if err != nil { 394 | return nil, err 395 | } 396 | 397 | // 复制并修改请求头 398 | for k, v := range r.Header { 399 | if k != "Host" && k != "Connection" && k != "Proxy-Connection" && k != "Authorization" { 400 | req.Header[k] = v 401 | } 402 | } 403 | req.Header.Set("Authorization", "Bearer "+key) 404 | return req, nil 405 | } 406 | 407 | // forwardResponse 将响应转发给客户端,支持流式和非流式 408 | func (ph *ProxyHandler) forwardResponse(w http.ResponseWriter, resp *http.Response) { 409 | // 设置响应头 410 | for k, v := range resp.Header { 411 | w.Header()[k] = v 412 | } 413 | w.WriteHeader(resp.StatusCode) 414 | 415 | // 处理流式响应 416 | if strings.Contains(resp.Header.Get("Content-Type"), "text/event-stream") || resp.Header.Get("Transfer-Encoding") == "chunked" { 417 | log.Println("[INFO] Handling streaming response") 418 | flusher, ok := w.(http.Flusher) 419 | if !ok { 420 | log.Println("[ERROR] Streaming unsupported by server") 421 | http.Error(w, "Streaming unsupported", http.StatusInternalServerError) 422 | return 423 | } 424 | reader := bufio.NewReader(resp.Body) 425 | for { 426 | line, err := reader.ReadBytes('\n') 427 | if err != nil { 428 | if err == io.EOF { 429 | log.Println("[INFO] Stream ended") 430 | break 431 | } 432 | log.Printf("[ERROR] Error reading stream: %v", err) 433 | http.Error(w, "Error reading stream", http.StatusInternalServerError) 434 | return 435 | } 436 | w.Write(line) 437 | flusher.Flush() 438 | } 439 | } else { 440 | // 非流式响应,直接复制 441 | _, err := io.Copy(w, resp.Body) 442 | if err != nil { 443 | log.Printf("[ERROR] Failed to forward response: %v", err) 444 | } 445 | } 446 | } 447 | 448 | // extractModelFromRequest 尝试从请求体中提取模型名称 449 | func (ph *ProxyHandler) extractModelFromRequest(r *http.Request) (string, error) { 450 | if r.Body == nil { 451 | return "", nil 452 | } 453 | body, err := io.ReadAll(r.Body) 454 | if err != nil { 455 | return "", err 456 | } 457 | r.Body = io.NopCloser(strings.NewReader(string(body))) 458 | 459 | var data map[string]interface{} 460 | if err := json.Unmarshal(body, &data); err != nil { 461 | return "", err 462 | } 463 | if model, ok := data["model"].(string); ok { 464 | return model, nil 465 | } 466 | return "", nil 467 | } 468 | 469 | // maskKey 直接返回原始密钥,不再进行掩码处理 470 | func maskKey(key string) string { 471 | return key 472 | } 473 | 474 | // main 函数,启动代理服务器 475 | func main() { 476 | // 解析配置 477 | cfg := parseFlags() 478 | if cfg.KeyFile == "" || cfg.TargetURL == "" || cfg.Password == "" { 479 | log.Println("[ERROR] Missing required flags: --key-file, --target-url, --password") 480 | flag.Usage() 481 | os.Exit(1) 482 | } 483 | 484 | // 输出实际使用的配置参数 485 | log.Printf("[INFO] Starting with configuration:") 486 | log.Printf("[INFO] - KeyFile: %s", cfg.KeyFile) 487 | log.Printf("[INFO] - TargetURL: %s", cfg.TargetURL) 488 | log.Printf("[INFO] - Address: %s", cfg.Address) 489 | log.Printf("[INFO] - Port: %s", cfg.Port) 490 | log.Printf("[INFO] - MaxWorkers: %d", cfg.MaxWorkers) 491 | log.Printf("[INFO] - MaxQueue: %d", cfg.MaxQueue) 492 | 493 | // 初始化密钥池 494 | keyPool, err := NewKeyPool(cfg.KeyFile) 495 | if err != nil { 496 | log.Printf("[ERROR] Failed to initialize key pool: %v", err) 497 | os.Exit(1) 498 | } 499 | 500 | // 创建代理处理器 501 | proxyHandler := NewProxyHandler(cfg, keyPool) 502 | 503 | // 初始化并启动工作池 504 | proxyHandler.InitWorkerPool(cfg.MaxWorkers, cfg.MaxQueue) 505 | 506 | // 启动服务器 507 | addr := cfg.Address + ":" + cfg.Port 508 | log.Printf("[INFO] Starting proxy server on %s", addr) 509 | if err := http.ListenAndServe(addr, proxyHandler); err != nil { 510 | log.Printf("[ERROR] Failed to start server: %v", err) 511 | os.Exit(1) 512 | } 513 | } -------------------------------------------------------------------------------- /augment2api_auth.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import json 4 | import os 5 | import secrets 6 | import urllib.parse 7 | from typing import Dict, Any 8 | 9 | import requests 10 | 11 | 12 | def base64url_encode(data: bytes) -> str: 13 | """将数据进行 base64url 编码""" 14 | return base64.urlsafe_b64encode(data).decode('utf-8').replace('=', '') 15 | 16 | 17 | def sha256_hash(input_data: bytes) -> bytes: 18 | """计算输入数据的 SHA-256 哈希值""" 19 | return hashlib.sha256(input_data).digest() 20 | 21 | 22 | def create_oauth_state() -> Dict[str, Any]: 23 | """创建 OAuth 状态对象""" 24 | code_verifier_bytes = secrets.token_bytes(32) 25 | code_verifier = base64url_encode(code_verifier_bytes) 26 | 27 | code_challenge_bytes = sha256_hash(code_verifier.encode('utf-8')) 28 | code_challenge = base64url_encode(code_challenge_bytes) 29 | 30 | state = base64url_encode(secrets.token_bytes(8)) 31 | 32 | oauth_state = { 33 | "codeVerifier": code_verifier, 34 | "codeChallenge": code_challenge, 35 | "state": state, 36 | "creationTime": int(import_time()) 37 | } 38 | 39 | return oauth_state 40 | 41 | 42 | def generate_authorize_url(oauth_state: Dict[str, Any]) -> str: 43 | """生成授权 URL""" 44 | client_id = "v" 45 | 46 | params = { 47 | "response_type": "code", 48 | "code_challenge": oauth_state["codeChallenge"], 49 | "client_id": client_id, 50 | "state": oauth_state["state"], 51 | "prompt": "login" 52 | } 53 | 54 | query_string = urllib.parse.urlencode(params) 55 | authorize_url = f"https://auth.augmentcode.com/authorize?{query_string}" 56 | 57 | return authorize_url 58 | 59 | 60 | def get_access_token(tenant_url: str, code_verifier: str, code: str) -> str: 61 | """获取访问令牌""" 62 | data = { 63 | "grant_type": "authorization_code", 64 | "client_id": "v", 65 | "code_verifier": code_verifier, 66 | "redirect_uri": "", 67 | "code": code 68 | } 69 | 70 | response = requests.post( 71 | f"{tenant_url}token", 72 | json=data, 73 | headers={"Content-Type": "application/json"} 74 | ) 75 | 76 | json_response = response.json() 77 | token = json_response.get("access_token") 78 | 79 | return token 80 | 81 | 82 | def parse_code(code_str: str) -> Dict[str, str]: 83 | """解析返回的代码""" 84 | parsed = json.loads(code_str) 85 | return { 86 | "code": parsed.get("code"), 87 | "state": parsed.get("state"), 88 | "tenant_url": parsed.get("tenant_url") 89 | } 90 | 91 | 92 | def import_time(): 93 | """获取当前时间戳""" 94 | import time 95 | return time.time() * 1000 96 | 97 | 98 | def main(): 99 | """主函数""" 100 | print("正在生成 OAuth 状态...") 101 | oauth_state = create_oauth_state() 102 | print("OAuth 状态已生成:") 103 | print(json.dumps(oauth_state, indent=2)) 104 | 105 | url = generate_authorize_url(oauth_state) 106 | print("\n请访问以下 URL 进行授权:") 107 | print(url) 108 | 109 | code_str = input("\n请输入返回的代码 (JSON 格式): ") 110 | 111 | try: 112 | parsed_code = parse_code(code_str) 113 | print("代码已解析:") 114 | print(json.dumps(parsed_code, indent=2)) 115 | 116 | print("\n正在获取访问令牌...") 117 | token = get_access_token( 118 | parsed_code["tenant_url"], 119 | oauth_state["codeVerifier"], 120 | parsed_code["code"] 121 | ) 122 | 123 | print("\n访问令牌:") 124 | print(token) 125 | 126 | except Exception as e: 127 | print(f"发生错误: {e}") 128 | 129 | 130 | if __name__ == "__main__": 131 | main() -------------------------------------------------------------------------------- /augment2api_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | OpenAI to Augment API Adapter 4 | 5 | 这个FastAPI应用程序将OpenAI API请求格式转换为Augment API格式, 6 | 允许OpenAI客户端直接与Augment服务通信。 7 | 所有配置参数都通过命令行参数提供,不依赖于环境变量或配置文件。 8 | """ 9 | 10 | import os 11 | import json 12 | import uuid 13 | import time 14 | import logging 15 | import argparse 16 | from typing import List, Optional, Dict, Any, Literal, Union 17 | from datetime import datetime 18 | 19 | import httpx 20 | from fastapi import FastAPI, Header, HTTPException, Depends, Request 21 | from fastapi.responses import StreamingResponse, JSONResponse 22 | from fastapi.middleware.cors import CORSMiddleware 23 | from pydantic import BaseModel, Field 24 | import uvicorn 25 | 26 | # 配置日志 27 | logging.basicConfig( 28 | level=logging.INFO, 29 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 30 | ) 31 | logger = logging.getLogger(__name__) 32 | 33 | ################################################# 34 | # 模型定义 35 | ################################################# 36 | 37 | # 新增:支持OpenAI新格式的内容项定义 38 | class ContentItem(BaseModel): 39 | """表示OpenAI聊天API中的内容项""" 40 | type: str # 例如 "text", "image_url" 等 41 | text: Optional[str] = None 42 | # 可以在这里添加其他内容类型的字段,如image_url等 43 | 44 | # OpenAI API 请求模型 45 | class ChatMessage(BaseModel): 46 | """表示OpenAI聊天API中的单条消息""" 47 | role: Literal["system", "user", "assistant", "function"] 48 | # 修改:content字段现在可以是字符串或内容项数组 49 | content: Optional[Union[str, List[ContentItem]]] = None 50 | name: Optional[str] = None 51 | 52 | class ChatCompletionRequest(BaseModel): 53 | """OpenAI聊天完成API请求模型""" 54 | model: str 55 | messages: List[ChatMessage] 56 | temperature: Optional[float] = 1.0 57 | top_p: Optional[float] = 1.0 58 | n: Optional[int] = 1 59 | stream: Optional[bool] = False 60 | max_tokens: Optional[int] = None 61 | presence_penalty: Optional[float] = 0 62 | frequency_penalty: Optional[float] = 0 63 | user: Optional[str] = None 64 | 65 | # OpenAI API 响应模型 66 | class ChatCompletionResponseChoice(BaseModel): 67 | """OpenAI聊天完成API响应中的单个选择""" 68 | index: int 69 | message: ChatMessage 70 | finish_reason: Optional[str] = None 71 | 72 | class Usage(BaseModel): 73 | """OpenAI API响应中的token使用信息""" 74 | prompt_tokens: int 75 | completion_tokens: int 76 | total_tokens: int 77 | 78 | class ChatCompletionResponse(BaseModel): 79 | """OpenAI聊天完成API响应模型""" 80 | id: str 81 | object: str = "chat.completion" 82 | created: int 83 | model: str 84 | choices: List[ChatCompletionResponseChoice] 85 | usage: Usage 86 | 87 | # OpenAI API 流式响应模型 88 | class ChatCompletionStreamResponseChoice(BaseModel): 89 | """OpenAI聊天完成流式API响应中的单个选择""" 90 | index: int 91 | delta: Dict[str, Any] 92 | finish_reason: Optional[str] = None 93 | 94 | class ChatCompletionStreamResponse(BaseModel): 95 | """OpenAI聊天完成流式API响应模型""" 96 | id: str 97 | object: str = "chat.completion.chunk" 98 | created: int 99 | model: str 100 | choices: List[ChatCompletionStreamResponseChoice] 101 | 102 | # 模型信息响应 103 | class ModelInfo(BaseModel): 104 | """OpenAI模型信息""" 105 | id: str 106 | object: str = "model" 107 | created: int 108 | owned_by: str = "augment" 109 | 110 | class ModelListResponse(BaseModel): 111 | """OpenAI模型列表响应""" 112 | object: str = "list" 113 | data: List[ModelInfo] 114 | 115 | # Augment API 请求相关模型 116 | class AugmentResponseNode(BaseModel): 117 | """Augment API响应节点""" 118 | id: int 119 | type: int 120 | content: str 121 | tool_use: Optional[Any] = None 122 | 123 | class AugmentChatHistoryItem(BaseModel): 124 | """Augment API聊天历史记录条目""" 125 | request_message: str 126 | response_text: str 127 | request_id: Optional[str] = None 128 | request_nodes: List[Any] = [] 129 | response_nodes: List[AugmentResponseNode] = [] 130 | 131 | class AugmentBlobs(BaseModel): 132 | """Augment API Blobs对象""" 133 | checkpoint_id: Optional[str] = None 134 | added_blobs: List[Any] = [] 135 | deleted_blobs: List[Any] = [] 136 | 137 | class AugmentVcsChange(BaseModel): 138 | """Augment API VCS更改""" 139 | working_directory_changes: List[Any] = [] 140 | 141 | class AugmentFeatureFlags(BaseModel): 142 | """Augment API功能标志""" 143 | support_raw_output: bool = True 144 | 145 | # 完整的Augment API请求模型 146 | class AugmentChatRequest(BaseModel): 147 | """Augment API聊天请求模型 - 基于抓包分析更新""" 148 | model: Optional[str] = None 149 | path: Optional[str] = None 150 | prefix: Optional[str] = None 151 | selected_code: Optional[str] = None 152 | suffix: Optional[str] = None 153 | message: str 154 | chat_history: List[AugmentChatHistoryItem] = [] 155 | lang: Optional[str] = None 156 | blobs: AugmentBlobs = AugmentBlobs() 157 | user_guided_blobs: List[Any] = [] 158 | context_code_exchange_request_id: Optional[str] = None 159 | vcs_change: AugmentVcsChange = AugmentVcsChange() 160 | recency_info_recent_changes: List[Any] = [] 161 | external_source_ids: List[Any] = [] 162 | disable_auto_external_sources: Optional[bool] = None 163 | user_guidelines: str = "" 164 | workspace_guidelines: str = "" 165 | feature_detection_flags: AugmentFeatureFlags = AugmentFeatureFlags() 166 | tool_definitions: List[Any] = [] 167 | nodes: List[Any] = [] 168 | mode: str = "CHAT" 169 | agent_memories: Optional[Any] = None 170 | system_prompt: Optional[str] = None # 保留此字段以兼容之前的代码 171 | 172 | # Augment API响应模型 173 | class AugmentResponseChunk(BaseModel): 174 | """Augment API响应块""" 175 | text: str 176 | unknown_blob_names: List[Any] = [] 177 | checkpoint_not_found: bool = False 178 | workspace_file_chunks: List[Any] = [] 179 | incorporated_external_sources: List[Any] = [] 180 | nodes: List[AugmentResponseNode] = [] 181 | 182 | ################################################# 183 | # 辅助函数 184 | ################################################# 185 | 186 | def generate_id(): 187 | """生成唯一ID,类似于OpenAI的格式""" 188 | return str(uuid.uuid4()).replace("-", "")[:24] 189 | 190 | def estimate_tokens(text): 191 | """ 192 | 估计文本的token数量 193 | 这是一个简单的估算,实际数量可能有所不同 194 | """ 195 | if not text: 196 | return 0 197 | # 简单估算:假设每个单词约等于1.3个token 198 | # 中文字符每个字约等于1个token 199 | words = len(text.split()) if text else 0 200 | chinese_chars = sum(1 for char in text if '\u4e00' <= char <= '\u9fff') if text else 0 201 | return int(words * 1.3 + chinese_chars) 202 | 203 | def map_model_name(openai_model: str) -> Optional[str]: 204 | """ 205 | 将OpenAI模型名称映射到Augment模型名称 206 | 207 | Args: 208 | openai_model: OpenAI格式的模型名称 209 | 210 | Returns: 211 | Augment格式的模型名称,或None表示使用自动选择 212 | """ 213 | # 模型名称映射规则 214 | if openai_model == "augment-auto": 215 | # 使用null表示自动选择模型 216 | return None 217 | elif openai_model.startswith("claude-"): 218 | # Claude模型名称,添加augment-前缀 219 | return f"augment-{openai_model}" 220 | elif openai_model.startswith("augment-"): 221 | # 已经是Augment格式的名称,直接使用 222 | return openai_model 223 | else: 224 | # 其他名称默认使用自动选择 225 | logger.info(f"未知模型名称 '{openai_model}',使用自动选择") 226 | return None 227 | 228 | # 新增:处理内容数组的函数 229 | def process_content_array(content_array: List[ContentItem]) -> str: 230 | """ 231 | 将内容数组转换为单个字符串 232 | 233 | Args: 234 | content_array: 内容项数组 235 | 236 | Returns: 237 | 合并后的文本内容 238 | """ 239 | result = "" 240 | for item in content_array: 241 | if item.type == "text" and item.text: 242 | result += item.text 243 | return result 244 | 245 | def convert_to_augment_request(openai_request: ChatCompletionRequest) -> AugmentChatRequest: 246 | """ 247 | 将OpenAI API请求转换为Augment API请求 248 | 249 | Args: 250 | openai_request: OpenAI API请求对象 251 | 252 | Returns: 253 | 转换后的Augment API请求对象 254 | 255 | Raises: 256 | HTTPException: 如果请求格式无效 257 | """ 258 | chat_history = [] 259 | system_message = None 260 | 261 | # 预处理所有消息,处理内容数组 262 | for i in range(len(openai_request.messages)): 263 | msg = openai_request.messages[i] 264 | if isinstance(msg.content, list): 265 | # 将内容数组转换为单个字符串 266 | openai_request.messages[i].content = process_content_array(msg.content) 267 | 268 | # 处理消息历史记录 269 | for i in range(len(openai_request.messages) - 1): 270 | msg = openai_request.messages[i] 271 | if msg.role == "system": 272 | system_message = msg.content 273 | elif msg.role == "user" and i + 1 < len(openai_request.messages) and openai_request.messages[i + 1].role == "assistant": 274 | user_msg = msg.content 275 | assistant_msg = openai_request.messages[i + 1].content 276 | 277 | # 创建历史记录条目,格式符合Augment API 278 | history_item = AugmentChatHistoryItem( 279 | request_message=user_msg, 280 | response_text=assistant_msg, 281 | request_id=generate_id(), 282 | response_nodes=[ 283 | AugmentResponseNode( 284 | id=0, 285 | type=0, 286 | content=assistant_msg, 287 | tool_use=None 288 | ) 289 | ] 290 | ) 291 | chat_history.append(history_item) 292 | 293 | # 获取当前用户消息 294 | current_message = None 295 | for msg in reversed(openai_request.messages): 296 | if msg.role == "user": 297 | current_message = msg.content 298 | break 299 | 300 | # 如果没有用户消息,则返回错误 301 | if current_message is None: 302 | raise HTTPException( 303 | status_code=400, 304 | detail="At least one user message is required" 305 | ) 306 | 307 | # 映射模型名称 308 | augment_model = map_model_name(openai_request.model) 309 | 310 | # 准备Augment请求体 311 | augment_request = AugmentChatRequest( 312 | model=augment_model, 313 | message=current_message, 314 | chat_history=chat_history, 315 | mode="CHAT" 316 | ) 317 | 318 | # 如果有系统消息,设置为用户指南 319 | if system_message: 320 | augment_request.user_guidelines = system_message 321 | 322 | return augment_request 323 | 324 | ################################################# 325 | # FastAPI应用 326 | ################################################# 327 | 328 | def create_app(augment_base_url, chat_endpoint, timeout, max_connections, max_keepalive, keepalive_expiry): 329 | """ 330 | 创建并配置FastAPI应用 331 | 332 | Args: 333 | augment_base_url: Augment API基础URL 334 | chat_endpoint: 聊天端点路径 335 | timeout: 请求超时时间 336 | max_connections: 连接池最大连接数 337 | max_keepalive: 保持活动的连接数 338 | keepalive_expiry: 连接保持活动的时间(秒) 339 | 340 | Returns: 341 | 配置好的FastAPI应用 342 | """ 343 | app = FastAPI( 344 | title="OpenAI to Augment API Adapter", 345 | description="A FastAPI adapter that converts OpenAI API requests to Augment API format", 346 | version="1.0.0" 347 | ) 348 | 349 | # 添加CORS中间件 350 | app.add_middleware( 351 | CORSMiddleware, 352 | allow_origins=["*"], 353 | allow_credentials=True, 354 | allow_methods=["*"], 355 | allow_headers=["*"], 356 | ) 357 | 358 | # HTTP客户端连接池 359 | http_client = None 360 | 361 | @app.on_event("startup") 362 | async def startup_event(): 363 | """应用启动时初始化HTTP客户端连接池""" 364 | nonlocal http_client 365 | http_client = httpx.AsyncClient( 366 | timeout=timeout, 367 | limits=httpx.Limits( 368 | max_connections=max_connections, 369 | max_keepalive_connections=max_keepalive, 370 | keepalive_expiry=keepalive_expiry 371 | ) 372 | ) 373 | logger.info(f"已初始化HTTP客户端连接池: 最大连接数={max_connections}, 保持活动连接数={max_keepalive}, 连接过期时间={keepalive_expiry}秒") 374 | 375 | @app.on_event("shutdown") 376 | async def shutdown_event(): 377 | """应用关闭时关闭HTTP客户端连接池""" 378 | nonlocal http_client 379 | if http_client: 380 | await http_client.aclose() 381 | logger.info("已关闭HTTP客户端连接池") 382 | 383 | ################################################# 384 | # 中间件和依赖项 385 | ################################################# 386 | 387 | @app.middleware("http") 388 | async def catch_exceptions_middleware(request: Request, call_next): 389 | """捕获所有未处理的异常,返回适当的错误响应""" 390 | try: 391 | return await call_next(request) 392 | except Exception as e: 393 | logger.exception("Unhandled exception") 394 | return JSONResponse( 395 | status_code=500, 396 | content={ 397 | "error": { 398 | "message": str(e), 399 | "type": "internal_server_error", 400 | "param": None, 401 | "code": "internal_server_error" 402 | } 403 | } 404 | ) 405 | 406 | async def verify_api_key(authorization: str = Header(...)): 407 | """ 408 | 验证API密钥 409 | 410 | Args: 411 | authorization: Authorization头部值 412 | 413 | Returns: 414 | 提取的API密钥 415 | 416 | Raises: 417 | HTTPException: 如果API密钥格式无效或为空 418 | """ 419 | if not authorization.startswith("Bearer "): 420 | raise HTTPException( 421 | status_code=401, 422 | detail={ 423 | "error": { 424 | "message": "Invalid API key format. Expected 'Bearer YOUR_API_KEY'", 425 | "type": "invalid_request_error", 426 | "param": "authorization", 427 | "code": "invalid_api_key" 428 | } 429 | } 430 | ) 431 | api_key = authorization.replace("Bearer ", "") 432 | if not api_key: 433 | raise HTTPException( 434 | status_code=401, 435 | detail={ 436 | "error": { 437 | "message": "API key cannot be empty", 438 | "type": "invalid_request_error", 439 | "param": "authorization", 440 | "code": "invalid_api_key" 441 | } 442 | } 443 | ) 444 | return api_key 445 | 446 | ################################################# 447 | # API端点 448 | ################################################# 449 | 450 | @app.get("/health") 451 | async def health_check(): 452 | """健康检查端点""" 453 | return {"status": "ok", "timestamp": datetime.now().isoformat()} 454 | 455 | @app.get("/v1/models") 456 | async def list_models(): 457 | """列出支持的模型""" 458 | # 返回支持的模型列表,包含Augment支持的模型 459 | models = [ 460 | ModelInfo(id="augment-auto", created=int(time.time())), 461 | ModelInfo(id="claude-3.7-sonnet", created=int(time.time())), 462 | ModelInfo(id="augment-claude-3.7-sonnet", created=int(time.time())), 463 | ] 464 | return ModelListResponse(data=models) 465 | 466 | @app.get("/v1/models/{model_id}") 467 | async def get_model(model_id: str): 468 | """获取特定模型的信息""" 469 | return ModelInfo(id=model_id, created=int(time.time())) 470 | 471 | @app.post("/v1/chat/completions") 472 | async def chat_completions( 473 | request: ChatCompletionRequest, 474 | api_key: str = Depends(verify_api_key) 475 | ): 476 | """ 477 | 聊天完成端点 - 将OpenAI API请求转换为Augment API请求 478 | 479 | Args: 480 | request: OpenAI格式的聊天完成请求 481 | api_key: 通过验证的API密钥 482 | 483 | Returns: 484 | OpenAI格式的聊天完成响应或流式响应 485 | """ 486 | try: 487 | # 转换为Augment请求格式 488 | augment_request = convert_to_augment_request(request) 489 | logger.debug(f"Converted request: {augment_request.model_dump(exclude_none=True)}") 490 | 491 | # 决定是否使用流式响应 492 | if request.stream: 493 | return StreamingResponse( 494 | stream_augment_response(http_client, augment_base_url, api_key, augment_request, request.model, chat_endpoint), 495 | media_type="text/event-stream" 496 | ) 497 | else: 498 | # 同步请求处理 499 | return await handle_sync_request(http_client, augment_base_url, api_key, augment_request, request.model, chat_endpoint) 500 | 501 | except httpx.TimeoutException: 502 | logger.error("Request to Augment API timed out") 503 | raise HTTPException( 504 | status_code=504, 505 | detail={ 506 | "error": { 507 | "message": "Request to Augment API timed out", 508 | "type": "timeout_error", 509 | "param": None, 510 | "code": "timeout" 511 | } 512 | } 513 | ) 514 | except httpx.HTTPError as e: 515 | logger.error(f"HTTP error: {str(e)}") 516 | raise HTTPException( 517 | status_code=502, 518 | detail={ 519 | "error": { 520 | "message": f"Error communicating with Augment API: {str(e)}", 521 | "type": "api_error", 522 | "param": None, 523 | "code": "api_error" 524 | } 525 | } 526 | ) 527 | except HTTPException: 528 | # 重新抛出HTTPException,以保持原始状态码和详细信息 529 | raise 530 | except Exception as e: 531 | logger.exception("Unexpected error") 532 | raise HTTPException( 533 | status_code=500, 534 | detail={ 535 | "error": { 536 | "message": f"Internal server error: {str(e)}", 537 | "type": "internal_server_error", 538 | "param": None, 539 | "code": "internal_server_error" 540 | } 541 | } 542 | ) 543 | 544 | return app 545 | 546 | async def handle_sync_request(client, base_url, api_key, augment_request, model_name, chat_endpoint): 547 | """ 548 | 处理同步请求 549 | 550 | Args: 551 | client: HTTP客户端连接池 552 | base_url: Augment API基础URL 553 | api_key: API密钥 554 | augment_request: Augment API请求对象 555 | model_name: 模型名称 556 | chat_endpoint: 聊天端点 557 | 558 | Returns: 559 | OpenAI格式的聊天完成响应 560 | """ 561 | # 排除None值,确保正确的JSON格式 562 | request_json = augment_request.model_dump(exclude_none=True) 563 | 564 | response = await client.post( 565 | f"{base_url.rstrip('/')}/{chat_endpoint}", 566 | json=request_json, 567 | headers={ 568 | "Content-Type": "application/json", 569 | "Authorization": f"Bearer {api_key}", 570 | "User-Agent": "Augment.openai-adapter/1.0.0", 571 | "Accept": "*/*" 572 | } 573 | ) 574 | 575 | if response.status_code != 200: 576 | logger.error(f"Augment API error: {response.status_code} - {response.text}") 577 | raise HTTPException( 578 | status_code=response.status_code, 579 | detail={ 580 | "error": { 581 | "message": f"Augment API error: {response.text}", 582 | "type": "api_error", 583 | "param": None, 584 | "code": "api_error" 585 | } 586 | } 587 | ) 588 | 589 | # 处理流式响应,合并为完整响应 590 | full_response = "" 591 | for line in response.text.split("\n"): 592 | if line.strip(): 593 | try: 594 | data = json.loads(line) 595 | if "text" in data and data["text"]: 596 | full_response += data["text"] 597 | except json.JSONDecodeError: 598 | logger.warning(f"Failed to parse JSON: {line}") 599 | 600 | # 估算token使用情况 601 | prompt_tokens = estimate_tokens(augment_request.message) 602 | completion_tokens = estimate_tokens(full_response) 603 | 604 | # 构建OpenAI格式响应 605 | return ChatCompletionResponse( 606 | id=f"chatcmpl-{generate_id()}", 607 | created=int(time.time()), 608 | model=model_name, 609 | choices=[ 610 | ChatCompletionResponseChoice( 611 | index=0, 612 | message=ChatMessage( 613 | role="assistant", 614 | content=full_response 615 | ), 616 | finish_reason="stop" 617 | ) 618 | ], 619 | usage=Usage( 620 | prompt_tokens=prompt_tokens, 621 | completion_tokens=completion_tokens, 622 | total_tokens=prompt_tokens + completion_tokens 623 | ) 624 | ) 625 | 626 | async def stream_augment_response(client, base_url, api_key, augment_request, model_name, chat_endpoint): 627 | """ 628 | 处理流式响应 629 | 630 | Args: 631 | client: HTTP客户端连接池 632 | base_url: Augment API基础URL 633 | api_key: API密钥 634 | augment_request: Augment API请求对象 635 | model_name: 模型名称 636 | chat_endpoint: 聊天端点 637 | 638 | Yields: 639 | 流式响应的数据块 640 | """ 641 | try: 642 | # 排除None值,确保正确的JSON格式 643 | request_json = augment_request.model_dump(exclude_none=True) 644 | 645 | async with client.stream( 646 | "POST", 647 | f"{base_url.rstrip('/')}/{chat_endpoint}", 648 | json=request_json, 649 | headers={ 650 | "Content-Type": "application/json", 651 | "Authorization": f"Bearer {api_key}", 652 | "User-Agent": "Augment.openai-adapter/1.0.0", 653 | "Accept": "*/*" 654 | } 655 | ) as response: 656 | 657 | if response.status_code != 200: 658 | error_detail = await response.aread() 659 | logger.error(f"Augment API error: {response.status_code} - {error_detail}") 660 | error_message = f"Error from Augment API: {error_detail.decode('utf-8', errors='replace')}" 661 | yield f"data: {json.dumps({'error': error_message})}\n\n" 662 | return 663 | 664 | # 生成唯一ID 665 | chat_id = f"chatcmpl-{generate_id()}" 666 | created_time = int(time.time()) 667 | 668 | # 初始化响应 669 | init_response = ChatCompletionStreamResponse( 670 | id=chat_id, 671 | created=created_time, 672 | model=model_name, 673 | choices=[ 674 | ChatCompletionStreamResponseChoice( 675 | index=0, 676 | delta={"role": "assistant"}, 677 | finish_reason=None 678 | ) 679 | ] 680 | ) 681 | init_data = json.dumps(init_response.model_dump()) 682 | yield f"data: {init_data}\n\n" 683 | 684 | # 处理流式响应 685 | buffer = "" 686 | async for line in response.aiter_lines(): 687 | if not line.strip(): 688 | continue 689 | 690 | try: 691 | # 解析Augment响应格式 692 | chunk = json.loads(line) 693 | if "text" in chunk and chunk["text"]: 694 | content = chunk["text"] 695 | 696 | # 发送增量更新 697 | stream_response = ChatCompletionStreamResponse( 698 | id=chat_id, 699 | created=created_time, 700 | model=model_name, 701 | choices=[ 702 | ChatCompletionStreamResponseChoice( 703 | index=0, 704 | delta={"content": content}, 705 | finish_reason=None 706 | ) 707 | ] 708 | ) 709 | response_data = json.dumps(stream_response.model_dump()) 710 | yield f"data: {response_data}\n\n" 711 | except json.JSONDecodeError: 712 | logger.warning(f"Failed to parse JSON: {line}") 713 | 714 | # 发送完成信号 715 | final_response = ChatCompletionStreamResponse( 716 | id=chat_id, 717 | created=created_time, 718 | model=model_name, 719 | choices=[ 720 | ChatCompletionStreamResponseChoice( 721 | index=0, 722 | delta={}, 723 | finish_reason="stop" 724 | ) 725 | ] 726 | ) 727 | final_data = json.dumps(final_response.model_dump()) 728 | yield f"data: {final_data}\n\n" 729 | 730 | # 发送[DONE]标记 731 | yield "data: [DONE]\n\n" 732 | 733 | except httpx.TimeoutException: 734 | logger.error("Request to Augment API timed out") 735 | yield f"data: {json.dumps({'error': 'Request to Augment API timed out'})}\n\n" 736 | except httpx.HTTPError as e: 737 | logger.error(f"HTTP error: {str(e)}") 738 | yield f"data: {json.dumps({'error': f'Error communicating with Augment API: {str(e)}'})}\n\n" 739 | except Exception as e: 740 | logger.exception("Unexpected error") 741 | yield f"data: {json.dumps({'error': f'Internal server error: {str(e)}'})}\n\n" 742 | 743 | def parse_args(): 744 | """解析命令行参数""" 745 | parser = argparse.ArgumentParser( 746 | description="OpenAI to Augment API Adapter", 747 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 748 | ) 749 | 750 | parser.add_argument( 751 | "--augment-url", 752 | default="https://d18.api.augmentcode.com/", 753 | help="Augment API基础URL" 754 | ) 755 | 756 | parser.add_argument( 757 | "--chat-endpoint", 758 | default="chat-stream", 759 | help="Augment聊天端点路径" 760 | ) 761 | 762 | parser.add_argument( 763 | "--host", 764 | default="0.0.0.0", 765 | help="服务器主机地址" 766 | ) 767 | 768 | parser.add_argument( 769 | "--port", 770 | type=int, 771 | default=8686, 772 | help="服务器端口" 773 | ) 774 | 775 | parser.add_argument( 776 | "--timeout", 777 | type=int, 778 | default=120, 779 | help="API请求超时时间(秒)" 780 | ) 781 | 782 | parser.add_argument( 783 | "--debug", 784 | action="store_true", 785 | help="启用调试模式" 786 | ) 787 | 788 | parser.add_argument( 789 | "--tenant-id", 790 | default="d18", 791 | help="Augment API租户ID (域名前缀)" 792 | ) 793 | 794 | # 连接池相关参数 795 | parser.add_argument( 796 | "--max-connections", 797 | type=int, 798 | default=100, 799 | help="HTTP连接池最大连接数" 800 | ) 801 | 802 | parser.add_argument( 803 | "--max-keepalive", 804 | type=int, 805 | default=20, 806 | help="HTTP连接池保持活动的连接数" 807 | ) 808 | 809 | parser.add_argument( 810 | "--keepalive-expiry", 811 | type=float, 812 | default=60.0, 813 | help="HTTP连接池连接保持活动的时间(秒)" 814 | ) 815 | 816 | return parser.parse_args() 817 | 818 | ################################################# 819 | # 主程序 820 | ################################################# 821 | 822 | def main(): 823 | """主函数""" 824 | args = parse_args() 825 | 826 | # 配置日志级别 827 | if args.debug: 828 | logging.getLogger().setLevel(logging.DEBUG) 829 | 830 | # 构建完整的Augment URL 831 | if args.augment_url == "https://d18.api.augmentcode.com/": 832 | # 如果使用默认URL,则应用tenant-id参数 833 | augment_base_url = f"https://{args.tenant_id}.api.augmentcode.com/" 834 | logger.info(f"Using tenant ID: {args.tenant_id}") 835 | else: 836 | # 否则使用提供的URL 837 | augment_base_url = args.augment_url 838 | 839 | # 创建应用 840 | app = create_app( 841 | augment_base_url=augment_base_url, 842 | chat_endpoint=args.chat_endpoint, 843 | timeout=args.timeout, 844 | max_connections=args.max_connections, 845 | max_keepalive=args.max_keepalive, 846 | keepalive_expiry=args.keepalive_expiry 847 | ) 848 | 849 | # 启动应用 850 | logger.info(f"Starting server on {args.host}:{args.port}") 851 | logger.info(f"Using Augment base URL: {augment_base_url}") 852 | logger.info(f"Using Augment chat endpoint: {args.chat_endpoint}") 853 | logger.info(f"HTTP连接池配置: 最大连接数={args.max_connections}, 保持活动连接数={args.max_keepalive}, 连接过期时间={args.keepalive_expiry}秒") 854 | 855 | uvicorn.run( 856 | app, 857 | host=args.host, 858 | port=args.port, 859 | log_level="info" if not args.debug else "debug" 860 | ) 861 | 862 | if __name__ == "__main__": 863 | main() -------------------------------------------------------------------------------- /auth2xapikey.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, Header 2 | import httpx 3 | import uvicorn 4 | from typing import Optional 5 | from fastapi.responses import StreamingResponse, Response 6 | import logging 7 | 8 | # 设置日志 9 | logging.basicConfig(level=logging.INFO, 10 | format='%(asctime)s [%(levelname)s] [%(name)s] %(message)s') 11 | logger = logging.getLogger(__name__) 12 | 13 | app = FastAPI() 14 | 15 | TARGET_API_URL = "https://rad.huddlz.xyz" # 替换为您的目标API地址 16 | 17 | @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"]) 18 | async def proxy_request(request: Request, path: str, authorization: Optional[str] = Header(None)): 19 | # 记录请求信息 20 | logger.info(f"接收到请求: {request.method} {request.url.path}") 21 | 22 | # 获取原始请求体 23 | body = await request.body() 24 | 25 | # 获取并处理原始请求头 26 | headers = dict(request.headers) 27 | logger.debug(f"原始请求头: {headers}") 28 | 29 | # 从Authorization中提取token并设置为x-api-key 30 | if authorization: 31 | token = authorization.replace("Bearer ", "") 32 | headers["x-api-key"] = token 33 | logger.debug(f"从Authorization提取并设置x-api-key") 34 | 35 | # 移除可能导致问题的请求头 36 | headers.pop("host", None) 37 | 38 | # 处理内容编码相关头信息 39 | # 移除Accept-Encoding头,让httpx自己处理内容压缩/解压缩 40 | if "accept-encoding" in headers: 41 | logger.debug(f"移除Accept-Encoding: {headers.get('accept-encoding')}") 42 | headers.pop("accept-encoding", None) 43 | 44 | # 构建完整的目标URL 45 | url = f"{TARGET_API_URL}/{path}" 46 | logger.info(f"转发请求到: {url}") 47 | 48 | # 获取查询参数 49 | params = dict(request.query_params) 50 | 51 | try: 52 | # 转发请求到目标API,禁用自动处理压缩内容 53 | async with httpx.AsyncClient(headers={"Accept-Encoding": "identity"}) as client: 54 | response = await client.request( 55 | method=request.method, 56 | url=url, 57 | params=params, 58 | headers=headers, 59 | content=body, 60 | timeout=60.0, 61 | follow_redirects=True 62 | ) 63 | 64 | logger.info(f"收到响应: 状态码 {response.status_code}") 65 | logger.debug(f"响应头: {response.headers}") 66 | 67 | # 处理响应头,移除可能导致问题的头信息 68 | response_headers = dict(response.headers) 69 | 70 | # 移除可能导致冲突的头信息 71 | headers_to_remove = [ 72 | "content-length", 73 | "transfer-encoding", 74 | "content-encoding", # 重要:移除内容编码头 75 | "server", 76 | "connection" 77 | ] 78 | 79 | for header in headers_to_remove: 80 | if header in response_headers: 81 | logger.debug(f"移除响应头: {header}") 82 | response_headers.pop(header, None) 83 | 84 | # 获取响应内容(已自动解压缩) 85 | content = await response.aread() 86 | 87 | # 返回未压缩的响应 88 | return Response( 89 | content=content, 90 | status_code=response.status_code, 91 | headers=response_headers 92 | ) 93 | except Exception as e: 94 | logger.error(f"请求处理过程中发生错误: {str(e)}", exc_info=True) 95 | return {"detail": f"代理服务器错误: {str(e)}"}, 500 96 | 97 | if __name__ == "__main__": 98 | uvicorn.run(app, host="0.0.0.0", port=9898) -------------------------------------------------------------------------------- /claude-card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Claude 刷新倒计时 - 终极炫彩背景 7 | 8 | 9 | 10 | 11 | 12 | 149 | 150 | 151 |
152 |
153 |

下次刷新倒计时

154 | --:--:-- 155 |
156 | 157 |
158 |

预计下次刷新

159 |

正在计算...
(北京时间)

160 |
161 | 162 |
163 |

上次刷新时间

164 |

正在计算...
(北京时间)

165 |
166 | 167 |
168 |

基准刷新时间

169 |

加载中...
(北京时间)

170 |
171 |
172 | 173 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /copilot-models.md: -------------------------------------------------------------------------------- 1 | # AI模型配置信息表 2 | 3 | ## 模型基本信息 4 | 5 | | 模型ID | 模型名称 | 厂商 | 版本 | 上下文窗口大小 | 官方最大提示词Token | 实测最大输入Token | 最大输出Token | 预览版 | 6 | |--------|---------|------|------|---------------|-------------------|-----------------|-------------|--------| 7 | | gpt-3.5-turbo | GPT 3.5 Turbo | Azure OpenAI | gpt-3.5-turbo-0613 | 16,384 | 16,384 | 12,288 | 4,096 | ❌ | 8 | | gpt-3.5-turbo-0613 | GPT 3.5 Turbo | Azure OpenAI | gpt-3.5-turbo-0613 | 16,384 | 16,384 | 12,288 | 4,096 | ❌ | 9 | | gpt-4o-mini | GPT-4o mini | Azure OpenAI | gpt-4o-mini-2024-07-18 | 128,000 | 64,000 | 12,288 | 4,096 | ❌ | 10 | | gpt-4o-mini-2024-07-18 | GPT-4o mini | Azure OpenAI | gpt-4o-mini-2024-07-18 | 128,000 | 64,000 | 12,288 | 4,096 | ❌ | 11 | | gpt-4 | GPT 4 | Azure OpenAI | gpt-4-0613 | 32,768 | 32,768 | 32,768 | 4,096 | ❌ | 12 | | gpt-4-0613 | GPT 4 | Azure OpenAI | gpt-4-0613 | 32,768 | 32,768 | 32,768 | 4,096 | ❌ | 13 | | gpt-4o | GPT-4o | Azure OpenAI | gpt-4o-2024-11-20 | 128,000 | 64,000 | 64,000 | 16,384 | ❌ | 14 | | gpt-4o-2024-11-20 | GPT-4o | Azure OpenAI | gpt-4o-2024-11-20 | 128,000 | 64,000 | 64,000 | 16,384 | ❌ | 15 | | gpt-4o-2024-05-13 | GPT-4o | Azure OpenAI | gpt-4o-2024-05-13 | 128,000 | 64,000 | 64,000 | 4,096 | ❌ | 16 | | gpt-4-o-preview | GPT-4o | Azure OpenAI | gpt-4o-2024-05-13 | 128,000 | 64,000 | 64,000 | 4,096 | ❌ | 17 | | gpt-4o-2024-08-06 | GPT-4o | Azure OpenAI | gpt-4o-2024-08-06 | 128,000 | 64,000 | 64,000 | 16,384 | ❌ | 18 | | o1 | o1 (Preview) | Azure OpenAI | o1-2024-12-17 | 200,000 | 16,384 | 20,000 | - | ✅ | 19 | | o1-2024-12-17 | o1 (Preview) | Azure OpenAI | o1-2024-12-17 | 200,000 | 16,384 | 20,000 | - | ✅ | 20 | | o3-mini | o3-mini | Azure OpenAI | o3-mini-2025-01-31 | 200,000 | 64,000 | 64,000 | 100,000 | ❌ | 21 | | o3-mini-2025-01-31 | o3-mini | Azure OpenAI | o3-mini-2025-01-31 | 200,000 | 64,000 | 64,000 | 100,000 | ❌ | 22 | | o3-mini-paygo | o3-mini | Azure OpenAI | o3-mini-paygo | 200,000 | 64,000 | 64,000 | 100,000 | ❌ | 23 | | text-embedding-ada-002 | Embedding V2 Ada | Azure OpenAI | text-embedding-3-small | - | - | - | - | ❌ | 24 | | text-embedding-3-small | Embedding V3 small | Azure OpenAI | text-embedding-3-small | - | - | - | - | ❌ | 25 | | text-embedding-3-small-inference | Embedding V3 small (Inference) | Azure OpenAI | text-embedding-3-small | - | - | - | - | ❌ | 26 | | claude-3.5-sonnet | Claude 3.5 Sonnet | Anthropic | claude-3.5-sonnet | 90,000 | 90,000 | 90,000 | 8,192 | ❌ | 27 | | claude-3.7-sonnet | Claude 3.7 Sonnet | Anthropic | claude-3.7-sonnet | 200,000 | 128,000 | 90,000 | 16,384 | ❌ | 28 | | claude-3.7-sonnet-thought | Claude 3.7 Sonnet Thinking | Anthropic | claude-3.7-sonnet-thought | 200,000 | 90,000 | 90,000 | 16,384 | ❌ | 29 | | gemini-2.0-flash-001 | Gemini 2.0 Flash | Google | gemini-2.0-flash-001 | 1,000,000 | 128,000 | 128,000 | 8,192 | ❌ | 30 | | gemini-2.5-pro | Gemini 2.5 Pro (Preview) | Google | gemini-2.5-pro-preview-03-25 | 128,000 | 128,000 | 128,000 | 64,000 | ✅ | 31 | | gemini-2.5-pro-preview-03-25 | Gemini 2.5 Pro (Preview) | Google | gemini-2.5-pro-preview-03-25 | 128,000 | 128,000 | 128,000 | 64,000 | ✅ | 32 | | o4-mini | o4-mini (Preview) | Azure OpenAI | o4-mini-2025-04-16 | 128,000 | 128,000 | 128,000 | 16,384 | ✅ | 33 | | o4-mini-2025-04-16 | o4-mini (Preview) | OpenAI | o4-mini-2025-04-16 | 128,000 | 128,000 | 128,000 | 16,384 | ✅ | 34 | | gpt-4.1 | GPT-4.1 (Preview) | Azure OpenAI | gpt-4.1-2025-04-14 | 128,000 | 128,000 | 128,000 | 16,384 | ✅ | 35 | | gpt-4.1-2025-04-14 | GPT-4.1 (Preview) | OpenAI | gpt-4.1-2025-04-14 | 128,000 | 128,000 | 128,000 | 16,384 | ✅ | 36 | 37 | ## 模型特殊能力支持情况 38 | 39 | | 模型ID | vision | tool_calls | parallel_tool_calls | streaming | structured_outputs | 40 | |--------|--------|-----------|---------------------|-----------|-------------------| 41 | | gpt-3.5-turbo | ❌ | ✅ | ❌ | ✅ | ❌ | 42 | | gpt-3.5-turbo-0613 | ❌ | ✅ | ❌ | ✅ | ❌ | 43 | | gpt-4o-mini | ❌ | ✅ | ✅ | ✅ | ❌ | 44 | | gpt-4o-mini-2024-07-18 | ❌ | ✅ | ✅ | ✅ | ❌ | 45 | | gpt-4 | ❌ | ✅ | ❌ | ✅ | ❌ | 46 | | gpt-4-0613 | ❌ | ✅ | ❌ | ✅ | ❌ | 47 | | gpt-4o | ✅ | ✅ | ✅ | ✅ | ❌ | 48 | | gpt-4o-2024-11-20 | ✅ | ✅ | ✅ | ✅ | ❌ | 49 | | gpt-4o-2024-05-13 | ✅ | ✅ | ✅ | ✅ | ❌ | 50 | | gpt-4-o-preview | ❌ | ✅ | ✅ | ✅ | ❌ | 51 | | gpt-4o-2024-08-06 | ❌ | ✅ | ✅ | ✅ | ❌ | 52 | | o1 | ❌ | ✅ | ❌ | ❌ | ✅ | 53 | | o1-2024-12-17 | ❌ | ✅ | ❌ | ❌ | ✅ | 54 | | o3-mini | ❌ | ✅ | ❌ | ✅ | ✅ | 55 | | o3-mini-2025-01-31 | ❌ | ✅ | ❌ | ✅ | ✅ | 56 | | o3-mini-paygo | ❌ | ✅ | ❌ | ✅ | ✅ | 57 | | claude-3.5-sonnet | ✅ | ✅ | ✅ | ✅ | ❌ | 58 | | claude-3.7-sonnet | ✅ | ✅ | ✅ | ✅ | ❌ | 59 | | claude-3.7-sonnet-thought | ✅ | ❌ | ❌ | ✅ | ❌ | 60 | | gemini-2.0-flash-001 | ✅ | ✅ | ✅ | ✅ | ❌ | 61 | | gemini-2.5-pro | ✅ | ✅ | ✅ | ✅ | ❌ | 62 | | gemini-2.5-pro-preview-03-25 | ✅ | ✅ | ✅ | ✅ | ❌ | 63 | | o4-mini | ❌ | ✅ | ✅ | ✅ | ✅ | 64 | | o4-mini-2025-04-16 | ❌ | ✅ | ✅ | ✅ | ✅ | 65 | | gpt-4.1 | ✅ | ✅ | ✅ | ✅ | ✅ | 66 | | gpt-4.1-2025-04-14 | ✅ | ✅ | ✅ | ✅ | ✅ | 67 | 68 | ## 嵌入模型 69 | 70 | | 模型ID | 模型名称 | 厂商 | 版本 | 最大输入 | 支持自定义维度 | Tokenizer | 71 | |--------|---------|------|------|---------|--------------|----------| 72 | | text-embedding-ada-002 | Embedding V2 Ada | Azure OpenAI | text-embedding-3-small | 512 | ❌ | cl100k_base | 73 | | text-embedding-3-small | Embedding V3 small | Azure OpenAI | text-embedding-3-small | 512 | ✅ | cl100k_base | 74 | | text-embedding-3-small-inference | Embedding V3 small (Inference) | Azure OpenAI | text-embedding-3-small | - | ✅ | cl100k_base | 75 | 76 | ## 模型Tokenizer信息 77 | 78 | | 模型类别 | 使用的Tokenizer | 79 | |---------|----------------| 80 | | GPT-3.5系列 | cl100k_base | 81 | | GPT-4系列 | cl100k_base | 82 | | GPT-4o系列 | o200k_base | 83 | | o1系列 | o200k_base | 84 | | o3系列 | o200k_base | 85 | | o4系列 | o200k_base | 86 | | Claude系列 | o200k_base | 87 | | Gemini系列 | o200k_base | 88 | | 嵌入模型 | cl100k_base | 89 | -------------------------------------------------------------------------------- /hfs/api-pool/Dockerfile: -------------------------------------------------------------------------------- 1 | # --- 第一阶段:构建阶段 (Builder Stage) --- 2 | # 使用官方 Go 镜像进行编译 3 | FROM golang:1.22-alpine AS builder 4 | 5 | # 设置工作目录 6 | WORKDIR /build 7 | 8 | # 复制 Go 源代码文件 9 | COPY api-pool.go . 10 | 11 | # 编译 Go 应用 12 | # CGO_ENABLED=0 尝试静态链接,减少依赖 13 | # -ldflags="-w -s" 减小二进制文件大小 14 | # -o /app/api-pool 指定输出路径和名称 15 | RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /app/api-pool api-pool.go 16 | 17 | # --- 第二阶段:运行阶段 (Final Stage) --- 18 | # 使用轻量的 Alpine 镜像作为最终运行环境 19 | FROM alpine:latest 20 | 21 | # 设置工作目录 22 | WORKDIR /app 23 | 24 | # 从构建阶段复制编译好的二进制文件 25 | COPY --from=builder /app/api-pool /app/api-pool 26 | 27 | # 复制启动脚本 28 | COPY entrypoint.sh /app/entrypoint.sh 29 | 30 | # 赋予执行权限 31 | RUN chmod +x /app/api-pool /app/entrypoint.sh 32 | 33 | # 暴露应用程序监听的端口 (根据您的参数是 6969) 34 | EXPOSE 6969 35 | 36 | # 设置容器的入口点为启动脚本 37 | ENTRYPOINT ["/app/entrypoint.sh"] 38 | 39 | # 注意:CMD 指令现在由 entrypoint.sh 脚本通过 exec 来执行 -------------------------------------------------------------------------------- /hfs/api-pool/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Key Pool # 您可以修改标题 3 | emoji: 🔑 # 您可以修改 Emoji 4 | colorFrom: green # 您可以修改颜色 5 | colorTo: blue # 您可以修改颜色 6 | sdk: docker 7 | app_port: 6969 # 必须与您的 --port 参数和 EXPOSE 端口一致 8 | pinned: false 9 | --- 10 | 11 | (在此添加您的 Space 描述) -------------------------------------------------------------------------------- /hfs/api-pool/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 如果命令失败则立即退出 3 | set -e 4 | 5 | # 定义临时密钥文件的路径 6 | KEY_FILE_PATH="/tmp/keys.txt" 7 | 8 | echo "--- 正在检查 Secrets ---" 9 | 10 | # 检查 API_PASSWORD 是否已设置 11 | if [ -z "${API_PASSWORD}" ]; then 12 | echo "[错误] 必须在 Hugging Face Secrets 中设置 API_PASSWORD !" 13 | exit 1 14 | fi 15 | 16 | # 检查 key_list 是否已设置 17 | if [ -z "${key_list}" ]; then 18 | echo "[错误] 必须在 Hugging Face Secrets 中设置 key_list !" 19 | exit 1 20 | fi 21 | 22 | echo "--- 正在从 Secret 'key_list' 创建临时密钥文件 (${KEY_FILE_PATH}) ---" 23 | 24 | # 从环境变量 key_list 读取内容,并写入临时文件 25 | # 使用 'echo -e' 来解释可能存在的 '\n' 换行符 26 | # 将标准错误重定向到 /dev/null 以避免打印潜在的密钥内容(尽管通常 echo 不会) 27 | echo -e "${key_list}" > "${KEY_FILE_PATH}" 2>/dev/null 28 | 29 | # 验证文件是否创建成功且非空 30 | if [ ! -s "${KEY_FILE_PATH}" ]; then 31 | echo "[错误] 创建密钥文件失败或文件为空!请检查 'key_list' Secret 的内容。" 32 | exit 1 33 | fi 34 | 35 | echo "--- 密钥文件已生成 ---" 36 | 37 | # !!! 【重要】生产环境中不要取消下面这行的注释,避免日志泄露 !!! 38 | # echo "密钥文件内容预览 (前几行):" 39 | # head -n 3 "${KEY_FILE_PATH}" 40 | 41 | echo "--- 正在启动 api-pool 服务 ---" 42 | 43 | # 使用 exec 执行 Go 程序,将脚本进程替换为 Go 程序进程 44 | # 将临时文件路径传给 --key-file 45 | # 将从 Secret 读取的密码传给 --password 46 | # 将地址设为 0.0.0.0 以便容器外访问 47 | # 传入其他您指定的参数 48 | exec /app/api-pool \ 49 | --key-file "${KEY_FILE_PATH}" \ 50 | --target-url "https://api.siliconflow.cn" \ 51 | --port "6969" \ 52 | --address "0.0.0.0" \ 53 | --password "${API_PASSWORD}" \ 54 | --max-workers=1000 \ 55 | --max-queue=2000 56 | # 注意:--max-workers 和 --max-queue 值较高,请关注 Space 资源使用情况 -------------------------------------------------------------------------------- /hfs/hunyuan2api/Dockerfile: -------------------------------------------------------------------------------- 1 | # --- 第一阶段:构建阶段 (Builder Stage) --- 2 | # 使用官方的 Go 语言镜像作为编译环境, Alpine 版本比较小巧 3 | FROM golang:1.22-alpine AS builder 4 | # 或者 FROM golang:1.22 # 如果 alpine 的 musl libc 与您的代码有兼容问题 5 | 6 | # 设置构建阶段的工作目录 7 | WORKDIR /build 8 | 9 | # 将你的 Go 源代码文件 (hunyuan2api.go) 复制到构建环境的 /build/ 目录下 10 | COPY hunyuan2api.go . 11 | 12 | # 编译 Go 应用程序 13 | # CGO_ENABLED=0 尝试进行静态链接,避免 C 库依赖问题,尤其是在使用 alpine 镜像时 14 | # -ldflags="-w -s" 用于减小编译后二进制文件的大小 15 | # -o /app/hunyuan2api 指定编译输出的可执行文件路径和名称 16 | # hunyuan2api.go 是你的源文件名 17 | RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /app/hunyuan2api hunyuan2api.go 18 | 19 | # --- 第二阶段:运行阶段 (Final Stage) --- 20 | # 使用一个非常精简的基础镜像来运行编译好的程序 21 | FROM alpine:latest 22 | # 注意:如果静态编译 (CGO_ENABLED=0) 失败或运行时仍有问题, 23 | # 可能需要换成基于 glibc 的镜像,例如 'debian:stable-slim' 24 | # FROM debian:stable-slim 25 | 26 | # 设置最终运行阶段的工作目录 27 | WORKDIR /app 28 | 29 | # 从第一阶段 (builder) 复制编译好的二进制文件到最终镜像的 /app/ 目录下 30 | COPY --from=builder /app/hunyuan2api /app/hunyuan2api 31 | 32 | # 确保复制过来的二进制文件具有执行权限 33 | RUN chmod +x /app/hunyuan2api 34 | 35 | # 暴露你的 Go 应用程序监听的网络端口 (根据你的启动参数是 6677) 36 | EXPOSE 6677 37 | 38 | # 设置容器启动时执行的命令 39 | # 这里的启动参数需要和您提供的一致 40 | CMD ["/app/hunyuan2api", "--address", "0.0.0.0", "--port", "6677", "--verify-ssl=false", "--dev", "--workers", "400", "--queue-size", "1000", "--max-concurrent", "400"] -------------------------------------------------------------------------------- /hfs/hunyuan2api/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hunyuan2api # 标题 3 | emoji: 🌍 # Emoji 4 | colorFrom: indigo # 渐变起始色 5 | colorTo: red # 渐变结束色 6 | sdk: docker # 指定使用 Docker SDK 7 | app_port: 6677 # 【新增】指定应用程序在容器内监听的端口 8 | pinned: false # 是否固定在个人资料页 9 | license: gpl-3.0 # 开源许可证 10 | --- -------------------------------------------------------------------------------- /hfs/qwen2api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-slim 2 | 3 | # 安装 git 4 | RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* 5 | 6 | # 设置工作目录 7 | WORKDIR /app 8 | 9 | # 克隆代码仓库 10 | RUN git clone https://github.com/Rfym21/Qwen2API . 11 | 12 | # 预先创建数据目录并设置权限 13 | RUN mkdir -p /app/data && \ 14 | chmod 777 /app/data && \ 15 | chmod 777 /app 16 | 17 | # 安装依赖 18 | RUN npm install 19 | 20 | # 暴露端口 21 | EXPOSE 3000 22 | 23 | # 创建不写入 .env 文件的启动脚本 24 | RUN echo '#!/bin/bash\n\ 25 | \n\ 26 | # 日志函数\n\ 27 | log() {\n\ 28 | echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1"\n\ 29 | }\n\ 30 | \n\ 31 | # 显示配置信息\n\ 32 | log "配置信息:"\n\ 33 | log "API_PREFIX: ${API_PREFIX:-(未设置)}"\n\ 34 | log "SERVICE_PORT: ${SERVICE_PORT:-3000}"\n\ 35 | log "API_KEY: ${API_KEY:+已设置} ${API_KEY:-未设置}"\n\ 36 | log "ACCOUNT_TOKENS: ${ACCOUNT_TOKENS:+已设置} ${ACCOUNT_TOKENS:-未设置}"\n\ 37 | log "SEARCH_INFO_MODE: ${SEARCH_INFO_MODE:-table}"\n\ 38 | \n\ 39 | # 直接使用环境变量启动服务\n\ 40 | log "正在启动 Qwen2API 服务..."\n\ 41 | node src/server.js\n\ 42 | ' > /app/start.sh && chmod +x /app/start.sh 43 | 44 | # 设置启动命令 45 | CMD ["/app/start.sh"] -------------------------------------------------------------------------------- /hfs/qwen2api/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Qwen2API 3 | emoji: 📚 4 | colorFrom: blue 5 | colorTo: green 6 | sdk: docker 7 | pinned: false 8 | app_port: 3000 9 | --- -------------------------------------------------------------------------------- /hunyuan2api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "crypto/tls" 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "log" 13 | "net/http" 14 | "os" 15 | "os/signal" 16 | "strings" 17 | "sync" 18 | "sync/atomic" 19 | "syscall" 20 | "time" 21 | ) 22 | 23 | // WorkerPool 工作池结构体,用于管理goroutine 24 | type WorkerPool struct { 25 | taskQueue chan *Task 26 | workerCount int 27 | shutdownChannel chan struct{} 28 | wg sync.WaitGroup 29 | } 30 | 31 | // Task 任务结构体,包含请求处理所需数据 32 | type Task struct { 33 | r *http.Request 34 | w http.ResponseWriter 35 | done chan struct{} 36 | reqID string 37 | isStream bool 38 | hunyuanReq HunyuanRequest 39 | } 40 | 41 | // NewWorkerPool 创建并启动一个新的工作池 42 | func NewWorkerPool(workerCount int, queueSize int) *WorkerPool { 43 | pool := &WorkerPool{ 44 | taskQueue: make(chan *Task, queueSize), 45 | workerCount: workerCount, 46 | shutdownChannel: make(chan struct{}), 47 | } 48 | 49 | pool.Start() 50 | return pool 51 | } 52 | 53 | // Start 启动工作池中的worker goroutines 54 | func (pool *WorkerPool) Start() { 55 | // 启动工作goroutine 56 | for i := 0; i < pool.workerCount; i++ { 57 | pool.wg.Add(1) 58 | go func(workerID int) { 59 | defer pool.wg.Done() 60 | 61 | logInfo("Worker %d 已启动", workerID) 62 | 63 | for { 64 | select { 65 | case task, ok := <-pool.taskQueue: 66 | if !ok { 67 | // 队列已关闭,退出worker 68 | logInfo("Worker %d 收到队列关闭信号,准备退出", workerID) 69 | return 70 | } 71 | 72 | logDebug("Worker %d 处理任务 reqID:%s", workerID, task.reqID) 73 | 74 | // 处理任务 75 | if task.isStream { 76 | err := handleStreamingRequest(task.w, task.r, task.hunyuanReq, task.reqID) 77 | if err != nil { 78 | logError("Worker %d 处理流式任务失败: %v", workerID, err) 79 | } 80 | } else { 81 | err := handleNonStreamingRequest(task.w, task.r, task.hunyuanReq, task.reqID) 82 | if err != nil { 83 | logError("Worker %d 处理非流式任务失败: %v", workerID, err) 84 | } 85 | } 86 | 87 | // 通知任务完成 88 | close(task.done) 89 | 90 | case <-pool.shutdownChannel: 91 | // 收到关闭信号,退出worker 92 | logInfo("Worker %d 收到关闭信号,准备退出", workerID) 93 | return 94 | } 95 | } 96 | }(i) 97 | } 98 | } 99 | 100 | // SubmitTask 提交任务到工作池,非阻塞 101 | func (pool *WorkerPool) SubmitTask(task *Task) (bool, error) { 102 | select { 103 | case pool.taskQueue <- task: 104 | // 任务成功添加到队列 105 | return true, nil 106 | default: 107 | // 队列已满 108 | return false, fmt.Errorf("任务队列已满") 109 | } 110 | } 111 | 112 | // Shutdown 关闭工作池 113 | func (pool *WorkerPool) Shutdown() { 114 | logInfo("正在关闭工作池...") 115 | 116 | // 发送关闭信号给所有worker 117 | close(pool.shutdownChannel) 118 | 119 | // 等待所有worker退出 120 | pool.wg.Wait() 121 | 122 | // 关闭任务队列 123 | close(pool.taskQueue) 124 | 125 | logInfo("工作池已关闭") 126 | } 127 | 128 | // Semaphore 信号量实现,用于限制并发数量 129 | type Semaphore struct { 130 | sem chan struct{} 131 | } 132 | 133 | // NewSemaphore 创建新的信号量 134 | func NewSemaphore(size int) *Semaphore { 135 | return &Semaphore{ 136 | sem: make(chan struct{}, size), 137 | } 138 | } 139 | 140 | // Acquire 获取信号量(阻塞) 141 | func (s *Semaphore) Acquire() { 142 | s.sem <- struct{}{} 143 | } 144 | 145 | // Release 释放信号量 146 | func (s *Semaphore) Release() { 147 | <-s.sem 148 | } 149 | 150 | // TryAcquire 尝试获取信号量(非阻塞) 151 | func (s *Semaphore) TryAcquire() bool { 152 | select { 153 | case s.sem <- struct{}{}: 154 | return true 155 | default: 156 | return false 157 | } 158 | } 159 | 160 | // 配置结构体用于存储命令行参数 161 | type Config struct { 162 | Port string // 代理服务器监听端口 163 | Address string // 代理服务器监听地址 164 | LogLevel string // 日志级别 165 | DevMode bool // 开发模式标志 166 | MaxRetries int // 最大重试次数 167 | Timeout int // 请求超时时间(秒) 168 | VerifySSL bool // 是否验证SSL证书 169 | ModelName string // 默认模型名称 170 | BearerToken string // Bearer Token (默认提供公开Token) 171 | WorkerCount int // 工作池中的worker数量 172 | QueueSize int // 任务队列大小 173 | MaxConcurrent int // 最大并发请求数 174 | } 175 | 176 | // 支持的模型列表 177 | var SupportedModels = []string{ 178 | "hunyuan-t1-latest", 179 | "hunyuan-turbos-latest", 180 | } 181 | 182 | // 腾讯混元 API 目标URL 183 | const ( 184 | TargetURL = "https://llm.hunyuan.tencent.com/aide/api/v2/triton_image/demo_text_chat/" 185 | Version = "1.0.0" // 版本号 186 | ) 187 | 188 | // 日志级别 189 | const ( 190 | LogLevelDebug = "debug" 191 | LogLevelInfo = "info" 192 | LogLevelWarn = "warn" 193 | LogLevelError = "error" 194 | ) 195 | 196 | // 解析命令行参数并返回 Config 实例 197 | func parseFlags() *Config { 198 | cfg := &Config{} 199 | flag.StringVar(&cfg.Port, "port", "6666", "Port to listen on") 200 | flag.StringVar(&cfg.Address, "address", "localhost", "Address to listen on") 201 | flag.StringVar(&cfg.LogLevel, "log-level", LogLevelInfo, "Log level (debug, info, warn, error)") 202 | flag.BoolVar(&cfg.DevMode, "dev", false, "Enable development mode with enhanced logging") 203 | flag.IntVar(&cfg.MaxRetries, "max-retries", 3, "Maximum number of retries for failed requests") 204 | flag.IntVar(&cfg.Timeout, "timeout", 300, "Request timeout in seconds") 205 | flag.BoolVar(&cfg.VerifySSL, "verify-ssl", true, "Verify SSL certificates") 206 | flag.StringVar(&cfg.ModelName, "model", "hunyuan-t1-latest", "Default Hunyuan model name") 207 | flag.StringVar(&cfg.BearerToken, "token", "7auGXNATFSKl7dF", "Bearer token for Hunyuan API") 208 | flag.IntVar(&cfg.WorkerCount, "workers", 50, "Number of worker goroutines in the pool") 209 | flag.IntVar(&cfg.QueueSize, "queue-size", 500, "Size of the task queue") 210 | flag.IntVar(&cfg.MaxConcurrent, "max-concurrent", 100, "Maximum number of concurrent requests") 211 | flag.Parse() 212 | 213 | // 如果开发模式开启,自动设置日志级别为debug 214 | if cfg.DevMode && cfg.LogLevel != LogLevelDebug { 215 | cfg.LogLevel = LogLevelDebug 216 | fmt.Println("开发模式已启用,日志级别设置为debug") 217 | } 218 | 219 | return cfg 220 | } 221 | 222 | // 全局配置变量 223 | var ( 224 | appConfig *Config 225 | ) 226 | 227 | // 性能指标 228 | var ( 229 | requestCounter int64 230 | successCounter int64 231 | errorCounter int64 232 | avgResponseTime int64 233 | latencyHistogram [10]int64 // 0-100ms, 100-200ms, ... >1s 234 | queuedRequests int64 // 当前在队列中的请求数 235 | rejectedRequests int64 // 被拒绝的请求数 236 | ) 237 | 238 | // 并发控制组件 239 | var ( 240 | workerPool *WorkerPool // 工作池 241 | requestSem *Semaphore // 请求信号量 242 | ) 243 | 244 | // 日志记录器 245 | var ( 246 | logger *log.Logger 247 | logLevel string 248 | logMutex sync.Mutex 249 | ) 250 | 251 | // 日志初始化 252 | func initLogger(level string) { 253 | logger = log.New(os.Stdout, "[HunyuanAPI] ", log.LstdFlags) 254 | logLevel = level 255 | } 256 | 257 | // 根据日志级别记录日志 258 | func logDebug(format string, v ...interface{}) { 259 | if logLevel == LogLevelDebug { 260 | logMutex.Lock() 261 | logger.Printf("[DEBUG] "+format, v...) 262 | logMutex.Unlock() 263 | } 264 | } 265 | 266 | func logInfo(format string, v ...interface{}) { 267 | if logLevel == LogLevelDebug || logLevel == LogLevelInfo { 268 | logMutex.Lock() 269 | logger.Printf("[INFO] "+format, v...) 270 | logMutex.Unlock() 271 | } 272 | } 273 | 274 | func logWarn(format string, v ...interface{}) { 275 | if logLevel == LogLevelDebug || logLevel == LogLevelInfo || logLevel == LogLevelWarn { 276 | logMutex.Lock() 277 | logger.Printf("[WARN] "+format, v...) 278 | logMutex.Unlock() 279 | } 280 | } 281 | 282 | func logError(format string, v ...interface{}) { 283 | logMutex.Lock() 284 | logger.Printf("[ERROR] "+format, v...) 285 | logMutex.Unlock() 286 | 287 | // 错误计数 288 | atomic.AddInt64(&errorCounter, 1) 289 | } 290 | 291 | // OpenAI/DeepSeek 消息格式 292 | type APIMessage struct { 293 | Role string `json:"role"` 294 | Content interface{} `json:"content"` // 使用interface{}以支持各种类型 295 | } 296 | 297 | // OpenAI/DeepSeek 请求格式 298 | type APIRequest struct { 299 | Model string `json:"model"` 300 | Messages []APIMessage `json:"messages"` 301 | Stream bool `json:"stream"` 302 | Temperature float64 `json:"temperature,omitempty"` 303 | MaxTokens int `json:"max_tokens,omitempty"` 304 | } 305 | 306 | // 腾讯混元请求格式 307 | type HunyuanRequest struct { 308 | Stream bool `json:"stream"` 309 | Model string `json:"model"` 310 | QueryID string `json:"query_id"` 311 | Messages []APIMessage `json:"messages"` 312 | StreamModeration bool `json:"stream_moderation"` 313 | EnableEnhancement bool `json:"enable_enhancement"` 314 | } 315 | 316 | // 腾讯混元响应格式 317 | type HunyuanResponse struct { 318 | ID string `json:"id"` 319 | Object string `json:"object"` 320 | Created int64 `json:"created"` 321 | Model string `json:"model"` 322 | SystemFingerprint string `json:"system_fingerprint"` 323 | Choices []Choice `json:"choices"` 324 | Note string `json:"note,omitempty"` 325 | } 326 | 327 | // 选择结构 328 | type Choice struct { 329 | Index int `json:"index"` 330 | Delta Delta `json:"delta"` 331 | FinishReason *string `json:"finish_reason"` 332 | } 333 | 334 | // Delta结构,包含内容和推理内容 335 | type Delta struct { 336 | Role string `json:"role,omitempty"` 337 | Content string `json:"content,omitempty"` 338 | ReasoningContent string `json:"reasoning_content,omitempty"` 339 | } 340 | 341 | // DeepSeek 流式响应格式 342 | type StreamChunk struct { 343 | ID string `json:"id"` 344 | Object string `json:"object"` 345 | Created int64 `json:"created"` 346 | Model string `json:"model"` 347 | Choices []struct { 348 | Index int `json:"index"` 349 | FinishReason *string `json:"finish_reason,omitempty"` 350 | Delta struct { 351 | Role string `json:"role,omitempty"` 352 | Content string `json:"content,omitempty"` 353 | ReasoningContent string `json:"reasoning_content,omitempty"` 354 | } `json:"delta"` 355 | } `json:"choices"` 356 | } 357 | 358 | // 非流式响应格式 359 | type CompletionResponse struct { 360 | ID string `json:"id"` 361 | Object string `json:"object"` 362 | Created int64 `json:"created"` 363 | Model string `json:"model"` 364 | Choices []struct { 365 | Index int `json:"index"` 366 | FinishReason string `json:"finish_reason"` 367 | Message struct { 368 | Role string `json:"role"` 369 | Content string `json:"content"` 370 | ReasoningContent string `json:"reasoning_content,omitempty"` 371 | } `json:"message"` 372 | } `json:"choices"` 373 | Usage struct { 374 | PromptTokens int `json:"prompt_tokens"` 375 | CompletionTokens int `json:"completion_tokens"` 376 | TotalTokens int `json:"total_tokens"` 377 | } `json:"usage"` 378 | } 379 | 380 | // 请求计数和互斥锁,用于监控 381 | var ( 382 | requestCount uint64 = 0 383 | countMutex sync.Mutex 384 | ) 385 | 386 | // 主入口函数 387 | func main() { 388 | // 解析配置 389 | appConfig = parseFlags() 390 | 391 | // 初始化日志 392 | initLogger(appConfig.LogLevel) 393 | 394 | logInfo("启动服务: TargetURL=%s, Address=%s, Port=%s, Version=%s, LogLevel=%s, 支持模型=%v, BearerToken=***, WorkerCount=%d, QueueSize=%d, MaxConcurrent=%d", 395 | TargetURL, appConfig.Address, appConfig.Port, Version, appConfig.LogLevel, SupportedModels, 396 | appConfig.WorkerCount, appConfig.QueueSize, appConfig.MaxConcurrent) 397 | 398 | // 创建工作池和信号量 399 | workerPool = NewWorkerPool(appConfig.WorkerCount, appConfig.QueueSize) 400 | requestSem = NewSemaphore(appConfig.MaxConcurrent) 401 | 402 | logInfo("工作池已创建: %d个worker, 队列大小为%d", appConfig.WorkerCount, appConfig.QueueSize) 403 | 404 | // 配置更高的并发处理能力 405 | http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100 406 | http.DefaultTransport.(*http.Transport).MaxIdleConns = 100 407 | http.DefaultTransport.(*http.Transport).IdleConnTimeout = 90 * time.Second 408 | 409 | // 创建自定义服务器,支持更高并发 410 | server := &http.Server{ 411 | Addr: appConfig.Address + ":" + appConfig.Port, 412 | ReadTimeout: time.Duration(appConfig.Timeout) * time.Second, 413 | WriteTimeout: time.Duration(appConfig.Timeout) * time.Second, 414 | IdleTimeout: 120 * time.Second, 415 | Handler: nil, // 使用默认的ServeMux 416 | } 417 | 418 | // 创建处理器 419 | http.HandleFunc("/v1/models", func(w http.ResponseWriter, r *http.Request) { 420 | setCORSHeaders(w) 421 | if r.Method == "OPTIONS" { 422 | w.WriteHeader(http.StatusOK) 423 | return 424 | } 425 | handleModelsRequest(w, r) 426 | }) 427 | 428 | http.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { 429 | setCORSHeaders(w) 430 | if r.Method == "OPTIONS" { 431 | w.WriteHeader(http.StatusOK) 432 | return 433 | } 434 | 435 | // 计数器增加 436 | countMutex.Lock() 437 | requestCount++ 438 | currentCount := requestCount 439 | countMutex.Unlock() 440 | 441 | logInfo("收到新请求 #%d", currentCount) 442 | 443 | // 请求计数 444 | atomic.AddInt64(&requestCounter, 1) 445 | 446 | // 尝试获取信号量 447 | if !requestSem.TryAcquire() { 448 | // 请求数量超过限制 449 | atomic.AddInt64(&rejectedRequests, 1) 450 | logWarn("请求 #%d 被拒绝: 当前并发请求数已达上限", currentCount) 451 | w.Header().Set("Retry-After", "30") 452 | http.Error(w, "Server is busy, please try again later", http.StatusServiceUnavailable) 453 | return 454 | } 455 | 456 | // 释放信号量(在函数返回时) 457 | defer requestSem.Release() 458 | 459 | // 处理请求 460 | handleChatCompletionRequestWithPool(w, r, currentCount) 461 | }) 462 | 463 | // 添加健康检查端点 464 | http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 465 | setCORSHeaders(w) 466 | if r.Method == "OPTIONS" { 467 | w.WriteHeader(http.StatusOK) 468 | return 469 | } 470 | 471 | // 获取各种计数器的值 472 | reqCount := atomic.LoadInt64(&requestCounter) 473 | succCount := atomic.LoadInt64(&successCounter) 474 | errCount := atomic.LoadInt64(&errorCounter) 475 | queuedCount := atomic.LoadInt64(&queuedRequests) 476 | rejectedCount := atomic.LoadInt64(&rejectedRequests) 477 | 478 | // 计算平均响应时间 479 | var avgTime int64 = 0 480 | if reqCount > 0 { 481 | avgTime = atomic.LoadInt64(&avgResponseTime) / max(reqCount, 1) 482 | } 483 | 484 | // 构建延迟直方图数据 485 | histogram := make([]int64, 10) 486 | for i := 0; i < 10; i++ { 487 | histogram[i] = atomic.LoadInt64(&latencyHistogram[i]) 488 | } 489 | 490 | // 构建响应 491 | stats := map[string]interface{}{ 492 | "status": "ok", 493 | "version": Version, 494 | "requests": reqCount, 495 | "success": succCount, 496 | "errors": errCount, 497 | "queued": queuedCount, 498 | "rejected": rejectedCount, 499 | "avg_time_ms": avgTime, 500 | "histogram_ms": histogram, 501 | "worker_count": workerPool.workerCount, 502 | "queue_size": len(workerPool.taskQueue), 503 | "queue_capacity": cap(workerPool.taskQueue), 504 | "queue_percent": float64(len(workerPool.taskQueue)) / float64(cap(workerPool.taskQueue)) * 100, 505 | "concurrent_limit": appConfig.MaxConcurrent, 506 | } 507 | 508 | w.Header().Set("Content-Type", "application/json") 509 | w.WriteHeader(http.StatusOK) 510 | json.NewEncoder(w).Encode(stats) 511 | }) 512 | 513 | // 创建停止通道 514 | stop := make(chan os.Signal, 1) 515 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM) 516 | 517 | // 在goroutine中启动服务器 518 | go func() { 519 | logInfo("Starting proxy server on %s", server.Addr) 520 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 521 | logError("Failed to start server: %v", err) 522 | os.Exit(1) 523 | } 524 | }() 525 | 526 | // 等待停止信号 527 | <-stop 528 | 529 | // 创建上下文用于优雅关闭 530 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 531 | defer cancel() 532 | 533 | // 优雅关闭服务器 534 | logInfo("Server is shutting down...") 535 | if err := server.Shutdown(ctx); err != nil { 536 | logError("Server shutdown failed: %v", err) 537 | } 538 | 539 | // 关闭工作池 540 | workerPool.Shutdown() 541 | 542 | logInfo("Server gracefully stopped") 543 | } 544 | 545 | // 设置CORS头 546 | func setCORSHeaders(w http.ResponseWriter) { 547 | w.Header().Set("Access-Control-Allow-Origin", "*") 548 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") 549 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 550 | } 551 | 552 | // 验证消息格式 553 | func validateMessages(messages []APIMessage) (bool, string) { 554 | reqID := generateRequestID() 555 | logDebug("[reqID:%s] 验证消息格式", reqID) 556 | 557 | if messages == nil || len(messages) == 0 { 558 | return false, "Messages array is required" 559 | } 560 | 561 | for _, msg := range messages { 562 | if msg.Role == "" || msg.Content == nil { 563 | return false, "Invalid message format: each message must have role and content" 564 | } 565 | } 566 | 567 | return true, "" 568 | } 569 | 570 | // 从请求头中提取令牌 571 | func extractToken(r *http.Request) (string, error) { 572 | // 获取 Authorization 头部 573 | authHeader := r.Header.Get("Authorization") 574 | if authHeader == "" { 575 | return "", fmt.Errorf("missing Authorization header") 576 | } 577 | 578 | // 验证格式并提取令牌 579 | if !strings.HasPrefix(authHeader, "Bearer ") { 580 | return "", fmt.Errorf("invalid Authorization header format, must start with 'Bearer '") 581 | } 582 | 583 | // 提取令牌值 584 | token := strings.TrimPrefix(authHeader, "Bearer ") 585 | if token == "" { 586 | return "", fmt.Errorf("empty token in Authorization header") 587 | } 588 | 589 | return token, nil 590 | } 591 | 592 | // 转换任意类型的内容为字符串 593 | func contentToString(content interface{}) string { 594 | if content == nil { 595 | return "" 596 | } 597 | 598 | switch v := content.(type) { 599 | case string: 600 | return v 601 | default: 602 | jsonBytes, err := json.Marshal(v) 603 | if err != nil { 604 | logWarn("将内容转换为JSON失败: %v", err) 605 | return "" 606 | } 607 | return string(jsonBytes) 608 | } 609 | } 610 | 611 | // 生成请求ID 612 | func generateQueryID() string { 613 | return fmt.Sprintf("%s%d", getRandomString(8), time.Now().UnixNano()) 614 | } 615 | 616 | // 判断模型是否在支持列表中 617 | func isModelSupported(modelName string) bool { 618 | for _, supportedModel := range SupportedModels { 619 | if modelName == supportedModel { 620 | return true 621 | } 622 | } 623 | return false 624 | } 625 | 626 | // 处理模型列表请求 627 | func handleModelsRequest(w http.ResponseWriter, r *http.Request) { 628 | logInfo("处理模型列表请求") 629 | 630 | // 返回模型列表 631 | w.Header().Set("Content-Type", "application/json") 632 | w.WriteHeader(http.StatusOK) 633 | 634 | // 构建模型数据 635 | modelData := make([]map[string]interface{}, 0, len(SupportedModels)) 636 | for _, model := range SupportedModels { 637 | modelData = append(modelData, map[string]interface{}{ 638 | "id": model, 639 | "object": "model", 640 | "created": time.Now().Unix(), 641 | "owned_by": "TencentCloud", 642 | "capabilities": map[string]interface{}{ 643 | "chat": true, 644 | "completions": true, 645 | "reasoning": true, 646 | }, 647 | }) 648 | } 649 | 650 | modelsList := map[string]interface{}{ 651 | "object": "list", 652 | "data": modelData, 653 | } 654 | 655 | json.NewEncoder(w).Encode(modelsList) 656 | } 657 | 658 | // 处理聊天补全请求(使用工作池) 659 | func handleChatCompletionRequestWithPool(w http.ResponseWriter, r *http.Request, requestNum uint64) { 660 | reqID := generateRequestID() 661 | startTime := time.Now() 662 | logInfo("[reqID:%s] 处理聊天补全请求 #%d", reqID, requestNum) 663 | 664 | // 设置超时上下文 665 | ctx, cancel := context.WithTimeout(r.Context(), time.Duration(appConfig.Timeout)*time.Second) 666 | defer cancel() 667 | 668 | // 包含超时上下文的请求 669 | r = r.WithContext(ctx) 670 | 671 | // 添加恢复机制,防止panic 672 | defer func() { 673 | if r := recover(); r != nil { 674 | logError("[reqID:%s] 处理请求时发生panic: %v", reqID, r) 675 | http.Error(w, "Internal server error", http.StatusInternalServerError) 676 | } 677 | }() 678 | 679 | // 解析请求体 680 | var apiReq APIRequest 681 | if err := json.NewDecoder(r.Body).Decode(&apiReq); err != nil { 682 | logError("[reqID:%s] 解析请求失败: %v", reqID, err) 683 | http.Error(w, "Invalid request body", http.StatusBadRequest) 684 | return 685 | } 686 | 687 | // 验证消息格式 688 | valid, errMsg := validateMessages(apiReq.Messages) 689 | if !valid { 690 | logError("[reqID:%s] 消息格式验证失败: %s", reqID, errMsg) 691 | http.Error(w, errMsg, http.StatusBadRequest) 692 | return 693 | } 694 | 695 | // 是否使用流式处理 696 | isStream := apiReq.Stream 697 | 698 | // 确定使用的模型 699 | modelName := appConfig.ModelName 700 | if apiReq.Model != "" { 701 | // 检查请求的模型是否是我们支持的 702 | if isModelSupported(apiReq.Model) { 703 | modelName = apiReq.Model 704 | } else { 705 | logWarn("[reqID:%s] 请求的模型 %s 不支持,使用默认模型 %s", reqID, apiReq.Model, modelName) 706 | } 707 | } 708 | 709 | logInfo("[reqID:%s] 使用模型: %s", reqID, modelName) 710 | 711 | // 创建混元API请求 712 | hunyuanReq := HunyuanRequest{ 713 | Stream: true, // 混元API总是使用流式响应 714 | Model: modelName, 715 | QueryID: generateQueryID(), 716 | Messages: apiReq.Messages, 717 | StreamModeration: true, 718 | EnableEnhancement: false, 719 | } 720 | 721 | // 创建任务 722 | task := &Task{ 723 | r: r, 724 | w: w, 725 | done: make(chan struct{}), 726 | reqID: reqID, 727 | isStream: isStream, 728 | hunyuanReq: hunyuanReq, 729 | } 730 | 731 | // 添加到任务队列 732 | atomic.AddInt64(&queuedRequests, 1) 733 | submitted, err := workerPool.SubmitTask(task) 734 | if !submitted { 735 | atomic.AddInt64(&queuedRequests, -1) 736 | atomic.AddInt64(&rejectedRequests, 1) 737 | logError("[reqID:%s] 提交任务失败: %v", reqID, err) 738 | w.Header().Set("Retry-After", "60") 739 | http.Error(w, "Server queue is full, please try again later", http.StatusServiceUnavailable) 740 | return 741 | } 742 | 743 | logInfo("[reqID:%s] 任务已提交到队列", reqID) 744 | 745 | // 等待任务完成或超时 746 | select { 747 | case <-task.done: 748 | // 任务已完成 749 | logInfo("[reqID:%s] 任务已完成", reqID) 750 | case <-r.Context().Done(): 751 | // 请求被取消或超时 752 | logWarn("[reqID:%s] 请求被取消或超时", reqID) 753 | // 注意:虽然请求被取消,但worker可能仍在处理任务 754 | } 755 | 756 | // 请求处理完成,更新指标 757 | atomic.AddInt64(&queuedRequests, -1) 758 | elapsed := time.Since(startTime).Milliseconds() 759 | 760 | // 更新延迟直方图 761 | bucketIndex := min(int(elapsed/100), 9) 762 | atomic.AddInt64(&latencyHistogram[bucketIndex], 1) 763 | 764 | // 更新平均响应时间 765 | atomic.AddInt64(&avgResponseTime, elapsed) 766 | 767 | if r.Context().Err() == nil { 768 | // 成功计数增加 769 | atomic.AddInt64(&successCounter, 1) 770 | logInfo("[reqID:%s] 请求处理成功,耗时: %dms", reqID, elapsed) 771 | } else { 772 | logError("[reqID:%s] 请求处理失败: %v, 耗时: %dms", reqID, r.Context().Err(), elapsed) 773 | } 774 | } 775 | 776 | // 处理聊天补全请求(原实现,已不使用) 777 | func handleChatCompletionRequest(w http.ResponseWriter, r *http.Request) { 778 | reqID := generateRequestID() 779 | startTime := time.Now() 780 | logInfo("[reqID:%s] 处理聊天补全请求", reqID) 781 | 782 | // 解析请求体 783 | var apiReq APIRequest 784 | if err := json.NewDecoder(r.Body).Decode(&apiReq); err != nil { 785 | logError("[reqID:%s] 解析请求失败: %v", reqID, err) 786 | http.Error(w, "Invalid request body", http.StatusBadRequest) 787 | return 788 | } 789 | 790 | // 验证消息格式 791 | valid, errMsg := validateMessages(apiReq.Messages) 792 | if !valid { 793 | logError("[reqID:%s] 消息格式验证失败: %s", reqID, errMsg) 794 | http.Error(w, errMsg, http.StatusBadRequest) 795 | return 796 | } 797 | 798 | // 是否使用流式处理 799 | isStream := apiReq.Stream 800 | 801 | // 确定使用的模型 802 | modelName := appConfig.ModelName 803 | if apiReq.Model != "" { 804 | // 检查请求的模型是否是我们支持的 805 | if isModelSupported(apiReq.Model) { 806 | modelName = apiReq.Model 807 | } else { 808 | logWarn("[reqID:%s] 请求的模型 %s 不支持,使用默认模型 %s", reqID, apiReq.Model, modelName) 809 | } 810 | } 811 | 812 | logInfo("[reqID:%s] 使用模型: %s", reqID, modelName) 813 | 814 | // 创建混元API请求 815 | hunyuanReq := HunyuanRequest{ 816 | Stream: true, // 混元API总是使用流式响应 817 | Model: modelName, 818 | QueryID: generateQueryID(), 819 | Messages: apiReq.Messages, 820 | StreamModeration: true, 821 | EnableEnhancement: false, 822 | } 823 | 824 | // 转发请求到混元API 825 | var responseErr error 826 | if isStream { 827 | responseErr = handleStreamingRequest(w, r, hunyuanReq, reqID) 828 | } else { 829 | responseErr = handleNonStreamingRequest(w, r, hunyuanReq, reqID) 830 | } 831 | 832 | // 请求处理完成,更新指标 833 | elapsed := time.Since(startTime).Milliseconds() 834 | 835 | // 更新延迟直方图 836 | bucketIndex := min(int(elapsed/100), 9) 837 | atomic.AddInt64(&latencyHistogram[bucketIndex], 1) 838 | 839 | // 更新平均响应时间 840 | atomic.AddInt64(&avgResponseTime, elapsed) 841 | 842 | if responseErr == nil { 843 | // 成功计数增加 844 | atomic.AddInt64(&successCounter, 1) 845 | logInfo("[reqID:%s] 请求处理成功,耗时: %dms", reqID, elapsed) 846 | } else { 847 | logError("[reqID:%s] 请求处理失败: %v, 耗时: %dms", reqID, responseErr, elapsed) 848 | } 849 | } 850 | 851 | // 安全的HTTP客户端,支持禁用SSL验证 852 | func getHTTPClient() *http.Client { 853 | tr := &http.Transport{ 854 | MaxIdleConnsPerHost: 100, 855 | IdleConnTimeout: 90 * time.Second, 856 | TLSClientConfig: nil, // 默认配置 857 | } 858 | 859 | // 如果配置了禁用SSL验证 860 | if !appConfig.VerifySSL { 861 | tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 862 | } 863 | 864 | return &http.Client{ 865 | Timeout: time.Duration(appConfig.Timeout) * time.Second, 866 | Transport: tr, 867 | } 868 | } 869 | 870 | // 处理流式请求 871 | func handleStreamingRequest(w http.ResponseWriter, r *http.Request, hunyuanReq HunyuanRequest, reqID string) error { 872 | logInfo("[reqID:%s] 处理流式请求", reqID) 873 | 874 | // 序列化请求 875 | jsonData, err := json.Marshal(hunyuanReq) 876 | if err != nil { 877 | logError("[reqID:%s] 序列化请求失败: %v", reqID, err) 878 | http.Error(w, "Internal server error", http.StatusInternalServerError) 879 | return err 880 | } 881 | 882 | // 创建请求 883 | httpReq, err := http.NewRequestWithContext(r.Context(), "POST", TargetURL, bytes.NewBuffer(jsonData)) 884 | if err != nil { 885 | logError("[reqID:%s] 创建请求失败: %v", reqID, err) 886 | http.Error(w, "Internal server error", http.StatusInternalServerError) 887 | return err 888 | } 889 | 890 | // 设置请求头 891 | httpReq.Header.Set("Content-Type", "application/json") 892 | httpReq.Header.Set("Model", hunyuanReq.Model) 893 | setCommonHeaders(httpReq) 894 | 895 | // 创建HTTP客户端 896 | client := getHTTPClient() 897 | 898 | // 发送请求 899 | resp, err := client.Do(httpReq) 900 | if err != nil { 901 | logError("[reqID:%s] 发送请求失败: %v", reqID, err) 902 | http.Error(w, "Failed to connect to API", http.StatusBadGateway) 903 | return err 904 | } 905 | defer resp.Body.Close() 906 | 907 | // 检查响应状态 908 | if resp.StatusCode != http.StatusOK { 909 | logError("[reqID:%s] API返回非200状态码: %d", reqID, resp.StatusCode) 910 | 911 | bodyBytes, _ := io.ReadAll(resp.Body) 912 | logError("[reqID:%s] 错误响应内容: %s", reqID, string(bodyBytes)) 913 | 914 | http.Error(w, fmt.Sprintf("API error with status code: %d", resp.StatusCode), resp.StatusCode) 915 | return fmt.Errorf("API返回非200状态码: %d", resp.StatusCode) 916 | } 917 | 918 | // 设置响应头 919 | w.Header().Set("Content-Type", "text/event-stream") 920 | w.Header().Set("Cache-Control", "no-cache") 921 | w.Header().Set("Connection", "keep-alive") 922 | 923 | // 创建响应ID和时间戳 924 | respID := fmt.Sprintf("chatcmpl-%s", getRandomString(10)) 925 | createdTime := time.Now().Unix() 926 | 927 | // 创建读取器 928 | reader := bufio.NewReaderSize(resp.Body, 16384) 929 | 930 | // 创建Flusher 931 | flusher, ok := w.(http.Flusher) 932 | if !ok { 933 | logError("[reqID:%s] Streaming not supported", reqID) 934 | http.Error(w, "Streaming not supported", http.StatusInternalServerError) 935 | return fmt.Errorf("streaming not supported") 936 | } 937 | 938 | // 发送角色块 939 | roleChunk := createRoleChunk(respID, createdTime, hunyuanReq.Model) 940 | w.Write([]byte("data: " + string(roleChunk) + "\n\n")) 941 | flusher.Flush() 942 | 943 | // 持续读取响应 944 | for { 945 | // 添加超时检测 946 | select { 947 | case <-r.Context().Done(): 948 | logWarn("[reqID:%s] 请求超时或被客户端取消", reqID) 949 | return fmt.Errorf("请求超时或被取消") 950 | default: 951 | // 继续处理 952 | } 953 | 954 | // 读取一行数据 955 | line, err := reader.ReadBytes('\n') 956 | if err != nil { 957 | if err != io.EOF { 958 | logError("[reqID:%s] 读取响应出错: %v", reqID, err) 959 | return err 960 | } 961 | break 962 | } 963 | 964 | // 处理数据行 965 | lineStr := string(line) 966 | if strings.HasPrefix(lineStr, "data: ") { 967 | jsonStr := strings.TrimPrefix(lineStr, "data: ") 968 | jsonStr = strings.TrimSpace(jsonStr) 969 | 970 | // 特殊处理[DONE]消息 971 | if jsonStr == "[DONE]" { 972 | logDebug("[reqID:%s] 收到[DONE]消息", reqID) 973 | w.Write([]byte("data: [DONE]\n\n")) 974 | flusher.Flush() 975 | break 976 | } 977 | 978 | // 解析混元响应 979 | var hunyuanResp HunyuanResponse 980 | if err := json.Unmarshal([]byte(jsonStr), &hunyuanResp); err != nil { 981 | logWarn("[reqID:%s] 解析JSON失败: %v, data: %s", reqID, err, jsonStr) 982 | continue 983 | } 984 | 985 | // 处理各种类型的内容 986 | for _, choice := range hunyuanResp.Choices { 987 | if choice.Delta.Content != "" { 988 | // 发送内容块 989 | contentChunk := createContentChunk(respID, createdTime, hunyuanReq.Model, choice.Delta.Content) 990 | w.Write([]byte("data: " + string(contentChunk) + "\n\n")) 991 | flusher.Flush() 992 | } 993 | 994 | if choice.Delta.ReasoningContent != "" { 995 | // 发送推理内容块 996 | reasoningChunk := createReasoningChunk(respID, createdTime, hunyuanReq.Model, choice.Delta.ReasoningContent) 997 | w.Write([]byte("data: " + string(reasoningChunk) + "\n\n")) 998 | flusher.Flush() 999 | } 1000 | 1001 | // 处理完成标志 1002 | if choice.FinishReason != nil { 1003 | finishReason := *choice.FinishReason 1004 | if finishReason != "" { 1005 | doneChunk := createDoneChunk(respID, createdTime, hunyuanReq.Model, finishReason) 1006 | w.Write([]byte("data: " + string(doneChunk) + "\n\n")) 1007 | flusher.Flush() 1008 | } 1009 | } 1010 | } 1011 | } 1012 | } 1013 | 1014 | // 发送结束信号(如果没有正常结束) 1015 | finishReason := "stop" 1016 | doneChunk := createDoneChunk(respID, createdTime, hunyuanReq.Model, finishReason) 1017 | w.Write([]byte("data: " + string(doneChunk) + "\n\n")) 1018 | w.Write([]byte("data: [DONE]\n\n")) 1019 | flusher.Flush() 1020 | 1021 | return nil 1022 | } 1023 | 1024 | // 处理非流式请求 1025 | func handleNonStreamingRequest(w http.ResponseWriter, r *http.Request, hunyuanReq HunyuanRequest, reqID string) error { 1026 | logInfo("[reqID:%s] 处理非流式请求", reqID) 1027 | 1028 | // 序列化请求 1029 | jsonData, err := json.Marshal(hunyuanReq) 1030 | if err != nil { 1031 | logError("[reqID:%s] 序列化请求失败: %v", reqID, err) 1032 | http.Error(w, "Internal server error", http.StatusInternalServerError) 1033 | return err 1034 | } 1035 | 1036 | // 创建请求 1037 | httpReq, err := http.NewRequestWithContext(r.Context(), "POST", TargetURL, bytes.NewBuffer(jsonData)) 1038 | if err != nil { 1039 | logError("[reqID:%s] 创建请求失败: %v", reqID, err) 1040 | http.Error(w, "Internal server error", http.StatusInternalServerError) 1041 | return err 1042 | } 1043 | 1044 | // 设置请求头 1045 | httpReq.Header.Set("Content-Type", "application/json") 1046 | httpReq.Header.Set("Model", hunyuanReq.Model) 1047 | setCommonHeaders(httpReq) 1048 | 1049 | // 创建HTTP客户端 1050 | client := getHTTPClient() 1051 | 1052 | // 发送请求 1053 | resp, err := client.Do(httpReq) 1054 | if err != nil { 1055 | logError("[reqID:%s] 发送请求失败: %v", reqID, err) 1056 | http.Error(w, "Failed to connect to API", http.StatusBadGateway) 1057 | return err 1058 | } 1059 | defer resp.Body.Close() 1060 | 1061 | // 检查响应状态 1062 | if resp.StatusCode != http.StatusOK { 1063 | logError("[reqID:%s] API返回非200状态码: %d", reqID, resp.StatusCode) 1064 | 1065 | bodyBytes, _ := io.ReadAll(resp.Body) 1066 | logError("[reqID:%s] 错误响应内容: %s", reqID, string(bodyBytes)) 1067 | 1068 | http.Error(w, fmt.Sprintf("API error with status code: %d", resp.StatusCode), resp.StatusCode) 1069 | return fmt.Errorf("API返回非200状态码: %d", resp.StatusCode) 1070 | } 1071 | 1072 | // 读取完整的流式响应 1073 | bodyBytes, err := io.ReadAll(resp.Body) 1074 | if err != nil { 1075 | logError("[reqID:%s] 读取响应失败: %v", reqID, err) 1076 | http.Error(w, "Failed to read API response", http.StatusInternalServerError) 1077 | return err 1078 | } 1079 | 1080 | // 解析流式响应并提取完整内容 1081 | fullContent, reasoningContent, err := extractFullContentFromStream(bodyBytes, reqID) 1082 | if err != nil { 1083 | logError("[reqID:%s] 解析流式响应失败: %v", reqID, err) 1084 | http.Error(w, "Failed to parse streaming response", http.StatusInternalServerError) 1085 | return err 1086 | } 1087 | 1088 | // 构建完整的非流式响应 1089 | completionResponse := CompletionResponse{ 1090 | ID: fmt.Sprintf("chatcmpl-%s", getRandomString(10)), 1091 | Object: "chat.completion", 1092 | Created: time.Now().Unix(), 1093 | Model: hunyuanReq.Model, 1094 | Choices: []struct { 1095 | Index int `json:"index"` 1096 | FinishReason string `json:"finish_reason"` 1097 | Message struct { 1098 | Role string `json:"role"` 1099 | Content string `json:"content"` 1100 | ReasoningContent string `json:"reasoning_content,omitempty"` 1101 | } `json:"message"` 1102 | }{ 1103 | { 1104 | Index: 0, 1105 | FinishReason: "stop", 1106 | Message: struct { 1107 | Role string `json:"role"` 1108 | Content string `json:"content"` 1109 | ReasoningContent string `json:"reasoning_content,omitempty"` 1110 | }{ 1111 | Role: "assistant", 1112 | Content: fullContent, 1113 | ReasoningContent: reasoningContent, 1114 | }, 1115 | }, 1116 | }, 1117 | Usage: struct { 1118 | PromptTokens int `json:"prompt_tokens"` 1119 | CompletionTokens int `json:"completion_tokens"` 1120 | TotalTokens int `json:"total_tokens"` 1121 | }{ 1122 | PromptTokens: len(formatMessages(hunyuanReq.Messages)) / 4, 1123 | CompletionTokens: len(fullContent) / 4, 1124 | TotalTokens: (len(formatMessages(hunyuanReq.Messages)) + len(fullContent)) / 4, 1125 | }, 1126 | } 1127 | 1128 | // 返回响应 1129 | w.Header().Set("Content-Type", "application/json") 1130 | if err := json.NewEncoder(w).Encode(completionResponse); err != nil { 1131 | logError("[reqID:%s] 编码响应失败: %v", reqID, err) 1132 | http.Error(w, "Failed to encode response", http.StatusInternalServerError) 1133 | return err 1134 | } 1135 | 1136 | return nil 1137 | } 1138 | 1139 | // 从流式响应中提取完整内容 1140 | func extractFullContentFromStream(bodyBytes []byte, reqID string) (string, string, error) { 1141 | bodyStr := string(bodyBytes) 1142 | lines := strings.Split(bodyStr, "\n") 1143 | 1144 | // 内容累积器 1145 | var contentBuilder strings.Builder 1146 | var reasoningBuilder strings.Builder 1147 | 1148 | // 解析每一行 1149 | for _, line := range lines { 1150 | if strings.HasPrefix(line, "data: ") && !strings.Contains(line, "[DONE]") { 1151 | jsonStr := strings.TrimPrefix(line, "data: ") 1152 | jsonStr = strings.TrimSpace(jsonStr) 1153 | 1154 | // 解析JSON 1155 | var hunyuanResp HunyuanResponse 1156 | if err := json.Unmarshal([]byte(jsonStr), &hunyuanResp); err != nil { 1157 | continue // 跳过无效JSON 1158 | } 1159 | 1160 | // 提取内容和推理内容 1161 | for _, choice := range hunyuanResp.Choices { 1162 | if choice.Delta.Content != "" { 1163 | contentBuilder.WriteString(choice.Delta.Content) 1164 | } 1165 | if choice.Delta.ReasoningContent != "" { 1166 | reasoningBuilder.WriteString(choice.Delta.ReasoningContent) 1167 | } 1168 | } 1169 | } 1170 | } 1171 | 1172 | return contentBuilder.String(), reasoningBuilder.String(), nil 1173 | } 1174 | 1175 | // 创建角色块 1176 | func createRoleChunk(id string, created int64, model string) []byte { 1177 | chunk := StreamChunk{ 1178 | ID: id, 1179 | Object: "chat.completion.chunk", 1180 | Created: created, 1181 | Model: model, 1182 | Choices: []struct { 1183 | Index int `json:"index"` 1184 | FinishReason *string `json:"finish_reason,omitempty"` 1185 | Delta struct { 1186 | Role string `json:"role,omitempty"` 1187 | Content string `json:"content,omitempty"` 1188 | ReasoningContent string `json:"reasoning_content,omitempty"` 1189 | } `json:"delta"` 1190 | }{ 1191 | { 1192 | Index: 0, 1193 | Delta: struct { 1194 | Role string `json:"role,omitempty"` 1195 | Content string `json:"content,omitempty"` 1196 | ReasoningContent string `json:"reasoning_content,omitempty"` 1197 | }{ 1198 | Role: "assistant", 1199 | }, 1200 | }, 1201 | }, 1202 | } 1203 | 1204 | data, _ := json.Marshal(chunk) 1205 | return data 1206 | } 1207 | 1208 | // 创建内容块 1209 | func createContentChunk(id string, created int64, model string, content string) []byte { 1210 | chunk := StreamChunk{ 1211 | ID: id, 1212 | Object: "chat.completion.chunk", 1213 | Created: created, 1214 | Model: model, 1215 | Choices: []struct { 1216 | Index int `json:"index"` 1217 | FinishReason *string `json:"finish_reason,omitempty"` 1218 | Delta struct { 1219 | Role string `json:"role,omitempty"` 1220 | Content string `json:"content,omitempty"` 1221 | ReasoningContent string `json:"reasoning_content,omitempty"` 1222 | } `json:"delta"` 1223 | }{ 1224 | { 1225 | Index: 0, 1226 | Delta: struct { 1227 | Role string `json:"role,omitempty"` 1228 | Content string `json:"content,omitempty"` 1229 | ReasoningContent string `json:"reasoning_content,omitempty"` 1230 | }{ 1231 | Content: content, 1232 | }, 1233 | }, 1234 | }, 1235 | } 1236 | 1237 | data, _ := json.Marshal(chunk) 1238 | return data 1239 | } 1240 | 1241 | // 创建推理内容块 1242 | func createReasoningChunk(id string, created int64, model string, reasoningContent string) []byte { 1243 | chunk := StreamChunk{ 1244 | ID: id, 1245 | Object: "chat.completion.chunk", 1246 | Created: created, 1247 | Model: model, 1248 | Choices: []struct { 1249 | Index int `json:"index"` 1250 | FinishReason *string `json:"finish_reason,omitempty"` 1251 | Delta struct { 1252 | Role string `json:"role,omitempty"` 1253 | Content string `json:"content,omitempty"` 1254 | ReasoningContent string `json:"reasoning_content,omitempty"` 1255 | } `json:"delta"` 1256 | }{ 1257 | { 1258 | Index: 0, 1259 | Delta: struct { 1260 | Role string `json:"role,omitempty"` 1261 | Content string `json:"content,omitempty"` 1262 | ReasoningContent string `json:"reasoning_content,omitempty"` 1263 | }{ 1264 | ReasoningContent: reasoningContent, 1265 | }, 1266 | }, 1267 | }, 1268 | } 1269 | 1270 | data, _ := json.Marshal(chunk) 1271 | return data 1272 | } 1273 | 1274 | // 创建完成块 1275 | func createDoneChunk(id string, created int64, model string, reason string) []byte { 1276 | finishReason := reason 1277 | chunk := StreamChunk{ 1278 | ID: id, 1279 | Object: "chat.completion.chunk", 1280 | Created: created, 1281 | Model: model, 1282 | Choices: []struct { 1283 | Index int `json:"index"` 1284 | FinishReason *string `json:"finish_reason,omitempty"` 1285 | Delta struct { 1286 | Role string `json:"role,omitempty"` 1287 | Content string `json:"content,omitempty"` 1288 | ReasoningContent string `json:"reasoning_content,omitempty"` 1289 | } `json:"delta"` 1290 | }{ 1291 | { 1292 | Index: 0, 1293 | FinishReason: &finishReason, 1294 | Delta: struct { 1295 | Role string `json:"role,omitempty"` 1296 | Content string `json:"content,omitempty"` 1297 | ReasoningContent string `json:"reasoning_content,omitempty"` 1298 | }{}, 1299 | }, 1300 | }, 1301 | } 1302 | 1303 | data, _ := json.Marshal(chunk) 1304 | return data 1305 | } 1306 | 1307 | // 设置常见的请求头 - 参考Python版本 1308 | func setCommonHeaders(req *http.Request) { 1309 | req.Header.Set("accept", "*/*") 1310 | req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7") 1311 | req.Header.Set("authorization", "Bearer "+appConfig.BearerToken) 1312 | req.Header.Set("dnt", "1") 1313 | req.Header.Set("origin", "https://llm.hunyuan.tencent.com") 1314 | req.Header.Set("polaris", "stream-server-online-sbs-10697") 1315 | req.Header.Set("priority", "u=1, i") 1316 | req.Header.Set("referer", "https://llm.hunyuan.tencent.com/") 1317 | req.Header.Set("sec-ch-ua", "\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"Google Chrome\";v=\"134\"") 1318 | req.Header.Set("sec-ch-ua-mobile", "?0") 1319 | req.Header.Set("sec-ch-ua-platform", "\"Windows\"") 1320 | req.Header.Set("sec-fetch-dest", "empty") 1321 | req.Header.Set("sec-fetch-mode", "cors") 1322 | req.Header.Set("sec-fetch-site", "same-origin") 1323 | req.Header.Set("staffname", "staryxzhang") 1324 | req.Header.Set("wsid", "10697") 1325 | req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36") 1326 | } 1327 | 1328 | // 生成请求ID 1329 | func generateRequestID() string { 1330 | return fmt.Sprintf("%x", time.Now().UnixNano()) 1331 | } 1332 | 1333 | // 生成随机字符串 1334 | func getRandomString(length int) string { 1335 | const charset = "abcdefghijklmnopqrstuvwxyz0123456789" 1336 | b := make([]byte, length) 1337 | for i := range b { 1338 | b[i] = charset[time.Now().UnixNano()%int64(len(charset))] 1339 | time.Sleep(1 * time.Nanosecond) 1340 | } 1341 | return string(b) 1342 | } 1343 | 1344 | // 格式化消息为字符串 1345 | func formatMessages(messages []APIMessage) string { 1346 | var result strings.Builder 1347 | for _, msg := range messages { 1348 | result.WriteString(msg.Role) 1349 | result.WriteString(": ") 1350 | result.WriteString(contentToString(msg.Content)) 1351 | result.WriteString("\n") 1352 | } 1353 | return result.String() 1354 | } 1355 | 1356 | // 获取两个整数中的最小值 1357 | func min(a, b int) int { 1358 | if a < b { 1359 | return a 1360 | } 1361 | return b 1362 | } 1363 | 1364 | // 获取两个整数中的最大值 1365 | func max(a, b int64) int64 { 1366 | if a > b { 1367 | return a 1368 | } 1369 | return b 1370 | } -------------------------------------------------------------------------------- /qwen2api-cf.js: -------------------------------------------------------------------------------- 1 | // 通义千问 OpenAI 兼容代理 - 完整版 2 | // 包括 /v1/models、/v1/chat/completions(流式和非流)、/v1/images/generations 以及图片上传功能 3 | // 把https://chat.qwen.ai/的Cookie中的token字段值作为APIKEY传入使用openai兼容性标准接口使用即可 4 | 5 | export default { 6 | // 内置模型列表(当获取接口失败时使用) 7 | defaultModels: [ 8 | "qwen-max-latest", 9 | "qwen-plus-latest", 10 | "qwen2.5-vl-72b-instruct", 11 | "qwen2.5-14b-instruct-1m", 12 | "qvq-72b-preview", 13 | "qwq-32b-preview", 14 | "qwen2.5-coder-32b-instruct", 15 | "qwen-turbo-latest", 16 | "qwen2.5-72b-instruct" 17 | ], 18 | 19 | // 主入口:根据 URL 路径分发请求 20 | async fetch(request, env, ctx) { 21 | const url = new URL(request.url); 22 | const path = url.pathname; 23 | const apiPrefix = env.API_PREFIX || ''; 24 | 25 | if (path === `${apiPrefix}/v1/models`) { 26 | return this.handleModels(request); 27 | } else if (path === `${apiPrefix}/v1/chat/completions`) { 28 | return this.handleChatCompletions(request); 29 | } else if (path === `${apiPrefix}/v1/images/generations`) { 30 | return this.handleImageGenerations(request); 31 | } 32 | 33 | return new Response("Not Found", { status: 404 }); 34 | }, 35 | 36 | // 从请求中提取 Authorization token 37 | getAuthToken(request) { 38 | const authHeader = request.headers.get('authorization'); 39 | if (!authHeader) return null; 40 | return authHeader.replace('Bearer ', ''); 41 | }, 42 | 43 | // 处理模型列表接口 44 | async handleModels(request) { 45 | const authToken = this.getAuthToken(request); 46 | let modelsList = []; 47 | 48 | if (authToken) { 49 | try { 50 | const response = await fetch('https://chat.qwen.ai/api/models', { 51 | headers: { 52 | 'Authorization': `Bearer ${authToken}`, 53 | 'User-Agent': 'Mozilla/5.0' 54 | } 55 | }); 56 | if (response.ok) { 57 | const data = await response.json(); 58 | modelsList = data.data.map(item => item.id); 59 | } else { 60 | modelsList = [...this.defaultModels]; 61 | } 62 | } catch (e) { 63 | console.error('获取模型列表失败:', e); 64 | modelsList = [...this.defaultModels]; 65 | } 66 | } else { 67 | modelsList = [...this.defaultModels]; 68 | } 69 | 70 | // 扩展模型列表,增加变种后缀 71 | const expandedModels = []; 72 | for (const model of modelsList) { 73 | expandedModels.push(model); 74 | expandedModels.push(model + '-thinking'); 75 | expandedModels.push(model + '-search'); 76 | expandedModels.push(model + '-thinking-search'); 77 | expandedModels.push(model + '-draw'); 78 | } 79 | 80 | return new Response(JSON.stringify({ 81 | object: "list", 82 | data: expandedModels.map(id => ({ 83 | id, 84 | object: "model", 85 | created: Date.now(), 86 | owned_by: "qwen" 87 | })) 88 | }), { headers: { 'Content-Type': 'application/json' } }); 89 | }, 90 | 91 | // 处理 /v1/chat/completions 接口 92 | async handleChatCompletions(request) { 93 | const authToken = this.getAuthToken(request); 94 | if (!authToken) { 95 | return new Response(JSON.stringify({ 96 | error: "请提供正确的 Authorization token" 97 | }), { status: 401, headers: { 'Content-Type': 'application/json' } }); 98 | } 99 | 100 | let body; 101 | try { 102 | body = await request.json(); 103 | } catch (error) { 104 | return new Response(JSON.stringify({ 105 | error: "无效的请求体,请提供有效的JSON" 106 | }), { status: 400, headers: { 'Content-Type': 'application/json' } }); 107 | } 108 | 109 | const stream = !!body.stream; 110 | const messages = body.messages || []; 111 | const requestId = crypto.randomUUID(); 112 | 113 | if (!Array.isArray(messages) || messages.length === 0) { 114 | return new Response(JSON.stringify({ 115 | error: "请提供有效的 messages 数组" 116 | }), { status: 400, headers: { 'Content-Type': 'application/json' } }); 117 | } 118 | 119 | let modelName = body.model || "qwen-turbo-latest"; 120 | let chatType = "t2t"; 121 | 122 | // 如果模型名包含 -draw,则走图像生成流程 123 | if (modelName.includes('-draw')) { 124 | return this.handleDrawRequest(messages, modelName, authToken); 125 | } 126 | 127 | // 如果是 -thinking 模式,则设置思考配置 128 | if (modelName.includes('-thinking')) { 129 | modelName = modelName.replace('-thinking', ''); 130 | if (messages[messages.length - 1]) { 131 | messages[messages.length - 1].feature_config = { thinking_enabled: true }; 132 | } 133 | } 134 | 135 | // 如果是 -search 模式,则修改 chat_type 136 | if (modelName.includes('-search')) { 137 | modelName = modelName.replace('-search', ''); 138 | chatType = "search"; 139 | if (messages[messages.length - 1]) { 140 | messages[messages.length - 1].chat_type = "search"; 141 | } 142 | } 143 | 144 | const requestBody = { 145 | model: modelName, 146 | messages, 147 | stream, 148 | chat_type: chatType, 149 | id: requestId 150 | }; 151 | 152 | // 处理图片消息(例如上传图片): 153 | const lastMessage = messages[messages.length - 1]; 154 | if (Array.isArray(lastMessage?.content)) { 155 | const imageItem = lastMessage.content.find(item => 156 | item.image_url && item.image_url.url 157 | ); 158 | if (imageItem) { 159 | const imageId = await this.uploadImage(imageItem.image_url.url, authToken); 160 | if (imageId) { 161 | const index = lastMessage.content.findIndex(item => 162 | item.image_url && item.image_url.url 163 | ); 164 | if (index >= 0) { 165 | lastMessage.content[index] = { 166 | type: "image", 167 | image: imageId 168 | }; 169 | } 170 | } 171 | } 172 | } 173 | 174 | try { 175 | const response = await fetch('https://chat.qwen.ai/api/chat/completions', { 176 | method: 'POST', 177 | headers: { 178 | 'Authorization': `Bearer ${authToken}`, 179 | 'Content-Type': 'application/json', 180 | 'User-Agent': 'Mozilla/5.0' 181 | }, 182 | body: JSON.stringify(requestBody) 183 | }); 184 | 185 | if (!response.ok) { 186 | const errText = await response.text(); 187 | console.error('Qwen 接口调用失败:', response.status, errText); 188 | return new Response(JSON.stringify({ 189 | error: `请求通义千问API失败: ${response.status}`, 190 | details: errText 191 | }), { status: response.status, headers: { 'Content-Type': 'application/json' } }); 192 | } 193 | 194 | if (stream) { 195 | return this.handleStreamResponse(response, requestId, modelName); 196 | } else { 197 | return this.handleNormalResponse(response, requestId, modelName); 198 | } 199 | } catch (e) { 200 | console.error('请求失败:', e); 201 | return new Response(JSON.stringify({ 202 | error: "请求通义千问API失败,请检查 token 是否正确" 203 | }), { status: 500, headers: { 'Content-Type': 'application/json' } }); 204 | } 205 | }, 206 | 207 | // ---------------------- 流式响应处理(改进) ---------------------- 208 | async handleStreamResponse(fetchResponse, requestId, modelName) { 209 | const { readable, writable } = new TransformStream(); 210 | const writer = writable.getWriter(); 211 | const encoder = new TextEncoder(); 212 | 213 | // 辅助函数:将 payload 包装为 SSE 格式后写入,并编码成字节 214 | const sendSSE = async (payload) => { 215 | await writer.write(encoder.encode(`data: ${payload}\n\n`)); 216 | }; 217 | 218 | // 用于去重和累积内容 219 | let previousDelta = ""; 220 | let cumulativeContent = ""; // 累积完整内容,解决断流问题 221 | 222 | const processStream = async () => { 223 | try { 224 | const reader = fetchResponse.body.getReader(); 225 | const decoder = new TextDecoder('utf-8'); 226 | let buffer = ''; 227 | let isFirstChunk = true; 228 | 229 | while (true) { 230 | const { done, value } = await reader.read(); 231 | if (done) { 232 | // 确保最后一个缓冲区也被处理 233 | if (buffer.trim()) { 234 | await processBuffer(buffer); 235 | } 236 | break; 237 | } 238 | 239 | const chunkStr = decoder.decode(value, { stream: true }); 240 | buffer += chunkStr; 241 | 242 | // 更可靠的处理方式:按照 SSE 规范处理双换行符分隔的消息 243 | await processBuffer(buffer); 244 | 245 | // 仅保留可能不完整的最后一部分 246 | const lastBoundaryIndex = buffer.lastIndexOf('\n\n'); 247 | if (lastBoundaryIndex !== -1) { 248 | buffer = buffer.substring(lastBoundaryIndex + 2); 249 | } 250 | } 251 | 252 | // 确保发送最终 DONE 信号 253 | console.log(`流处理完成,累积内容长度: ${cumulativeContent.length}`); 254 | await sendSSE('[DONE]'); 255 | } catch (err) { 256 | console.error('处理 SSE 流时出错:', err); 257 | const errorChunk = { 258 | id: `chatcmpl-${requestId}`, 259 | object: 'chat.completion.chunk', 260 | created: Date.now(), 261 | model: modelName, 262 | choices: [ 263 | { 264 | index: 0, 265 | delta: { content: '【流式处理出错,请重试】' }, 266 | finish_reason: 'error' 267 | } 268 | ] 269 | }; 270 | try { 271 | await sendSSE(JSON.stringify(errorChunk)); 272 | await sendSSE('[DONE]'); 273 | } catch (_) {} 274 | } finally { 275 | await writer.close(); 276 | } 277 | }; 278 | 279 | // 处理缓冲区内的完整 SSE 消息 280 | const processBuffer = async (buffer) => { 281 | // 按 data: 行分割 282 | const dataLineRegex = /^data: (.+)$/gm; 283 | let match; 284 | 285 | while ((match = dataLineRegex.exec(buffer)) !== null) { 286 | const dataStr = match[1].trim(); 287 | 288 | if (dataStr === '[DONE]') { 289 | await sendSSE('[DONE]'); 290 | console.log('收到 [DONE],流结束'); 291 | continue; 292 | } 293 | 294 | try { 295 | const jsonData = JSON.parse(dataStr); 296 | const delta = jsonData?.choices?.[0]?.delta; 297 | if (!delta) continue; 298 | 299 | let currentDelta = delta.content || ""; 300 | 301 | // 改进的去重逻辑:如果有完整内容,检查是否为前缀 302 | if (currentDelta) { 303 | let newContent = currentDelta; 304 | let needsSending = true; 305 | 306 | if (previousDelta && currentDelta.startsWith(previousDelta)) { 307 | // 只提取新增部分 308 | newContent = currentDelta.substring(previousDelta.length); 309 | // 如果没有新增内容,跳过发送 310 | if (!newContent) needsSending = false; 311 | } 312 | 313 | if (needsSending) { 314 | // 创建并发送内容块 315 | const openaiChunk = { 316 | id: `chatcmpl-${requestId}`, 317 | object: 'chat.completion.chunk', 318 | created: Date.now(), 319 | model: modelName, 320 | choices: [ 321 | { 322 | index: 0, 323 | delta: isFirstChunk 324 | ? { role: 'assistant', content: newContent } 325 | : { content: newContent }, 326 | finish_reason: null 327 | } 328 | ] 329 | }; 330 | 331 | if (isFirstChunk) isFirstChunk = false; 332 | await sendSSE(JSON.stringify(openaiChunk)); 333 | 334 | // 累积内容 335 | cumulativeContent += newContent; 336 | } 337 | 338 | // 更新之前的内容为当前完整内容 339 | previousDelta = currentDelta; 340 | } 341 | 342 | // 处理完成标志 343 | if (jsonData?.choices?.[0]?.finish_reason) { 344 | const finishChunk = { 345 | id: `chatcmpl-${requestId}`, 346 | object: 'chat.completion.chunk', 347 | created: Date.now(), 348 | model: modelName, 349 | choices: [ 350 | { 351 | index: 0, 352 | delta: {}, 353 | finish_reason: jsonData.choices[0].finish_reason 354 | } 355 | ] 356 | }; 357 | await sendSSE(JSON.stringify(finishChunk)); 358 | } 359 | } catch (err) { 360 | console.error('解析 SSE JSON 失败:', dataStr, err); 361 | } 362 | } 363 | }; 364 | 365 | processStream(); 366 | return new Response(readable, { 367 | headers: { 368 | 'Content-Type': 'text/event-stream', 369 | 'Connection': 'keep-alive', 370 | 'Cache-Control': 'no-cache', 371 | 'X-Accel-Buffering': 'no' 372 | } 373 | }); 374 | }, 375 | 376 | // ---------------------- 普通(非流)响应 ---------------------- 377 | async handleNormalResponse(fetchResponse, requestId, modelName) { 378 | try { 379 | const data = await fetchResponse.json(); 380 | const content = data?.choices?.[0]?.message?.content || ''; 381 | const finishReason = data?.choices?.[0]?.finish_reason || 'stop'; 382 | 383 | return new Response(JSON.stringify({ 384 | id: `chatcmpl-${requestId}`, 385 | object: 'chat.completion', 386 | created: Date.now(), 387 | model: modelName, 388 | choices: [ 389 | { 390 | index: 0, 391 | message: { 392 | role: 'assistant', 393 | content 394 | }, 395 | finish_reason: finishReason 396 | } 397 | ], 398 | usage: data?.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } 399 | }), { headers: { 'Content-Type': 'application/json' } }); 400 | } catch (e) { 401 | console.error('解析普通响应失败:', e); 402 | return new Response(JSON.stringify({ 403 | error: "解析 Qwen 响应出错" 404 | }), { status: 500, headers: { 'Content-Type': 'application/json' } }); 405 | } 406 | }, 407 | 408 | // ---------------------- 图像生成请求(handleDrawRequest) ---------------------- 409 | async handleDrawRequest(messages, model, authToken) { 410 | const prompt = messages[messages.length - 1].content; 411 | const size = '1024*1024'; 412 | const pureModelName = model.replace('-draw', '').replace('-thinking', '').replace('-search', ''); 413 | 414 | try { 415 | // 创建图像生成任务 416 | const createResponse = await fetch('https://chat.qwen.ai/api/chat/completions', { 417 | method: 'POST', 418 | headers: { 419 | 'Authorization': `Bearer ${authToken}`, 420 | 'Content-Type': 'application/json', 421 | 'User-Agent': 'Mozilla/5.0' 422 | }, 423 | body: JSON.stringify({ 424 | stream: false, 425 | incremental_output: true, 426 | chat_type: "t2i", 427 | model: pureModelName, 428 | messages: [ 429 | { 430 | role: "user", 431 | content: prompt, 432 | chat_type: "t2i", 433 | extra: {}, 434 | feature_config: { thinking_enabled: false } 435 | } 436 | ], 437 | id: crypto.randomUUID(), 438 | size: size 439 | }) 440 | }); 441 | 442 | if (!createResponse.ok) { 443 | const errorText = await createResponse.text(); 444 | return new Response(JSON.stringify({ 445 | error: "图像生成任务创建失败", 446 | details: errorText 447 | }), { 448 | status: 500, 449 | headers: { 'Content-Type': 'application/json' } 450 | }); 451 | } 452 | 453 | const createData = await createResponse.json(); 454 | let taskId = null; 455 | 456 | // 查找任务ID 457 | for (const msg of createData.messages) { 458 | if (msg.role === 'assistant' && msg.extra?.wanx?.task_id) { 459 | taskId = msg.extra.wanx.task_id; 460 | break; 461 | } 462 | } 463 | 464 | if (!taskId) { 465 | return new Response(JSON.stringify({ 466 | error: "无法获取图像生成任务ID" 467 | }), { 468 | status: 500, 469 | headers: { 'Content-Type': 'application/json' } 470 | }); 471 | } 472 | 473 | // 轮询等待图像生成完成(最多 30 次,每次间隔6秒) 474 | let imageUrl = null; 475 | for (let i = 0; i < 30; i++) { 476 | try { 477 | const statusResponse = await fetch(`https://chat.qwen.ai/api/v1/tasks/status/${taskId}`, { 478 | headers: { 479 | 'Authorization': `Bearer ${authToken}`, 480 | 'User-Agent': 'Mozilla/5.0' 481 | } 482 | }); 483 | if (statusResponse.ok) { 484 | const statusData = await statusResponse.json(); 485 | if (statusData.content) { 486 | imageUrl = statusData.content; 487 | break; 488 | } 489 | } 490 | } catch (error) { 491 | // 忽略单次错误 492 | } 493 | await new Promise(resolve => setTimeout(resolve, 6000)); 494 | } 495 | 496 | if (!imageUrl) { 497 | return new Response(JSON.stringify({ 498 | error: "图像生成超时" 499 | }), { 500 | status: 500, 501 | headers: { 'Content-Type': 'application/json' } 502 | }); 503 | } 504 | 505 | // 返回 OpenAI 标准格式的响应(使用 Markdown 格式嵌入图片) 506 | return new Response(JSON.stringify({ 507 | id: `chatcmpl-${crypto.randomUUID()}`, 508 | object: "chat.completion", 509 | created: Date.now(), 510 | model: model, 511 | choices: [ 512 | { 513 | index: 0, 514 | message: { 515 | role: "assistant", 516 | content: `![${imageUrl}](${imageUrl})` 517 | }, 518 | finish_reason: "stop" 519 | } 520 | ], 521 | usage: { 522 | prompt_tokens: 1024, 523 | completion_tokens: 1024, 524 | total_tokens: 2048 525 | } 526 | }), { 527 | headers: { 'Content-Type': 'application/json' } 528 | }); 529 | } catch (error) { 530 | console.error('图像生成失败:', error); 531 | return new Response(JSON.stringify({ 532 | error: "图像生成请求失败" 533 | }), { 534 | status: 500, 535 | headers: { 'Content-Type': 'application/json' } 536 | }); 537 | } 538 | }, 539 | 540 | // ---------------------- 图像生成接口(/v1/images/generations) ---------------------- 541 | async handleImageGenerations(request) { 542 | const authToken = this.getAuthToken(request); 543 | if (!authToken) { 544 | return new Response(JSON.stringify({ 545 | error: "请提供正确的 Authorization token" 546 | }), { 547 | status: 401, 548 | headers: { 'Content-Type': 'application/json' } 549 | }); 550 | } 551 | 552 | let body; 553 | try { 554 | body = await request.json(); 555 | } catch (error) { 556 | return new Response(JSON.stringify({ 557 | error: "无效的请求体,请提供有效的JSON" 558 | }), { 559 | status: 400, 560 | headers: { 'Content-Type': 'application/json' } 561 | }); 562 | } 563 | 564 | const { model = "qwen-max-latest-draw", prompt, n = 1, size = '1024*1024' } = body; 565 | const pureModelName = model.replace('-draw', '').replace('-thinking', '').replace('-search', ''); 566 | 567 | try { 568 | // 创建图像生成任务(非流式,incremental_output: true) 569 | const createResponse = await fetch('https://chat.qwen.ai/api/chat/completions', { 570 | method: 'POST', 571 | headers: { 572 | 'Authorization': `Bearer ${authToken}`, 573 | 'Content-Type': 'application/json', 574 | 'User-Agent': 'Mozilla/5.0' 575 | }, 576 | body: JSON.stringify({ 577 | stream: false, 578 | incremental_output: true, 579 | chat_type: "t2i", 580 | model: pureModelName, 581 | messages: [ 582 | { 583 | role: "user", 584 | content: prompt, 585 | chat_type: "t2i", 586 | extra: {}, 587 | feature_config: { thinking_enabled: false } 588 | } 589 | ], 590 | id: crypto.randomUUID(), 591 | size: size 592 | }) 593 | }); 594 | 595 | if (!createResponse.ok) { 596 | const errorText = await createResponse.text(); 597 | return new Response(JSON.stringify({ 598 | error: "图像生成任务创建失败", 599 | details: errorText 600 | }), { 601 | status: 500, 602 | headers: { 'Content-Type': 'application/json' } 603 | }); 604 | } 605 | 606 | const createData = await createResponse.json(); 607 | let taskId = null; 608 | for (const msg of createData.messages) { 609 | if (msg.role === 'assistant' && msg.extra?.wanx?.task_id) { 610 | taskId = msg.extra.wanx.task_id; 611 | break; 612 | } 613 | } 614 | if (!taskId) { 615 | return new Response(JSON.stringify({ 616 | error: "无法获取图像生成任务ID" 617 | }), { 618 | status: 500, 619 | headers: { 'Content-Type': 'application/json' } 620 | }); 621 | } 622 | 623 | let imageUrl = null; 624 | for (let i = 0; i < 30; i++) { 625 | try { 626 | const statusResponse = await fetch(`https://chat.qwen.ai/api/v1/tasks/status/${taskId}`, { 627 | headers: { 628 | 'Authorization': `Bearer ${authToken}`, 629 | 'User-Agent': 'Mozilla/5.0' 630 | } 631 | }); 632 | if (statusResponse.ok) { 633 | const statusData = await statusResponse.json(); 634 | if (statusData.content) { 635 | imageUrl = statusData.content; 636 | break; 637 | } 638 | } 639 | } catch (error) { 640 | // 忽略错误 641 | } 642 | await new Promise(resolve => setTimeout(resolve, 6000)); 643 | } 644 | 645 | if (!imageUrl) { 646 | return new Response(JSON.stringify({ 647 | error: "图像生成超时" 648 | }), { 649 | status: 500, 650 | headers: { 'Content-Type': 'application/json' } 651 | }); 652 | } 653 | 654 | // 构造 OpenAI 标准格式的响应数据(返回图片列表) 655 | const images = Array(n).fill().map(() => ({ url: imageUrl })); 656 | return new Response(JSON.stringify({ 657 | created: Date.now(), 658 | data: images 659 | }), { 660 | headers: { 'Content-Type': 'application/json' } 661 | }); 662 | } catch (error) { 663 | console.error('图像生成失败:', error); 664 | return new Response(JSON.stringify({ 665 | error: "图像生成请求失败" 666 | }), { 667 | status: 500, 668 | headers: { 'Content-Type': 'application/json' } 669 | }); 670 | } 671 | }, 672 | 673 | // ---------------------- 图片上传接口 ---------------------- 674 | async uploadImage(base64Data, authToken) { 675 | try { 676 | // 从 base64 数据中提取图片数据 677 | const base64Image = base64Data.split(';base64,').pop(); 678 | const imageData = atob(base64Image); 679 | const arrayBuffer = new ArrayBuffer(imageData.length); 680 | const uint8Array = new Uint8Array(arrayBuffer); 681 | for (let i = 0; i < imageData.length; i++) { 682 | uint8Array[i] = imageData.charCodeAt(i); 683 | } 684 | const formData = new FormData(); 685 | const blob = new Blob([uint8Array], { type: 'image/jpeg' }); 686 | formData.append('file', blob, `image-${Date.now()}.jpg`); 687 | 688 | const response = await fetch('https://chat.qwen.ai/api/v1/files/', { 689 | method: 'POST', 690 | headers: { 691 | 'Authorization': `Bearer ${authToken}`, 692 | 'User-Agent': 'Mozilla/5.0' 693 | }, 694 | body: formData 695 | }); 696 | 697 | if (response.ok) { 698 | const data = await response.json(); 699 | return data.id; 700 | } 701 | return null; 702 | } catch (error) { 703 | console.error('图片上传失败:', error); 704 | return null; 705 | } 706 | } 707 | }; 708 | -------------------------------------------------------------------------------- /qwen2api-cf.md: -------------------------------------------------------------------------------- 1 | # Qwen2API 2 | ## 项目简介 3 | 4 | Qwen2API,用于将通义千问(Qwen AI)的WEB转换为OpenAI兼容的API接口格式,让您可以通过标准的OpenAI API调用方式来使用通义千问模型。该代理支持包括模型列表查询、聊天补全(流式和非流式)、图像生成,以及图片上传功能,为开发者提供了便捷的集成方式。 5 | 6 | ## 特性 7 | 8 | - **OpenAI API兼容**: 提供与OpenAI API格式兼容的接口,方便现有OpenAI项目迁移 9 | - **模型支持**: 支持通义千问的各类模型,包括qwen-max、qwen-plus等 10 | - **模型变体**: 自动扩展模型名称,支持以下后缀功能: 11 | - `-thinking`: 启用思考模式 12 | - `-search`: 启用搜索增强 13 | - `-draw`: 启用图像生成 【可能存在问题】 14 | - 以上后缀可组合使用,如`qwen-max-latest-thinking-search` 15 | - **流式输出**: 支持流式响应,减少首字等待时间 16 | - **多模态交互**: 支持图片上传和图像生成【可能存在问题】 17 | - **图像生成**: 提供专用的图像生成接口【可能存在问题】 18 | 19 | ## 部署要求 20 | 21 | - CloudFlare账号 22 | - CloudFlare Workers服务 23 | 24 | ## 安装部署 25 | 26 | 1. 登录CloudFlare Workers控制台 27 | 2. 创建新的Worker 28 | 3. 将[qwen2api-cf.js](qwen2api-cf.js)代码复制到Worker编辑器中 29 | 4. 保存并部署 30 | 31 | ## 配置选项 32 | 33 | 您可以通过环境变量配置以下选项: 34 | 35 | | 变量名 | 描述 | 默认值 | 36 | |-------|------|--------| 37 | | API_PREFIX | API路径前缀,可用于自定义路由 | 空字符串 | 38 | 39 | ## 使用方法 40 | 41 | ### 认证 42 | 43 | 使用通义千问的Token作为API密钥,在请求头中设置`Authorization: Bearer {YOUR_QWEN_TOKEN}`。 44 | 45 | **获取Token方法**: 46 | 1. 访问[通义千问官网](https://chat.qwen.ai/) 47 | 2. 登录您的账号 48 | 3. 从Cookie中提取`token`字段的值 49 | 50 | ### 支持的API端点 51 | 52 | #### 1. 获取模型列表 53 | 54 | ``` 55 | GET /v1/models 56 | ``` 57 | 58 | **响应示例**: 59 | ```json 60 | { 61 | "object": "list", 62 | "data": [ 63 | { 64 | "id": "qwen-max-latest", 65 | "object": "model", 66 | "created": 1709128113453, 67 | "owned_by": "qwen" 68 | }, 69 | { 70 | "id": "qwen-max-latest-thinking", 71 | "object": "model", 72 | "created": 1709128113453, 73 | "owned_by": "qwen" 74 | }, 75 | // 更多模型... 76 | ] 77 | } 78 | ``` 79 | 80 | #### 2. 聊天补全 81 | 82 | ``` 83 | POST /v1/chat/completions 84 | ``` 85 | 86 | **请求体示例**: 87 | ```json 88 | { 89 | "model": "qwen-max-latest", 90 | "messages": [ 91 | { 92 | "role": "user", 93 | "content": "你好,请介绍一下自己" 94 | } 95 | ], 96 | "stream": false 97 | } 98 | ``` 99 | 100 | **特殊功能**: 101 | - 使用`-thinking`后缀启用思考模式 102 | - 使用`-search`后缀启用搜索增强 103 | - 同时传递图片(多模态) 104 | 105 | **多模态示例**: 106 | ```json 107 | { 108 | "model": "qwen2.5-vl-72b-instruct", 109 | "messages": [ 110 | { 111 | "role": "user", 112 | "content": [ 113 | { 114 | "type": "text", 115 | "text": "这张图片是什么?" 116 | }, 117 | { 118 | "type": "image_url", 119 | "image_url": { 120 | "url": "data:image/jpeg;base64,/9j/4AAQ..." 121 | } 122 | } 123 | ] 124 | } 125 | ] 126 | } 127 | ``` 128 | 129 | #### 3. 图像生成 130 | 131 | **方法1**: 使用聊天接口 132 | 133 | ``` 134 | POST /v1/chat/completions 135 | ``` 136 | 137 | ```json 138 | { 139 | "model": "qwen-max-latest-draw", 140 | "messages": [ 141 | { 142 | "role": "user", 143 | "content": "画一只可爱的猫咪" 144 | } 145 | ] 146 | } 147 | ``` 148 | 149 | **方法2**: 使用专用图像生成接口 150 | 151 | ``` 152 | POST /v1/images/generations 153 | ``` 154 | 155 | ```json 156 | { 157 | "model": "qwen-max-latest-draw", 158 | "prompt": "画一只可爱的猫咪", 159 | "n": 1, 160 | "size": "1024*1024" 161 | } 162 | ``` 163 | 164 | ## 支持的模型 165 | 166 | 系统内置了以下默认模型,当API获取失败时会使用这些模型: 167 | 168 | - qwen-max-latest 169 | - qwen-plus-latest 170 | - qwen2.5-vl-72b-instruct 171 | - qwen2.5-14b-instruct-1m 172 | - qvq-72b-preview 173 | - qwq-32b-preview 174 | - qwen2.5-coder-32b-instruct 175 | - qwen-turbo-latest 176 | - qwen2.5-72b-instruct 177 | 178 | 每个模型都支持添加后缀变体(-thinking、-search、-draw)。 179 | 180 | ## 技术实现细节 181 | 182 | ### 架构概述 183 | 184 | 该代理作为中间层,将OpenAI格式的请求转换为通义千问API格式,并将通义千问的响应转换回OpenAI格式。主要处理流程包括: 185 | 186 | 1. 解析请求和提取Token 187 | 2. 根据URL路径分发到不同处理函数 188 | 3. 转换请求格式并调用通义千问API 189 | 4. 处理响应并转换格式 190 | 5. 特殊处理流式响应和图像生成 191 | 192 | ### 模型处理机制 193 | 194 | - 基础模型名称处理:从请求中提取模型名称 195 | - 后缀功能处理:解析后缀并应用相应的配置 196 | - `-thinking`: 设置`feature_config.thinking_enabled = true` 197 | - `-search`: 设置`chat_type = "search"` 198 | - `-draw`: 切换到图像生成流程 199 | 200 | ### 流式响应处理 201 | 202 | 代理实现了高效的流式响应处理机制: 203 | 1. 使用TransformStream处理数据流 204 | 2. 对数据进行SSE(Server-Sent Events)格式转换 205 | 3. 实现增量去重逻辑,确保内容不重复 206 | 4. 处理完成标记和结束流 207 | 208 | ### 图像生成实现 209 | 210 | 图像生成使用任务创建和状态轮询机制: 211 | 1. 创建图像生成任务并获取taskId 212 | 2. 定期轮询任务状态(最多30次,每6秒一次) 213 | 3. 获取生成的图像URL并返回 214 | 215 | ## 常见问题与解决方案 216 | 217 | ### Token无效或过期 218 | 219 | **症状**: 请求返回401错误 220 | **解决方案**: 重新获取通义千问Cookie中的token值 221 | 222 | ### 模型列表获取失败 223 | 224 | **症状**: 仅显示默认模型列表 225 | **解决方案**: 检查网络连接和token有效性 226 | 227 | ### 图像生成超时 228 | 229 | **症状**: 返回"图像生成超时"错误 230 | **解决方案**: 231 | - 检查网络连接 232 | - 尝试简化图像描述 233 | - 尝试减小图像尺寸 234 | 235 | ### 流式响应中断 236 | 237 | **症状**: 响应突然停止 238 | **解决方案**: 239 | - 检查网络稳定性 240 | - 减少请求的复杂度 241 | 242 | --------------------------------------------------------------------------------