ohmyapi commited on
Commit
b163b3f
·
1 Parent(s): d1606b0

Switch to kiro-rs (Rust) - build from source

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +0 -21
  2. Dockerfile +22 -16
  3. LICENSE +0 -674
  4. README.md +3 -8
  5. VERSION +0 -1
  6. configs/api-potluck-data.json.example +0 -29
  7. configs/api-potluck-keys.json.example +0 -16
  8. configs/config.json +0 -30
  9. configs/config.json.example +0 -63
  10. configs/kiro/chatgpt_account1_kiro-auth-token/chatgpt_account1_kiro-auth-token.json +0 -9
  11. configs/kiro/chatgpt_account2_kiro-auth-token/chatgpt_account2_kiro-auth-token.json +0 -9
  12. configs/kiro/fresh1_kiro-auth-token/fresh1_kiro-auth-token.json +0 -9
  13. configs/kiro/personal_kiro-auth-token/personal_kiro-auth-token.json +0 -7
  14. configs/plugins.json +0 -6
  15. configs/plugins.json.example +0 -12
  16. configs/provider_pools.json +0 -43
  17. configs/provider_pools.json.example +0 -213
  18. configs/pwd +0 -1
  19. entrypoint.sh +28 -0
  20. healthcheck.js +0 -46
  21. package-lock.json +0 -0
  22. package.json +0 -42
  23. src/auth/codex-oauth.js +0 -1056
  24. src/auth/gemini-oauth.js +0 -504
  25. src/auth/iflow-oauth.js +0 -539
  26. src/auth/index.js +0 -35
  27. src/auth/kiro-oauth.js +0 -1149
  28. src/auth/oauth-handlers.js +0 -27
  29. src/auth/qwen-oauth.js +0 -343
  30. src/convert/convert-old.js +0 -0
  31. src/convert/convert.js +0 -392
  32. src/converters/BaseConverter.js +0 -115
  33. src/converters/ConverterFactory.js +0 -183
  34. src/converters/register-converters.js +0 -29
  35. src/converters/strategies/ClaudeConverter.js +0 -2234
  36. src/converters/strategies/CodexConverter.js +0 -1327
  37. src/converters/strategies/GeminiConverter.js +0 -1529
  38. src/converters/strategies/GrokConverter.js +0 -1153
  39. src/converters/strategies/OpenAIConverter.js +0 -1769
  40. src/converters/strategies/OpenAIResponsesConverter.js +0 -1032
  41. src/converters/utils.js +0 -369
  42. src/core/config-manager.js +0 -249
  43. src/core/master.js +0 -395
  44. src/core/plugin-manager.js +0 -549
  45. src/handlers/request-handler.js +0 -271
  46. src/plugins/ai-monitor/index.js +0 -145
  47. src/plugins/api-potluck/api-routes.js +0 -452
  48. src/plugins/api-potluck/index.js +0 -208
  49. src/plugins/api-potluck/key-manager.js +0 -486
  50. src/plugins/api-potluck/middleware.js +0 -166
.dockerignore DELETED
@@ -1,21 +0,0 @@
1
- node_modules
2
- npm-debug.log
3
- .git
4
- .gitignore
5
- Dockerfile
6
- .dockerignore
7
- .nyc_output
8
- coverage
9
- .nyc_output
10
- .coverage
11
- .env
12
- .env.local
13
- .env.*.local
14
- credentials.json
15
- *.md
16
- LICENSE
17
- jest.config.js
18
- babel.config.js
19
- tests/
20
- run-tests.js
21
- test-summary.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,21 +1,27 @@
1
- FROM node:20-alpine
2
-
3
- RUN apk add --no-cache tar git procps
4
-
 
 
 
 
 
 
 
 
5
  WORKDIR /app
 
6
 
7
- COPY package*.json ./
8
- RUN npm install --omit=dev
9
-
10
- COPY . .
11
-
12
- # Create directories
13
- RUN mkdir -p /app/logs /app/configs
14
 
15
- # HuggingFace Spaces requires port 7860
16
  EXPOSE 7860
 
17
 
18
- HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
19
- CMD node healthcheck.js || exit 1
20
-
21
- CMD ["node", "src/core/master.js", "--port", "7860"]
 
1
+ # ── Stage 1: Build Admin UI ──
2
+ FROM node:22-alpine AS frontend-builder
3
+ RUN apk add --no-cache git
4
+ RUN git clone --depth 1 https://github.com/hank9999/kiro.rs.git /app
5
+ WORKDIR /app/admin-ui
6
+ RUN npm install -g pnpm && pnpm install && pnpm build
7
+
8
+ # ── Stage 2: Build Rust Binary ──
9
+ FROM rust:1.82-alpine AS builder
10
+ RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static git
11
+ RUN git clone --depth 1 https://github.com/hank9999/kiro.rs.git /app
12
+ COPY --from=frontend-builder /app/admin-ui/dist /app/admin-ui/dist
13
  WORKDIR /app
14
+ RUN cargo build --release
15
 
16
+ # ── Stage 3: Runtime ──
17
+ FROM alpine:3.21
18
+ RUN apk add --no-cache ca-certificates
19
+ WORKDIR /app
20
+ COPY --from=builder /app/target/release/kiro-rs /app/kiro-rs
21
+ COPY entrypoint.sh /app/entrypoint.sh
22
+ RUN chmod +x /app/entrypoint.sh
23
 
 
24
  EXPOSE 7860
25
+ VOLUME ["/app/config"]
26
 
27
+ ENTRYPOINT ["/app/entrypoint.sh"]
 
 
 
LICENSE DELETED
@@ -1,674 +0,0 @@
1
- GNU GENERAL PUBLIC LICENSE
2
- Version 3, 29 June 2007
3
-
4
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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
- <one line to give the program's name and a brief idea of what it does.>
635
- Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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
- <program> Copyright (C) <year> <name of author>
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
- <https://www.gnu.org/licenses/>.
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
- <https://www.gnu.org/licenses/why-not-lgpl.html>.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,13 +1,8 @@
1
  ---
2
- title: kiro2api
3
- emoji: 🔌
4
  colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
- pinned: false
8
  app_port: 7860
9
  ---
10
-
11
- # kiro2api
12
-
13
- OpenAI/Anthropic-compatible API proxy for Kiro (Amazon Q Developer) powered by [aiclient-2-api](https://github.com/justlovemaki/aiclient-2-api).
 
1
  ---
2
+ title: Kiro API Proxy
3
+ emoji: 🔑
4
  colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
 
7
  app_port: 7860
8
  ---
 
 
 
 
VERSION DELETED
@@ -1 +0,0 @@
1
- 2.11.5.2
 
 
configs/api-potluck-data.json.example DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "config": {
3
- "defaultDailyLimit": 500,
4
- "bonusPerCredential": 300,
5
- "bonusValidityDays": 30,
6
- "persistInterval": 5000
7
- },
8
- "users": {
9
- "maki_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": {
10
- "credentials": [
11
- {
12
- "id": "cred_0000000000000_xxxxxx",
13
- "path": "configs/kiro/example_kiro-auth-token/example_kiro-auth-token.json",
14
- "provider": "claude-kiro-oauth",
15
- "authMethod": "refresh-token",
16
- "addedAt": "2026-01-01T00:00:00.000Z"
17
- }
18
- ],
19
- "credentialBonuses": [
20
- {
21
- "credentialId": "cred_0000000000000_xxxxxx",
22
- "grantedAt": "2026-01-01T00:00:00.000Z",
23
- "usedCount": 0
24
- }
25
- ],
26
- "createdAt": "2026-01-01T00:00:00.000Z"
27
- }
28
- }
29
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/api-potluck-keys.json.example DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "keys": {
3
- "maki_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": {
4
- "id": "maki_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
5
- "name": "示例用户",
6
- "createdAt": "2026-01-01T00:00:00.000Z",
7
- "dailyLimit": 500,
8
- "todayUsage": 0,
9
- "totalUsage": 0,
10
- "lastResetDate": "2026-01-01",
11
- "lastUsedAt": null,
12
- "enabled": true,
13
- "bonusRemaining": 0
14
- }
15
- }
16
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/config.json DELETED
@@ -1,30 +0,0 @@
1
- {
2
- "REQUIRED_API_KEY": "sk-kiro2api",
3
- "SERVER_PORT": 7860,
4
- "HOST": "0.0.0.0",
5
- "MODEL_PROVIDER": "claude-kiro-oauth",
6
- "SYSTEM_PROMPT_FILE_PATH": "",
7
- "SYSTEM_PROMPT_MODE": "overwrite",
8
- "PROMPT_LOG_BASE_NAME": "prompt_log",
9
- "PROMPT_LOG_MODE": "none",
10
- "REQUEST_MAX_RETRIES": 3,
11
- "REQUEST_BASE_DELAY": 1000,
12
- "CRON_NEAR_MINUTES": 1,
13
- "CRON_REFRESH_TOKEN": true,
14
- "PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json",
15
- "MAX_ERROR_COUNT": 3,
16
- "providerFallbackChain": {},
17
- "modelFallbackMapping": {},
18
- "PROXY_URL": null,
19
- "PROXY_ENABLED_PROVIDERS": [],
20
- "LOG_ENABLED": true,
21
- "LOG_OUTPUT_MODE": "all",
22
- "LOG_LEVEL": "info",
23
- "LOG_DIR": "logs",
24
- "LOG_INCLUDE_REQUEST_ID": true,
25
- "LOG_INCLUDE_TIMESTAMP": true,
26
- "LOG_MAX_FILE_SIZE": 10485760,
27
- "LOG_MAX_FILES": 5,
28
- "TLS_SIDECAR_ENABLED": false,
29
- "TLS_SIDECAR_PORT": 9090
30
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/config.json.example DELETED
@@ -1,63 +0,0 @@
1
- {
2
- "REQUIRED_API_KEY": "123456",
3
- "SERVER_PORT": 3000,
4
- "HOST": "0.0.0.0",
5
- "MODEL_PROVIDER": "gemini-cli-oauth",
6
- "SYSTEM_PROMPT_FILE_PATH": "configs/input_system_prompt.txt",
7
- "SYSTEM_PROMPT_MODE": "overwrite",
8
- "PROMPT_LOG_BASE_NAME": "prompt_log",
9
- "PROMPT_LOG_MODE": "none",
10
- "REQUEST_MAX_RETRIES": 3,
11
- "REQUEST_BASE_DELAY": 1000,
12
- "CRON_NEAR_MINUTES": 1,
13
- "CRON_REFRESH_TOKEN": false,
14
- "PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json",
15
- "MAX_ERROR_COUNT": 3,
16
- "GROK_COOKIE_TOKEN": "your-sso-cookie-token",
17
- "GROK_CF_CLEARANCE": "your-cf-clearance-cookie",
18
- "GROK_USER_AGENT": "Mozilla/5.0 ...",
19
- "GROK_BASE_URL": "https://grok.com",
20
- "providerFallbackChain": {
21
- "gemini-cli-oauth": ["gemini-antigravity"],
22
- "gemini-antigravity": ["gemini-cli-oauth"],
23
- "claude-kiro-oauth": ["claude-custom"],
24
- "claude-custom": ["claude-kiro-oauth"]
25
- },
26
- "modelFallbackMapping": {
27
- "gemini-claude-opus-4-5-thinking": {
28
- "targetProviderType": "claude-kiro-oauth",
29
- "targetModel": "claude-opus-4-5"
30
- },
31
- "gemini-claude-sonnet-4-5-thinking": {
32
- "targetProviderType": "claude-kiro-oauth",
33
- "targetModel": "claude-sonnet-4-5"
34
- },
35
- "gemini-claude-sonnet-4-5": {
36
- "targetProviderType": "claude-kiro-oauth",
37
- "targetModel": "claude-sonnet-4-5"
38
- },
39
- "claude-opus-4-5": {
40
- "targetProviderType": "gemini-antigravity",
41
- "targetModel": "gemini-claude-opus-4-5-thinking"
42
- },
43
- "claude-sonnet-4-5": {
44
- "targetProviderType": "gemini-antigravity",
45
- "targetModel": "gemini-claude-sonnet-4-5"
46
- }
47
- },
48
- "PROXY_URL": "http://127.0.0.1:1089",
49
- "PROXY_ENABLED_PROVIDERS": [
50
- "gemini-cli-oauth",
51
- "gemini-antigravity"
52
- ],
53
- "LOG_ENABLED": true,
54
- "LOG_OUTPUT_MODE": "all",
55
- "LOG_LEVEL": "info",
56
- "LOG_DIR": "logs",
57
- "LOG_INCLUDE_REQUEST_ID": true,
58
- "LOG_INCLUDE_TIMESTAMP": true,
59
- "LOG_MAX_FILE_SIZE": 10485760,
60
- "LOG_MAX_FILES": 10,
61
- "TLS_SIDECAR_ENABLED": false,
62
- "TLS_SIDECAR_PORT": 9090
63
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/kiro/chatgpt_account1_kiro-auth-token/chatgpt_account1_kiro-auth-token.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "accessToken": "aoaAAAAAGm84HYPHOFvOn_ATRhHeZMUuLcDZDbu_-hQrPp473ye8B5xlec8-gRa79ZftJpCSeNkJALBNwDuVc7wAkBkc0:MGUCMQDjWbPSbPmIX4c+42/YduE1MW3xBKwG4r32W2DCk8BYCBwvD638JC+R3VG4KYdWAE0CMGHsWX+omV0sLGLmX/jmmXOG1rzzXy81I///G7jJE2iYy4rPYXFwM/4Tujjufozm8A",
3
- "refreshToken": "aorAAAAAGozeWAnmIAixMWulOIdkLKdTGAv5fe87WhdVZEfvvM7pWbuZLdqc2SaUFCrxxRm6MTp9Vm40nBH1nfrgwBkc0:MGQCMFN2i+2A0Ol6PaQ+7poIrguBIFxilhiO73eD/iMpgNADCXzc6eLmLurejfvf6yZ8GQIwY7EnLvAmpIlmR7FW2uhxC/6vLQoHHYnRJkx23sC8aqA+X03j24cDmXyxwlR5azIk",
4
- "clientId": "gabI3snTPwTQZGF1TzuWc3VzLWVhc3QtMQ",
5
- "clientSecret": "eyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0.eyJzZXJpYWxpemVkIjoie1wiY2xpZW50SWRcIjp7XCJ2YWx1ZVwiOlwiZ2FiSTNzblRQd1RRWkdGMVR6dVdjM1Z6TFdWaGMzUXRNUVwifSxcImlkZW1wb3RlbnRLZXlcIjpudWxsLFwidGVuYW50SWRcIjpudWxsLFwiY2xpZW50TmFtZVwiOlwiQW1hem9uIFEgRGV2ZWxvcGVyIGZvciBjb21tYW5kIGxpbmVcIixcImJhY2tmaWxsVmVyc2lvblwiOm51bGwsXCJjbGllbnRUeXBlXCI6XCJQVUJMSUNcIixcInRlbXBsYXRlQXJuXCI6bnVsbCxcInRlbXBsYXRlQ29udGV4dFwiOm51bGwsXCJleHBpcmF0aW9uVGltZXN0YW1wXCI6MTc4MTc1ODMxMC42NDQxMjIwNzYsXCJjcmVhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMC42NDQxMjIwNzYsXCJ1cGRhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMC42NDQxMjIwNzYsXCJjcmVhdGVkQnlcIjpudWxsLFwidXBkYXRlZEJ5XCI6bnVsbCxcInN0YXR1c1wiOm51bGwsXCJpbml0aWF0ZUxvZ2luVXJpXCI6bnVsbCxcImVudGl0bGVkUmVzb3VyY2VJZFwiOm51bGwsXCJlbnRpdGxlZFJlc291cmNlQ29udGFpbmVySWRcIjpudWxsLFwiZXh0ZXJuYWxJZFwiOm51bGwsXCJzb2Z0d2FyZUlkXCI6bnVsbCxcInNjb3Blc1wiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29tcGxldGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiY29tcGxldGlvbnNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6YW5hbHlzaXNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiYW5hbHlzaXNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn1dLFwiYXV0aGVudGljYXRpb25Db25maWd1cmF0aW9uXCI6bnVsbCxcInNoYWRvd0F1dGhlbnRpY2F0aW9uQ29uZmlndXJhdGlvblwiOm51bGwsXCJlbmFibGVkR3JhbnRzXCI6bnVsbCxcImVuZm9yY2VBdXRoTkNvbmZpZ3VyYXRpb25cIjpudWxsLFwib3duZXJBY2NvdW50SWRcIjpudWxsLFwic3NvSW5zdGFuY2VBY2NvdW50SWRcIjpudWxsLFwidXNlckNvbnNlbnRcIjpudWxsLFwibm9uSW50ZXJhY3RpdmVTZXNzaW9uc0VuYWJsZWRcIjpudWxsLFwiYXNzb2NpYXRlZEluc3RhbmNlQXJuXCI6bnVsbCxcImdyb3VwU2NvcGVzQnlGcmllbmRseUlkXCI6e1wiY29kZXdoaXNwZXJlclwiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmFuYWx5c2lzXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInVzZUNhc2VBY3Rpb25cIjpcImFuYWx5c2lzXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmNvbXBsZXRpb25zXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInVzZUNhc2VBY3Rpb25cIjpcImNvbXBsZXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn1dfSxcInNob3VsZEdldFZhbHVlRnJvbVRlbXBsYXRlXCI6dHJ1ZSxcImhhc1JlcXVlc3RlZFNjb3Blc1wiOmZhbHNlLFwiY29udGFpbnNPbmx5U3NvU2NvcGVzXCI6ZmFsc2UsXCJzc29TY29wZXNcIjpbXSxcImlzVjFCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc1YyQmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNWM0JhY2tmaWxsZWRcIjpmYWxzZSxcImlzVjRCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc0V4cGlyZWRcIjpmYWxzZSxcImlzQmFja2ZpbGxlZFwiOmZhbHNlLFwiaGFzSW5pdGlhbFNjb3Blc1wiOnRydWUsXCJhcmVBbGxTY29wZXNDb25zZW50ZWRUb1wiOmZhbHNlfSJ9.gsmsFkKFajDcDb4-c31lLTGxYQIqHKTzgZvwB4Wfta0xuDBNw0gzThEXSDZlqP6q",
6
- "authMethod": "builder-id",
7
- "idcRegion": "us-east-1",
8
- "expiresAt": "2026-03-20T06:00:00Z"
9
- }
 
 
 
 
 
 
 
 
 
 
configs/kiro/chatgpt_account2_kiro-auth-token/chatgpt_account2_kiro-auth-token.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "accessToken": "aoaAAAAAGm84HYLp9bxse52TpenBsslXdOVeCse0scBl5EXeX46G47SUJ7cg1Ok2VFQ8uNIg41dAzJ0NV9z7lWvmYBkc0:MGQCMC7JUwaYATAtN9fqgYsXBA7ajXJd0SuVa5jdmpHZ1b+unFh71kJdXsRs4Rs3VIQ+MwIwcCL2gGDRXPli8wGzvlgrtNti8orkDFfkqHkH7pyHJLoDu1bYq7iefaHErMY8kAlZ",
3
- "refreshToken": "aorAAAAAGozeWIYaVvTd3pwQl7-_GyGoAvrkgXW5puoFCn1Ck_kxYwNi2Yswz3Z2NaTSkfpP5LpQDYVCu6Yw43oYwBkc0:MGUCMCIe9f1tl2C3QXQda5IdFeBpJPHcReZIr3Zt8bms2hLijzdTtVfzsKQT7a0q9Sq/JAIxAMAmLFxl6kOqqPNfbWVy5dunHn+9+K3O/OXT/lk+1LjOWT1fNkY/+thpKYHM16bVzw",
4
- "clientId": "1LFoSWRMsTHl8MdMQKrMM3VzLWVhc3QtMQ",
5
- "clientSecret": "eyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0.eyJzZXJpYWxpemVkIjoie1wiY2xpZW50SWRcIjp7XCJ2YWx1ZVwiOlwiMUxGb1NXUk1zVEhsOE1kTVFLck1NM1Z6TFdWaGMzUXRNUVwifSxcImlkZW1wb3RlbnRLZXlcIjpudWxsLFwidGVuYW50SWRcIjpudWxsLFwiY2xpZW50TmFtZVwiOlwiQW1hem9uIFEgRGV2ZWxvcGVyIGZvciBjb21tYW5kIGxpbmVcIixcImJhY2tmaWxsVmVyc2lvblwiOm51bGwsXCJjbGllbnRUeXBlXCI6XCJQVUJMSUNcIixcInRlbXBsYXRlQXJuXCI6bnVsbCxcInRlbXBsYXRlQ29udGV4dFwiOm51bGwsXCJleHBpcmF0aW9uVGltZXN0YW1wXCI6MTc4MTc1ODMxMS4zNjI2MDY5NTMsXCJjcmVhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMS4zNjI2MDY5NTMsXCJ1cGRhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMS4zNjI2MDY5NTMsXCJjcmVhdGVkQnlcIjpudWxsLFwidXBkYXRlZEJ5XCI6bnVsbCxcInN0YXR1c1wiOm51bGwsXCJpbml0aWF0ZUxvZ2luVXJpXCI6bnVsbCxcImVudGl0bGVkUmVzb3VyY2VJZFwiOm51bGwsXCJlbnRpdGxlZFJlc291cmNlQ29udGFpbmVySWRcIjpudWxsLFwiZXh0ZXJuYWxJZFwiOm51bGwsXCJzb2Z0d2FyZUlkXCI6bnVsbCxcInNjb3Blc1wiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29tcGxldGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiY29tcGxldGlvbnNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6YW5hbHlzaXNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiYW5hbHlzaXNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn1dLFwiYXV0aGVudGljYXRpb25Db25maWd1cmF0aW9uXCI6bnVsbCxcInNoYWRvd0F1dGhlbnRpY2F0aW9uQ29uZmlndXJhdGlvblwiOm51bGwsXCJlbmFibGVkR3JhbnRzXCI6bnVsbCxcImVuZm9yY2VBdXRoTkNvbmZpZ3VyYXRpb25cIjpudWxsLFwib3duZXJBY2NvdW50SWRcIjpudWxsLFwic3NvSW5zdGFuY2VBY2NvdW50SWRcIjpudWxsLFwidXNlckNvbnNlbnRcIjpudWxsLFwibm9uSW50ZXJhY3RpdmVTZXNzaW9uc0VuYWJsZWRcIjpudWxsLFwiYXNzb2NpYXRlZEluc3RhbmNlQXJuXCI6bnVsbCxcInNob3VsZEdldFZhbHVlRnJvbVRlbXBsYXRlXCI6dHJ1ZSxcImhhc1JlcXVlc3RlZFNjb3Blc1wiOmZhbHNlLFwiY29udGFpbnNPbmx5U3NvU2NvcGVzXCI6ZmFsc2UsXCJzc29TY29wZXNcIjpbXSxcImlzVjFCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc1YyQmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNWM0JhY2tmaWxsZWRcIjpmYWxzZSxcImlzVjRCYWNrZmlsbGVkXCI6ZmFsc2UsXCJncm91cFNjb3Blc0J5RnJpZW5kbHlJZFwiOntcImNvZGV3aGlzcGVyZXJcIjpbe1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmFuYWx5c2lzXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInVzZUNhc2VBY3Rpb25cIjpcImFuYWx5c2lzXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmNvbnZlcnNhdGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiY29udmVyc2F0aW9uc1wiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCJ9LHtcImZ1bGxTY29wZVwiOlwiY29kZXdoaXNwZXJlcjpjb21wbGV0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb21wbGV0aW9uc1wiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCJ9XX0sXCJpc0V4cGlyZWRcIjpmYWxzZSxcImlzQmFja2ZpbGxlZFwiOmZhbHNlLFwiaGFzSW5pdGlhbFNjb3Blc1wiOnRydWUsXCJhcmVBbGxTY29wZXNDb25zZW50ZWRUb1wiOmZhbHNlfSJ9.ZFTKp-PJaA7vKXI3UiBkqbnO-CvjX2kJw75UUIraS04b8HveyUsInBDcc40yKKf1",
6
- "authMethod": "builder-id",
7
- "idcRegion": "us-east-1",
8
- "expiresAt": "2026-03-20T06:00:00Z"
9
- }
 
 
 
 
 
 
 
 
 
 
configs/kiro/fresh1_kiro-auth-token/fresh1_kiro-auth-token.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "accessToken": "aoaAAAAAGm8epsqQIJk5Bj1QoXwTAMGHRMumJWCVJ13MJODTLwCdljXWm-8jmbsXAghY3niBcLEuNr9xTljmhrN-oBkc0:MGUCMQCH/mlgYPdL1PfKCnU1eSRf5NEMyhPCW/2+Y8K2bFmMH2gMFAE6ftTRveCFSe+kTjQCMC/u4EdVSdTr9Bui6OsE280xUPAH7CSDCIHNTTPk2CiYbVtge/oYlRxVoNQizk1AMQ",
3
- "refreshToken": "aorAAAAAGozE4YEfji2Ny6mvPuscUKbyPuUc2XD3Nf7SdFbQcE1Z2qoUOR-gZI8cUKubWvDXOivKqghUnyZOMXp7sBkc0:MGQCMCrPdIeyBVkVlB0rtjlIvHhlPURSovG8mcs9qmDBx1VYH5LPdcWvQd74O3XFYCGMfwIwIESOUIiPVFFAMKs+N1YgEoe13glxpAP7Ri0memy2/WdqY0cFEFNqMq/CndDx0dbJ",
4
- "clientId": "H6JYDDvxcMALs2QKZfIKgHVzLWVhc3QtMQ",
5
- "clientSecret": "eyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0.eyJzZXJpYWxpemVkIjoie1wiY2xpZW50SWRcIjp7XCJ2YWx1ZVwiOlwiSDZKWUREdnhjTUFMczJRS1pmSUtnSFZ6TFdWaGMzUXRNUVwifSxcImlkZW1wb3RlbnRLZXlcIjpudWxsLFwidGVuYW50SWRcIjpudWxsLFwiY2xpZW50TmFtZVwiOlwiQW1hem9uIFEgRGV2ZWxvcGVyIGZvciBjb21tYW5kIGxpbmVcIixcImJhY2tmaWxsVmVyc2lvblwiOm51bGwsXCJjbGllbnRUeXBlXCI6XCJQVUJMSUNcIixcInRlbXBsYXRlQXJuXCI6bnVsbCxcInRlbXBsYXRlQ29udGV4dFwiOm51bGwsXCJleHBpcmF0aW9uVGltZXN0YW1wXCI6MTc4MTczMjIzNS43OTM5MDAyNzIsXCJjcmVhdGVkVGltZXN0YW1wXCI6MTc3Mzk1NjIzNS43OTM5MDAyNzIsXCJ1cGRhdGVkVGltZXN0YW1wXCI6MTc3Mzk1NjIzNS43OTM5MDAyNzIsXCJjcmVhdGVkQnlcIjpudWxsLFwidXBkYXRlZEJ5XCI6bnVsbCxcInN0YXR1c1wiOm51bGwsXCJpbml0aWF0ZUxvZ2luVXJpXCI6bnVsbCxcImVudGl0bGVkUmVzb3VyY2VJZFwiOm51bGwsXCJlbnRpdGxlZFJlc291cmNlQ29udGFpbmVySWRcIjpudWxsLFwiZXh0ZXJuYWxJZFwiOm51bGwsXCJzb2Z0d2FyZUlkXCI6bnVsbCxcInNjb3Blc1wiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29tcGxldGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwidXNlQ2FzZUFjdGlvblwiOlwiY29tcGxldGlvbnNcIixcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6YW5hbHlzaXNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwidXNlQ2FzZUFjdGlvblwiOlwiYW5hbHlzaXNcIixcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn1dLFwiYXV0aGVudGljYXRpb25Db25maWd1cmF0aW9uXCI6bnVsbCxcInNoYWRvd0F1dGhlbnRpY2F0aW9uQ29uZmlndXJhdGlvblwiOm51bGwsXCJlbmFibGVkR3JhbnRzXCI6bnVsbCxcImVuZm9yY2VBdXRoTkNvbmZpZ3VyYXRpb25cIjpudWxsLFwib3duZXJBY2NvdW50SWRcIjpudWxsLFwic3NvSW5zdGFuY2VBY2NvdW50SWRcIjpudWxsLFwidXNlckNvbnNlbnRcIjpudWxsLFwibm9uSW50ZXJhY3RpdmVTZXNzaW9uc0VuYWJsZWRcIjpudWxsLFwiYXNzb2NpYXRlZEluc3RhbmNlQXJuXCI6bnVsbCxcImNvbnRhaW5zT25seVNzb1Njb3Blc1wiOmZhbHNlLFwic3NvU2NvcGVzXCI6W10sXCJpc1YxQmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNWMkJhY2tmaWxsZWRcIjpmYWxzZSxcImlzVjNCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc1Y0QmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNFeHBpcmVkXCI6ZmFsc2UsXCJpc0JhY2tmaWxsZWRcIjpmYWxzZSxcImhhc0luaXRpYWxTY29wZXNcIjp0cnVlLFwiYXJlQWxsU2NvcGVzQ29uc2VudGVkVG9cIjpmYWxzZSxcImdyb3VwU2NvcGVzQnlGcmllbmRseUlkXCI6e1wiY29kZXdoaXNwZXJlclwiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmNvbXBsZXRpb25zXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcInVzZUNhc2VBY3Rpb25cIjpcImNvbXBsZXRpb25zXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmFuYWx5c2lzXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcInVzZUNhc2VBY3Rpb25cIjpcImFuYWx5c2lzXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn1dfSxcInNob3VsZEdldFZhbHVlRnJvbVRlbXBsYXRlXCI6dHJ1ZSxcImhhc1JlcXVlc3RlZFNjb3Blc1wiOmZhbHNlfSJ9.57MdZkhyW6vz98g6JDcRSqh1c8z1Ta1eyH6d9YbsW3TUTrfJ6WxobBzkaP69aK4r",
6
- "authMethod": "builder-id",
7
- "idcRegion": "us-east-1",
8
- "expiresAt": "2026-03-20T07:00:00Z"
9
- }
 
 
 
 
 
 
 
 
 
 
configs/kiro/personal_kiro-auth-token/personal_kiro-auth-token.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "accessToken": "aoaAAAAAGm82R4v3zSqMlkmiPwSHdKAAQAbZ6yu4TywrlVXmFU7XMVtQQb3phAA-gOci2L0S62LjsBj-9NFLMeQPMBkc0:MGUCMQDT2c8RFXspAv/U52k+YkcOeYIge/+GA+axl/jxo48IfcXYRM2RnclaR816zx0v/rkCMGEoQS1GOAZAZN7pUJzu5xgYKo62NwzNNNoUEyyGcPwujYtWRQFbNKgAvlL0O7VFGg",
3
- "refreshToken": "",
4
- "authMethod": "social",
5
- "region": "us-east-1",
6
- "expiresAt": "2026-03-20T06:00:00Z"
7
- }
 
 
 
 
 
 
 
 
configs/plugins.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "plugins": {
3
- "api-potluck": {"enabled": false},
4
- "default-auth": {"enabled": true}
5
- }
6
- }
 
 
 
 
 
 
 
configs/plugins.json.example DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "plugins": {
3
- "api-potluck": {
4
- "enabled": false,
5
- "description": "API 大锅饭 - Key 管理和用量统计插件"
6
- },
7
- "default-auth": {
8
- "enabled": true,
9
- "description": "默认 API Key 认证插件(内置)"
10
- }
11
- }
12
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/provider_pools.json DELETED
@@ -1,43 +0,0 @@
1
- {
2
- "claude-kiro-oauth": [
3
- {
4
- "customName": "Personal Account (cs.shenhao)",
5
- "KIRO_OAUTH_CREDS_FILE_PATH": "configs/kiro/personal_kiro-auth-token/personal_kiro-auth-token.json",
6
- "uuid": "personal-0000-4000-a000-000000000000",
7
- "checkModelName": null,
8
- "checkHealth": false,
9
- "isHealthy": true,
10
- "isDisabled": false,
11
- "lastUsed": null,
12
- "usageCount": 0,
13
- "errorCount": 0,
14
- "lastErrorTime": null
15
- },
16
- {
17
- "customName": "ChatGPT Mail Account 1 (kr63b7278504@prestige-leadership.org)",
18
- "KIRO_OAUTH_CREDS_FILE_PATH": "configs/kiro/chatgpt_account1_kiro-auth-token/chatgpt_account1_kiro-auth-token.json",
19
- "uuid": "chatgpt-0001-4000-a000-000000000001",
20
- "checkModelName": null,
21
- "checkHealth": true,
22
- "isHealthy": true,
23
- "isDisabled": false,
24
- "lastUsed": null,
25
- "usageCount": 0,
26
- "errorCount": 0,
27
- "lastErrorTime": null
28
- },
29
- {
30
- "customName": "ChatGPT Mail Account 2 (kr0a92d5ad15@mailsa.biz.id)",
31
- "KIRO_OAUTH_CREDS_FILE_PATH": "configs/kiro/chatgpt_account2_kiro-auth-token/chatgpt_account2_kiro-auth-token.json",
32
- "uuid": "chatgpt-0002-4000-a000-000000000002",
33
- "checkModelName": null,
34
- "checkHealth": true,
35
- "isHealthy": true,
36
- "isDisabled": false,
37
- "lastUsed": null,
38
- "usageCount": 0,
39
- "errorCount": 0,
40
- "lastErrorTime": null
41
- }
42
- ]
43
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/provider_pools.json.example DELETED
@@ -1,213 +0,0 @@
1
- {
2
- "openai-custom": [
3
- {
4
- "customName": "OpenAI节点1",
5
- "OPENAI_API_KEY": "sk-openai-key1",
6
- "OPENAI_BASE_URL": "https://api.openai.com/v1",
7
- "checkModelName": null,
8
- "checkHealth": false,
9
- "notSupportedModels": ["gpt-4-turbo"],
10
- "uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c",
11
- "isHealthy": true,
12
- "isDisabled": false,
13
- "lastUsed": null,
14
- "usageCount": 0,
15
- "errorCount": 0,
16
- "lastErrorTime": null
17
- },
18
- {
19
- "customName": "OpenAI节点2",
20
- "OPENAI_API_KEY": "sk-openai-key2",
21
- "OPENAI_BASE_URL": "https://api.openai.com/v1",
22
- "checkModelName": null,
23
- "checkHealth": false,
24
- "notSupportedModels": ["gpt-4-turbo", "gpt-4"],
25
- "uuid": "e284628d-302f-456d-91f3-6095386fb3b8",
26
- "isHealthy": true,
27
- "isDisabled": true,
28
- "lastUsed": null,
29
- "usageCount": 0,
30
- "errorCount": 0,
31
- "lastErrorTime": null
32
- }
33
- ],
34
- "openaiResponses-custom": [
35
- {
36
- "customName": "OpenAI Responses节点",
37
- "OPENAI_API_KEY": "sk-openai-key",
38
- "OPENAI_BASE_URL": "https://api.openai.com/v1",
39
- "checkModelName": null,
40
- "checkHealth": false,
41
- "uuid": "e284628d-302f-456d-91f3-609538678968",
42
- "isHealthy": true,
43
- "isDisabled": false,
44
- "lastUsed": null,
45
- "usageCount": 0,
46
- "errorCount": 0,
47
- "lastErrorTime": null
48
- }
49
- ],
50
- "gemini-cli-oauth": [
51
- {
52
- "customName": "Gemini OAuth节点1",
53
- "GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json",
54
- "PROJECT_ID": "your-project-id-1",
55
- "checkModelName": null,
56
- "checkHealth": false,
57
- "uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3",
58
- "isHealthy": true,
59
- "isDisabled": false,
60
- "lastUsed": null,
61
- "usageCount": 0,
62
- "errorCount": 0,
63
- "lastErrorTime": null
64
- },
65
- {
66
- "customName": "Gemini OAuth节点2",
67
- "GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials2.json",
68
- "PROJECT_ID": "your-project-id-2",
69
- "checkModelName": null,
70
- "checkHealth": false,
71
- "uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54",
72
- "isHealthy": true,
73
- "isDisabled": false,
74
- "lastUsed": null,
75
- "usageCount": 0,
76
- "errorCount": 0,
77
- "lastErrorTime": null
78
- }
79
- ],
80
- "claude-custom": [
81
- {
82
- "customName": "Claude节点1",
83
- "CLAUDE_API_KEY": "sk-claude-key1",
84
- "CLAUDE_BASE_URL": "https://api.anthropic.com",
85
- "checkModelName": null,
86
- "checkHealth": false,
87
- "uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128",
88
- "isHealthy": true,
89
- "isDisabled": false,
90
- "lastUsed": null,
91
- "usageCount": 0,
92
- "errorCount": 0,
93
- "lastErrorTime": null
94
- },
95
- {
96
- "customName": "Claude节点2",
97
- "CLAUDE_API_KEY": "sk-claude-key2",
98
- "CLAUDE_BASE_URL": "https://api.anthropic.com",
99
- "checkModelName": null,
100
- "checkHealth": false,
101
- "uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a",
102
- "isHealthy": true,
103
- "isDisabled": false,
104
- "lastUsed": null,
105
- "usageCount": 0,
106
- "errorCount": 0,
107
- "lastErrorTime": null
108
- }
109
- ],
110
- "claude-kiro-oauth": [
111
- {
112
- "customName": "Kiro OAuth节点1",
113
- "KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds1.json",
114
- "uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd",
115
- "checkModelName": null,
116
- "checkHealth": false,
117
- "isHealthy": true,
118
- "isDisabled": false,
119
- "lastUsed": null,
120
- "usageCount": 0,
121
- "errorCount": 0,
122
- "lastErrorTime": null
123
- },
124
- {
125
- "customName": "Kiro OAuth节点2",
126
- "KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds2.json",
127
- "uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2",
128
- "checkModelName": null,
129
- "checkHealth": false,
130
- "isHealthy": true,
131
- "isDisabled": false,
132
- "lastUsed": null,
133
- "usageCount": 0,
134
- "errorCount": 0,
135
- "lastErrorTime": null
136
- }
137
- ],
138
- "openai-qwen-oauth": [
139
- {
140
- "customName": "Qwen OAuth节点",
141
- "QWEN_OAUTH_CREDS_FILE_PATH": "./qwen_creds.json",
142
- "uuid": "658a2114-c4c9-d713-b8d4-ceabf0e0bf18",
143
- "checkModelName": null,
144
- "checkHealth": false,
145
- "isHealthy": true,
146
- "isDisabled": false,
147
- "lastUsed": null,
148
- "usageCount": 0,
149
- "errorCount": 0,
150
- "lastErrorTime": null
151
- }
152
- ],
153
- "gemini-antigravity": [
154
- {
155
- "customName": "Antigravity节点1",
156
- "ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds1.json",
157
- "PROJECT_ID": "antigravity-project-1",
158
- "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
159
- "checkModelName": null,
160
- "checkHealth": false,
161
- "isHealthy": true,
162
- "isDisabled": false,
163
- "lastUsed": null,
164
- "usageCount": 0,
165
- "errorCount": 0,
166
- "lastErrorTime": null
167
- },
168
- {
169
- "customName": "Antigravity节点2",
170
- "ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds2.json",
171
- "PROJECT_ID": "antigravity-project-2",
172
- "uuid": "f0e9d8c7-b6a5-4321-fedc-ba9876543210",
173
- "checkModelName": null,
174
- "checkHealth": false,
175
- "isHealthy": true,
176
- "isDisabled": false,
177
- "lastUsed": null,
178
- "usageCount": 0,
179
- "errorCount": 0,
180
- "lastErrorTime": null
181
- }
182
- ],
183
- "openai-iflow": [
184
- {
185
- "customName": "iFlow Token节点1",
186
- "IFLOW_TOKEN_FILE_PATH": "./configs/iflow/iflow_token.json",
187
- "IFLOW_BASE_URL": "https://apis.iflow.cn/v1",
188
- "uuid": "11223344-5566-7788-99aa-bbccddeeff00",
189
- "checkModelName": "gpt-4o",
190
- "checkHealth": false,
191
- "isHealthy": true,
192
- "isDisabled": false,
193
- "lastUsed": null,
194
- "usageCount": 0,
195
- "errorCount": 0,
196
- "lastErrorTime": null
197
- },
198
- {
199
- "customName": "iFlow Token节点2",
200
- "IFLOW_TOKEN_FILE_PATH": "./configs/iflow/iflow_token2.json",
201
- "IFLOW_BASE_URL": "https://apis.iflow.cn/v1",
202
- "uuid": "aabbccdd-eeff-0011-2233-445566778899",
203
- "checkModelName": "gpt-4o",
204
- "checkHealth": false,
205
- "isHealthy": true,
206
- "isDisabled": false,
207
- "lastUsed": null,
208
- "usageCount": 0,
209
- "errorCount": 0,
210
- "lastErrorTime": null
211
- }
212
- ]
213
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/pwd DELETED
@@ -1 +0,0 @@
1
- kiro2024
 
 
entrypoint.sh ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ CONFIG_DIR="/app/config"
5
+ mkdir -p "${CONFIG_DIR}"
6
+
7
+ API_KEY="${API_KEY:-sk-kiro2api}"
8
+ ADMIN_API_KEY="${ADMIN_API_KEY:-sk-admin-kiro2api}"
9
+ PROXY="${HTTPS_PROXY:-${ALL_PROXY:-}}"
10
+
11
+ cat > "${CONFIG_DIR}/config.json" <<EOF
12
+ {
13
+ "host": "0.0.0.0",
14
+ "port": 7860,
15
+ "apiKey": "${API_KEY}",
16
+ "adminApiKey": "${ADMIN_API_KEY}",
17
+ "region": "us-east-1",
18
+ "tlsBackend": "rustls",
19
+ "proxyUrl": "${PROXY}",
20
+ "loadBalancingMode": "balanced"
21
+ }
22
+ EOF
23
+
24
+ # Empty credentials array — populated via admin API sync
25
+ echo "[]" > "${CONFIG_DIR}/credentials.json"
26
+
27
+ echo "Config generated (port=7860, proxy=${PROXY:+set}${PROXY:-none})"
28
+ exec ./kiro-rs -c "${CONFIG_DIR}/config.json" --credentials "${CONFIG_DIR}/credentials.json"
healthcheck.js DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * Docker健康检查脚本
3
- * 用于检查API服务器是否正常运行
4
- */
5
-
6
- import http from 'http';
7
-
8
- // 从环境变量获取主机和端口,如果没有设置则使用默认值
9
- const HOST = process.env.HOST || 'localhost';
10
- const PORT = process.env.SERVER_PORT || 3000;
11
-
12
- // 发送HTTP请求到健康检查端点
13
- const options = {
14
- hostname: HOST,
15
- port: PORT,
16
- path: '/health',
17
- method: 'GET',
18
- timeout: 2000 // 2秒超时
19
- };
20
-
21
- const req = http.request(options, (res) => {
22
- // 如果状态码是200,表示服务健康
23
- if (res.statusCode === 200) {
24
- console.log('Health check passed');
25
- process.exit(0);
26
- } else {
27
- console.log(`Health check failed with status code: ${res.statusCode}`);
28
- process.exit(1);
29
- }
30
- });
31
-
32
- // 处理请求错误
33
- req.on('error', (e) => {
34
- console.error(`Health check failed: ${e.message}`);
35
- process.exit(1);
36
- });
37
-
38
- // 设置超时处理
39
- req.on('timeout', () => {
40
- console.error('Health check timed out');
41
- req.destroy();
42
- process.exit(1);
43
- });
44
-
45
- // 结束请求
46
- req.end();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
package.json DELETED
@@ -1,42 +0,0 @@
1
- {
2
- "type": "module",
3
- "dependencies": {
4
- "@anthropic-ai/tokenizer": "^0.0.4",
5
- "adm-zip": "^0.5.16",
6
- "axios": "^1.10.0",
7
- "deepmerge": "^4.3.1",
8
- "dotenv": "^16.4.5",
9
- "google-auth-library": "^10.1.0",
10
- "http-proxy-agent": "^7.0.2",
11
- "https-proxy-agent": "^7.0.6",
12
- "lodash": "^4.17.21",
13
- "multer": "^2.0.2",
14
- "open": "^10.2.0",
15
- "socks-proxy-agent": "^8.0.5",
16
- "undici": "^7.12.0",
17
- "uuid": "^11.1.0",
18
- "ws": "^8.19.0"
19
- },
20
- "devDependencies": {
21
- "@babel/preset-env": "^7.28.0",
22
- "@jest/globals": "^29.7.0",
23
- "babel-jest": "^30.0.5",
24
- "babel-plugin-transform-import-meta": "^2.3.3",
25
- "jest": "^29.7.0",
26
- "jest-environment-node": "^29.7.0",
27
- "supertest": "^6.3.3"
28
- },
29
- "scripts": {
30
- "start": "node src/core/master.js",
31
- "start:standalone": "node src/services/api-server.js",
32
- "start:dev": "node src/core/master.js --dev",
33
- "test": "jest",
34
- "test:watch": "jest --watch",
35
- "test:coverage": "jest --coverage",
36
- "test:verbose": "jest --verbose",
37
- "test:silent": "jest --silent",
38
- "test:unit": "node run-tests.js --unit",
39
- "test:integration": "node run-tests.js --integration",
40
- "test:summary": "node test-summary.js"
41
- }
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/auth/codex-oauth.js DELETED
@@ -1,1056 +0,0 @@
1
- import http from 'http';
2
- import logger from '../utils/logger.js';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import crypto from 'crypto';
6
- import open from 'open';
7
- import axios from 'axios';
8
- import { broadcastEvent } from '../services/ui-manager.js';
9
- import { autoLinkProviderConfigs } from '../services/service-manager.js';
10
- import { CONFIG } from '../core/config-manager.js';
11
- import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
12
-
13
- /**
14
- * Codex OAuth 配置
15
- */
16
- const CODEX_OAUTH_CONFIG = {
17
- clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
18
- authUrl: 'https://auth.openai.com/oauth/authorize',
19
- tokenUrl: 'https://auth.openai.com/oauth/token',
20
- redirectUri: 'http://localhost:1455/auth/callback',
21
- port: 1455,
22
- scopes: 'openid email profile offline_access',
23
- logPrefix: '[Codex Auth]'
24
- };
25
-
26
- /**
27
- * 活动的服务器实例管理(与 gemini-oauth 一致)
28
- */
29
- const activeServers = new Map();
30
-
31
- /**
32
- * 关闭指定端口的活动服务器
33
- */
34
- async function closeActiveServer(provider, port = null) {
35
- const existing = activeServers.get(provider);
36
-
37
- if (existing) {
38
- try {
39
- // 1. 使用 Promise.race() 添加 2 秒超时
40
- const closePromise = new Promise((resolve, reject) => {
41
- existing.server.close((err) => {
42
- if (err) reject(err);
43
- else resolve();
44
- });
45
- });
46
-
47
- const timeoutPromise = new Promise((_, reject) => {
48
- setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
49
- });
50
-
51
- await Promise.race([closePromise, timeoutPromise]);
52
- logger.info(`[Codex Auth] ${provider} server closed successfully`);
53
- } catch (error) {
54
- // 2. try-catch 捕获错误
55
- logger.warn(`[Codex Auth] Server close failed or timed out: ${error.message}`);
56
- } finally {
57
- // 3. finally 块强制清理,防止阻塞
58
- activeServers.delete(provider);
59
- }
60
- }
61
-
62
- if (port) {
63
- for (const [p, info] of activeServers.entries()) {
64
- if (info.port === port) {
65
- // 递归调用处理端口冲突的情况
66
- await closeActiveServer(p);
67
- }
68
- }
69
- }
70
- }
71
-
72
- /**
73
- * Codex OAuth 认证类
74
- * 实现 OAuth2 + PKCE 流程
75
- */
76
- class CodexAuth {
77
- constructor(config) {
78
- this.config = config;
79
-
80
- // 配置代理支持
81
- const axiosConfig = { timeout: 30000 };
82
- const proxyConfig = getProxyConfigForProvider(config, 'openai-codex-oauth');
83
- if (proxyConfig) {
84
- axiosConfig.httpAgent = proxyConfig.httpAgent;
85
- axiosConfig.httpsAgent = proxyConfig.httpsAgent;
86
- logger.info('[Codex Auth] Proxy enabled for OAuth requests');
87
- }
88
-
89
- this.httpClient = axios.create(axiosConfig);
90
- this.server = null; // 存储服务器实例
91
- }
92
-
93
- /**
94
- * 生成 PKCE 代码
95
- * @returns {{verifier: string, challenge: string}}
96
- */
97
- generatePKCECodes() {
98
- // 生成 code verifier (96 随机字节 → 128 base64url 字符)
99
- const verifier = crypto.randomBytes(96)
100
- .toString('base64url');
101
-
102
- // 生成 code challenge (SHA256 of verifier)
103
- const challenge = crypto.createHash('sha256')
104
- .update(verifier)
105
- .digest('base64url');
106
-
107
- return { verifier, challenge };
108
- }
109
-
110
- /**
111
- * 生成授权 URL(不启动完整流程)
112
- * @returns {{authUrl: string, state: string, pkce: Object, server: Object}}
113
- */
114
- async generateAuthUrl() {
115
- const pkce = this.generatePKCECodes();
116
- const state = crypto.randomBytes(16).toString('hex');
117
-
118
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Generating auth URL...`);
119
-
120
- // 启动本地回调服务器
121
- const server = await this.startCallbackServer();
122
- this.server = server;
123
-
124
- // 构建授权 URL
125
- const authUrl = new URL(CODEX_OAUTH_CONFIG.authUrl);
126
- authUrl.searchParams.set('client_id', CODEX_OAUTH_CONFIG.clientId);
127
- authUrl.searchParams.set('response_type', 'code');
128
- authUrl.searchParams.set('redirect_uri', CODEX_OAUTH_CONFIG.redirectUri);
129
- authUrl.searchParams.set('scope', CODEX_OAUTH_CONFIG.scopes);
130
- authUrl.searchParams.set('state', state);
131
- authUrl.searchParams.set('code_challenge', pkce.challenge);
132
- authUrl.searchParams.set('code_challenge_method', 'S256');
133
- authUrl.searchParams.set('prompt', 'login');
134
- authUrl.searchParams.set('id_token_add_organizations', 'true');
135
- authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
136
-
137
- return {
138
- authUrl: authUrl.toString(),
139
- state,
140
- pkce,
141
- server
142
- };
143
- }
144
-
145
- /**
146
- * 完成 OAuth 流程(在收到回调后调用)
147
- * @param {string} code - 授权码
148
- * @param {string} state - 状态参数
149
- * @param {string} expectedState - 期望的状态参数
150
- * @param {Object} pkce - PKCE 代码
151
- * @returns {Promise<Object>} tokens 和凭据路径
152
- */
153
- async completeOAuthFlow(code, state, expectedState, pkce) {
154
- // 验证 state
155
- if (state !== expectedState) {
156
- throw new Error('State mismatch - possible CSRF attack');
157
- }
158
-
159
- // 用 code 换取 tokens
160
- const tokens = await this.exchangeCodeForTokens(code, pkce.verifier);
161
-
162
- // 解析 JWT 提取账户信息
163
- const claims = this.parseJWT(tokens.id_token);
164
-
165
- // 保存凭据(遵循 CLIProxyAPI 格式)
166
- const credentials = {
167
- id_token: tokens.id_token,
168
- access_token: tokens.access_token,
169
- refresh_token: tokens.refresh_token,
170
- account_id: claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub,
171
- last_refresh: new Date().toISOString(),
172
- email: claims.email,
173
- type: 'codex',
174
- expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString()
175
- };
176
-
177
- // 保存凭据并获取路径
178
- const saveResult = await this.saveCredentials(credentials);
179
- const credPath = saveResult.credsPath;
180
- const relativePath = saveResult.relativePath;
181
-
182
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`);
183
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`);
184
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`);
185
-
186
- // 关闭服务器
187
- if (this.server) {
188
- this.server.close();
189
- this.server = null;
190
- }
191
-
192
- return {
193
- ...credentials,
194
- credPath,
195
- relativePath
196
- };
197
- }
198
-
199
- /**
200
- * 启动 OAuth 流程
201
- * @returns {Promise<Object>} 返回 tokens
202
- */
203
- async startOAuthFlow() {
204
- const pkce = this.generatePKCECodes();
205
- const state = crypto.randomBytes(16).toString('hex');
206
-
207
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Starting OAuth flow...`);
208
-
209
- // 启动本地回调服务器
210
- const server = await this.startCallbackServer();
211
-
212
- // 构建授权 URL
213
- const authUrl = new URL(CODEX_OAUTH_CONFIG.authUrl);
214
- authUrl.searchParams.set('client_id', CODEX_OAUTH_CONFIG.clientId);
215
- authUrl.searchParams.set('response_type', 'code');
216
- authUrl.searchParams.set('redirect_uri', CODEX_OAUTH_CONFIG.redirectUri);
217
- authUrl.searchParams.set('scope', CODEX_OAUTH_CONFIG.scopes);
218
- authUrl.searchParams.set('state', state);
219
- authUrl.searchParams.set('code_challenge', pkce.challenge);
220
- authUrl.searchParams.set('code_challenge_method', 'S256');
221
- authUrl.searchParams.set('prompt', 'login');
222
- authUrl.searchParams.set('id_token_add_organizations', 'true');
223
- authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
224
-
225
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Opening browser for authentication...`);
226
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} If browser doesn't open, visit: ${authUrl.toString()}`);
227
-
228
- try {
229
- await open(authUrl.toString());
230
- } catch (error) {
231
- logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to open browser automatically:`, error.message);
232
- }
233
-
234
- // 等待回调
235
- const result = await this.waitForCallback(server, state);
236
-
237
- // 用 code 换取 tokens
238
- const tokens = await this.exchangeCodeForTokens(result.code, pkce.verifier);
239
-
240
- // 解析 JWT 提取账户信息
241
- const claims = this.parseJWT(tokens.id_token);
242
-
243
- // 保存凭据(遵循 CLIProxyAPI 格式)
244
- const credentials = {
245
- id_token: tokens.id_token,
246
- access_token: tokens.access_token,
247
- refresh_token: tokens.refresh_token,
248
- account_id: claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub,
249
- last_refresh: new Date().toISOString(),
250
- email: claims.email,
251
- type: 'codex',
252
- expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString()
253
- };
254
-
255
- await this.saveCredentials(credentials);
256
-
257
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`);
258
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`);
259
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`);
260
-
261
- return credentials;
262
- }
263
-
264
- /**
265
- * 启动回调服务器
266
- * @returns {Promise<http.Server>}
267
- */
268
- async startCallbackServer() {
269
- // 先清理该提供商或该端口的旧服务器
270
- await closeActiveServer('openai-codex-oauth', CODEX_OAUTH_CONFIG.port);
271
-
272
- return new Promise((resolve, reject) => {
273
- const server = http.createServer();
274
-
275
- server.on('request', (req, res) => {
276
- if (req.url.startsWith('/auth/callback')) {
277
- const url = new URL(req.url, `http://localhost:${CODEX_OAUTH_CONFIG.port}`);
278
- const code = url.searchParams.get('code');
279
- const state = url.searchParams.get('state');
280
- const error = url.searchParams.get('error');
281
- const errorDescription = url.searchParams.get('error_description');
282
-
283
- if (error) {
284
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
285
- res.end(`
286
- <!DOCTYPE html>
287
- <html>
288
- <head>
289
- <title>Authentication Failed</title>
290
- <style>
291
- body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
292
- h1 { color: #d32f2f; }
293
- p { color: #666; }
294
- </style>
295
- </head>
296
- <body>
297
- <h1>❌ Authentication Failed</h1>
298
- <p>${errorDescription || error}</p>
299
- <p>You can close this window and try again.</p>
300
- </body>
301
- </html>
302
- `);
303
- server.emit('auth-error', new Error(errorDescription || error));
304
- } else if (code && state) {
305
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
306
- res.end(`
307
- <!DOCTYPE html>
308
- <html>
309
- <head>
310
- <title>Authentication Successful</title>
311
- <style>
312
- body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
313
- h1 { color: #4caf50; }
314
- p { color: #666; }
315
- .countdown { font-size: 24px; font-weight: bold; color: #2196f3; }
316
- </style>
317
- <script>
318
- let countdown = 10;
319
- setInterval(() => {
320
- countdown--;
321
- document.getElementById('countdown').textContent = countdown;
322
- if (countdown <= 0) {
323
- window.close();
324
- }
325
- }, 1000);
326
- </script>
327
- </head>
328
- <body>
329
- <h1>✅ Authentication Successful!</h1>
330
- <p>You can now close this window and return to the application.</p>
331
- <p>This window will close automatically in <span id="countdown" class="countdown">10</span> seconds.</p>
332
- </body>
333
- </html>
334
- `);
335
- server.emit('auth-success', { code, state });
336
- }
337
- } else if (req.url === '/success') {
338
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
339
- res.end('<h1>Success!</h1>');
340
- }
341
- });
342
-
343
- server.listen(CODEX_OAUTH_CONFIG.port, () => {
344
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Callback server listening on port ${CODEX_OAUTH_CONFIG.port}`);
345
- activeServers.set('openai-codex-oauth', { server, port: CODEX_OAUTH_CONFIG.port });
346
- resolve(server);
347
- });
348
-
349
- server.on('error', (error) => {
350
- if (error.code === 'EADDRINUSE') {
351
- reject(new Error(`Port ${CODEX_OAUTH_CONFIG.port} is already in use. Please close other applications using this port.`));
352
- } else {
353
- reject(error);
354
- }
355
- });
356
- });
357
- }
358
-
359
- /**
360
- * 等待 OAuth 回调
361
- * @param {http.Server} server
362
- * @param {string} expectedState
363
- * @returns {Promise<{code: string, state: string}>}
364
- */
365
- async waitForCallback(server, expectedState) {
366
- return new Promise((resolve, reject) => {
367
- const timeout = setTimeout(() => {
368
- server.close();
369
- reject(new Error('Authentication timeout (10 minutes)'));
370
- }, 10 * 60 * 1000); // 10 分钟
371
-
372
- server.once('auth-success', (result) => {
373
- clearTimeout(timeout);
374
- server.close();
375
-
376
- if (result.state !== expectedState) {
377
- reject(new Error('State mismatch - possible CSRF attack'));
378
- } else {
379
- resolve(result);
380
- }
381
- });
382
-
383
- server.once('auth-error', (error) => {
384
- clearTimeout(timeout);
385
- server.close();
386
- reject(error);
387
- });
388
- });
389
- }
390
-
391
- /**
392
- * 用授权码换取 tokens
393
- * @param {string} code
394
- * @param {string} codeVerifier
395
- * @returns {Promise<Object>}
396
- */
397
- async exchangeCodeForTokens(code, codeVerifier) {
398
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Exchanging authorization code for tokens...`);
399
-
400
- try {
401
- const response = await this.httpClient.post(
402
- CODEX_OAUTH_CONFIG.tokenUrl,
403
- new URLSearchParams({
404
- grant_type: 'authorization_code',
405
- client_id: CODEX_OAUTH_CONFIG.clientId,
406
- code: code,
407
- redirect_uri: CODEX_OAUTH_CONFIG.redirectUri,
408
- code_verifier: codeVerifier
409
- }).toString(),
410
- {
411
- headers: {
412
- 'Content-Type': 'application/x-www-form-urlencoded',
413
- 'Accept': 'application/json'
414
- }
415
- }
416
- );
417
-
418
- return response.data;
419
- } catch (error) {
420
- logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token exchange failed:`, error.response?.data || error.message);
421
- throw new Error(`Failed to exchange code for tokens: ${error.response?.data?.error_description || error.message}`);
422
- }
423
- }
424
-
425
- /**
426
- * 刷新 tokens
427
- * @param {string} refreshToken
428
- * @returns {Promise<Object>}
429
- */
430
- async refreshTokens(refreshToken) {
431
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Refreshing access token...`);
432
-
433
- try {
434
- const response = await this.httpClient.post(
435
- CODEX_OAUTH_CONFIG.tokenUrl,
436
- new URLSearchParams({
437
- grant_type: 'refresh_token',
438
- client_id: CODEX_OAUTH_CONFIG.clientId,
439
- refresh_token: refreshToken
440
- }).toString(),
441
- {
442
- headers: {
443
- 'Content-Type': 'application/x-www-form-urlencoded',
444
- 'Accept': 'application/json'
445
- }
446
- }
447
- );
448
-
449
- const tokens = response.data;
450
- const claims = this.parseJWT(tokens.id_token);
451
-
452
- return {
453
- id_token: tokens.id_token,
454
- access_token: tokens.access_token,
455
- refresh_token: tokens.refresh_token || refreshToken,
456
- account_id: claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub,
457
- last_refresh: new Date().toISOString(),
458
- email: claims.email,
459
- type: 'codex',
460
- expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString()
461
- };
462
- } catch (error) {
463
- logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token refresh failed:`, error.response?.data || error.message);
464
- throw new Error(`Failed to refresh tokens: ${error.response?.data?.error_description || error.message}`);
465
- }
466
- }
467
-
468
- /**
469
- * 解析 JWT token
470
- * @param {string} token
471
- * @returns {Object}
472
- */
473
- parseJWT(token) {
474
- try {
475
- const parts = token.split('.');
476
- if (parts.length !== 3) {
477
- throw new Error('Invalid JWT token format');
478
- }
479
-
480
- // 解码 payload (base64url)
481
- const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
482
- return JSON.parse(payload);
483
- } catch (error) {
484
- logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to parse JWT:`, error.message);
485
- throw new Error(`Failed to parse JWT token: ${error.message}`);
486
- }
487
- }
488
-
489
- /**
490
- * 保存凭据到文件
491
- * @param {Object} creds
492
- * @returns {Promise<Object>}
493
- */
494
- async saveCredentials(creds) {
495
- const email = creds.email || this.config.CODEX_EMAIL || 'default';
496
-
497
- // 优先使用配置中指定的路径,否则保存到 configs/codex 目录
498
- let credsPath;
499
- if (this.config.CODEX_OAUTH_CREDS_FILE_PATH) {
500
- credsPath = this.config.CODEX_OAUTH_CREDS_FILE_PATH;
501
- } else {
502
- // 保存到 configs/codex 目录(与其他供应商一致)
503
- const projectDir = process.cwd();
504
- const targetDir = path.join(projectDir, 'configs', 'codex');
505
- await fs.promises.mkdir(targetDir, { recursive: true });
506
- const timestamp = Date.now();
507
- const filename = `${timestamp}_codex-${email}.json`;
508
- credsPath = path.join(targetDir, filename);
509
- }
510
-
511
- try {
512
- const credsDir = path.dirname(credsPath);
513
- await fs.promises.mkdir(credsDir, { recursive: true });
514
- await fs.promises.writeFile(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
515
-
516
- const relativePath = path.relative(process.cwd(), credsPath);
517
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Credentials saved to ${relativePath}`);
518
-
519
- // 返回保存路径供后续使用
520
- return { credsPath, relativePath };
521
- } catch (error) {
522
- logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to save credentials:`, error.message);
523
- throw new Error(`Failed to save credentials: ${error.message}`);
524
- }
525
- }
526
-
527
- /**
528
- * 加载凭据
529
- * @param {string} email
530
- * @returns {Promise<Object|null>}
531
- */
532
- async loadCredentials(email) {
533
- // 优先使用配置中指定的路径,否则从 configs/codex 目录加载
534
- let credsPath;
535
- if (this.config.CODEX_OAUTH_CREDS_FILE_PATH) {
536
- credsPath = this.config.CODEX_OAUTH_CREDS_FILE_PATH;
537
- } else {
538
- // 从 configs/codex 目录加载(与其他供应商一致)
539
- const projectDir = process.cwd();
540
- const targetDir = path.join(projectDir, 'configs', 'codex');
541
-
542
- // 扫描目录找到匹配的凭据文件
543
- try {
544
- const files = await fs.promises.readdir(targetDir);
545
- const emailPattern = email || 'default';
546
- const matchingFile = files
547
- .filter(f => f.includes(`codex-${emailPattern}`) && f.endsWith('.json'))
548
- .sort()
549
- .pop(); // 获取最新的文件
550
-
551
- if (matchingFile) {
552
- credsPath = path.join(targetDir, matchingFile);
553
- } else {
554
- return null;
555
- }
556
- } catch (error) {
557
- if (error.code === 'ENOENT') {
558
- return null;
559
- }
560
- throw error;
561
- }
562
- }
563
-
564
- try {
565
- const data = await fs.promises.readFile(credsPath, 'utf8');
566
- return JSON.parse(data);
567
- } catch (error) {
568
- if (error.code === 'ENOENT') {
569
- return null; // 文件不存在
570
- }
571
- throw error;
572
- }
573
- }
574
-
575
- /**
576
- * 检查凭据文件是否存在
577
- * @param {string} email
578
- * @returns {Promise<boolean>}
579
- */
580
- async credentialsExist(email) {
581
- // 优先使用配置中指定的路径,否则从 configs/codex 目录检查
582
- let credsPath;
583
- if (this.config.CODEX_OAUTH_CREDS_FILE_PATH) {
584
- credsPath = this.config.CODEX_OAUTH_CREDS_FILE_PATH;
585
- } else {
586
- const projectDir = process.cwd();
587
- const targetDir = path.join(projectDir, 'configs', 'codex');
588
-
589
- try {
590
- const files = await fs.promises.readdir(targetDir);
591
- const emailPattern = email || 'default';
592
- const hasMatch = files.some(f =>
593
- f.includes(`codex-${emailPattern}`) && f.endsWith('.json')
594
- );
595
- return hasMatch;
596
- } catch (error) {
597
- return false;
598
- }
599
- }
600
-
601
- try {
602
- await fs.promises.access(credsPath);
603
- return true;
604
- } catch {
605
- return false;
606
- }
607
- }
608
-
609
- /**
610
- * 检查凭据是否已存在(基于 account_id 或 refresh_token)
611
- * @param {string} accountId
612
- * @param {string} refreshToken
613
- * @returns {Promise<{isDuplicate: boolean, existingPath?: string}>}
614
- */
615
- async checkDuplicate(accountId, refreshToken) {
616
- const projectDir = process.cwd();
617
- const targetDir = path.join(projectDir, 'configs', 'codex');
618
-
619
- try {
620
- if (!fs.existsSync(targetDir)) {
621
- return { isDuplicate: false };
622
- }
623
-
624
- const files = await fs.promises.readdir(targetDir);
625
- for (const file of files) {
626
- if (file.endsWith('.json')) {
627
- try {
628
- const fullPath = path.join(targetDir, file);
629
- const content = await fs.promises.readFile(fullPath, 'utf8');
630
- const credentials = JSON.parse(content);
631
-
632
- if ((accountId && credentials.account_id === accountId) || (refreshToken && credentials.refresh_token === refreshToken)) {
633
- const relativePath = path.relative(process.cwd(), fullPath);
634
- return {
635
- isDuplicate: true,
636
- existingPath: relativePath
637
- };
638
- }
639
- } catch (e) {
640
- // 忽略解析错误
641
- }
642
- }
643
- }
644
- return { isDuplicate: false };
645
- } catch (error) {
646
- logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message);
647
- return { isDuplicate: false };
648
- }
649
- }
650
- }
651
-
652
- /**
653
- * 批量导入 Codex Token 并生成凭据文件(流式版本)
654
- * @param {Object[]} tokens - Token 对象数组
655
- * @param {Function} onProgress - 进度回调函数
656
- * @param {boolean} skipDuplicateCheck - 是否跳过重复检查
657
- * @returns {Promise<Object>} 批量处理结果
658
- */
659
- export async function batchImportCodexTokensStream(tokens, onProgress = null, skipDuplicateCheck = false) {
660
- const auth = new CodexAuth({});
661
- const results = {
662
- total: tokens.length,
663
- success: 0,
664
- failed: 0,
665
- details: []
666
- };
667
-
668
- for (let i = 0; i < tokens.length; i++) {
669
- const tokenData = tokens[i];
670
- const progressData = {
671
- index: i + 1,
672
- total: tokens.length,
673
- current: null
674
- };
675
-
676
- try {
677
- // 验证 token 数据
678
- if (!tokenData.access_token || !tokenData.id_token) {
679
- throw new Error('Token 缺少必需字段 (access_token 或 id_token)');
680
- }
681
-
682
- // 解析 JWT 提取账户信息
683
- const claims = auth.parseJWT(tokenData.id_token);
684
- const accountId = claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub;
685
- const email = claims.email;
686
-
687
- // 检查重复
688
- if (!skipDuplicateCheck) {
689
- const duplicateCheck = await auth.checkDuplicate(accountId, tokenData.refresh_token);
690
- if (duplicateCheck.isDuplicate) {
691
- progressData.current = {
692
- index: i + 1,
693
- success: false,
694
- error: 'duplicate',
695
- existingPath: duplicateCheck.existingPath
696
- };
697
- results.failed++;
698
- results.details.push(progressData.current);
699
- if (onProgress) {
700
- onProgress({
701
- ...progressData,
702
- successCount: results.success,
703
- failedCount: results.failed
704
- });
705
- }
706
- continue;
707
- }
708
- }
709
-
710
- // 构建凭据对象
711
- const credentials = {
712
- id_token: tokenData.id_token,
713
- access_token: tokenData.access_token,
714
- refresh_token: tokenData.refresh_token,
715
- account_id: accountId,
716
- last_refresh: new Date().toISOString(),
717
- email: email,
718
- type: 'codex',
719
- expired: new Date(Date.now() + (tokenData.expires_in || 3600) * 1000).toISOString()
720
- };
721
-
722
- // 保存凭据
723
- const saveResult = await auth.saveCredentials(credentials);
724
- const relativePath = saveResult.relativePath;
725
-
726
- logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Token ${i + 1} imported: ${relativePath}`);
727
-
728
- progressData.current = {
729
- index: i + 1,
730
- success: true,
731
- path: relativePath
732
- };
733
- results.success++;
734
-
735
- // 自动关联到 Pools
736
- await autoLinkProviderConfigs(CONFIG, {
737
- onlyCurrentCred: true,
738
- credPath: relativePath
739
- });
740
-
741
- } catch (error) {
742
- logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token ${i + 1} import failed:`, error.message);
743
-
744
- progressData.current = {
745
- index: i + 1,
746
- success: false,
747
- error: error.message
748
- };
749
- results.failed++;
750
- }
751
-
752
- results.details.push(progressData.current);
753
-
754
- if (onProgress) {
755
- onProgress({
756
- ...progressData,
757
- successCount: results.success,
758
- failedCount: results.failed
759
- });
760
- }
761
- }
762
-
763
- if (results.success > 0) {
764
- broadcastEvent('oauth_batch_success', {
765
- provider: 'openai-codex-oauth',
766
- count: results.success,
767
- timestamp: new Date().toISOString()
768
- });
769
- }
770
-
771
- return results;
772
- }
773
-
774
- /**
775
- * 带重试的 Codex token 刷新
776
- * @param {string} refreshToken
777
- * @param {Object} config
778
- * @param {number} maxRetries
779
- * @returns {Promise<Object>}
780
- */
781
- export async function refreshCodexTokensWithRetry(refreshToken, config = {}, maxRetries = 3) {
782
- const auth = new CodexAuth(config);
783
- let lastError;
784
-
785
- for (let i = 0; i < maxRetries; i++) {
786
- try {
787
- return await auth.refreshTokens(refreshToken);
788
- } catch (error) {
789
- lastError = error;
790
- logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Retry ${i + 1}/${maxRetries} failed:`, error.message);
791
-
792
- if (i < maxRetries - 1) {
793
- // 指数退避
794
- const delay = Math.min(1000 * Math.pow(2, i), 10000);
795
- await new Promise(resolve => setTimeout(resolve, delay));
796
- }
797
- }
798
- }
799
-
800
- throw lastError;
801
- }
802
-
803
- /**
804
- * 处理 Codex OAuth 认证
805
- * @param {Object} currentConfig - 当前配置
806
- * @param {Object} options - 选项
807
- * @returns {Promise<Object>} 返回认证结果
808
- */
809
- export async function handleCodexOAuth(currentConfig, options = {}) {
810
- const auth = new CodexAuth(currentConfig);
811
-
812
- try {
813
- logger.info('[Codex Auth] Generating OAuth URL...');
814
-
815
- // 清理所有旧的会话和服务器
816
- if (global.codexOAuthSessions && global.codexOAuthSessions.size > 0) {
817
- logger.info('[Codex Auth] Cleaning up old OAuth sessions...');
818
- for (const [sessionId, session] of global.codexOAuthSessions.entries()) {
819
- try {
820
- // 清理定时器
821
- if (session.pollTimer) {
822
- clearInterval(session.pollTimer);
823
- }
824
- // 不在这里显式关闭 server,由 startCallbackServer 中的 closeActiveServer 处理
825
- global.codexOAuthSessions.delete(sessionId);
826
- } catch (error) {
827
- logger.warn(`[Codex Auth] Failed to clean up session ${sessionId}:`, error.message);
828
- }
829
- }
830
- }
831
-
832
- // 生成授权 URL 和启动回调服务器
833
- const { authUrl, state, pkce, server } = await auth.generateAuthUrl();
834
-
835
- logger.info('[Codex Auth] OAuth URL generated successfully');
836
-
837
- // 存储 OAuth 会话信息,供后续回调使用
838
- if (!global.codexOAuthSessions) {
839
- global.codexOAuthSessions = new Map();
840
- }
841
-
842
- const sessionId = state; // 使用 state 作为 session ID
843
-
844
- // 轮询计数器
845
- let pollCount = 0;
846
- const maxPollCount = 100; // 增加到约 5 分钟 (100 * 3s = 300s)
847
- const pollInterval = 3000; // 轮询间隔(毫秒)
848
- let pollTimer = null;
849
- let isCompleted = false;
850
-
851
- // 创建会话对象
852
- const session = {
853
- auth,
854
- state,
855
- pkce,
856
- server,
857
- pollTimer: null,
858
- createdAt: Date.now()
859
- };
860
-
861
- global.codexOAuthSessions.set(sessionId, session);
862
-
863
- // 启动轮询日志
864
- pollTimer = setInterval(() => {
865
- pollCount++;
866
- if (pollCount <= maxPollCount && !isCompleted) {
867
- logger.info(`[Codex Auth] Waiting for callback... (${pollCount}/${maxPollCount})`);
868
- }
869
-
870
- if (pollCount >= maxPollCount && !isCompleted) {
871
- clearInterval(pollTimer);
872
- const totalSeconds = (maxPollCount * pollInterval) / 1000;
873
- logger.info(`[Codex Auth] Polling timeout (${totalSeconds}s), releasing session for next authorization`);
874
-
875
- // 清理会话
876
- if (global.codexOAuthSessions.has(sessionId)) {
877
- global.codexOAuthSessions.delete(sessionId);
878
- }
879
- }
880
- }, pollInterval);
881
-
882
- // 将 pollTimer 存储到会话中
883
- session.pollTimer = pollTimer;
884
-
885
- // 监听回调服务器的 auth-success 事件,自动完成 OAuth 流程
886
- server.once('auth-success', async (result) => {
887
- isCompleted = true;
888
- if (pollTimer) {
889
- clearInterval(pollTimer);
890
- }
891
-
892
- try {
893
- logger.info('[Codex Auth] Received auth callback, completing OAuth flow...');
894
-
895
- const session = global.codexOAuthSessions.get(sessionId);
896
- if (!session) {
897
- logger.error('[Codex Auth] Session not found');
898
- return;
899
- }
900
-
901
- // 完成 OAuth 流程
902
- const credentials = await auth.completeOAuthFlow(result.code, result.state, session.state, session.pkce);
903
-
904
- // 清理会话
905
- global.codexOAuthSessions.delete(sessionId);
906
-
907
- // 广播认证成功事件
908
- broadcastEvent('oauth_success', {
909
- provider: 'openai-codex-oauth',
910
- credPath: credentials.credPath,
911
- relativePath: credentials.relativePath,
912
- timestamp: new Date().toISOString(),
913
- email: credentials.email,
914
- accountId: credentials.account_id
915
- });
916
-
917
- // 自动关联新生成的凭据到 Pools
918
- await autoLinkProviderConfigs(CONFIG, {
919
- onlyCurrentCred: true,
920
- credPath: credentials.relativePath
921
- });
922
-
923
- logger.info('[Codex Auth] OAuth flow completed successfully');
924
- } catch (error) {
925
- logger.error('[Codex Auth] Failed to complete OAuth flow:', error.message);
926
-
927
- // 广播认证失败事件
928
- broadcastEvent('oauth_error', {
929
- provider: 'openai-codex-oauth',
930
- error: error.message,
931
- timestamp: new Date().toISOString()
932
- });
933
- }
934
- });
935
-
936
- // 监听 auth-error 事件
937
- server.once('auth-error', (error) => {
938
- isCompleted = true;
939
- if (pollTimer) {
940
- clearInterval(pollTimer);
941
- }
942
-
943
- logger.error('[Codex Auth] Auth error:', error.message);
944
- global.codexOAuthSessions.delete(sessionId);
945
-
946
- broadcastEvent('oauth_error', {
947
- provider: 'openai-codex-oauth',
948
- error: error.message,
949
- timestamp: new Date().toISOString()
950
- });
951
- });
952
-
953
- return {
954
- success: true,
955
- authUrl: authUrl,
956
- authInfo: {
957
- provider: 'openai-codex-oauth',
958
- method: 'oauth2-pkce',
959
- sessionId: sessionId,
960
- redirectUri: CODEX_OAUTH_CONFIG.redirectUri,
961
- port: CODEX_OAUTH_CONFIG.port,
962
- instructions: [
963
- '1. 点击下方按钮在浏览器中打开授权链接',
964
- '2. 使用您的 OpenAI 账户登录',
965
- '3. 授权应用访问您的 Codex API',
966
- '4. 授权成功后会自动保存凭据',
967
- '5. 如果浏览器未自动跳转,请手动复制回调 URL'
968
- ]
969
- }
970
- };
971
- } catch (error) {
972
- logger.error('[Codex Auth] Failed to generate OAuth URL:', error.message);
973
-
974
- return {
975
- success: false,
976
- error: error.message,
977
- authInfo: {
978
- provider: 'openai-codex-oauth',
979
- method: 'oauth2-pkce',
980
- instructions: [
981
- `1. 确保端口 ${CODEX_OAUTH_CONFIG.port} 未被占用`,
982
- '2. 确保可以访问 auth.openai.com',
983
- '3. 确保浏览器可以正常打开',
984
- '4. 如果问题持续,请检查网络连接'
985
- ]
986
- }
987
- };
988
- }
989
- }
990
-
991
- /**
992
- * 处理 Codex OAuth 回调
993
- * @param {string} code - 授权码
994
- * @param {string} state - 状态参数
995
- * @returns {Promise<Object>} 返回认证结果
996
- */
997
- export async function handleCodexOAuthCallback(code, state) {
998
- try {
999
- if (!global.codexOAuthSessions || !global.codexOAuthSessions.has(state)) {
1000
- throw new Error('Invalid or expired OAuth session');
1001
- }
1002
-
1003
- const session = global.codexOAuthSessions.get(state);
1004
- const { auth, state: expectedState, pkce } = session;
1005
-
1006
- logger.info('[Codex Auth] Processing OAuth callback...');
1007
-
1008
- // 完成 OAuth 流程
1009
- const result = await auth.completeOAuthFlow(code, state, expectedState, pkce);
1010
-
1011
- // 清理会话
1012
- global.codexOAuthSessions.delete(state);
1013
-
1014
- // 广播认证成功事件(与 gemini 格式一致)
1015
- broadcastEvent('oauth_success', {
1016
- provider: 'openai-codex-oauth',
1017
- credPath: result.credPath,
1018
- relativePath: result.relativePath,
1019
- timestamp: new Date().toISOString(),
1020
- email: result.email,
1021
- accountId: result.account_id
1022
- });
1023
-
1024
- // 自动关联新生成的凭据到 Pools
1025
- await autoLinkProviderConfigs(CONFIG, {
1026
- onlyCurrentCred: true,
1027
- credPath: result.relativePath
1028
- });
1029
-
1030
- logger.info('[Codex Auth] OAuth callback processed successfully');
1031
-
1032
- return {
1033
- success: true,
1034
- message: 'Codex authentication successful',
1035
- credentials: result,
1036
- email: result.email,
1037
- accountId: result.account_id,
1038
- credPath: result.credPath,
1039
- relativePath: result.relativePath
1040
- };
1041
- } catch (error) {
1042
- logger.error('[Codex Auth] OAuth callback failed:', error.message);
1043
-
1044
- // 广播认证失败事件
1045
- broadcastEvent('oauth_error', {
1046
- provider: 'openai-codex-oauth',
1047
- error: error.message,
1048
- timestamp: new Date().toISOString()
1049
- });
1050
-
1051
- return {
1052
- success: false,
1053
- error: error.message
1054
- };
1055
- }
1056
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/auth/gemini-oauth.js DELETED
@@ -1,504 +0,0 @@
1
- import { OAuth2Client } from 'google-auth-library';
2
- import logger from '../utils/logger.js';
3
- import http from 'http';
4
- import fs from 'fs';
5
- import path from 'path';
6
- import os from 'os';
7
- import { broadcastEvent } from '../services/ui-manager.js';
8
- import { autoLinkProviderConfigs } from '../services/service-manager.js';
9
- import { CONFIG } from '../core/config-manager.js';
10
- import { getGoogleAuthProxyConfig } from '../utils/proxy-utils.js';
11
-
12
- /**
13
- * OAuth 提供商配置
14
- */
15
- const OAUTH_PROVIDERS = {
16
- 'gemini-cli-oauth': {
17
- clientId: '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com',
18
- clientSecret: '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',
19
- port: 8085,
20
- credentialsDir: '.gemini',
21
- credentialsFile: 'oauth_creds.json',
22
- scope: ['https://www.googleapis.com/auth/cloud-platform'],
23
- logPrefix: '[Gemini Auth]'
24
- },
25
- 'gemini-antigravity': {
26
- clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
27
- clientSecret: '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',
28
- port: 8086,
29
- credentialsDir: '.antigravity',
30
- credentialsFile: 'oauth_creds.json',
31
- scope: ['https://www.googleapis.com/auth/cloud-platform'],
32
- logPrefix: '[Antigravity Auth]'
33
- }
34
- };
35
-
36
- /**
37
- * 活动的服务器实例管理
38
- */
39
- const activeServers = new Map();
40
-
41
- /**
42
- * 生成 HTML 响应页面
43
- * @param {boolean} isSuccess - 是否成功
44
- * @param {string} message - 显示消息
45
- * @returns {string} HTML 内容
46
- */
47
- function generateResponsePage(isSuccess, message) {
48
- const title = isSuccess ? '授权成功!' : '授权失败';
49
-
50
- return `<!DOCTYPE html>
51
- <html lang="zh-CN">
52
- <head>
53
- <meta charset="utf-8">
54
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
55
- <title>${title}</title>
56
- </head>
57
- <body>
58
- <div class="container">
59
- <h1>${title}</h1>
60
- <p>${message}</p>
61
- </div>
62
- </body>
63
- </html>`;
64
- }
65
-
66
- /**
67
- * 关闭指定端口的活动服务器
68
- * @param {number} port - 端口号
69
- * @returns {Promise<void>}
70
- */
71
- async function closeActiveServer(provider, port = null) {
72
- // 1. 关闭该提供商之前的所有服务器
73
- const existing = activeServers.get(provider);
74
- if (existing) {
75
- // 清理轮询定时器
76
- if (existing.pollTimer) {
77
- clearInterval(existing.pollTimer);
78
- existing.pollTimer = null;
79
- }
80
-
81
- try {
82
- const closePromise = new Promise((resolve, reject) => {
83
- existing.server.close((err) => {
84
- if (err) reject(err);
85
- else resolve();
86
- });
87
- });
88
-
89
- const timeoutPromise = new Promise((_, reject) => {
90
- setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
91
- });
92
-
93
- await Promise.race([closePromise, timeoutPromise]);
94
- logger.info(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
95
- } catch (error) {
96
- logger.warn(`[OAuth] 关闭提供商 ${provider} 服务器失败或超时: ${error.message}`);
97
- } finally {
98
- activeServers.delete(provider);
99
- }
100
- }
101
-
102
- // 2. 如果指定了端口,检查是否有其他提供商占用了该端口
103
- if (port) {
104
- for (const [p, info] of activeServers.entries()) {
105
- if (info.port === port) {
106
- await closeActiveServer(p);
107
- }
108
- }
109
- }
110
- }
111
-
112
- /**
113
- * 创建 OAuth 回调服务器
114
- * @param {Object} config - OAuth 提供商配置
115
- * @param {string} redirectUri - 重定向 URI
116
- * @param {OAuth2Client} authClient - OAuth2 客户端
117
- * @param {string} credPath - 凭据保存路径
118
- * @param {string} provider - 提供商标识
119
- * @returns {Promise<http.Server>} HTTP 服务器实例
120
- */
121
- async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) {
122
- const port = parseInt(options.port) || config.port;
123
- // 先关闭该提供商之前可能运行的所有服务器,或该端口上的旧服务器
124
- await closeActiveServer(provider, port);
125
-
126
- return new Promise((resolve, reject) => {
127
- let pollCount = 0;
128
- const maxPollCount = 100; // 约 5 分钟 (100 * 3s = 300s)
129
- const pollInterval = 3000;
130
- let pollTimer = null;
131
-
132
- const clearPollTimer = () => {
133
- if (pollTimer) {
134
- clearInterval(pollTimer);
135
- pollTimer = null;
136
- }
137
- };
138
-
139
- const server = http.createServer(async (req, res) => {
140
- try {
141
- const url = new URL(req.url, redirectUri);
142
- const code = url.searchParams.get('code');
143
- const errorParam = url.searchParams.get('error');
144
-
145
- if (code) {
146
- clearPollTimer();
147
- logger.info(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`);
148
-
149
- try {
150
- const { tokens } = await authClient.getToken(code);
151
- let finalCredPath = credPath;
152
-
153
- // 如果指定了保存到 configs 目录
154
- if (options.saveToConfigs) {
155
- const providerDir = options.providerDir;
156
- const targetDir = path.join(process.cwd(), 'configs', providerDir);
157
- await fs.promises.mkdir(targetDir, { recursive: true });
158
- const timestamp = Date.now();
159
- const filename = `${timestamp}_oauth_creds.json`;
160
- finalCredPath = path.join(targetDir, filename);
161
- }
162
-
163
- await fs.promises.mkdir(path.dirname(finalCredPath), { recursive: true });
164
- await fs.promises.writeFile(finalCredPath, JSON.stringify(tokens, null, 2));
165
- logger.info(`${config.logPrefix} 新令牌已接收并保存到文件: ${finalCredPath}`);
166
-
167
- const relativePath = path.relative(process.cwd(), finalCredPath);
168
-
169
- // 广播授权成功事件
170
- broadcastEvent('oauth_success', {
171
- provider: provider,
172
- credPath: finalCredPath,
173
- relativePath: relativePath,
174
- timestamp: new Date().toISOString()
175
- });
176
-
177
- // 自动关联新生成的凭据到 Pools
178
- await autoLinkProviderConfigs(CONFIG, {
179
- onlyCurrentCred: true,
180
- credPath: relativePath
181
- });
182
-
183
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
184
- res.end(generateResponsePage(true, '您可以关闭此页面'));
185
- } catch (tokenError) {
186
- logger.error(`${config.logPrefix} 获取令牌失败:`, tokenError);
187
- res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
188
- res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`));
189
- } finally {
190
- server.close(() => {
191
- activeServers.delete(provider);
192
- });
193
- }
194
- } else if (errorParam) {
195
- clearPollTimer();
196
- const errorMessage = `授权失败。Google 返回错误: ${errorParam}`;
197
- logger.error(`${config.logPrefix}`, errorMessage);
198
-
199
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
200
- res.end(generateResponsePage(false, errorMessage));
201
- server.close(() => {
202
- activeServers.delete(provider);
203
- });
204
- } else {
205
- logger.info(`${config.logPrefix} 忽略无关请求: ${req.url}`);
206
- res.writeHead(204);
207
- res.end();
208
- }
209
- } catch (error) {
210
- clearPollTimer();
211
- logger.error(`${config.logPrefix} 处理回调时出错:`, error);
212
- res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
213
- res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
214
-
215
- if (server.listening) {
216
- server.close(() => {
217
- activeServers.delete(provider);
218
- });
219
- }
220
- }
221
- });
222
-
223
- server.on('error', (err) => {
224
- clearPollTimer();
225
- if (err.code === 'EADDRINUSE') {
226
- logger.error(`${config.logPrefix} 端口 ${port} 已被占用`);
227
- reject(new Error(`端口 ${port} 已被占用`));
228
- } else {
229
- logger.error(`${config.logPrefix} 服务器错误:`, err);
230
- reject(err);
231
- }
232
- });
233
-
234
- const host = '0.0.0.0';
235
- server.listen(port, host, () => {
236
- logger.info(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
237
-
238
- // 启动轮询日志
239
- pollTimer = setInterval(() => {
240
- pollCount++;
241
- if (pollCount <= maxPollCount) {
242
- logger.info(`${config.logPrefix} Waiting for callback... (${pollCount}/${maxPollCount})`);
243
- } else {
244
- clearPollTimer();
245
- logger.warn(`${config.logPrefix} Polling timeout, closing server...`);
246
- if (server.listening) {
247
- server.close(() => {
248
- activeServers.delete(provider);
249
- });
250
- }
251
- }
252
- }, pollInterval);
253
-
254
- activeServers.set(provider, { server, port, pollTimer });
255
- resolve(server);
256
- });
257
- });
258
- }
259
-
260
- /**
261
- * 处理 Google OAuth 授权(通用函数)
262
- * @param {string} providerKey - 提供商键名
263
- * @param {Object} currentConfig - 当前配置对象
264
- * @param {Object} options - 额外选项
265
- * @returns {Promise<Object>} 返回授权URL和相关信息
266
- */
267
- async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
268
- const config = OAUTH_PROVIDERS[providerKey];
269
- if (!config) {
270
- throw new Error(`未知的提供商: ${providerKey}`);
271
- }
272
-
273
- const port = parseInt(options.port) || config.port;
274
- const host = 'localhost';
275
- const redirectUri = `http://${host}:${port}`;
276
-
277
- // 获取代理配置
278
- const proxyConfig = getGoogleAuthProxyConfig(currentConfig, providerKey);
279
-
280
- // 构建 OAuth2Client 选项
281
- const oauth2Options = {
282
- clientId: config.clientId,
283
- clientSecret: config.clientSecret,
284
- };
285
-
286
- if (proxyConfig) {
287
- oauth2Options.transporterOptions = proxyConfig;
288
- logger.info(`${config.logPrefix} Using proxy for OAuth token exchange`);
289
- }
290
-
291
- const authClient = new OAuth2Client(oauth2Options);
292
- authClient.redirectUri = redirectUri;
293
-
294
- const authUrl = authClient.generateAuthUrl({
295
- access_type: 'offline',
296
- prompt: 'select_account',
297
- scope: config.scope
298
- });
299
-
300
- // 启动回调服务器
301
- const credPath = path.join(os.homedir(), config.credentialsDir, config.credentialsFile);
302
-
303
- try {
304
- await createOAuthCallbackServer(config, redirectUri, authClient, credPath, providerKey, options);
305
- } catch (error) {
306
- throw new Error(`启动回调服务器失败: ${error.message}`);
307
- }
308
-
309
- return {
310
- authUrl,
311
- authInfo: {
312
- provider: providerKey,
313
- redirectUri: redirectUri,
314
- port: port,
315
- ...options
316
- }
317
- };
318
- }
319
-
320
- /**
321
- * 处理 Gemini CLI OAuth 授权
322
- * @param {Object} currentConfig - 当前配置对象
323
- * @param {Object} options - 额外选项
324
- * @returns {Promise<Object>} 返回授权URL和相关信息
325
- */
326
- export async function handleGeminiCliOAuth(currentConfig, options = {}) {
327
- return handleGoogleOAuth('gemini-cli-oauth', currentConfig, options);
328
- }
329
-
330
- /**
331
- * 处理 Gemini Antigravity OAuth 授权
332
- * @param {Object} currentConfig - 当前配置对象
333
- * @param {Object} options - 额外选项
334
- * @returns {Promise<Object>} 返回授权URL和相关信息
335
- */
336
- export async function handleGeminiAntigravityOAuth(currentConfig, options = {}) {
337
- return handleGoogleOAuth('gemini-antigravity', currentConfig, options);
338
- }
339
-
340
- /**
341
- * 检查 Gemini 凭据是否已存在(基于 refresh_token)
342
- * @param {string} providerType - 提供商类型
343
- * @param {string} refreshToken - 要检查的 refreshToken
344
- * @returns {Promise<{isDuplicate: boolean, existingPath?: string}>} 检查结果
345
- */
346
- export async function checkGeminiCredentialsDuplicate(providerType, refreshToken) {
347
- const config = OAUTH_PROVIDERS[providerType];
348
- if (!config) return { isDuplicate: false };
349
-
350
- const providerDir = config.credentialsDir.replace('.', '');
351
- const targetDir = path.join(process.cwd(), 'configs', providerDir);
352
-
353
- try {
354
- if (!fs.existsSync(targetDir)) {
355
- return { isDuplicate: false };
356
- }
357
-
358
- const files = await fs.promises.readdir(targetDir);
359
- for (const file of files) {
360
- if (file.endsWith('.json')) {
361
- try {
362
- const fullPath = path.join(targetDir, file);
363
- const content = await fs.promises.readFile(fullPath, 'utf8');
364
- const credentials = JSON.parse(content);
365
-
366
- if (credentials.refresh_token === refreshToken) {
367
- const relativePath = path.relative(process.cwd(), fullPath);
368
- return {
369
- isDuplicate: true,
370
- existingPath: relativePath
371
- };
372
- }
373
- } catch (e) {
374
- // 忽略解析错误
375
- }
376
- }
377
- }
378
- return { isDuplicate: false };
379
- } catch (error) {
380
- logger.warn(`[Gemini Auth] Error checking duplicates for ${providerType}:`, error.message);
381
- return { isDuplicate: false };
382
- }
383
- }
384
-
385
- /**
386
- * 批量导入 Gemini Token 并生成凭据文件(流式版本,支持实时进度回调)
387
- * @param {string} providerType - 提供商类型 ('gemini-cli-oauth' 或 'gemini-antigravity')
388
- * @param {Object[]} tokens - Token 对象数组
389
- * @param {Function} onProgress - 进度回调函数
390
- * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
391
- * @returns {Promise<Object>} 批量处理结果
392
- */
393
- export async function batchImportGeminiTokensStream(providerType, tokens, onProgress = null, skipDuplicateCheck = false) {
394
- const config = OAUTH_PROVIDERS[providerType];
395
- if (!config) {
396
- throw new Error(`未知的提供商: ${providerType}`);
397
- }
398
-
399
- const results = {
400
- total: tokens.length,
401
- success: 0,
402
- failed: 0,
403
- details: []
404
- };
405
-
406
- for (let i = 0; i < tokens.length; i++) {
407
- const token = tokens[i];
408
- const progressData = {
409
- index: i + 1,
410
- total: tokens.length,
411
- current: null
412
- };
413
-
414
- try {
415
- // 验证 token 是否包含必需字段 (通常是 access_token 和 refresh_token)
416
- if (!token.access_token || !token.refresh_token) {
417
- throw new Error('Token 缺少必需字段 (access_token 或 refresh_token)');
418
- }
419
-
420
- // 检查重复
421
- if (!skipDuplicateCheck) {
422
- const duplicateCheck = await checkGeminiCredentialsDuplicate(providerType, token.refresh_token);
423
- if (duplicateCheck.isDuplicate) {
424
- progressData.current = {
425
- index: i + 1,
426
- success: false,
427
- error: 'duplicate',
428
- existingPath: duplicateCheck.existingPath
429
- };
430
- results.failed++;
431
- results.details.push(progressData.current);
432
- if (onProgress) {
433
- onProgress({
434
- ...progressData,
435
- successCount: results.success,
436
- failedCount: results.failed
437
- });
438
- }
439
- continue;
440
- }
441
- }
442
-
443
- // 生成文件路径
444
- const timestamp = Date.now();
445
- const providerDir = config.credentialsDir.replace('.', ''); // 去掉开头的点
446
- const targetDir = path.join(process.cwd(), 'configs', providerDir);
447
- await fs.promises.mkdir(targetDir, { recursive: true });
448
-
449
- const filename = `${timestamp}_${i}_oauth_creds.json`;
450
- const credPath = path.join(targetDir, filename);
451
-
452
- await fs.promises.writeFile(credPath, JSON.stringify(token, null, 2));
453
-
454
- const relativePath = path.relative(process.cwd(), credPath);
455
-
456
- logger.info(`${config.logPrefix} Token ${i + 1} 已导入并保存: ${relativePath}`);
457
-
458
- progressData.current = {
459
- index: i + 1,
460
- success: true,
461
- path: relativePath
462
- };
463
- results.success++;
464
-
465
- // 自动关联新生成的凭据到 Pools
466
- await autoLinkProviderConfigs(CONFIG, {
467
- onlyCurrentCred: true,
468
- credPath: relativePath
469
- });
470
-
471
- } catch (error) {
472
- logger.error(`${config.logPrefix} Token ${i + 1} 导入失败:`, error.message);
473
-
474
- progressData.current = {
475
- index: i + 1,
476
- success: false,
477
- error: error.message
478
- };
479
- results.failed++;
480
- }
481
-
482
- results.details.push(progressData.current);
483
-
484
- // 发送进度更新
485
- if (onProgress) {
486
- onProgress({
487
- ...progressData,
488
- successCount: results.success,
489
- failedCount: results.failed
490
- });
491
- }
492
- }
493
-
494
- // 如果有成功的,广播事件
495
- if (results.success > 0) {
496
- broadcastEvent('oauth_batch_success', {
497
- provider: providerType,
498
- count: results.success,
499
- timestamp: new Date().toISOString()
500
- });
501
- }
502
-
503
- return results;
504
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/auth/iflow-oauth.js DELETED
@@ -1,539 +0,0 @@
1
- import http from 'http';
2
- import logger from '../utils/logger.js';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import os from 'os';
6
- import crypto from 'crypto';
7
- import { broadcastEvent } from '../services/ui-manager.js';
8
- import { autoLinkProviderConfigs } from '../services/service-manager.js';
9
- import { CONFIG } from '../core/config-manager.js';
10
- import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
11
-
12
- /**
13
- * iFlow OAuth 配置
14
- */
15
- const IFLOW_OAUTH_CONFIG = {
16
- // OAuth 端点
17
- tokenEndpoint: 'https://iflow.cn/oauth/token',
18
- authorizeEndpoint: 'https://iflow.cn/oauth',
19
- userInfoEndpoint: 'https://iflow.cn/api/oauth/getUserInfo',
20
- successRedirectURL: 'https://iflow.cn/oauth/success',
21
-
22
- // 客户端凭据
23
- clientId: '10009311001',
24
- clientSecret: '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW',
25
-
26
- // 本地回调端口
27
- callbackPort: 8087,
28
-
29
- // 凭据存储
30
- credentialsDir: '.iflow',
31
- credentialsFile: 'oauth_creds.json',
32
-
33
- // 日志前缀
34
- logPrefix: '[iFlow Auth]'
35
- };
36
-
37
- /**
38
- * 活动的 iFlow 回调服务器管理
39
- */
40
- const activeIFlowServers = new Map();
41
-
42
- /**
43
- * 创建带代理支持的 fetch 请求
44
- * 使用 axios 替代原生 fetch,以正确支持代理配置
45
- * @param {string} url - 请求 URL
46
- * @param {Object} options - fetch 选项(兼容 fetch API 格式)
47
- * @param {string} providerType - 提供商类型,用于获取代理配置
48
- * @returns {Promise<Object>} 返回类似 fetch Response 的对象
49
- */
50
- async function fetchWithProxy(url, options = {}, providerType) {
51
- const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
52
-
53
- // 构建 axios 配置
54
- const axiosConfig = {
55
- url,
56
- method: options.method || 'GET',
57
- headers: options.headers || {},
58
- timeout: 30000, // 30 秒超时
59
- };
60
-
61
- // 处理请求体
62
- if (options.body) {
63
- axiosConfig.data = options.body;
64
- }
65
-
66
- // 配置代理
67
- if (proxyConfig) {
68
- axiosConfig.httpAgent = proxyConfig.httpAgent;
69
- axiosConfig.httpsAgent = proxyConfig.httpsAgent;
70
- axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent
71
- logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`);
72
- }
73
-
74
- try {
75
- const axios = (await import('axios')).default;
76
- const response = await axios(axiosConfig);
77
-
78
- // 返回类似 fetch Response 的对象
79
- return {
80
- ok: response.status >= 200 && response.status < 300,
81
- status: response.status,
82
- statusText: response.statusText,
83
- headers: response.headers,
84
- json: async () => response.data,
85
- text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
86
- };
87
- } catch (error) {
88
- // 处理 axios 错误,转换为类似 fetch 的响应格式
89
- if (error.response) {
90
- // 服务器返回了错误状态码
91
- return {
92
- ok: false,
93
- status: error.response.status,
94
- statusText: error.response.statusText,
95
- headers: error.response.headers,
96
- json: async () => error.response.data,
97
- text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data),
98
- };
99
- }
100
- // 网络错误或其他错误
101
- throw error;
102
- }
103
- }
104
-
105
- /**
106
- * 生成 HTML 响应页面
107
- * @param {boolean} isSuccess - 是否成功
108
- * @param {string} message - 显示消息
109
- * @returns {string} HTML 内容
110
- */
111
- function generateResponsePage(isSuccess, message) {
112
- const title = isSuccess ? '授权成功!' : '授权失败';
113
-
114
- return `<!DOCTYPE html>
115
- <html lang="zh-CN">
116
- <head>
117
- <meta charset="utf-8">
118
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
119
- <title>${title}</title>
120
- </head>
121
- <body>
122
- <div class="container">
123
- <h1>${title}</h1>
124
- <p>${message}</p>
125
- </div>
126
- </body>
127
- </html>`;
128
- }
129
-
130
- /**
131
- * 生成 iFlow 授权链接
132
- * @param {string} state - 状态参数
133
- * @param {number} port - 回调端口
134
- * @returns {Object} 包含 authUrl 和 redirectUri
135
- */
136
- function generateIFlowAuthorizationURL(state, port) {
137
- const redirectUri = `http://localhost:${port}/oauth2callback`;
138
- const params = new URLSearchParams({
139
- loginMethod: 'phone',
140
- type: 'phone',
141
- redirect: redirectUri,
142
- state: state,
143
- client_id: IFLOW_OAUTH_CONFIG.clientId
144
- });
145
- const authUrl = `${IFLOW_OAUTH_CONFIG.authorizeEndpoint}?${params.toString()}`;
146
- return { authUrl, redirectUri };
147
- }
148
-
149
- /**
150
- * 交换授权码获取 iFlow 令牌
151
- * @param {string} code - 授权码
152
- * @param {string} redirectUri - 重定向 URI
153
- * @returns {Promise<Object>} 令牌数据
154
- */
155
- async function exchangeIFlowCodeForTokens(code, redirectUri) {
156
- const form = new URLSearchParams({
157
- grant_type: 'authorization_code',
158
- code: code,
159
- redirect_uri: redirectUri,
160
- client_id: IFLOW_OAUTH_CONFIG.clientId,
161
- client_secret: IFLOW_OAUTH_CONFIG.clientSecret
162
- });
163
-
164
- // 生成 Basic Auth 头
165
- const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64');
166
-
167
- const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, {
168
- method: 'POST',
169
- headers: {
170
- 'Content-Type': 'application/x-www-form-urlencoded',
171
- 'Accept': 'application/json',
172
- 'Authorization': `Basic ${basicAuth}`
173
- },
174
- body: form.toString()
175
- }, 'openai-iflow');
176
-
177
- if (!response.ok) {
178
- const errorText = await response.text();
179
- throw new Error(`iFlow token exchange failed: ${response.status} ${errorText}`);
180
- }
181
-
182
- const tokenData = await response.json();
183
-
184
- if (!tokenData.access_token) {
185
- throw new Error('iFlow token: missing access token in response');
186
- }
187
-
188
- return {
189
- accessToken: tokenData.access_token,
190
- refreshToken: tokenData.refresh_token,
191
- tokenType: tokenData.token_type,
192
- scope: tokenData.scope,
193
- expiresIn: tokenData.expires_in,
194
- expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString()
195
- };
196
- }
197
-
198
- /**
199
- * 获取 iFlow 用户信息(包含 API Key)
200
- * @param {string} accessToken - 访问令牌
201
- * @returns {Promise<Object>} 用户信息
202
- */
203
- async function fetchIFlowUserInfo(accessToken) {
204
- if (!accessToken || accessToken.trim() === '') {
205
- throw new Error('iFlow api key: access token is empty');
206
- }
207
-
208
- const endpoint = `${IFLOW_OAUTH_CONFIG.userInfoEndpoint}?accessToken=${encodeURIComponent(accessToken)}`;
209
-
210
- const response = await fetchWithProxy(endpoint, {
211
- method: 'GET',
212
- headers: {
213
- 'Accept': 'application/json'
214
- }
215
- }, 'openai-iflow');
216
-
217
- if (!response.ok) {
218
- const errorText = await response.text();
219
- throw new Error(`iFlow user info failed: ${response.status} ${errorText}`);
220
- }
221
-
222
- const result = await response.json();
223
-
224
- if (!result.success) {
225
- throw new Error('iFlow api key: request not successful');
226
- }
227
-
228
- if (!result.data || !result.data.apiKey) {
229
- throw new Error('iFlow api key: missing api key in response');
230
- }
231
-
232
- // 获取邮箱或手机号作为账户标识
233
- let email = (result.data.email || '').trim();
234
- if (!email) {
235
- email = (result.data.phone || '').trim();
236
- }
237
- if (!email) {
238
- throw new Error('iFlow token: missing account email/phone in user info');
239
- }
240
-
241
- return {
242
- apiKey: result.data.apiKey,
243
- email: email,
244
- phone: result.data.phone || ''
245
- };
246
- }
247
-
248
- /**
249
- * 关闭 iFlow 服务器
250
- * @param {string} provider - 提供商标识
251
- * @param {number} port - 端口号(可选)
252
- */
253
- async function closeIFlowServer(provider, port = null) {
254
- const existing = activeIFlowServers.get(provider);
255
- if (existing) {
256
- try {
257
- const closePromise = new Promise((resolve, reject) => {
258
- existing.server.close((err) => {
259
- if (err) reject(err);
260
- else resolve();
261
- });
262
- });
263
-
264
- const timeoutPromise = new Promise((_, reject) => {
265
- setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
266
- });
267
-
268
- await Promise.race([closePromise, timeoutPromise]);
269
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
270
- } catch (error) {
271
- logger.warn(`${IFLOW_OAUTH_CONFIG.logPrefix} 关闭提供商 ${provider} 服务器失败或超时: ${error.message}`);
272
- } finally {
273
- activeIFlowServers.delete(provider);
274
- }
275
- }
276
-
277
- if (port) {
278
- for (const [p, info] of activeIFlowServers.entries()) {
279
- if (info.port === port) {
280
- await closeIFlowServer(p);
281
- }
282
- }
283
- }
284
- }
285
-
286
- /**
287
- * 创建 iFlow OAuth 回调服务器
288
- * @param {number} port - 端口号
289
- * @param {string} redirectUri - 重定向 URI
290
- * @param {string} expectedState - 预期的 state 参数
291
- * @param {Object} options - 额外选项
292
- * @returns {Promise<http.Server>} HTTP 服务器实例
293
- */
294
- function createIFlowCallbackServer(port, redirectUri, expectedState, options = {}) {
295
- return new Promise((resolve, reject) => {
296
- const server = http.createServer(async (req, res) => {
297
- try {
298
- const url = new URL(req.url, `http://localhost:${port}`);
299
-
300
- if (url.pathname === '/oauth2callback') {
301
- const code = url.searchParams.get('code');
302
- const state = url.searchParams.get('state');
303
- const errorParam = url.searchParams.get('error');
304
-
305
- if (errorParam) {
306
- logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 授权失败: ${errorParam}`);
307
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
308
- res.end(generateResponsePage(false, `授权失败: ${errorParam}`));
309
- server.close(() => {
310
- activeIFlowServers.delete('openai-iflow');
311
- });
312
- return;
313
- }
314
-
315
- if (state !== expectedState) {
316
- logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} State 验证失败`);
317
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
318
- res.end(generateResponsePage(false, 'State 验证失败'));
319
- server.close(() => {
320
- activeIFlowServers.delete('openai-iflow');
321
- });
322
- return;
323
- }
324
-
325
- if (!code) {
326
- logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 缺少授权码`);
327
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
328
- res.end(generateResponsePage(false, '缺少授权码'));
329
- server.close(() => {
330
- activeIFlowServers.delete('openai-iflow');
331
- });
332
- return;
333
- }
334
-
335
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`);
336
-
337
- try {
338
- // 1. 交换授权码获取令牌
339
- const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri);
340
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`);
341
-
342
- // 2. 获取用户信息(包含 API Key)
343
- const userInfo = await fetchIFlowUserInfo(tokenData.accessToken);
344
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`);
345
-
346
- // 3. 组合完整的凭据数据
347
- const credentialsData = {
348
- access_token: tokenData.accessToken,
349
- refresh_token: tokenData.refreshToken,
350
- expiry_date: new Date(tokenData.expiresAt).getTime(),
351
- token_type: tokenData.tokenType,
352
- scope: tokenData.scope,
353
- apiKey: userInfo.apiKey
354
- };
355
-
356
- // 4. 保存凭据
357
- let credPath = path.join(os.homedir(), IFLOW_OAUTH_CONFIG.credentialsDir, IFLOW_OAUTH_CONFIG.credentialsFile);
358
-
359
- if (options.saveToConfigs) {
360
- const providerDir = options.providerDir || 'iflow';
361
- const targetDir = path.join(process.cwd(), 'configs', providerDir);
362
- await fs.promises.mkdir(targetDir, { recursive: true });
363
- const timestamp = Date.now();
364
- const filename = `${timestamp}_oauth_creds.json`;
365
- credPath = path.join(targetDir, filename);
366
- }
367
-
368
- await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
369
- await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
370
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 凭据已保存: ${credPath}`);
371
-
372
- const relativePath = path.relative(process.cwd(), credPath);
373
-
374
- // 5. 广播授权成功事件
375
- broadcastEvent('oauth_success', {
376
- provider: 'openai-iflow',
377
- credPath: credPath,
378
- relativePath: relativePath,
379
- email: userInfo.email,
380
- timestamp: new Date().toISOString()
381
- });
382
-
383
- // 6. 自动关联新生成的凭据到 Pools
384
- await autoLinkProviderConfigs(CONFIG, {
385
- onlyCurrentCred: true,
386
- credPath: relativePath
387
- });
388
-
389
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
390
- res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可���关闭此页面`));
391
-
392
- } catch (tokenError) {
393
- logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌处理失败:`, tokenError);
394
- res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
395
- res.end(generateResponsePage(false, `令牌处理失败: ${tokenError.message}`));
396
- } finally {
397
- server.close(() => {
398
- activeIFlowServers.delete('openai-iflow');
399
- });
400
- }
401
- } else {
402
- // 忽略其他请求
403
- res.writeHead(204);
404
- res.end();
405
- }
406
- } catch (error) {
407
- logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error);
408
- res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
409
- res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
410
-
411
- if (server.listening) {
412
- server.close(() => {
413
- activeIFlowServers.delete('openai-iflow');
414
- });
415
- }
416
- }
417
- });
418
-
419
- server.on('error', (err) => {
420
- if (err.code === 'EADDRINUSE') {
421
- logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`);
422
- reject(new Error(`端口 ${port} 已被占用`));
423
- } else {
424
- logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err);
425
- reject(err);
426
- }
427
- });
428
-
429
- const host = '0.0.0.0';
430
- server.listen(port, host, () => {
431
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
432
- resolve(server);
433
- });
434
-
435
- // 10 分钟超时自动关闭
436
- setTimeout(() => {
437
- if (server.listening) {
438
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`);
439
- server.close(() => {
440
- activeIFlowServers.delete('openai-iflow');
441
- });
442
- }
443
- }, 10 * 60 * 1000);
444
- });
445
- }
446
-
447
- /**
448
- * 处理 iFlow OAuth 授权
449
- * @param {Object} currentConfig - 当前配置对象
450
- * @param {Object} options - 额外选项
451
- * - port: 自定义端口号
452
- * - saveToConfigs: 是否保存到 configs 目录
453
- * - providerDir: 提供商目录名
454
- * @returns {Promise<Object>} 返回授权URL和相关信息
455
- */
456
- export async function handleIFlowOAuth(currentConfig, options = {}) {
457
- const port = parseInt(options.port) || IFLOW_OAUTH_CONFIG.callbackPort;
458
- const providerKey = 'openai-iflow';
459
-
460
- // 生成 state 参数
461
- const state = crypto.randomBytes(16).toString('base64url');
462
-
463
- // 生成授权链接
464
- const { authUrl, redirectUri } = generateIFlowAuthorizationURL(state, port);
465
-
466
- logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`);
467
-
468
- // 关闭之前可能存在的服务器
469
- await closeIFlowServer(providerKey, port);
470
-
471
- // 启动回调服务器
472
- try {
473
- const server = await createIFlowCallbackServer(port, redirectUri, state, options);
474
- activeIFlowServers.set(providerKey, { server, port });
475
- } catch (error) {
476
- throw new Error(`启动 iFlow 回调服务器失败: ${error.message}`);
477
- }
478
-
479
- return {
480
- authUrl,
481
- authInfo: {
482
- provider: 'openai-iflow',
483
- redirectUri: redirectUri,
484
- callbackPort: port,
485
- state: state,
486
- ...options
487
- }
488
- };
489
- }
490
-
491
- /**
492
- * 使用 refresh_token 刷新 iFlow 令牌
493
- * @param {string} refreshToken - 刷新令牌
494
- * @returns {Promise<Object>} 新的令牌数据
495
- */
496
- export async function refreshIFlowTokens(refreshToken) {
497
- const form = new URLSearchParams({
498
- grant_type: 'refresh_token',
499
- refresh_token: refreshToken,
500
- client_id: IFLOW_OAUTH_CONFIG.clientId,
501
- client_secret: IFLOW_OAUTH_CONFIG.clientSecret
502
- });
503
-
504
- // 生成 Basic Auth 头
505
- const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64');
506
-
507
- const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, {
508
- method: 'POST',
509
- headers: {
510
- 'Content-Type': 'application/x-www-form-urlencoded',
511
- 'Accept': 'application/json',
512
- 'Authorization': `Basic ${basicAuth}`
513
- },
514
- body: form.toString()
515
- }, 'openai-iflow');
516
-
517
- if (!response.ok) {
518
- const errorText = await response.text();
519
- throw new Error(`iFlow token refresh failed: ${response.status} ${errorText}`);
520
- }
521
-
522
- const tokenData = await response.json();
523
-
524
- if (!tokenData.access_token) {
525
- throw new Error('iFlow token refresh: missing access token in response');
526
- }
527
-
528
- // 获取用户信息以更新 API Key
529
- const userInfo = await fetchIFlowUserInfo(tokenData.access_token);
530
-
531
- return {
532
- access_token: tokenData.access_token,
533
- refresh_token: tokenData.refresh_token,
534
- expiry_date: Date.now() + tokenData.expires_in * 1000,
535
- token_type: tokenData.token_type,
536
- scope: tokenData.scope,
537
- apiKey: userInfo.apiKey
538
- };
539
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/auth/index.js DELETED
@@ -1,35 +0,0 @@
1
- // Codex OAuth
2
- export {
3
- refreshCodexTokensWithRetry,
4
- handleCodexOAuth,
5
- handleCodexOAuthCallback,
6
- batchImportCodexTokensStream
7
- } from './codex-oauth.js';
8
-
9
- // Gemini OAuth
10
- export {
11
- handleGeminiCliOAuth,
12
- handleGeminiAntigravityOAuth,
13
- batchImportGeminiTokensStream,
14
- checkGeminiCredentialsDuplicate
15
- } from './gemini-oauth.js';
16
-
17
- // Qwen OAuth
18
- export {
19
- handleQwenOAuth
20
- } from './qwen-oauth.js';
21
-
22
- // Kiro OAuth
23
- export {
24
- handleKiroOAuth,
25
- checkKiroCredentialsDuplicate,
26
- batchImportKiroRefreshTokens,
27
- batchImportKiroRefreshTokensStream,
28
- importAwsCredentials
29
- } from './kiro-oauth.js';
30
-
31
- // iFlow OAuth
32
- export {
33
- handleIFlowOAuth,
34
- refreshIFlowTokens
35
- } from './iflow-oauth.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/auth/kiro-oauth.js DELETED
@@ -1,1149 +0,0 @@
1
- import http from 'http';
2
- import logger from '../utils/logger.js';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import crypto from 'crypto';
6
- import os from 'os';
7
- import { broadcastEvent } from '../services/ui-manager.js';
8
- import { autoLinkProviderConfigs } from '../services/service-manager.js';
9
- import { CONFIG } from '../core/config-manager.js';
10
- import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
11
-
12
- /**
13
- * Kiro OAuth 配置(支持多种认证方式)
14
- */
15
- const KIRO_OAUTH_CONFIG = {
16
- // Kiro Auth Service 端点 (用于 Social Auth)
17
- authServiceEndpoint: 'https://prod.us-east-1.auth.desktop.kiro.dev',
18
-
19
- // AWS SSO OIDC 端点 (用于 Builder ID)
20
- ssoOIDCEndpoint: 'https://oidc.{{region}}.amazonaws.com',
21
-
22
- // AWS Builder ID 起始 URL
23
- builderIDStartURL: 'https://view.awsapps.com/start',
24
-
25
- // 本地回调端口范围(用于 Social Auth HTTP 回调)
26
- callbackPortStart: 19876,
27
- callbackPortEnd: 19880,
28
-
29
- // 超时配置
30
- authTimeout: 10 * 60 * 1000, // 10 分钟
31
- pollInterval: 5000, // 5 秒
32
-
33
- // CodeWhisperer Scopes
34
- scopes: [
35
- 'codewhisperer:completions',
36
- 'codewhisperer:analysis',
37
- 'codewhisperer:conversations',
38
- // 'codewhisperer:transformations',
39
- // 'codewhisperer:taskassist'
40
- ],
41
-
42
- // 凭据存储(符合现有规范)
43
- credentialsDir: '.kiro',
44
- credentialsFile: 'oauth_creds.json',
45
-
46
- // 日志前缀
47
- logPrefix: '[Kiro Auth]'
48
- };
49
-
50
- /**
51
- * 活动的 Kiro 回调服务器管理
52
- */
53
- const activeKiroServers = new Map();
54
-
55
- /**
56
- * 活动的 Kiro 轮询任务管理(用于 Builder ID Device Code)
57
- */
58
- const activeKiroPollingTasks = new Map();
59
-
60
- /**
61
- * 创建带代理支持的 fetch 请求
62
- * 使用 axios 替代原生 fetch,以正确支持代理配置
63
- * @param {string} url - 请求 URL
64
- * @param {Object} options - fetch 选项(兼容 fetch API 格式)
65
- * @param {string} providerType - 提供商类型,用于获取代理配置
66
- * @returns {Promise<Object>} 返回类似 fetch Response 的对象
67
- */
68
- async function fetchWithProxy(url, options = {}, providerType) {
69
- const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
70
-
71
- // 构建 axios 配置
72
- const axiosConfig = {
73
- url,
74
- method: options.method || 'GET',
75
- headers: options.headers || {},
76
- timeout: 30000, // 30 秒超时
77
- };
78
-
79
- // 处理请求体
80
- if (options.body) {
81
- axiosConfig.data = options.body;
82
- }
83
-
84
- // 配置代理
85
- if (proxyConfig) {
86
- axiosConfig.httpAgent = proxyConfig.httpAgent;
87
- axiosConfig.httpsAgent = proxyConfig.httpsAgent;
88
- axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent
89
- logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`);
90
- }
91
-
92
- try {
93
- const axios = (await import('axios')).default;
94
- const response = await axios(axiosConfig);
95
-
96
- // 返回类似 fetch Response 的对象
97
- return {
98
- ok: response.status >= 200 && response.status < 300,
99
- status: response.status,
100
- statusText: response.statusText,
101
- headers: response.headers,
102
- json: async () => response.data,
103
- text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
104
- };
105
- } catch (error) {
106
- // 处理 axios 错误,转换为类似 fetch 的响应格式
107
- if (error.response) {
108
- // 服务器返回了错误状态码
109
- return {
110
- ok: false,
111
- status: error.response.status,
112
- statusText: error.response.statusText,
113
- headers: error.response.headers,
114
- json: async () => error.response.data,
115
- text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data),
116
- };
117
- }
118
- // 网络错误或其他错误
119
- throw error;
120
- }
121
- }
122
-
123
- /**
124
- * 生成 HTML 响应页面
125
- * @param {boolean} isSuccess - 是否成功
126
- * @param {string} message - 显示消息
127
- * @returns {string} HTML 内容
128
- */
129
- function generateResponsePage(isSuccess, message) {
130
- const title = isSuccess ? '授权成功!' : '授权失败';
131
-
132
- return `<!DOCTYPE html>
133
- <html lang="zh-CN">
134
- <head>
135
- <meta charset="utf-8">
136
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
137
- <title>${title}</title>
138
- </head>
139
- <body>
140
- <div class="container">
141
- <h1>${title}</h1>
142
- <p>${message}</p>
143
- </div>
144
- </body>
145
- </html>`;
146
- }
147
-
148
- /**
149
- * 生成 PKCE 代码验证器
150
- * @returns {string} Base64URL 编码的随机字符串
151
- */
152
- function generateCodeVerifier() {
153
- return crypto.randomBytes(32).toString('base64url');
154
- }
155
-
156
- /**
157
- * 生成 PKCE 代码挑战
158
- * @param {string} codeVerifier - 代码验证器
159
- * @returns {string} Base64URL 编码的 SHA256 哈希
160
- */
161
- function generateCodeChallenge(codeVerifier) {
162
- const hash = crypto.createHash('sha256');
163
- hash.update(codeVerifier);
164
- return hash.digest('base64url');
165
- }
166
-
167
- /**
168
- * 处理 Kiro OAuth 授权(统一入口)
169
- * @param {Object} currentConfig - 当前配置对象
170
- * @param {Object} options - 额外选项
171
- * - method: 'google' | 'github' | 'builder-id'
172
- * - saveToConfigs: boolean
173
- * @returns {Promise<Object>} 返回授权URL和相关信息
174
- */
175
- export async function handleKiroOAuth(currentConfig, options = {}) {
176
- const method = options.method || options.authMethod || 'google'; // 默认使用 Google,同时支持 authMethod 参数
177
-
178
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`);
179
-
180
- switch (method) {
181
- case 'google':
182
- return handleKiroSocialAuth('Google', currentConfig, options);
183
- case 'github':
184
- return handleKiroSocialAuth('Github', currentConfig, options);
185
- case 'builder-id':
186
- return handleKiroBuilderIDDeviceCode(currentConfig, options);
187
- default:
188
- throw new Error(`不支持的认证方式: ${method}`);
189
- }
190
- }
191
-
192
- /**
193
- * Kiro Social Auth (Google/GitHub) - 使用 HTTP localhost 回调
194
- */
195
- async function handleKiroSocialAuth(provider, currentConfig, options = {}) {
196
- // 生成 PKCE 参数
197
- const codeVerifier = generateCodeVerifier();
198
- const codeChallenge = generateCodeChallenge(codeVerifier);
199
- const state = crypto.randomBytes(16).toString('base64url');
200
-
201
- // 启动本地回调服务器并获取端口
202
- let handlerPort;
203
- const providerKey = 'claude-kiro-oauth';
204
- if (options.port) {
205
- const port = parseInt(options.port);
206
- await closeKiroServer(providerKey, port);
207
- const server = await createKiroHttpCallbackServer(port, codeVerifier, state, options);
208
- activeKiroServers.set(providerKey, { server, port });
209
- handlerPort = port;
210
- } else {
211
- handlerPort = await startKiroCallbackServer(codeVerifier, state, options);
212
- }
213
-
214
- // 使用 HTTP localhost 作为 redirect_uri
215
- const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`;
216
-
217
- // 构建授权 URL
218
- const authUrl = `${KIRO_OAUTH_CONFIG.authServiceEndpoint}/login?` +
219
- `idp=${provider}&` +
220
- `redirect_uri=${encodeURIComponent(redirectUri)}&` +
221
- `code_challenge=${codeChallenge}&` +
222
- `code_challenge_method=S256&` +
223
- `state=${state}&` +
224
- `prompt=select_account`;
225
-
226
- return {
227
- authUrl,
228
- authInfo: {
229
- provider: 'claude-kiro-oauth',
230
- authMethod: 'social',
231
- socialProvider: provider,
232
- port: handlerPort,
233
- redirectUri: redirectUri,
234
- state: state,
235
- ...options
236
- }
237
- };
238
- }
239
-
240
- /**
241
- * Kiro Builder ID - Device Code Flow(类似 Qwen OAuth 模式)
242
- */
243
- async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
244
- // 停止之前的轮询任务
245
- for (const [existingTaskId] of activeKiroPollingTasks.entries()) {
246
- if (existingTaskId.startsWith('kiro-')) {
247
- stopKiroPollingTask(existingTaskId);
248
- }
249
- }
250
-
251
- // 获取 Builder ID Start URL(优先使用前端传入的值,否则使用默认值)
252
- const builderIDStartURL = options.builderIDStartURL || KIRO_OAUTH_CONFIG.builderIDStartURL;
253
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Using Builder ID Start URL: ${builderIDStartURL}`);
254
-
255
- // 1. 注册 OIDC 客户端
256
- const region = options.region || 'us-east-1';
257
- const ssoOIDCEndpoint = KIRO_OAUTH_CONFIG.ssoOIDCEndpoint.replace('{{region}}', region);
258
-
259
- const regResponse = await fetchWithProxy(`${ssoOIDCEndpoint}/client/register`, {
260
- method: 'POST',
261
- headers: {
262
- 'Content-Type': 'application/json',
263
- 'User-Agent': 'KiroIDE'
264
- },
265
- body: JSON.stringify({
266
- clientName: 'Kiro IDE',
267
- clientType: 'public',
268
- scopes: KIRO_OAUTH_CONFIG.scopes,
269
- // grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
270
- })
271
- }, 'claude-kiro-oauth');
272
-
273
- if (!regResponse.ok) {
274
- throw new Error(`Kiro OAuth 客户端注册失败: ${regResponse.status}`);
275
- }
276
-
277
- const regData = await regResponse.json();
278
-
279
- // 2. 启动设备授权
280
- const authResponse = await fetchWithProxy(`${ssoOIDCEndpoint}/device_authorization`, {
281
- method: 'POST',
282
- headers: {
283
- 'Content-Type': 'application/json'
284
- },
285
- body: JSON.stringify({
286
- clientId: regData.clientId,
287
- clientSecret: regData.clientSecret,
288
- startUrl: builderIDStartURL
289
- })
290
- }, 'claude-kiro-oauth');
291
-
292
- if (!authResponse.ok) {
293
- throw new Error(`Kiro OAuth 设备授权失败: ${authResponse.status}`);
294
- }
295
-
296
- const deviceAuth = await authResponse.json();
297
-
298
- // 3. 启动后台轮询(类似 Qwen OAuth 的模式)
299
- const taskId = `kiro-${deviceAuth.deviceCode.substring(0, 8)}-${Date.now()}`;
300
-
301
-
302
- // 异步轮询
303
- pollKiroBuilderIDToken(
304
- regData.clientId,
305
- regData.clientSecret,
306
- deviceAuth.deviceCode,
307
- 5,
308
- 300,
309
- taskId,
310
- { ...options, region }
311
- ).catch(error => {
312
- logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
313
- broadcastEvent('oauth_error', {
314
- provider: 'claude-kiro-oauth',
315
- error: error.message,
316
- timestamp: new Date().toISOString()
317
- });
318
- });
319
-
320
- return {
321
- authUrl: deviceAuth.verificationUriComplete,
322
- authInfo: {
323
- provider: 'claude-kiro-oauth',
324
- authMethod: 'builder-id',
325
- deviceCode: deviceAuth.deviceCode,
326
- userCode: deviceAuth.userCode,
327
- verificationUri: deviceAuth.verificationUri,
328
- verificationUriComplete: deviceAuth.verificationUriComplete,
329
- expiresIn: deviceAuth.expiresIn,
330
- interval: deviceAuth.interval,
331
- ...options
332
- }
333
- };
334
- }
335
-
336
- /**
337
- * 轮询获取 Kiro Builder ID Token
338
- */
339
- async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interval, expiresIn, taskId, options = {}) {
340
- let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile);
341
- const maxAttempts = Math.floor(expiresIn / interval);
342
- let attempts = 0;
343
-
344
- const taskControl = { shouldStop: false };
345
- activeKiroPollingTasks.set(taskId, taskControl);
346
-
347
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`);
348
-
349
- const poll = async () => {
350
- if (taskControl.shouldStop) {
351
- throw new Error('轮询任务已被取消');
352
- }
353
-
354
- if (attempts >= maxAttempts) {
355
- activeKiroPollingTasks.delete(taskId);
356
- throw new Error('授权超时');
357
- }
358
-
359
- attempts++;
360
-
361
- try {
362
- const region = options.region || 'us-east-1';
363
- const ssoOIDCEndpoint = KIRO_OAUTH_CONFIG.ssoOIDCEndpoint.replace('{{region}}', region);
364
- const response = await fetchWithProxy(`${ssoOIDCEndpoint}/token`, {
365
- method: 'POST',
366
- headers: {
367
- 'Content-Type': 'application/json',
368
- 'User-Agent': 'KiroIDE'
369
- },
370
- body: JSON.stringify({
371
- clientId,
372
- clientSecret,
373
- deviceCode,
374
- grantType: 'urn:ietf:params:oauth:grant-type:device_code'
375
- })
376
- }, 'claude-kiro-oauth');
377
-
378
- const data = await response.json();
379
-
380
- if (response.ok && data.accessToken) {
381
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
382
-
383
- // 保存令牌(符合现有规范)
384
- if (options.saveToConfigs) {
385
- const timestamp = Date.now();
386
- const folderName = `${timestamp}_kiro-auth-token`;
387
- const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
388
- await fs.promises.mkdir(targetDir, { recursive: true });
389
- credPath = path.join(targetDir, `${folderName}.json`);
390
- }
391
-
392
- const tokenData = {
393
- accessToken: data.accessToken,
394
- refreshToken: data.refreshToken,
395
- expiresAt: new Date(Date.now() + data.expiresIn * 1000).toISOString(),
396
- authMethod: 'builder-id',
397
- clientId,
398
- clientSecret,
399
- idcRegion: options.region || 'us-east-1'
400
- };
401
-
402
- await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
403
- await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
404
-
405
- activeKiroPollingTasks.delete(taskId);
406
-
407
- // 广播成功事件(符合现有规范)
408
- broadcastEvent('oauth_success', {
409
- provider: 'claude-kiro-oauth',
410
- credPath,
411
- relativePath: path.relative(process.cwd(), credPath),
412
- timestamp: new Date().toISOString()
413
- });
414
-
415
- // 自动关联新生成的凭据到 Pools
416
- await autoLinkProviderConfigs(CONFIG, {
417
- onlyCurrentCred: true,
418
- credPath: path.relative(process.cwd(), credPath)
419
- });
420
-
421
- return tokenData;
422
- }
423
-
424
- // 检查错误类型
425
- if (data.error === 'authorization_pending') {
426
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (${attempts}/${maxAttempts})`);
427
- await new Promise(resolve => setTimeout(resolve, interval * 1000));
428
- return poll();
429
- } else if (data.error === 'slow_down') {
430
- await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000));
431
- return poll();
432
- } else {
433
- activeKiroPollingTasks.delete(taskId);
434
- throw new Error(`授权失败: ${data.error || '未知错误'}`);
435
- }
436
- } catch (error) {
437
- if (error.message.includes('授权') || error.message.includes('取消')) {
438
- throw error;
439
- }
440
- await new Promise(resolve => setTimeout(resolve, interval * 1000));
441
- return poll();
442
- }
443
- };
444
-
445
- return poll();
446
- }
447
-
448
- /**
449
- * 停止 Kiro 轮询任务
450
- */
451
- function stopKiroPollingTask(taskId) {
452
- const task = activeKiroPollingTasks.get(taskId);
453
- if (task) {
454
- task.shouldStop = true;
455
- activeKiroPollingTasks.delete(taskId);
456
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`);
457
- }
458
- }
459
-
460
- /**
461
- * 启动 Kiro 回调服务器(用于 Social Auth HTTP 回调)
462
- */
463
- async function startKiroCallbackServer(codeVerifier, expectedState, options = {}) {
464
- const portStart = KIRO_OAUTH_CONFIG.callbackPortStart;
465
- const portEnd = KIRO_OAUTH_CONFIG.callbackPortEnd;
466
-
467
- for (let port = portStart; port <= portEnd; port++) {
468
- // 关闭已存在的服务器
469
- await closeKiroServer(port);
470
-
471
- try {
472
- const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options);
473
- activeKiroServers.set('claude-kiro-oauth', { server, port });
474
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`);
475
- return port;
476
- } catch (err) {
477
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`);
478
- }
479
- }
480
-
481
- throw new Error('所有端口都被占用');
482
- }
483
-
484
- /**
485
- * 关闭 Kiro 服务器
486
- */
487
- async function closeKiroServer(provider, port = null) {
488
- const existing = activeKiroServers.get(provider);
489
- if (existing) {
490
- try {
491
- const closePromise = new Promise((resolve, reject) => {
492
- existing.server.close((err) => {
493
- if (err) reject(err);
494
- else resolve();
495
- });
496
- });
497
-
498
- const timeoutPromise = new Promise((_, reject) => {
499
- setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
500
- });
501
-
502
- await Promise.race([closePromise, timeoutPromise]);
503
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
504
- } catch (error) {
505
- logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} 关闭提供商 ${provider} 服务器失败或超时: ${error.message}`);
506
- } finally {
507
- activeKiroServers.delete(provider);
508
- }
509
- }
510
-
511
- if (port) {
512
- for (const [p, info] of activeKiroServers.entries()) {
513
- if (info.port === port) {
514
- await closeKiroServer(p);
515
- }
516
- }
517
- }
518
- }
519
-
520
- /**
521
- * 创建 Kiro HTTP 回调服务器
522
- */
523
- function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options = {}) {
524
- const redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
525
-
526
- return new Promise((resolve, reject) => {
527
- const server = http.createServer(async (req, res) => {
528
- try {
529
- const url = new URL(req.url, `http://127.0.0.1:${port}`);
530
-
531
- if (url.pathname === '/oauth/callback') {
532
- const code = url.searchParams.get('code');
533
- const state = url.searchParams.get('state');
534
- const errorParam = url.searchParams.get('error');
535
-
536
- if (errorParam) {
537
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
538
- res.end(generateResponsePage(false, `授权失败: ${errorParam}`));
539
- return;
540
- }
541
-
542
- if (state !== expectedState) {
543
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
544
- res.end(generateResponsePage(false, 'State 验证失败'));
545
- return;
546
- }
547
-
548
- // 交换 Code 获取 Token(使用动态的 redirect_uri)
549
- const tokenResponse = await fetchWithProxy(`${KIRO_OAUTH_CONFIG.authServiceEndpoint}/oauth/token`, {
550
- method: 'POST',
551
- headers: {
552
- 'Content-Type': 'application/json',
553
- 'User-Agent': 'AIClient-2-API/1.0.0'
554
- },
555
- body: JSON.stringify({
556
- code,
557
- code_verifier: codeVerifier,
558
- redirect_uri: redirectUri
559
- })
560
- }, 'claude-kiro-oauth');
561
-
562
- if (!tokenResponse.ok) {
563
- const errorText = await tokenResponse.text();
564
- logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token exchange failed:`, errorText);
565
- res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
566
- res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`));
567
- return;
568
- }
569
-
570
- const tokenData = await tokenResponse.json();
571
-
572
- // 保存令牌
573
- let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile);
574
-
575
- if (options.saveToConfigs) {
576
- const timestamp = Date.now();
577
- const folderName = `${timestamp}_kiro-auth-token`;
578
- const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
579
- await fs.promises.mkdir(targetDir, { recursive: true });
580
- credPath = path.join(targetDir, `${folderName}.json`);
581
- }
582
-
583
- const saveData = {
584
- accessToken: tokenData.accessToken,
585
- refreshToken: tokenData.refreshToken,
586
- profileArn: tokenData.profileArn,
587
- expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(),
588
- authMethod: 'social',
589
- region: 'us-east-1'
590
- };
591
-
592
- await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
593
- await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2));
594
-
595
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 令牌已保存: ${credPath}`);
596
-
597
- // 广播成功事件
598
- broadcastEvent('oauth_success', {
599
- provider: 'claude-kiro-oauth',
600
- credPath,
601
- relativePath: path.relative(process.cwd(), credPath),
602
- timestamp: new Date().toISOString()
603
- });
604
-
605
- // 自动关联新生成的凭据到 Pools
606
- await autoLinkProviderConfigs(CONFIG, {
607
- onlyCurrentCred: true,
608
- credPath: path.relative(process.cwd(), credPath)
609
- });
610
-
611
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
612
- res.end(generateResponsePage(true, '授权成功!您可以关闭此页面'));
613
-
614
- // 关闭服务器
615
- server.close(() => {
616
- activeKiroServers.delete('claude-kiro-oauth');
617
- });
618
-
619
- } else {
620
- res.writeHead(204);
621
- res.end();
622
- }
623
- } catch (error) {
624
- logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error);
625
- res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
626
- res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
627
- }
628
- });
629
-
630
- server.on('error', reject);
631
- server.listen(port, '127.0.0.1', () => resolve(server));
632
-
633
- // 超时自动关闭
634
- setTimeout(() => {
635
- if (server.listening) {
636
- server.close(() => {
637
- activeKiroServers.delete('claude-kiro-oauth');
638
- });
639
- }
640
- }, KIRO_OAUTH_CONFIG.authTimeout);
641
- });
642
- }
643
-
644
- /**
645
- * Kiro Token 刷新常量
646
- */
647
- const KIRO_REFRESH_CONSTANTS = {
648
- REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
649
- REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token',
650
- CONTENT_TYPE_JSON: 'application/json',
651
- AUTH_METHOD_SOCIAL: 'social',
652
- DEFAULT_PROVIDER: 'Google',
653
- REQUEST_TIMEOUT: 30000,
654
- DEFAULT_REGION: 'us-east-1',
655
- IDC_REGION: 'us-east-1' // 用于 REFRESH_IDC_URL 的区域配置
656
- };
657
-
658
- /**
659
- * 通过 refreshToken 获取 accessToken
660
- * @param {string} refreshToken - Kiro 的 refresh token
661
- * @param {string} region - AWS 区域 (默认: us-east-1)
662
- * @returns {Promise<Object>} 包含 accessToken 等信息的对象
663
- */
664
- async function refreshKiroToken(refreshToken, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) {
665
- const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_URL.replace('{{region}}', region);
666
-
667
- const controller = new AbortController();
668
- const timeoutId = setTimeout(() => controller.abort(), KIRO_REFRESH_CONSTANTS.REQUEST_TIMEOUT);
669
-
670
- try {
671
- const response = await fetchWithProxy(refreshUrl, {
672
- method: 'POST',
673
- headers: {
674
- 'Content-Type': KIRO_REFRESH_CONSTANTS.CONTENT_TYPE_JSON
675
- },
676
- body: JSON.stringify({ refreshToken }),
677
- signal: controller.signal
678
- }, 'claude-kiro-oauth');
679
-
680
- clearTimeout(timeoutId);
681
-
682
- if (!response.ok) {
683
- const errorText = await response.text();
684
- throw new Error(`HTTP ${response.status}: ${errorText}`);
685
- }
686
-
687
- const data = await response.json();
688
-
689
- if (!data.accessToken) {
690
- throw new Error('Invalid refresh response: Missing accessToken');
691
- }
692
-
693
- const expiresIn = data.expiresIn || 3600;
694
- const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
695
-
696
- return {
697
- accessToken: data.accessToken,
698
- refreshToken: data.refreshToken || refreshToken,
699
- profileArn: data.profileArn || '',
700
- expiresAt: expiresAt,
701
- authMethod: KIRO_REFRESH_CONSTANTS.AUTH_METHOD_SOCIAL,
702
- provider: KIRO_REFRESH_CONSTANTS.DEFAULT_PROVIDER,
703
- region: region
704
- };
705
- } catch (error) {
706
- clearTimeout(timeoutId);
707
- if (error.name === 'AbortError') {
708
- throw new Error('Request timeout');
709
- }
710
- throw error;
711
- }
712
- }
713
-
714
- /**
715
- * 检查 Kiro 凭据是否已存在(基于 refreshToken + provider 组合)
716
- * @param {string} refreshToken - 要检查的 refreshToken
717
- * @param {string} provider - 提供商名称 (默认: 'claude-kiro-oauth')
718
- * @returns {Promise<{isDuplicate: boolean, existingPath?: string}>} 检查结果
719
- */
720
- export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'claude-kiro-oauth') {
721
- const kiroDir = path.join(process.cwd(), 'configs', 'kiro');
722
-
723
- try {
724
- // 检查 configs/kiro 目录是否存在
725
- if (!fs.existsSync(kiroDir)) {
726
- return { isDuplicate: false };
727
- }
728
-
729
- // 递归扫描所有 JSON 文件
730
- const scanDirectory = async (dirPath) => {
731
- const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
732
-
733
- for (const entry of entries) {
734
- const fullPath = path.join(dirPath, entry.name);
735
-
736
- if (entry.isDirectory()) {
737
- const result = await scanDirectory(fullPath);
738
- if (result.isDuplicate) {
739
- return result;
740
- }
741
- } else if (entry.isFile() && entry.name.endsWith('.json')) {
742
- try {
743
- const content = await fs.promises.readFile(fullPath, 'utf8');
744
- const credentials = JSON.parse(content);
745
-
746
- // 检查 refreshToken 是否匹配
747
- if (credentials.refreshToken && credentials.refreshToken === refreshToken) {
748
- const relativePath = path.relative(process.cwd(), fullPath);
749
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Found duplicate refreshToken in: ${relativePath}`);
750
- return {
751
- isDuplicate: true,
752
- existingPath: relativePath
753
- };
754
- }
755
- } catch (parseError) {
756
- // 忽略解析错误的文件
757
- }
758
- }
759
- }
760
-
761
- return { isDuplicate: false };
762
- };
763
-
764
- return await scanDirectory(kiroDir);
765
-
766
- } catch (error) {
767
- logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message);
768
- return { isDuplicate: false };
769
- }
770
- }
771
-
772
- /**
773
- * 批量导入 Kiro refreshToken 并生成凭据文件
774
- * @param {string[]} refreshTokens - refreshToken 数组
775
- * @param {string} region - AWS 区域 (默认: us-east-1)
776
- * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
777
- * @returns {Promise<Object>} 批量处理结果
778
- */
779
- export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, skipDuplicateCheck = false) {
780
- const results = {
781
- total: refreshTokens.length,
782
- success: 0,
783
- failed: 0,
784
- details: []
785
- };
786
-
787
- for (let i = 0; i < refreshTokens.length; i++) {
788
- const refreshToken = refreshTokens[i].trim();
789
-
790
- if (!refreshToken) {
791
- results.details.push({
792
- index: i + 1,
793
- success: false,
794
- error: 'Empty token'
795
- });
796
- results.failed++;
797
- continue;
798
- }
799
-
800
- // 检查重复
801
- if (!skipDuplicateCheck) {
802
- const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken);
803
- if (duplicateCheck.isDuplicate) {
804
- results.details.push({
805
- index: i + 1,
806
- success: false,
807
- error: 'duplicate',
808
- existingPath: duplicateCheck.existingPath
809
- });
810
- results.failed++;
811
- continue;
812
- }
813
- }
814
-
815
- try {
816
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`);
817
-
818
- const tokenData = await refreshKiroToken(refreshToken, region);
819
-
820
- // 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
821
- const timestamp = Date.now();
822
- const folderName = `${timestamp}_kiro-auth-token`;
823
- const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
824
- await fs.promises.mkdir(targetDir, { recursive: true });
825
-
826
- const credPath = path.join(targetDir, `${folderName}.json`);
827
- await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
828
-
829
- const relativePath = path.relative(process.cwd(), credPath);
830
-
831
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`);
832
-
833
- results.details.push({
834
- index: i + 1,
835
- success: true,
836
- path: relativePath,
837
- expiresAt: tokenData.expiresAt
838
- });
839
- results.success++;
840
-
841
- } catch (error) {
842
- logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message);
843
-
844
- results.details.push({
845
- index: i + 1,
846
- success: false,
847
- error: error.message
848
- });
849
- results.failed++;
850
- }
851
- }
852
-
853
- // 如果有成功的,广播事件并自动关联
854
- if (results.success > 0) {
855
- broadcastEvent('oauth_batch_success', {
856
- provider: 'claude-kiro-oauth',
857
- count: results.success,
858
- timestamp: new Date().toISOString()
859
- });
860
-
861
- // 自动关联新生成的凭据到 Pools
862
- for (const detail of results.details) {
863
- if (detail.success && detail.path) {
864
- await autoLinkProviderConfigs(CONFIG, {
865
- onlyCurrentCred: true,
866
- credPath: detail.path
867
- });
868
- }
869
- }
870
- }
871
-
872
- return results;
873
- }
874
-
875
- /**
876
- * 批量导入 Kiro refreshToken 并生成凭据文件(流式版本,支持实时进度回调)
877
- * @param {string[]} refreshTokens - refreshToken 数组
878
- * @param {string} region - AWS 区域 (默认: us-east-1)
879
- * @param {Function} onProgress - 进度回调函数,每处理完一个 token 调用
880
- * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
881
- * @returns {Promise<Object>} 批量处理结果
882
- */
883
- export async function batchImportKiroRefreshTokensStream(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, onProgress = null, skipDuplicateCheck = false) {
884
- const results = {
885
- total: refreshTokens.length,
886
- success: 0,
887
- failed: 0,
888
- details: []
889
- };
890
-
891
- for (let i = 0; i < refreshTokens.length; i++) {
892
- const refreshToken = refreshTokens[i].trim();
893
- const progressData = {
894
- index: i + 1,
895
- total: refreshTokens.length,
896
- current: null
897
- };
898
-
899
- if (!refreshToken) {
900
- progressData.current = {
901
- index: i + 1,
902
- success: false,
903
- error: 'Empty token'
904
- };
905
- results.details.push(progressData.current);
906
- results.failed++;
907
-
908
- // 发送进度更新
909
- if (onProgress) {
910
- onProgress({
911
- ...progressData,
912
- successCount: results.success,
913
- failedCount: results.failed
914
- });
915
- }
916
- continue;
917
- }
918
-
919
- // 检查重复
920
- if (!skipDuplicateCheck) {
921
- const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken);
922
- if (duplicateCheck.isDuplicate) {
923
- progressData.current = {
924
- index: i + 1,
925
- success: false,
926
- error: 'duplicate',
927
- existingPath: duplicateCheck.existingPath
928
- };
929
- results.details.push(progressData.current);
930
- results.failed++;
931
-
932
- // 发送进度更新
933
- if (onProgress) {
934
- onProgress({
935
- ...progressData,
936
- successCount: results.success,
937
- failedCount: results.failed
938
- });
939
- }
940
- continue;
941
- }
942
- }
943
-
944
- try {
945
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`);
946
-
947
- const tokenData = await refreshKiroToken(refreshToken, region);
948
-
949
- // 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
950
- const timestamp = Date.now();
951
- const folderName = `${timestamp}_kiro-auth-token`;
952
- const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
953
- await fs.promises.mkdir(targetDir, { recursive: true });
954
-
955
- const credPath = path.join(targetDir, `${folderName}.json`);
956
- await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
957
-
958
- const relativePath = path.relative(process.cwd(), credPath);
959
-
960
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`);
961
-
962
- progressData.current = {
963
- index: i + 1,
964
- success: true,
965
- path: relativePath,
966
- expiresAt: tokenData.expiresAt
967
- };
968
- results.details.push(progressData.current);
969
- results.success++;
970
-
971
- } catch (error) {
972
- logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message);
973
-
974
- progressData.current = {
975
- index: i + 1,
976
- success: false,
977
- error: error.message
978
- };
979
- results.details.push(progressData.current);
980
- results.failed++;
981
- }
982
-
983
- // 发送进度更新
984
- if (onProgress) {
985
- onProgress({
986
- ...progressData,
987
- successCount: results.success,
988
- failedCount: results.failed
989
- });
990
- }
991
- }
992
-
993
- // 如果有成功的,广播事件并自动关联
994
- if (results.success > 0) {
995
- broadcastEvent('oauth_batch_success', {
996
- provider: 'claude-kiro-oauth',
997
- count: results.success,
998
- timestamp: new Date().toISOString()
999
- });
1000
-
1001
- // 自动关联新生成的凭据到 Pools
1002
- for (const detail of results.details) {
1003
- if (detail.success && detail.path) {
1004
- await autoLinkProviderConfigs(CONFIG, {
1005
- onlyCurrentCred: true,
1006
- credPath: detail.path
1007
- });
1008
- }
1009
- }
1010
- }
1011
-
1012
- return results;
1013
- }
1014
-
1015
- /**
1016
- * 导入 AWS SSO 凭据用于 Kiro (Builder ID 模式)
1017
- * 从用户上传的 AWS SSO cache 文件中导入凭据
1018
- * @param {Object} credentials - 合并后的凭据对象,需包含 clientId 和 clientSecret
1019
- * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
1020
- * @returns {Promise<Object>} 导入结果
1021
- */
1022
- export async function importAwsCredentials(credentials, skipDuplicateCheck = false) {
1023
- try {
1024
- // 验证必需字段 - 需要四个字段都存在
1025
- const missingFields = [];
1026
- if (!credentials.clientId) missingFields.push('clientId');
1027
- if (!credentials.clientSecret) missingFields.push('clientSecret');
1028
- if (!credentials.accessToken) missingFields.push('accessToken');
1029
- if (!credentials.refreshToken) missingFields.push('refreshToken');
1030
-
1031
- if (missingFields.length > 0) {
1032
- return {
1033
- success: false,
1034
- error: `Missing required fields: ${missingFields.join(', ')}`
1035
- };
1036
- }
1037
-
1038
- // 检查重复凭据
1039
- if (!skipDuplicateCheck) {
1040
- const duplicateCheck = await checkKiroCredentialsDuplicate(credentials.refreshToken);
1041
- if (duplicateCheck.isDuplicate) {
1042
- return {
1043
- success: false,
1044
- error: 'duplicate',
1045
- existingPath: duplicateCheck.existingPath
1046
- };
1047
- }
1048
- }
1049
-
1050
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Importing AWS credentials...`);
1051
-
1052
- // 准备凭据数据 - 四个字段都是必需的
1053
- const credentialsData = {
1054
- clientId: credentials.clientId,
1055
- clientSecret: credentials.clientSecret,
1056
- accessToken: credentials.accessToken,
1057
- refreshToken: credentials.refreshToken,
1058
- authMethod: credentials.authMethod || 'builder-id',
1059
- // region: credentials.region || KIRO_REFRESH_CONSTANTS.DEFAULT_REGION,
1060
- idcRegion: credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION
1061
- };
1062
-
1063
- // 可选字段
1064
- if (credentials.expiresAt) {
1065
- credentialsData.expiresAt = credentials.expiresAt;
1066
- }
1067
- if (credentials.startUrl) {
1068
- credentialsData.startUrl = credentials.startUrl;
1069
- }
1070
- if (credentials.registrationExpiresAt) {
1071
- credentialsData.registrationExpiresAt = credentials.registrationExpiresAt;
1072
- }
1073
-
1074
- // 尝试刷新获取最新的 accessToken
1075
- try {
1076
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Attempting to refresh token with provided credentials...`);
1077
-
1078
- const refreshRegion = credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION;
1079
- const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', refreshRegion);
1080
-
1081
- const refreshResponse = await fetchWithProxy(refreshUrl, {
1082
- method: 'POST',
1083
- headers: {
1084
- 'Content-Type': 'application/json'
1085
- },
1086
- body: JSON.stringify({
1087
- refreshToken: credentials.refreshToken,
1088
- clientId: credentials.clientId,
1089
- clientSecret: credentials.clientSecret,
1090
- grantType: 'refresh_token'
1091
- })
1092
- }, 'claude-kiro-oauth');
1093
-
1094
- if (refreshResponse.ok) {
1095
- const tokenData = await refreshResponse.json();
1096
- credentialsData.accessToken = tokenData.accessToken;
1097
- credentialsData.refreshToken = tokenData.refreshToken;
1098
- const expiresIn = tokenData.expiresIn || 3600;
1099
- credentialsData.expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
1100
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`);
1101
- } else {
1102
- logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`);
1103
- }
1104
- } catch (refreshError) {
1105
- logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh error:`, refreshError.message);
1106
- // 继续保存原始凭据
1107
- }
1108
-
1109
- // 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
1110
- const timestamp = Date.now();
1111
- const folderName = `${timestamp}_kiro-auth-token`;
1112
- const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
1113
- await fs.promises.mkdir(targetDir, { recursive: true });
1114
-
1115
- const credPath = path.join(targetDir, `${folderName}.json`);
1116
- await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
1117
-
1118
- const relativePath = path.relative(process.cwd(), credPath);
1119
-
1120
- logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials saved to: ${relativePath}`);
1121
-
1122
- // 广播事件
1123
- broadcastEvent('oauth_success', {
1124
- provider: 'claude-kiro-oauth',
1125
- relativePath: relativePath,
1126
- timestamp: new Date().toISOString()
1127
- });
1128
-
1129
- // 自动关联新生成的凭据到 Pools
1130
- await autoLinkProviderConfigs(CONFIG, {
1131
- onlyCurrentCred: true,
1132
- credPath: relativePath
1133
- });
1134
-
1135
- return {
1136
- success: true,
1137
- path: relativePath
1138
- };
1139
-
1140
- } catch (error) {
1141
- logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error);
1142
- return {
1143
- success: false,
1144
- error: error.message
1145
- };
1146
- }
1147
- }
1148
-
1149
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/auth/oauth-handlers.js DELETED
@@ -1,27 +0,0 @@
1
- // OAuth 处理器统一导出文件
2
- // 此文件已按提供商拆分为多个独立文件,请从 index.js 导入
3
-
4
- // 重新导出所有 OAuth 处理函数以保持向后兼容
5
- export {
6
- // Codex OAuth
7
- refreshCodexTokensWithRetry,
8
- handleCodexOAuth,
9
- handleCodexOAuthCallback,
10
- batchImportCodexTokensStream,
11
- // Gemini OAuth
12
- handleGeminiCliOAuth,
13
- handleGeminiAntigravityOAuth,
14
- batchImportGeminiTokensStream,
15
- checkGeminiCredentialsDuplicate,
16
- // Qwen OAuth
17
- handleQwenOAuth,
18
- // Kiro OAuth
19
- handleKiroOAuth,
20
- checkKiroCredentialsDuplicate,
21
- batchImportKiroRefreshTokens,
22
- batchImportKiroRefreshTokensStream,
23
- importAwsCredentials,
24
- // iFlow OAuth
25
- handleIFlowOAuth,
26
- refreshIFlowTokens,
27
- } from './index.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/auth/qwen-oauth.js DELETED
@@ -1,343 +0,0 @@
1
- import fs from 'fs';
2
- import logger from '../utils/logger.js';
3
- import path from 'path';
4
- import os from 'os';
5
- import crypto from 'crypto';
6
- import { broadcastEvent } from '../services/ui-manager.js';
7
- import { autoLinkProviderConfigs } from '../services/service-manager.js';
8
- import { CONFIG } from '../core/config-manager.js';
9
- import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
10
-
11
- /**
12
- * Qwen OAuth 配置
13
- */
14
- const QWEN_OAUTH_CONFIG = {
15
- clientId: 'f0304373b74a44d2b584a3fb70ca9e56',
16
- scope: 'openid profile email model.completion',
17
- deviceCodeEndpoint: 'https://chat.qwen.ai/api/v1/oauth2/device/code',
18
- tokenEndpoint: 'https://chat.qwen.ai/api/v1/oauth2/token',
19
- grantType: 'urn:ietf:params:oauth:grant-type:device_code',
20
- credentialsDir: '.qwen',
21
- credentialsFile: 'oauth_creds.json',
22
- logPrefix: '[Qwen Auth]'
23
- };
24
-
25
- /**
26
- * 活动的轮询任务管理
27
- */
28
- const activePollingTasks = new Map();
29
-
30
- /**
31
- * 创建带代理支持的 fetch 请求
32
- * 使用 axios 替代原生 fetch,以正确支持代理配置
33
- * @param {string} url - 请求 URL
34
- * @param {Object} options - fetch 选项(兼容 fetch API 格式)
35
- * @param {string} providerType - 提供商类型,用于获取代理配置
36
- * @returns {Promise<Object>} 返回类似 fetch Response 的对象
37
- */
38
- async function fetchWithProxy(url, options = {}, providerType) {
39
- const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
40
-
41
- // 构建 axios 配置
42
- const axiosConfig = {
43
- url,
44
- method: options.method || 'GET',
45
- headers: options.headers || {},
46
- timeout: 30000, // 30 秒超时
47
- };
48
-
49
- // 处理请求体
50
- if (options.body) {
51
- axiosConfig.data = options.body;
52
- }
53
-
54
- // 配置代理
55
- if (proxyConfig) {
56
- axiosConfig.httpAgent = proxyConfig.httpAgent;
57
- axiosConfig.httpsAgent = proxyConfig.httpsAgent;
58
- axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent
59
- logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`);
60
- }
61
-
62
- try {
63
- const axios = (await import('axios')).default;
64
- const response = await axios(axiosConfig);
65
-
66
- // 返回类似 fetch Response 的对象
67
- return {
68
- ok: response.status >= 200 && response.status < 300,
69
- status: response.status,
70
- statusText: response.statusText,
71
- headers: response.headers,
72
- json: async () => response.data,
73
- text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
74
- };
75
- } catch (error) {
76
- // 处理 axios 错误,转换为类似 fetch 的响应格式
77
- if (error.response) {
78
- // 服务器返回了错误状态码
79
- return {
80
- ok: false,
81
- status: error.response.status,
82
- statusText: error.response.statusText,
83
- headers: error.response.headers,
84
- json: async () => error.response.data,
85
- text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data),
86
- };
87
- }
88
- // 网络错误或其他错误
89
- throw error;
90
- }
91
- }
92
-
93
- /**
94
- * 生成 PKCE 代码验证器
95
- * @returns {string} Base64URL 编码的随机字符串
96
- */
97
- function generateCodeVerifier() {
98
- return crypto.randomBytes(32).toString('base64url');
99
- }
100
-
101
- /**
102
- * 生成 PKCE 代码挑战
103
- * @param {string} codeVerifier - 代码验证器
104
- * @returns {string} Base64URL 编码的 SHA256 哈希
105
- */
106
- function generateCodeChallenge(codeVerifier) {
107
- const hash = crypto.createHash('sha256');
108
- hash.update(codeVerifier);
109
- return hash.digest('base64url');
110
- }
111
-
112
- /**
113
- * 停止活动的轮询任务
114
- * @param {string} taskId - 任务标识符
115
- */
116
- function stopPollingTask(taskId) {
117
- const task = activePollingTasks.get(taskId);
118
- if (task) {
119
- task.shouldStop = true;
120
- activePollingTasks.delete(taskId);
121
- logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`);
122
- }
123
- }
124
-
125
- /**
126
- * 轮询获取 Qwen OAuth 令牌
127
- * @param {string} deviceCode - 设备代码
128
- * @param {string} codeVerifier - PKCE 代码验证器
129
- * @param {number} interval - 轮询间隔(秒)
130
- * @param {number} expiresIn - 过期时间(秒)
131
- * @param {string} taskId - 任务标识符
132
- * @param {Object} options - 额外选项
133
- * @returns {Promise<Object>} 返回令牌信息
134
- */
135
- async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = 300, taskId = 'default', options = {}) {
136
- let credPath = path.join(os.homedir(), QWEN_OAUTH_CONFIG.credentialsDir, QWEN_OAUTH_CONFIG.credentialsFile);
137
- const maxAttempts = Math.floor(expiresIn / interval);
138
- let attempts = 0;
139
-
140
- // 创建任务控制对象
141
- const taskControl = { shouldStop: false };
142
- activePollingTasks.set(taskId, taskControl);
143
-
144
- logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}],间隔 ${interval} 秒,最多尝试 ${maxAttempts} 次`);
145
-
146
- const poll = async () => {
147
- // 检查是否需要停止
148
- if (taskControl.shouldStop) {
149
- logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询任务 [${taskId}] 已被停止`);
150
- throw new Error('轮询任务已被取消');
151
- }
152
-
153
- if (attempts >= maxAttempts) {
154
- activePollingTasks.delete(taskId);
155
- throw new Error('授权超时,请重新开始授权流程');
156
- }
157
-
158
- attempts++;
159
-
160
- const bodyData = {
161
- client_id: QWEN_OAUTH_CONFIG.clientId,
162
- device_code: deviceCode,
163
- grant_type: QWEN_OAUTH_CONFIG.grantType,
164
- code_verifier: codeVerifier
165
- };
166
-
167
- const formBody = Object.entries(bodyData)
168
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
169
- .join('&');
170
-
171
- try {
172
- const response = await fetchWithProxy(QWEN_OAUTH_CONFIG.tokenEndpoint, {
173
- method: 'POST',
174
- headers: {
175
- 'Content-Type': 'application/x-www-form-urlencoded',
176
- 'Accept': 'application/json'
177
- },
178
- body: formBody
179
- }, 'openai-qwen-oauth');
180
-
181
- const data = await response.json();
182
-
183
- if (response.ok && data.access_token) {
184
- // 成功获取令牌
185
- logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
186
-
187
- // 如果指定了保存到 configs 目录
188
- if (options.saveToConfigs) {
189
- const targetDir = path.join(process.cwd(), 'configs', options.providerDir);
190
- await fs.promises.mkdir(targetDir, { recursive: true });
191
- const timestamp = Date.now();
192
- const filename = `${timestamp}_oauth_creds.json`;
193
- credPath = path.join(targetDir, filename);
194
- }
195
-
196
- // 保存令牌到文件
197
- await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
198
- await fs.promises.writeFile(credPath, JSON.stringify(data, null, 2));
199
- logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 令牌已保存到 ${credPath}`);
200
-
201
- const relativePath = path.relative(process.cwd(), credPath);
202
-
203
- // 清理任务
204
- activePollingTasks.delete(taskId);
205
-
206
- // 广播授权成功事件
207
- broadcastEvent('oauth_success', {
208
- provider: 'openai-qwen-oauth',
209
- credPath: credPath,
210
- relativePath: relativePath,
211
- timestamp: new Date().toISOString()
212
- });
213
-
214
- // 自动关联新生成的凭据到 Pools
215
- await autoLinkProviderConfigs(CONFIG, {
216
- onlyCurrentCred: true,
217
- credPath: relativePath
218
- });
219
-
220
- return data;
221
- }
222
-
223
- // 检查错误类型
224
- if (data.error === 'authorization_pending') {
225
- // 用户尚未完成授权,继续轮询
226
- logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (第 ${attempts}/${maxAttempts} 次尝试)`);
227
- await new Promise(resolve => setTimeout(resolve, interval * 1000));
228
- return poll();
229
- } else if (data.error === 'slow_down') {
230
- // 需要降低轮询频率
231
- logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 降低轮询频率`);
232
- await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000));
233
- return poll();
234
- } else if (data.error === 'expired_token') {
235
- activePollingTasks.delete(taskId);
236
- throw new Error('设备代码已过期,请重新开始授权流程');
237
- } else if (data.error === 'access_denied') {
238
- activePollingTasks.delete(taskId);
239
- throw new Error('用户拒绝了授权请求');
240
- } else {
241
- activePollingTasks.delete(taskId);
242
- throw new Error(`授权失败: ${data.error || '未知错误'}`);
243
- }
244
- } catch (error) {
245
- if (error.message.includes('授权') || error.message.includes('过期') || error.message.includes('拒绝')) {
246
- throw error;
247
- }
248
- logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询出错:`, error);
249
- // 网络错误,继续重试
250
- await new Promise(resolve => setTimeout(resolve, interval * 1000));
251
- return poll();
252
- }
253
- };
254
-
255
- return poll();
256
- }
257
-
258
- /**
259
- * 处理 Qwen OAuth 授权(设备授权流程)
260
- * @param {Object} currentConfig - 当前配置对象
261
- * @param {Object} options - 额外选项
262
- * @returns {Promise<Object>} 返回授权URL和相关信息
263
- */
264
- export async function handleQwenOAuth(currentConfig, options = {}) {
265
- const codeVerifier = generateCodeVerifier();
266
- const codeChallenge = generateCodeChallenge(codeVerifier);
267
-
268
- const bodyData = {
269
- client_id: QWEN_OAUTH_CONFIG.clientId,
270
- scope: QWEN_OAUTH_CONFIG.scope,
271
- code_challenge: codeChallenge,
272
- code_challenge_method: 'S256'
273
- };
274
-
275
- const formBody = Object.entries(bodyData)
276
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
277
- .join('&');
278
-
279
- try {
280
- const response = await fetchWithProxy(QWEN_OAUTH_CONFIG.deviceCodeEndpoint, {
281
- method: 'POST',
282
- headers: {
283
- 'Content-Type': 'application/x-www-form-urlencoded',
284
- 'Accept': 'application/json'
285
- },
286
- body: formBody
287
- }, 'openai-qwen-oauth');
288
-
289
- if (!response.ok) {
290
- throw new Error(`Qwen OAuth请求失败: ${response.status} ${response.statusText}`);
291
- }
292
-
293
- const deviceAuth = await response.json();
294
-
295
- if (!deviceAuth.device_code || !deviceAuth.verification_uri_complete) {
296
- throw new Error('Qwen OAuth响应格式错误,缺少必要字段');
297
- }
298
-
299
- // 启动后台轮询获取令牌
300
- const interval = 5;
301
- // const expiresIn = deviceAuth.expires_in || 1800;
302
- const expiresIn = 300;
303
-
304
- // 生成唯一的任务ID
305
- const taskId = `qwen-${deviceAuth.device_code.substring(0, 8)}-${Date.now()}`;
306
-
307
- // 先停止之前可能存在的所有 Qwen 轮询任务
308
- for (const [existingTaskId] of activePollingTasks.entries()) {
309
- if (existingTaskId.startsWith('qwen-')) {
310
- stopPollingTask(existingTaskId);
311
- }
312
- }
313
-
314
- // 不等待轮询完成,立即返回授权信息
315
- pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId, options)
316
- .catch(error => {
317
- logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
318
- // 广播授权失败事件
319
- broadcastEvent('oauth_error', {
320
- provider: 'openai-qwen-oauth',
321
- error: error.message,
322
- timestamp: new Date().toISOString()
323
- });
324
- });
325
-
326
- return {
327
- authUrl: deviceAuth.verification_uri_complete,
328
- authInfo: {
329
- provider: 'openai-qwen-oauth',
330
- deviceCode: deviceAuth.device_code,
331
- userCode: deviceAuth.user_code,
332
- verificationUri: deviceAuth.verification_uri,
333
- verificationUriComplete: deviceAuth.verification_uri_complete,
334
- expiresIn: expiresIn,
335
- interval: interval,
336
- codeVerifier: codeVerifier
337
- }
338
- };
339
- } catch (error) {
340
- logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error);
341
- throw new Error(`Qwen OAuth 授权失败: ${error.message}`);
342
- }
343
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/convert/convert-old.js DELETED
The diff for this file is too large to render. See raw diff
 
src/convert/convert.js DELETED
@@ -1,392 +0,0 @@
1
- /**
2
- * 协议转换模块 - 新架构版本
3
- * 使用重构后的转换器架构
4
- *
5
- * 这个文件展示了如何使用新的转换器架构
6
- * 可以逐步替换原有的 convert.js
7
- */
8
-
9
- import { v4 as uuidv4 } from 'uuid';
10
- import logger from '../utils/logger.js';
11
- import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js';
12
- import { ConverterFactory } from '../converters/ConverterFactory.js';
13
- import {
14
- generateResponseCreated,
15
- generateResponseInProgress,
16
- generateOutputItemAdded,
17
- generateContentPartAdded,
18
- generateOutputTextDone,
19
- generateContentPartDone,
20
- generateOutputItemDone,
21
- generateResponseCompleted
22
- } from '../providers/openai/openai-responses-core.mjs';
23
-
24
- // =============================================================================
25
- // 初始化:注册所有转换器
26
- // =============================================================================
27
-
28
- // =============================================================================
29
- // 主转换函数
30
- // =============================================================================
31
-
32
- /**
33
- * 通用数据转换函数(新架构版本)
34
- * @param {object} data - 要转换的数据(请求体或响应)
35
- * @param {string} type - 转换类型:'request', 'response', 'streamChunk', 'modelList'
36
- * @param {string} fromProvider - 源模型提供商
37
- * @param {string} toProvider - 目标模型提供商
38
- * @param {string} [model] - 可选的模型名称(用于响应转换)
39
- * @returns {object} 转换后的数据
40
- * @throws {Error} 如果找不到合适的转换函数
41
- */
42
- export function convertData(data, type, fromProvider, toProvider, model, requestId) {
43
- try {
44
- // 获取协议前缀
45
- const fromProtocol = getProtocolPrefix(fromProvider);
46
- const toProtocol = getProtocolPrefix(toProvider);
47
-
48
- // 如果目标协议为 forward,直接返回原始数据,无需转换
49
- if (toProtocol === MODEL_PROTOCOL_PREFIX.FORWARD || fromProtocol === MODEL_PROTOCOL_PREFIX.FORWARD) {
50
- logger.info(`[Convert] Target protocol is forward, skipping conversion`);
51
- return data;
52
- }
53
-
54
- // 从工厂获取转换器
55
- const converter = ConverterFactory.getConverter(fromProtocol);
56
-
57
- if (!converter) {
58
- throw new Error(`No converter found for protocol: ${fromProtocol}`);
59
- }
60
-
61
- // 根据类型调用相应的转换方法
62
- switch (type) {
63
- case 'request':
64
- return converter.convertRequest(data, toProtocol);
65
-
66
- case 'response':
67
- return converter.convertResponse(data, toProtocol, model);
68
-
69
- case 'streamChunk':
70
- return converter.convertStreamChunk(data, toProtocol, model, requestId);
71
-
72
- case 'modelList':
73
- return converter.convertModelList(data, toProtocol);
74
-
75
- default:
76
- throw new Error(`Unsupported conversion type: ${type}`);
77
- }
78
- } catch (error) {
79
- logger.error(`Conversion error: ${error.message}`);
80
- throw error;
81
- }
82
- }
83
-
84
- // =============================================================================
85
- // 向后兼容的导出函数
86
- // =============================================================================
87
-
88
- /**
89
- * 以下函数保持与原有API的兼容性
90
- * 内部使用新的转换器架构
91
- */
92
-
93
- // OpenAI 相关转换
94
- export function toOpenAIRequestFromGemini(geminiRequest) {
95
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
96
- return converter.toOpenAIRequest(geminiRequest);
97
- }
98
-
99
- export function toOpenAIRequestFromClaude(claudeRequest) {
100
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
101
- return converter.toOpenAIRequest(claudeRequest);
102
- }
103
-
104
- export function toOpenAIChatCompletionFromGemini(geminiResponse, model) {
105
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
106
- return converter.toOpenAIResponse(geminiResponse, model);
107
- }
108
-
109
- export function toOpenAIChatCompletionFromClaude(claudeResponse, model) {
110
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
111
- return converter.toOpenAIResponse(claudeResponse, model);
112
- }
113
-
114
- export function toOpenAIStreamChunkFromGemini(geminiChunk, model) {
115
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
116
- return converter.toOpenAIStreamChunk(geminiChunk, model);
117
- }
118
-
119
- export function toOpenAIStreamChunkFromClaude(claudeChunk, model) {
120
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
121
- return converter.toOpenAIStreamChunk(claudeChunk, model);
122
- }
123
-
124
- export function toOpenAIModelListFromGemini(geminiModels) {
125
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
126
- return converter.toOpenAIModelList(geminiModels);
127
- }
128
-
129
- export function toOpenAIModelListFromClaude(claudeModels) {
130
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
131
- return converter.toOpenAIModelList(claudeModels);
132
- }
133
-
134
- // Claude 相关转换
135
- export function toClaudeRequestFromOpenAI(openaiRequest) {
136
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
137
- return converter.toClaudeRequest(openaiRequest);
138
- }
139
-
140
- export function toClaudeRequestFromOpenAIResponses(responsesRequest) {
141
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
142
- return converter.toClaudeRequest(responsesRequest);
143
- }
144
-
145
- export function toClaudeChatCompletionFromOpenAI(openaiResponse, model) {
146
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
147
- return converter.toClaudeResponse(openaiResponse, model);
148
- }
149
-
150
- export function toClaudeChatCompletionFromGemini(geminiResponse, model) {
151
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
152
- return converter.toClaudeResponse(geminiResponse, model);
153
- }
154
-
155
- export function toClaudeStreamChunkFromOpenAI(openaiChunk, model) {
156
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
157
- return converter.toClaudeStreamChunk(openaiChunk, model);
158
- }
159
-
160
- export function toClaudeStreamChunkFromGemini(geminiChunk, model) {
161
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
162
- return converter.toClaudeStreamChunk(geminiChunk, model);
163
- }
164
-
165
- export function toClaudeModelListFromOpenAI(openaiModels) {
166
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
167
- return converter.toClaudeModelList(openaiModels);
168
- }
169
-
170
- export function toClaudeModelListFromGemini(geminiModels) {
171
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
172
- return converter.toClaudeModelList(geminiModels);
173
- }
174
-
175
- // Gemini 相关转换
176
- export function toGeminiRequestFromOpenAI(openaiRequest) {
177
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
178
- return converter.toGeminiRequest(openaiRequest);
179
- }
180
-
181
- export function toGeminiRequestFromClaude(claudeRequest) {
182
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
183
- return converter.toGeminiRequest(claudeRequest);
184
- }
185
-
186
- export function toGeminiRequestFromOpenAIResponses(responsesRequest) {
187
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
188
- return converter.toGeminiRequest(responsesRequest);
189
- }
190
-
191
- // OpenAI Responses 相关转换
192
- export function toOpenAIResponsesFromOpenAI(openaiResponse, model) {
193
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
194
- return converter.toOpenAIResponsesResponse(openaiResponse, model);
195
- }
196
-
197
- export function toOpenAIResponsesFromClaude(claudeResponse, model) {
198
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
199
- return converter.toOpenAIResponsesResponse(claudeResponse, model);
200
- }
201
-
202
- export function toOpenAIResponsesFromGemini(geminiResponse, model) {
203
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
204
- return converter.toOpenAIResponsesResponse(geminiResponse, model);
205
- }
206
-
207
- export function toOpenAIResponsesStreamChunkFromOpenAI(openaiChunk, model, requestId) {
208
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
209
- return converter.toOpenAIResponsesStreamChunk(openaiChunk, model, requestId);
210
- }
211
-
212
- export function toOpenAIResponsesStreamChunkFromClaude(claudeChunk, model, requestId) {
213
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
214
- return converter.toOpenAIResponsesStreamChunk(claudeChunk, model, requestId);
215
- }
216
-
217
- export function toOpenAIResponsesStreamChunkFromGemini(geminiChunk, model, requestId) {
218
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
219
- return converter.toOpenAIResponsesStreamChunk(geminiChunk, model, requestId);
220
- }
221
-
222
- // 从 OpenAI Responses 转换到其他格式
223
- export function toOpenAIRequestFromOpenAIResponses(responsesRequest) {
224
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
225
- return converter.toOpenAIRequest(responsesRequest);
226
- }
227
-
228
- export function toOpenAIChatCompletionFromOpenAIResponses(responsesResponse, model) {
229
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
230
- return converter.toOpenAIResponse(responsesResponse, model);
231
- }
232
-
233
- export function toOpenAIStreamChunkFromOpenAIResponses(responsesChunk, model) {
234
- const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
235
- return converter.toOpenAIStreamChunk(responsesChunk, model);
236
- }
237
-
238
- // 辅助函数导出
239
- export async function extractAndProcessSystemMessages(messages) {
240
- const { Utils } = await import('../converters/utils.js');
241
- return Utils.extractSystemMessages(messages);
242
- }
243
-
244
- export async function extractTextFromMessageContent(content) {
245
- const { Utils } = await import('../converters/utils.js');
246
- return Utils.extractText(content);
247
- }
248
-
249
- // =============================================================================
250
- // 工具函数
251
- // =============================================================================
252
-
253
- /**
254
- * 获取所有已注册的协议
255
- * @returns {Array<string>} 协议前缀数组
256
- */
257
- export function getRegisteredProtocols() {
258
- return ConverterFactory.getRegisteredProtocols();
259
- }
260
-
261
- /**
262
- * 检查协议是否已注册
263
- * @param {string} protocol - 协议前缀
264
- * @returns {boolean} 是否已注册
265
- */
266
- export function isProtocolRegistered(protocol) {
267
- return ConverterFactory.isProtocolRegistered(protocol);
268
- }
269
-
270
- /**
271
- * 清除所有转换器缓存
272
- */
273
- export function clearConverterCache() {
274
- ConverterFactory.clearCache();
275
- }
276
-
277
- /**
278
- * 获取转换器实例(用于高级用法)
279
- * @param {string} protocol - 协议前缀
280
- * @returns {BaseConverter} 转换器实例
281
- */
282
- export function getConverter(protocol) {
283
- return ConverterFactory.getConverter(protocol);
284
- }
285
-
286
- // =============================================================================
287
- // 辅助函数 - 从原 convert.js 迁移
288
- // =============================================================================
289
-
290
- /**
291
- * 生成 OpenAI 流式响应的停止块
292
- * @param {string} model - 模型名称
293
- * @returns {Object} OpenAI 流式停止块
294
- */
295
- export function getOpenAIStreamChunkStop(model) {
296
- return {
297
- id: `chatcmpl-${uuidv4()}`,
298
- object: "chat.completion.chunk",
299
- created: Math.floor(Date.now() / 1000),
300
- model: model,
301
- system_fingerprint: "",
302
- choices: [{
303
- index: 0,
304
- delta: {
305
- content: "",
306
- reasoning_content: ""
307
- },
308
- finish_reason: 'stop',
309
- message: {
310
- content: "",
311
- reasoning_content: ""
312
- }
313
- }],
314
- usage:{
315
- prompt_tokens: 0,
316
- completion_tokens: 0,
317
- total_tokens: 0,
318
- },
319
- };
320
- }
321
-
322
- /**
323
- * 生成 OpenAI Responses 流式响应的开始事件
324
- * @param {string} id - 响应 ID
325
- * @param {string} model - 模型名称
326
- * @returns {Array} 开始事件数组
327
- */
328
- export function getOpenAIResponsesStreamChunkBegin(id, model) {
329
- return [
330
- generateResponseCreated(id, model),
331
- generateResponseInProgress(id),
332
- generateOutputItemAdded(id),
333
- generateContentPartAdded(id)
334
- ];
335
- }
336
-
337
- /**
338
- * 生成 OpenAI Responses 流式响应的结束事件
339
- * @param {string} id - 响应 ID
340
- * @returns {Array} 结束事件数组
341
- */
342
- export function getOpenAIResponsesStreamChunkEnd(id) {
343
- return [
344
- generateOutputTextDone(id),
345
- generateContentPartDone(id),
346
- generateOutputItemDone(id),
347
- generateResponseCompleted(id)
348
- ];
349
- }
350
-
351
- // =============================================================================
352
- // 默认导出
353
- // =============================================================================
354
-
355
- export default {
356
- convertData,
357
- getRegisteredProtocols,
358
- isProtocolRegistered,
359
- clearConverterCache,
360
- getConverter,
361
- // 向后兼容的函数
362
- toOpenAIRequestFromGemini,
363
- toOpenAIRequestFromClaude,
364
- toOpenAIChatCompletionFromGemini,
365
- toOpenAIChatCompletionFromClaude,
366
- toOpenAIStreamChunkFromGemini,
367
- toOpenAIStreamChunkFromClaude,
368
- toOpenAIModelListFromGemini,
369
- toOpenAIModelListFromClaude,
370
- toClaudeRequestFromOpenAI,
371
- toClaudeChatCompletionFromOpenAI,
372
- toClaudeChatCompletionFromGemini,
373
- toClaudeStreamChunkFromOpenAI,
374
- toClaudeStreamChunkFromGemini,
375
- toClaudeModelListFromOpenAI,
376
- toClaudeModelListFromGemini,
377
- toGeminiRequestFromOpenAI,
378
- toGeminiRequestFromClaude,
379
- toOpenAIResponsesFromOpenAI,
380
- toOpenAIResponsesFromClaude,
381
- toOpenAIResponsesFromGemini,
382
- toOpenAIResponsesStreamChunkFromOpenAI,
383
- toOpenAIResponsesStreamChunkFromClaude,
384
- toOpenAIResponsesStreamChunkFromGemini,
385
- toOpenAIRequestFromOpenAIResponses,
386
- toOpenAIChatCompletionFromOpenAIResponses,
387
- toOpenAIStreamChunkFromOpenAIResponses,
388
- toClaudeRequestFromOpenAIResponses,
389
- toGeminiRequestFromOpenAIResponses,
390
- };
391
-
392
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/BaseConverter.js DELETED
@@ -1,115 +0,0 @@
1
- /**
2
- * 转换器基类
3
- * 使用策略模式定义转换器的通用接口
4
- */
5
-
6
- /**
7
- * 抽象转换器基类
8
- * 所有具体的协议转换器都应继承此类
9
- */
10
- export class BaseConverter {
11
- constructor(protocolName) {
12
- if (new.target === BaseConverter) {
13
- throw new Error('BaseConverter是抽象类,不能直接实例化');
14
- }
15
- this.protocolName = protocolName;
16
- }
17
-
18
- /**
19
- * 转换请求
20
- * @param {Object} data - 请求数据
21
- * @param {string} targetProtocol - 目标协议
22
- * @returns {Object} 转换后的请求
23
- */
24
- convertRequest(data, targetProtocol) {
25
- throw new Error('convertRequest方法必须被子类实现');
26
- }
27
-
28
- /**
29
- * 转换响应
30
- * @param {Object} data - 响应数据
31
- * @param {string} targetProtocol - 目标协议
32
- * @param {string} model - 模型名称
33
- * @returns {Object} 转换后的响应
34
- */
35
- convertResponse(data, targetProtocol, model) {
36
- throw new Error('convertResponse方法必须被子类实现');
37
- }
38
-
39
- /**
40
- * 转换流式响应块
41
- * @param {Object} chunk - 流式响应块
42
- * @param {string} targetProtocol - 目标协议
43
- * @param {string} model - 模型名称
44
- * @returns {Object} 转换后的流式响应块
45
- */
46
- convertStreamChunk(chunk, targetProtocol, model) {
47
- throw new Error('convertStreamChunk方法必须被子类实现');
48
- }
49
-
50
- /**
51
- * 转换模型列表
52
- * @param {Object} data - 模型列表数据
53
- * @param {string} targetProtocol - 目标协议
54
- * @returns {Object} 转换后的模型列表
55
- */
56
- convertModelList(data, targetProtocol) {
57
- throw new Error('convertModelList方法必须被子类实现');
58
- }
59
-
60
- /**
61
- * 获取协议名称
62
- * @returns {string} 协议名称
63
- */
64
- getProtocolName() {
65
- return this.protocolName;
66
- }
67
- }
68
-
69
- /**
70
- * 内容处理器接口
71
- * 用于处理不同类型的内容(文本、图片、音频等)
72
- */
73
- export class ContentProcessor {
74
- /**
75
- * 处理内容
76
- * @param {*} content - 内容数据
77
- * @returns {*} 处理后的内容
78
- */
79
- process(content) {
80
- throw new Error('process方法必须被子类实现');
81
- }
82
- }
83
-
84
- /**
85
- * 工具处理器接口
86
- * 用于处理工具调用相关的转换
87
- */
88
- export class ToolProcessor {
89
- /**
90
- * 处理工具定义
91
- * @param {Array} tools - 工具定义数组
92
- * @returns {Array} 处理后的工具定义
93
- */
94
- processToolDefinitions(tools) {
95
- throw new Error('processToolDefinitions方法必须被子类实现');
96
- }
97
-
98
- /**
99
- * 处理工具调用
100
- * @param {Object} toolCall - 工具调用数据
101
- * @returns {Object} 处理后的工具调用
102
- */
103
- processToolCall(toolCall) {
104
- throw new Error('processToolCall方法必须被子类实现');
105
- }
106
-
107
- /**
108
- * 处理工具结果
109
- * @param {Object} toolResult - 工具结果数据
110
- * @returns {Object} 处理后的工具结果
111
- */
112
- processToolResult(toolResult) {
113
- throw new Error('processToolResult方法必须被子类实现');
114
- }
115
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/ConverterFactory.js DELETED
@@ -1,183 +0,0 @@
1
- /**
2
- * 转换器工厂类
3
- * 使用工厂模式管理转换器实例的创建和缓存
4
- */
5
-
6
- import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js';
7
- import logger from '../utils/logger.js';
8
-
9
- /**
10
- * 转换器工厂(单例模式 + 工厂模式)
11
- */
12
- export class ConverterFactory {
13
- // 私有静态属性:存储转换器实例
14
- static #converters = new Map();
15
-
16
- // 私有静态属性:存储转换器类
17
- static #converterClasses = new Map();
18
-
19
- /**
20
- * 注册转换器类
21
- * @param {string} protocolPrefix - 协议前缀
22
- * @param {Class} ConverterClass - 转换器类
23
- */
24
- static registerConverter(protocolPrefix, ConverterClass) {
25
- this.#converterClasses.set(protocolPrefix, ConverterClass);
26
- }
27
-
28
- /**
29
- * 获取转换器实例(带缓存)
30
- * @param {string} protocolPrefix - 协议前缀
31
- * @returns {BaseConverter} 转换器实例
32
- */
33
- static getConverter(protocolPrefix) {
34
- // 检查缓存
35
- if (this.#converters.has(protocolPrefix)) {
36
- return this.#converters.get(protocolPrefix);
37
- }
38
-
39
- // 创建新实例
40
- const converter = this.createConverter(protocolPrefix);
41
-
42
- // 缓存实例
43
- if (converter) {
44
- this.#converters.set(protocolPrefix, converter);
45
- }
46
-
47
- return converter;
48
- }
49
-
50
- /**
51
- * 创建转换器实例
52
- * @param {string} protocolPrefix - 协议前缀
53
- * @returns {BaseConverter} 转换器实例
54
- */
55
- static createConverter(protocolPrefix) {
56
- const ConverterClass = this.#converterClasses.get(protocolPrefix);
57
-
58
- if (!ConverterClass) {
59
- throw new Error(`No converter registered for protocol: ${protocolPrefix}`);
60
- }
61
-
62
- return new ConverterClass();
63
- }
64
-
65
- /**
66
- * 清除所有缓存的转换器
67
- */
68
- static clearCache() {
69
- this.#converters.clear();
70
- }
71
-
72
- /**
73
- * 清除特定协议的转换器缓存
74
- * @param {string} protocolPrefix - 协议前缀
75
- */
76
- static clearConverterCache(protocolPrefix) {
77
- this.#converters.delete(protocolPrefix);
78
- }
79
-
80
- /**
81
- * 获取所有已注册的协议
82
- * @returns {Array<string>} 协议前缀数组
83
- */
84
- static getRegisteredProtocols() {
85
- return Array.from(this.#converterClasses.keys());
86
- }
87
-
88
- /**
89
- * 检查协议是否已注册
90
- * @param {string} protocolPrefix - 协议前缀
91
- * @returns {boolean} 是否已注册
92
- */
93
- static isProtocolRegistered(protocolPrefix) {
94
- return this.#converterClasses.has(protocolPrefix);
95
- }
96
- }
97
-
98
- /**
99
- * 内容处理器工厂
100
- */
101
- export class ContentProcessorFactory {
102
- static #processors = new Map();
103
-
104
- /**
105
- * 获取内容处理器
106
- * @param {string} sourceFormat - 源格式
107
- * @param {string} targetFormat - 目标格式
108
- * @returns {ContentProcessor} 内容处理器实例
109
- */
110
- static getProcessor(sourceFormat, targetFormat) {
111
- const key = `${sourceFormat}_to_${targetFormat}`;
112
-
113
- if (!this.#processors.has(key)) {
114
- this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat));
115
- }
116
-
117
- return this.#processors.get(key);
118
- }
119
-
120
- /**
121
- * 创建内容处理器
122
- * @param {string} sourceFormat - 源格式
123
- * @param {string} targetFormat - 目标格式
124
- * @returns {ContentProcessor} 内容处理器实例
125
- */
126
- static createProcessor(sourceFormat, targetFormat) {
127
- // 这里返回null,实际使用时需要导入具体的处理器类
128
- // 为了避免循环依赖,处理器类应该在使用时动态导入
129
- logger.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
130
- return null;
131
- }
132
-
133
- /**
134
- * 清除所有缓存的处理器
135
- */
136
- static clearCache() {
137
- this.#processors.clear();
138
- }
139
- }
140
-
141
- /**
142
- * 工具处理器工厂
143
- */
144
- export class ToolProcessorFactory {
145
- static #processors = new Map();
146
-
147
- /**
148
- * 获取工具处理器
149
- * @param {string} sourceFormat - 源格式
150
- * @param {string} targetFormat - 目标格式
151
- * @returns {ToolProcessor} 工具处理器实例
152
- */
153
- static getProcessor(sourceFormat, targetFormat) {
154
- const key = `${sourceFormat}_to_${targetFormat}`;
155
-
156
- if (!this.#processors.has(key)) {
157
- this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat));
158
- }
159
-
160
- return this.#processors.get(key);
161
- }
162
-
163
- /**
164
- * 创建工具处理器
165
- * @param {string} sourceFormat - 源格式
166
- * @param {string} targetFormat - 目标格式
167
- * @returns {ToolProcessor} 工具处理器实例
168
- */
169
- static createProcessor(sourceFormat, targetFormat) {
170
- logger.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
171
- return null;
172
- }
173
-
174
- /**
175
- * 清除所有缓存的处理器
176
- */
177
- static clearCache() {
178
- this.#processors.clear();
179
- }
180
- }
181
-
182
- // 导出工厂类
183
- export default ConverterFactory;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/register-converters.js DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * 转换器注册模块
3
- * 用于注册所有转换器到工厂,避免循环依赖问题
4
- */
5
-
6
- import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js';
7
- import { ConverterFactory } from './ConverterFactory.js';
8
- import { OpenAIConverter } from './strategies/OpenAIConverter.js';
9
- import { OpenAIResponsesConverter } from './strategies/OpenAIResponsesConverter.js';
10
- import { ClaudeConverter } from './strategies/ClaudeConverter.js';
11
- import { GeminiConverter } from './strategies/GeminiConverter.js';
12
- import { CodexConverter } from './strategies/CodexConverter.js';
13
- import { GrokConverter } from './strategies/GrokConverter.js';
14
-
15
- /**
16
- * 注册所有转换器到工厂
17
- * 此函数应在应用启动时调用一次
18
- */
19
- export function registerAllConverters() {
20
- ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI, OpenAIConverter);
21
- ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES, OpenAIResponsesConverter);
22
- ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.CLAUDE, ClaudeConverter);
23
- ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.GEMINI, GeminiConverter);
24
- ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.CODEX, CodexConverter);
25
- ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.GROK, GrokConverter);
26
- }
27
-
28
- // 自动注册所有转换器
29
- registerAllConverters();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/strategies/ClaudeConverter.js DELETED
@@ -1,2234 +0,0 @@
1
- /**
2
- * Claude转换器
3
- * 处理Claude(Anthropic)协议与其他协议之间的转换
4
- */
5
-
6
- import { v4 as uuidv4 } from 'uuid';
7
- import logger from '../../utils/logger.js';
8
- import { BaseConverter } from '../BaseConverter.js';
9
- import {
10
- checkAndAssignOrDefault,
11
- cleanJsonSchemaProperties as cleanJsonSchema,
12
- determineReasoningEffortFromBudget,
13
- OPENAI_DEFAULT_MAX_TOKENS,
14
- OPENAI_DEFAULT_TEMPERATURE,
15
- OPENAI_DEFAULT_TOP_P,
16
- GEMINI_DEFAULT_MAX_TOKENS,
17
- GEMINI_DEFAULT_TEMPERATURE,
18
- GEMINI_DEFAULT_TOP_P,
19
- GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
20
- GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT
21
- } from '../utils.js';
22
- import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
23
- import {
24
- generateResponseCreated,
25
- generateResponseInProgress,
26
- generateOutputItemAdded,
27
- generateContentPartAdded,
28
- generateOutputTextDone,
29
- generateContentPartDone,
30
- generateOutputItemDone,
31
- generateResponseCompleted
32
- } from '../../providers/openai/openai-responses-core.mjs';
33
-
34
- /**
35
- * Claude转换器类
36
- * 实现Claude协议到其他协议的转换
37
- */
38
- export class ClaudeConverter extends BaseConverter {
39
- constructor() {
40
- super('claude');
41
- }
42
-
43
- /**
44
- * 转换请求
45
- */
46
- convertRequest(data, targetProtocol) {
47
- switch (targetProtocol) {
48
- case MODEL_PROTOCOL_PREFIX.OPENAI:
49
- return this.toOpenAIRequest(data);
50
- case MODEL_PROTOCOL_PREFIX.GEMINI:
51
- return this.toGeminiRequest(data);
52
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
53
- return this.toOpenAIResponsesRequest(data);
54
- case MODEL_PROTOCOL_PREFIX.CODEX:
55
- return this.toCodexRequest(data);
56
- case MODEL_PROTOCOL_PREFIX.GROK:
57
- return this.toGrokRequest(data);
58
- default:
59
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
60
- }
61
- }
62
-
63
- /**
64
- * 转换响应
65
- */
66
- convertResponse(data, targetProtocol, model) {
67
- switch (targetProtocol) {
68
- case MODEL_PROTOCOL_PREFIX.OPENAI:
69
- return this.toOpenAIResponse(data, model);
70
- case MODEL_PROTOCOL_PREFIX.GEMINI:
71
- return this.toGeminiResponse(data, model);
72
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
73
- return this.toOpenAIResponsesResponse(data, model);
74
- case MODEL_PROTOCOL_PREFIX.CODEX:
75
- return this.toCodexResponse(data, model);
76
- default:
77
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
78
- }
79
- }
80
-
81
- /**
82
- * 转换流式响应块
83
- */
84
- convertStreamChunk(chunk, targetProtocol, model) {
85
- switch (targetProtocol) {
86
- case MODEL_PROTOCOL_PREFIX.OPENAI:
87
- return this.toOpenAIStreamChunk(chunk, model);
88
- case MODEL_PROTOCOL_PREFIX.GEMINI:
89
- return this.toGeminiStreamChunk(chunk, model);
90
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
91
- return this.toOpenAIResponsesStreamChunk(chunk, model);
92
- case MODEL_PROTOCOL_PREFIX.CODEX:
93
- return this.toCodexStreamChunk(chunk, model);
94
- default:
95
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
96
- }
97
- }
98
-
99
- /**
100
- * 转换模型列表
101
- */
102
- convertModelList(data, targetProtocol) {
103
- switch (targetProtocol) {
104
- case MODEL_PROTOCOL_PREFIX.OPENAI:
105
- return this.toOpenAIModelList(data);
106
- case MODEL_PROTOCOL_PREFIX.GEMINI:
107
- return this.toGeminiModelList(data);
108
- default:
109
- return data;
110
- }
111
- }
112
-
113
- // =========================================================================
114
- // Claude -> OpenAI 转换
115
- // =========================================================================
116
-
117
- /**
118
- * Claude请求 -> OpenAI请求
119
- */
120
- toOpenAIRequest(claudeRequest) {
121
- const openaiMessages = [];
122
- let systemMessageContent = '';
123
-
124
- // 添加系统消息
125
- if (claudeRequest.system) {
126
- systemMessageContent = claudeRequest.system;
127
- }
128
-
129
- // 处理消息
130
- if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
131
- const tempOpenAIMessages = [];
132
- for (const msg of claudeRequest.messages) {
133
- const role = msg.role;
134
-
135
- // 处理用户的工具结果消息
136
- if (role === "user" && Array.isArray(msg.content)) {
137
- const hasToolResult = msg.content.some(
138
- item => item && typeof item === 'object' && item.type === "tool_result"
139
- );
140
-
141
- if (hasToolResult) {
142
- for (const item of msg.content) {
143
- if (item && typeof item === 'object' && item.type === "tool_result") {
144
- const toolUseId = item.tool_use_id || item.id || "";
145
- let contentStr = item.content || "";
146
- if (typeof contentStr === 'object') {
147
- contentStr = JSON.stringify(contentStr);
148
- } else {
149
- contentStr = String(contentStr);
150
- }
151
- tempOpenAIMessages.push({
152
- role: "tool",
153
- tool_call_id: toolUseId,
154
- content: contentStr,
155
- });
156
- }
157
- }
158
- continue;
159
- }
160
- }
161
-
162
- // 处理assistant消息中的工具调用
163
- if (role === "assistant" && Array.isArray(msg.content) && msg.content.length > 0) {
164
- const firstPart = msg.content[0];
165
- if (firstPart.type === "tool_use") {
166
- const funcName = firstPart.name || "";
167
- const funcArgs = firstPart.input || {};
168
- tempOpenAIMessages.push({
169
- role: "assistant",
170
- content: '',
171
- tool_calls: [
172
- {
173
- id: firstPart.id || `call_${funcName}_1`,
174
- type: "function",
175
- function: {
176
- name: funcName,
177
- arguments: JSON.stringify(funcArgs)
178
- },
179
- index: firstPart.index || 0
180
- }
181
- ]
182
- });
183
- continue;
184
- }
185
- }
186
-
187
- // 普通文本消息
188
- const contentConverted = this.processClaudeContentToOpenAIContent(msg.content || "");
189
- if (contentConverted && (Array.isArray(contentConverted) ? contentConverted.length > 0 : contentConverted.trim().length > 0)) {
190
- tempOpenAIMessages.push({
191
- role: role,
192
- content: contentConverted
193
- });
194
- }
195
- }
196
-
197
- // OpenAI兼容性校验
198
- const validatedMessages = [];
199
- for (let idx = 0; idx < tempOpenAIMessages.length; idx++) {
200
- const m = tempOpenAIMessages[idx];
201
- if (m.role === "assistant" && m.tool_calls) {
202
- const callIds = m.tool_calls.map(tc => tc.id).filter(id => id);
203
- let unmatched = new Set(callIds);
204
- for (let laterIdx = idx + 1; laterIdx < tempOpenAIMessages.length; laterIdx++) {
205
- const later = tempOpenAIMessages[laterIdx];
206
- if (later.role === "tool" && unmatched.has(later.tool_call_id)) {
207
- unmatched.delete(later.tool_call_id);
208
- }
209
- if (unmatched.size === 0) break;
210
- }
211
- if (unmatched.size > 0) {
212
- m.tool_calls = m.tool_calls.filter(tc => !unmatched.has(tc.id));
213
- if (m.tool_calls.length === 0) {
214
- delete m.tool_calls;
215
- if (m.content === null) m.content = "";
216
- }
217
- }
218
- }
219
- validatedMessages.push(m);
220
- }
221
- openaiMessages.push(...validatedMessages);
222
- }
223
-
224
- const openaiRequest = {
225
- model: claudeRequest.model,
226
- messages: openaiMessages,
227
- max_tokens: checkAndAssignOrDefault(claudeRequest.max_tokens, OPENAI_DEFAULT_MAX_TOKENS),
228
- temperature: checkAndAssignOrDefault(claudeRequest.temperature, OPENAI_DEFAULT_TEMPERATURE),
229
- top_p: checkAndAssignOrDefault(claudeRequest.top_p, OPENAI_DEFAULT_TOP_P),
230
- stream: claudeRequest.stream,
231
- };
232
-
233
- // 处理工具
234
- if (claudeRequest.tools) {
235
- const openaiTools = [];
236
- for (const tool of claudeRequest.tools) {
237
- openaiTools.push({
238
- type: "function",
239
- function: {
240
- name: tool.name || "",
241
- description: tool.description || "",
242
- parameters: cleanJsonSchema(tool.input_schema || {})
243
- }
244
- });
245
- }
246
- openaiRequest.tools = openaiTools;
247
- openaiRequest.tool_choice = "auto";
248
- }
249
-
250
- // 处理thinking转换
251
- if (claudeRequest.thinking && claudeRequest.thinking.type === "enabled") {
252
- const budgetTokens = claudeRequest.thinking.budget_tokens;
253
- const reasoningEffort = determineReasoningEffortFromBudget(budgetTokens);
254
- openaiRequest.reasoning_effort = reasoningEffort;
255
-
256
- let maxCompletionTokens = null;
257
- if (claudeRequest.max_tokens !== undefined) {
258
- maxCompletionTokens = claudeRequest.max_tokens;
259
- delete openaiRequest.max_tokens;
260
- } else {
261
- const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS;
262
- if (envMaxTokens) {
263
- try {
264
- maxCompletionTokens = parseInt(envMaxTokens, 10);
265
- } catch (e) {
266
- logger.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}'`);
267
- }
268
- }
269
- if (!envMaxTokens) {
270
- throw new Error("For OpenAI reasoning models, max_completion_tokens is required.");
271
- }
272
- }
273
- openaiRequest.max_completion_tokens = maxCompletionTokens;
274
- }
275
-
276
- // 添加系统消息
277
- if (systemMessageContent) {
278
- let stringifiedSystemMessageContent = systemMessageContent;
279
- if (Array.isArray(systemMessageContent)) {
280
- stringifiedSystemMessageContent = systemMessageContent.map(item =>
281
- typeof item === 'string' ? item : item.text).join('\n');
282
- }
283
- openaiRequest.messages.unshift({ role: 'system', content: stringifiedSystemMessageContent });
284
- }
285
-
286
- return openaiRequest;
287
- }
288
-
289
- /**
290
- * Claude响应 -> OpenAI响应
291
- */
292
- toOpenAIResponse(claudeResponse, model) {
293
- if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) {
294
- return {
295
- id: `chatcmpl-${uuidv4()}`,
296
- object: "chat.completion",
297
- created: Math.floor(Date.now() / 1000),
298
- model: model,
299
- choices: [{
300
- index: 0,
301
- message: {
302
- role: "assistant",
303
- content: "",
304
- },
305
- finish_reason: "stop",
306
- }],
307
- usage: {
308
- prompt_tokens: claudeResponse.usage?.input_tokens || 0,
309
- completion_tokens: claudeResponse.usage?.output_tokens || 0,
310
- total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0),
311
- },
312
- };
313
- }
314
-
315
- // Extract thinking blocks into OpenAI-style `reasoning_content`.
316
- let reasoningContent = '';
317
- if (Array.isArray(claudeResponse.content)) {
318
- for (const block of claudeResponse.content) {
319
- if (!block || typeof block !== 'object') continue;
320
- if (block.type === 'thinking') {
321
- reasoningContent += (block.thinking ?? block.text ?? '');
322
- }
323
- }
324
- }
325
-
326
- // 检查是否包含 tool_use
327
- const hasToolUse = claudeResponse.content.some(block => block && block.type === 'tool_use');
328
-
329
- let message = {
330
- role: "assistant",
331
- content: null
332
- };
333
-
334
- if (hasToolUse) {
335
- // 处理包含工具调用的响应
336
- const toolCalls = [];
337
- let textContent = '';
338
-
339
- for (const block of claudeResponse.content) {
340
- if (!block) continue;
341
-
342
- if (block.type === 'text') {
343
- textContent += block.text || '';
344
- } else if (block.type === 'tool_use') {
345
- toolCalls.push({
346
- id: block.id || `call_${block.name}_${Date.now()}`,
347
- type: "function",
348
- function: {
349
- name: block.name || '',
350
- arguments: JSON.stringify(block.input || {})
351
- }
352
- });
353
- }
354
- }
355
-
356
- message.content = textContent || null;
357
- if (toolCalls.length > 0) {
358
- message.tool_calls = toolCalls;
359
- }
360
- } else {
361
- // 处理普通文本响应
362
- message.content = this.processClaudeResponseContent(claudeResponse.content);
363
- }
364
-
365
- if (reasoningContent) {
366
- message.reasoning_content = reasoningContent;
367
- }
368
-
369
- // 处理 finish_reason
370
- let finishReason = 'stop';
371
- if (claudeResponse.stop_reason === 'end_turn') {
372
- finishReason = 'stop';
373
- } else if (claudeResponse.stop_reason === 'max_tokens') {
374
- finishReason = 'length';
375
- } else if (claudeResponse.stop_reason === 'tool_use') {
376
- finishReason = 'tool_calls';
377
- } else if (claudeResponse.stop_reason) {
378
- finishReason = claudeResponse.stop_reason;
379
- }
380
-
381
- return {
382
- id: `chatcmpl-${uuidv4()}`,
383
- object: "chat.completion",
384
- created: Math.floor(Date.now() / 1000),
385
- model: model,
386
- choices: [{
387
- index: 0,
388
- message: message,
389
- finish_reason: finishReason,
390
- }],
391
- usage: {
392
- prompt_tokens: claudeResponse.usage?.input_tokens || 0,
393
- completion_tokens: claudeResponse.usage?.output_tokens || 0,
394
- total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0),
395
- cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0,
396
- prompt_tokens_details: {
397
- cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0
398
- }
399
- },
400
- };
401
- }
402
-
403
- /**
404
- * Claude流式响应 -> OpenAI流式响应
405
- */
406
- toOpenAIStreamChunk(claudeChunk, model) {
407
- if (!claudeChunk) return null;
408
-
409
- // 处理 Claude 流式事件
410
- const chunkId = `chatcmpl-${uuidv4()}`;
411
- const timestamp = Math.floor(Date.now() / 1000);
412
-
413
- // message_start 事件
414
- if (claudeChunk.type === 'message_start') {
415
- return {
416
- id: chunkId,
417
- object: "chat.completion.chunk",
418
- created: timestamp,
419
- model: model,
420
- system_fingerprint: "",
421
- choices: [{
422
- index: 0,
423
- delta: {
424
- role: "assistant",
425
- content: ""
426
- },
427
- finish_reason: null
428
- }],
429
- usage: {
430
- prompt_tokens: claudeChunk.message?.usage?.input_tokens || 0,
431
- completion_tokens: 0,
432
- total_tokens: claudeChunk.message?.usage?.input_tokens || 0,
433
- cached_tokens: claudeChunk.message?.usage?.cache_read_input_tokens || 0
434
- }
435
- };
436
- }
437
-
438
- // content_block_start 事件
439
- if (claudeChunk.type === 'content_block_start') {
440
- const contentBlock = claudeChunk.content_block;
441
-
442
- // 处理 tool_use 类型
443
- if (contentBlock && contentBlock.type === 'tool_use') {
444
- return {
445
- id: chunkId,
446
- object: "chat.completion.chunk",
447
- created: timestamp,
448
- model: model,
449
- system_fingerprint: "",
450
- choices: [{
451
- index: 0,
452
- delta: {
453
- tool_calls: [{
454
- index: claudeChunk.index || 0,
455
- id: contentBlock.id,
456
- type: "function",
457
- function: {
458
- name: contentBlock.name,
459
- arguments: ""
460
- }
461
- }]
462
- },
463
- finish_reason: null
464
- }]
465
- };
466
- }
467
-
468
- // 处理 text 类型
469
- return {
470
- id: chunkId,
471
- object: "chat.completion.chunk",
472
- created: timestamp,
473
- model: model,
474
- system_fingerprint: "",
475
- choices: [{
476
- index: 0,
477
- delta: {
478
- content: ""
479
- },
480
- finish_reason: null
481
- }]
482
- };
483
- }
484
-
485
- // content_block_delta 事件
486
- if (claudeChunk.type === 'content_block_delta') {
487
- const delta = claudeChunk.delta;
488
-
489
- // 处理 text_delta
490
- if (delta && delta.type === 'text_delta') {
491
- return {
492
- id: chunkId,
493
- object: "chat.completion.chunk",
494
- created: timestamp,
495
- model: model,
496
- system_fingerprint: "",
497
- choices: [{
498
- index: 0,
499
- delta: {
500
- content: delta.text || ""
501
- },
502
- finish_reason: null
503
- }]
504
- };
505
- }
506
-
507
- // 处理 thinking_delta (推理内容)
508
- if (delta && delta.type === 'thinking_delta') {
509
- return {
510
- id: chunkId,
511
- object: "chat.completion.chunk",
512
- created: timestamp,
513
- model: model,
514
- system_fingerprint: "",
515
- choices: [{
516
- index: 0,
517
- delta: {
518
- reasoning_content: delta.thinking || ""
519
- },
520
- finish_reason: null
521
- }]
522
- };
523
- }
524
-
525
- // 处理 input_json_delta (tool arguments)
526
- if (delta && delta.type === 'input_json_delta') {
527
- return {
528
- id: chunkId,
529
- object: "chat.completion.chunk",
530
- created: timestamp,
531
- model: model,
532
- system_fingerprint: "",
533
- choices: [{
534
- index: 0,
535
- delta: {
536
- tool_calls: [{
537
- index: claudeChunk.index || 0,
538
- function: {
539
- arguments: delta.partial_json || ""
540
- }
541
- }]
542
- },
543
- finish_reason: null
544
- }]
545
- };
546
- }
547
- }
548
-
549
- // content_block_stop 事件
550
- if (claudeChunk.type === 'content_block_stop') {
551
- return {
552
- id: chunkId,
553
- object: "chat.completion.chunk",
554
- created: timestamp,
555
- model: model,
556
- system_fingerprint: "",
557
- choices: [{
558
- index: 0,
559
- delta: {},
560
- finish_reason: null
561
- }]
562
- };
563
- }
564
-
565
- // message_delta 事件
566
- if (claudeChunk.type === 'message_delta') {
567
- const stopReason = claudeChunk.delta?.stop_reason;
568
- const finishReason = stopReason === 'end_turn' ? 'stop' :
569
- stopReason === 'max_tokens' ? 'length' :
570
- stopReason === 'tool_use' ? 'tool_calls' :
571
- stopReason || 'stop';
572
-
573
- const chunk = {
574
- id: chunkId,
575
- object: "chat.completion.chunk",
576
- created: timestamp,
577
- model: model,
578
- system_fingerprint: "",
579
- choices: [{
580
- index: 0,
581
- delta: {},
582
- finish_reason: finishReason
583
- }]
584
- };
585
-
586
- if(claudeChunk.usage){
587
- chunk.usage = {
588
- prompt_tokens: claudeChunk.usage.input_tokens || 0,
589
- completion_tokens: claudeChunk.usage.output_tokens || 0,
590
- total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0),
591
- cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0,
592
- prompt_tokens_details: {
593
- cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0
594
- }
595
- };
596
- }
597
-
598
- return chunk;
599
- }
600
-
601
- // message_stop 事件
602
- if (claudeChunk.type === 'message_stop') {
603
- return null;
604
- // const chunk = {
605
- // id: chunkId,
606
- // object: "chat.completion.chunk",
607
- // created: timestamp,
608
- // model: model,
609
- // system_fingerprint: "",
610
- // choices: [{
611
- // index: 0,
612
- // delta: {},
613
- // finish_reason: 'stop'
614
- // }]
615
- // };
616
- // return chunk;
617
- }
618
-
619
- // 兼容旧格式:如果是字符串,直接作为文本内容
620
- if (typeof claudeChunk === 'string') {
621
- return {
622
- id: chunkId,
623
- object: "chat.completion.chunk",
624
- created: timestamp,
625
- model: model,
626
- system_fingerprint: "",
627
- choices: [{
628
- index: 0,
629
- delta: {
630
- content: claudeChunk
631
- },
632
- finish_reason: null
633
- }]
634
- };
635
- }
636
-
637
- return null;
638
- }
639
-
640
- /**
641
- * Claude模型列表 -> OpenAI模型列表
642
- */
643
- toOpenAIModelList(claudeModels) {
644
- return {
645
- object: "list",
646
- data: claudeModels.models.map(m => {
647
- const modelId = m.id || m.name;
648
- return {
649
- id: modelId,
650
- object: "model",
651
- created: Math.floor(Date.now() / 1000),
652
- owned_by: "anthropic",
653
- display_name: modelId,
654
- };
655
- }),
656
- };
657
- }
658
-
659
- /**
660
- * 将 Claude 模型列表转换为 Gemini 模型列表
661
- */
662
- toGeminiModelList(claudeModels) {
663
- const models = claudeModels.models || [];
664
- return {
665
- models: models.map(m => ({
666
- name: `models/${m.id || m.name}`,
667
- version: m.version || "1.0.0",
668
- displayName: m.displayName || m.id || m.name,
669
- description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`,
670
- inputTokenLimit: m.inputTokenLimit || GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
671
- outputTokenLimit: m.outputTokenLimit || GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT,
672
- supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
673
- }))
674
- };
675
- }
676
-
677
- /**
678
- * 处理Claude内容到OpenAI格式
679
- */
680
- processClaudeContentToOpenAIContent(content) {
681
- if (!content) return [];
682
-
683
- // 如果是字符串,直接转换为 OpenAI 的文本块格式
684
- if (typeof content === 'string') {
685
- return [{
686
- type: 'text',
687
- text: content
688
- }];
689
- }
690
-
691
- if (!Array.isArray(content)) return [];
692
-
693
- const contentArray = [];
694
-
695
- content.forEach(block => {
696
- if (!block) return;
697
-
698
- switch (block.type) {
699
- case 'text':
700
- if (block.text) {
701
- contentArray.push({
702
- type: 'text',
703
- text: block.text
704
- });
705
- }
706
- break;
707
-
708
- case 'image':
709
- if (block.source && block.source.type === 'base64') {
710
- contentArray.push({
711
- type: 'image_url',
712
- image_url: {
713
- url: `data:${block.source.media_type};base64,${block.source.data}`
714
- }
715
- });
716
- }
717
- break;
718
-
719
- case 'tool_use':
720
- contentArray.push({
721
- type: 'text',
722
- text: `[Tool use: ${block.name}]`
723
- });
724
- break;
725
-
726
- case 'tool_result':
727
- contentArray.push({
728
- type: 'text',
729
- text: typeof block.content === 'string' ? block.content : JSON.stringify(block.content)
730
- });
731
- break;
732
-
733
- default:
734
- if (block.text) {
735
- contentArray.push({
736
- type: 'text',
737
- text: block.text
738
- });
739
- }
740
- }
741
- });
742
-
743
- return contentArray;
744
- }
745
-
746
- /**
747
- * 处理Claude响应内容
748
- */
749
- processClaudeResponseContent(content) {
750
- if (!content) return '';
751
-
752
- if (typeof content === 'string') return content;
753
-
754
- if (!Array.isArray(content)) return '';
755
-
756
- const contentArray = [];
757
-
758
- content.forEach(block => {
759
- if (!block) return;
760
-
761
- switch (block.type) {
762
- case 'text':
763
- contentArray.push({
764
- type: 'text',
765
- text: block.text || ''
766
- });
767
- break;
768
-
769
- case 'image':
770
- if (block.source && block.source.type === 'base64') {
771
- contentArray.push({
772
- type: 'image_url',
773
- image_url: {
774
- url: `data:${block.source.media_type};base64,${block.source.data}`
775
- }
776
- });
777
- }
778
- break;
779
-
780
- default:
781
- if (block.text) {
782
- contentArray.push({
783
- type: 'text',
784
- text: block.text
785
- });
786
- }
787
- }
788
- });
789
-
790
- return contentArray.length === 1 && contentArray[0].type === 'text'
791
- ? contentArray[0].text
792
- : contentArray;
793
- }
794
-
795
- // =========================================================================
796
- // Claude -> Gemini 转换
797
- // =========================================================================
798
-
799
- // Gemini Claude thought signature constant
800
- static GEMINI_CLAUDE_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
801
-
802
- /**
803
- * Claude请求 -> Gemini请求
804
- */
805
- toGeminiRequest(claudeRequest) {
806
- if (!claudeRequest || typeof claudeRequest !== 'object') {
807
- logger.warn("Invalid claudeRequest provided to toGeminiRequest.");
808
- return { contents: [] };
809
- }
810
-
811
- const geminiRequest = {
812
- contents: []
813
- };
814
-
815
- // 处理系统指令 - 支持数组和字符串格式
816
- if (claudeRequest.system) {
817
- if (Array.isArray(claudeRequest.system)) {
818
- // 数组格式的系统指令
819
- const systemParts = [];
820
- claudeRequest.system.forEach(systemPrompt => {
821
- if (systemPrompt && systemPrompt.type === 'text' && typeof systemPrompt.text === 'string') {
822
- systemParts.push({ text: systemPrompt.text });
823
- }
824
- });
825
- if (systemParts.length > 0) {
826
- geminiRequest.systemInstruction = {
827
- role: 'user',
828
- parts: systemParts
829
- };
830
- }
831
- } else if (typeof claudeRequest.system === 'string') {
832
- // 字符串格式的系统指令
833
- geminiRequest.systemInstruction = {
834
- parts: [{ text: claudeRequest.system }]
835
- };
836
- } else if (typeof claudeRequest.system === 'object') {
837
- // 对象格式的系统指令
838
- geminiRequest.systemInstruction = {
839
- parts: [{ text: JSON.stringify(claudeRequest.system) }]
840
- };
841
- }
842
- }
843
-
844
- // 处理消息
845
- if (Array.isArray(claudeRequest.messages)) {
846
- claudeRequest.messages.forEach(message => {
847
- if (!message || typeof message !== 'object' || !message.role) {
848
- logger.warn("Skipping invalid message in claudeRequest.messages.");
849
- return;
850
- }
851
-
852
- const geminiRole = message.role === 'assistant' ? 'model' : 'user';
853
- const content = message.content;
854
-
855
- // 处理内容
856
- if (Array.isArray(content)) {
857
- const parts = [];
858
-
859
- content.forEach(block => {
860
- if (!block || typeof block !== 'object') return;
861
-
862
- switch (block.type) {
863
- case 'text':
864
- if (typeof block.text === 'string') {
865
- parts.push({ text: block.text });
866
- }
867
- break;
868
-
869
- // 添加 thinking 块处理
870
- case 'thinking':
871
- if (typeof block.thinking === 'string' && block.thinking.length > 0) {
872
- const thinkingPart = {
873
- text: block.thinking,
874
- thought: true
875
- };
876
- // 如果有签名,添加 thoughtSignature
877
- if (block.signature && block.signature.length >= 50) {
878
- thinkingPart.thoughtSignature = block.signature;
879
- }
880
- parts.push(thinkingPart);
881
- }
882
- break;
883
-
884
- // [FIX] 处理 redacted_thinking 块
885
- case 'redacted_thinking':
886
- // 将 redacted_thinking 转换为普通文本
887
- if (block.data) {
888
- parts.push({
889
- text: `[Redacted Thinking: ${block.data}]`
890
- });
891
- }
892
- break;
893
-
894
- case 'tool_use':
895
- // 转换为 Gemini functionCall 格式
896
- if (block.name && block.input) {
897
- const args = typeof block.input === 'string'
898
- ? block.input
899
- : JSON.stringify(block.input);
900
-
901
- // 验证 args 是有效的 JSON 对象
902
- try {
903
- const parsedArgs = JSON.parse(args);
904
- if (parsedArgs && typeof parsedArgs === 'object') {
905
- parts.push({
906
- thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
907
- functionCall: {
908
- name: block.name,
909
- args: parsedArgs
910
- }
911
- });
912
- }
913
- } catch (e) {
914
- // 如果解析失败,尝试直接使用 input
915
- if (block.input && typeof block.input === 'object') {
916
- parts.push({
917
- thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
918
- functionCall: {
919
- name: block.name,
920
- args: block.input
921
- }
922
- });
923
- }
924
- }
925
- }
926
- break;
927
-
928
- case 'tool_result':
929
- // 转换为 Gemini functionResponse 格式
930
- // 的实现,正确处理 tool_use_id 到函数名的映射
931
- const toolCallId = block.tool_use_id;
932
- if (toolCallId) {
933
- // 尝试从之前的 tool_use 块中查找对应的函数名
934
- // 如果找不到,则从 tool_use_id 中提取
935
- let funcName = toolCallId;
936
-
937
- // 检查是否有缓存的 tool_id -> name 映射
938
- // 格式通常是 "funcName-uuid" 或 "toolu_xxx"
939
- if (toolCallId.startsWith('toolu_')) {
940
- // Claude 格式的 tool_use_id,需要从上下文中查找函数名
941
- // 这里我们保留原始 ID 作为 name(Gemini 会处理)
942
- funcName = toolCallId;
943
- } else {
944
- const toolCallIdParts = toolCallId.split('-');
945
- if (toolCallIdParts.length > 1) {
946
- // 移除最后一个部分(UUID),保留函数名
947
- funcName = toolCallIdParts.slice(0, -1).join('-');
948
- }
949
- }
950
-
951
- // 获取响应数据
952
- let responseData = block.content;
953
-
954
- // 的 tool_result_compressor 逻辑
955
- // 处理嵌套的 content 数组(如图片等)
956
- if (Array.isArray(responseData)) {
957
- // 提取文本内容
958
- const textParts = responseData
959
- .filter(item => item && item.type === 'text')
960
- .map(item => item.text)
961
- .join('\n');
962
- responseData = textParts || JSON.stringify(responseData);
963
- } else if (typeof responseData !== 'string') {
964
- responseData = JSON.stringify(responseData);
965
- }
966
-
967
- parts.push({
968
- functionResponse: {
969
- name: funcName,
970
- response: {
971
- result: responseData
972
- }
973
- }
974
- });
975
- }
976
- break;
977
-
978
- case 'image':
979
- if (block.source && block.source.type === 'base64') {
980
- parts.push({
981
- inlineData: {
982
- mimeType: block.source.media_type,
983
- data: block.source.data
984
- }
985
- });
986
- }
987
- break;
988
- }
989
- });
990
-
991
- if (parts.length > 0) {
992
- geminiRequest.contents.push({
993
- role: geminiRole,
994
- parts: parts
995
- });
996
- }
997
- } else if (typeof content === 'string') {
998
- // 字符串内容
999
- geminiRequest.contents.push({
1000
- role: geminiRole,
1001
- parts: [{ text: content }]
1002
- });
1003
- }
1004
- });
1005
- }
1006
-
1007
- // 添加生成配置
1008
- const generationConfig = {};
1009
-
1010
- if (claudeRequest.max_tokens !== undefined) {
1011
- generationConfig.maxOutputTokens = claudeRequest.max_tokens;
1012
- }
1013
- if (claudeRequest.temperature !== undefined) {
1014
- generationConfig.temperature = claudeRequest.temperature;
1015
- }
1016
- if (claudeRequest.top_p !== undefined) {
1017
- generationConfig.topP = claudeRequest.top_p;
1018
- }
1019
- if (claudeRequest.top_k !== undefined) {
1020
- generationConfig.topK = claudeRequest.top_k;
1021
- }
1022
-
1023
- // 处理 thinking 配置 - 转换为 Gemini thinkingBudget
1024
- if (claudeRequest.thinking && claudeRequest.thinking.type === 'enabled') {
1025
- if (claudeRequest.thinking.budget_tokens !== undefined) {
1026
- const budget = claudeRequest.thinking.budget_tokens;
1027
- if (!generationConfig.thinkingConfig) {
1028
- generationConfig.thinkingConfig = {};
1029
- }
1030
- generationConfig.thinkingConfig.thinkingBudget = budget;
1031
- generationConfig.thinkingConfig.include_thoughts = true;
1032
- }
1033
- }
1034
-
1035
- if (Object.keys(generationConfig).length > 0) {
1036
- geminiRequest.generationConfig = generationConfig;
1037
- }
1038
-
1039
- // 处理工具 - 使用 parametersJsonSchema 格式
1040
- if (Array.isArray(claudeRequest.tools) && claudeRequest.tools.length > 0) {
1041
- const functionDeclarations = [];
1042
-
1043
- claudeRequest.tools.forEach(tool => {
1044
- if (!tool || typeof tool !== 'object' || !tool.name) {
1045
- logger.warn("Skipping invalid tool declaration in claudeRequest.tools.");
1046
- return;
1047
- }
1048
-
1049
- // 清理 input_schema
1050
- let inputSchema = tool.input_schema;
1051
- if (inputSchema && typeof inputSchema === 'object') {
1052
- // 创建副本以避免修改原始对象
1053
- inputSchema = JSON.parse(JSON.stringify(inputSchema));
1054
- // 清理不需要的字段
1055
- delete inputSchema.$schema;
1056
- // 清理 URL 格式(Gemini 不支持)
1057
- this.cleanUrlFormatFromSchema(inputSchema);
1058
- }
1059
-
1060
- const funcDecl = {
1061
- name: String(tool.name),
1062
- description: String(tool.description || '')
1063
- };
1064
-
1065
- // 使用 parametersJsonSchema 而不是 parameters
1066
- if (inputSchema) {
1067
- funcDecl.parametersJsonSchema = inputSchema;
1068
- }
1069
-
1070
- functionDeclarations.push(funcDecl);
1071
- });
1072
-
1073
- if (functionDeclarations.length > 0) {
1074
- geminiRequest.tools = [{
1075
- functionDeclarations: functionDeclarations
1076
- }];
1077
- }
1078
- }
1079
-
1080
- // 处理tool_choice
1081
- if (claudeRequest.tool_choice) {
1082
- geminiRequest.toolConfig = this.buildGeminiToolConfigFromClaude(claudeRequest.tool_choice);
1083
- }
1084
-
1085
- // 添加默认安全设置
1086
- geminiRequest.safetySettings = this.getDefaultSafetySettings();
1087
-
1088
- return geminiRequest;
1089
- }
1090
-
1091
- /**
1092
- * 清理 JSON Schema 中的 URL 格式
1093
- * Gemini 不支持 "format": "uri"
1094
- */
1095
- cleanUrlFormatFromSchema(schema) {
1096
- if (!schema || typeof schema !== 'object') return;
1097
-
1098
- // 如果是属性对象,检查并清理 format
1099
- if (schema.type === 'string' && schema.format === 'uri') {
1100
- delete schema.format;
1101
- }
1102
-
1103
- // 递归处理 properties
1104
- if (schema.properties && typeof schema.properties === 'object') {
1105
- Object.values(schema.properties).forEach(prop => {
1106
- this.cleanUrlFormatFromSchema(prop);
1107
- });
1108
- }
1109
-
1110
- // 递归处理 items(数组类型)
1111
- if (schema.items) {
1112
- this.cleanUrlFormatFromSchema(schema.items);
1113
- }
1114
-
1115
- // 递归处理 additionalProperties
1116
- if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
1117
- this.cleanUrlFormatFromSchema(schema.additionalProperties);
1118
- }
1119
- }
1120
-
1121
- /**
1122
- * 获取默认的 Gemini 安全设置
1123
- */
1124
- getDefaultSafetySettings() {
1125
- return [
1126
- { category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
1127
- { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
1128
- { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
1129
- { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
1130
- { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
1131
- ];
1132
- }
1133
-
1134
- /**
1135
- * Claude响应 -> Gemini响应
1136
- */
1137
- toGeminiResponse(claudeResponse, model) {
1138
- if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) {
1139
- return { candidates: [], usageMetadata: {} };
1140
- }
1141
-
1142
- const parts = [];
1143
-
1144
- // 处理内容块
1145
- for (const block of claudeResponse.content) {
1146
- if (!block) continue;
1147
-
1148
- switch (block.type) {
1149
- case 'text':
1150
- if (block.text) {
1151
- parts.push({ text: block.text });
1152
- }
1153
- break;
1154
-
1155
- // 添加 thinking 块处理
1156
- case 'thinking':
1157
- if (block.thinking) {
1158
- const thinkingPart = {
1159
- text: block.thinking,
1160
- thought: true
1161
- };
1162
- // 如果有签名,添加 thoughtSignature
1163
- if (block.signature && block.signature.length >= 50) {
1164
- thinkingPart.thoughtSignature = block.signature;
1165
- }
1166
- parts.push(thinkingPart);
1167
- }
1168
- break;
1169
-
1170
- case 'tool_use':
1171
- // [FIX] 添加 id 和 thoughtSignature 支持
1172
- const functionCallPart = {
1173
- functionCall: {
1174
- name: block.name,
1175
- args: block.input || {}
1176
- }
1177
- };
1178
- // 添加 id(如果存在)
1179
- if (block.id) {
1180
- functionCallPart.functionCall.id = block.id;
1181
- }
1182
- // 添加签名(如果存在)
1183
- if (block.signature && block.signature.length >= 50) {
1184
- functionCallPart.thoughtSignature = block.signature;
1185
- }
1186
- parts.push(functionCallPart);
1187
- break;
1188
-
1189
- case 'image':
1190
- if (block.source && block.source.type === 'base64') {
1191
- parts.push({
1192
- inlineData: {
1193
- mimeType: block.source.media_type,
1194
- data: block.source.data
1195
- }
1196
- });
1197
- }
1198
- break;
1199
-
1200
- default:
1201
- if (block.text) {
1202
- parts.push({ text: block.text });
1203
- }
1204
- }
1205
- }
1206
-
1207
- // 映射finish_reason
1208
- const finishReasonMap = {
1209
- 'end_turn': 'STOP',
1210
- 'max_tokens': 'MAX_TOKENS',
1211
- 'tool_use': 'STOP',
1212
- 'stop_sequence': 'STOP'
1213
- };
1214
-
1215
- return {
1216
- candidates: [{
1217
- content: {
1218
- role: 'model',
1219
- parts: parts
1220
- },
1221
- finishReason: finishReasonMap[claudeResponse.stop_reason] || 'STOP'
1222
- }],
1223
- usageMetadata: claudeResponse.usage ? {
1224
- promptTokenCount: claudeResponse.usage.input_tokens || 0,
1225
- candidatesTokenCount: claudeResponse.usage.output_tokens || 0,
1226
- totalTokenCount: (claudeResponse.usage.input_tokens || 0) + (claudeResponse.usage.output_tokens || 0),
1227
- cachedContentTokenCount: claudeResponse.usage.cache_read_input_tokens || 0,
1228
- promptTokensDetails: [{
1229
- modality: "TEXT",
1230
- tokenCount: claudeResponse.usage.input_tokens || 0
1231
- }],
1232
- candidatesTokensDetails: [{
1233
- modality: "TEXT",
1234
- tokenCount: claudeResponse.usage.output_tokens || 0
1235
- }]
1236
- } : {}
1237
- };
1238
- }
1239
-
1240
- /**
1241
- * Claude流式响应 -> Gemini流式响应
1242
- */
1243
- toGeminiStreamChunk(claudeChunk, model) {
1244
- if (!claudeChunk) return null;
1245
-
1246
- // 处理Claude流式事件
1247
- if (typeof claudeChunk === 'object' && !Array.isArray(claudeChunk)) {
1248
- // content_block_start 事件 - 处理 thinking 块开始
1249
- if (claudeChunk.type === 'content_block_start') {
1250
- const contentBlock = claudeChunk.content_block;
1251
- if (contentBlock && contentBlock.type === 'thinking') {
1252
- // thinking 块开始,返回空(等待 delta)
1253
- return null;
1254
- }
1255
- if (contentBlock && contentBlock.type === 'tool_use') {
1256
- // tool_use 块开始
1257
- return {
1258
- candidates: [{
1259
- content: {
1260
- role: "model",
1261
- parts: [{
1262
- functionCall: {
1263
- name: contentBlock.name,
1264
- args: {},
1265
- id: contentBlock.id
1266
- }
1267
- }]
1268
- }
1269
- }]
1270
- };
1271
- }
1272
- }
1273
-
1274
- // content_block_delta 事件
1275
- if (claudeChunk.type === 'content_block_delta') {
1276
- const delta = claudeChunk.delta;
1277
-
1278
- // 处理 text_delta
1279
- if (delta && delta.type === 'text_delta') {
1280
- return {
1281
- candidates: [{
1282
- content: {
1283
- role: "model",
1284
- parts: [{
1285
- text: delta.text || ""
1286
- }]
1287
- }
1288
- }]
1289
- };
1290
- }
1291
-
1292
- // [FIX] 处理 thinking_delta - 转换为 Gemini 的 thought 格式
1293
- if (delta && delta.type === 'thinking_delta') {
1294
- return {
1295
- candidates: [{
1296
- content: {
1297
- role: "model",
1298
- parts: [{
1299
- text: delta.thinking || "",
1300
- thought: true
1301
- }]
1302
- }
1303
- }]
1304
- };
1305
- }
1306
-
1307
- // [FIX] 处理 signature_delta
1308
- if (delta && delta.type === 'signature_delta') {
1309
- // 签名通常与前一个 thinking 块关联
1310
- // 在流式场景中,我们可以忽略或记录
1311
- return null;
1312
- }
1313
-
1314
- // [FIX] 处理 input_json_delta (tool arguments)
1315
- if (delta && delta.type === 'input_json_delta') {
1316
- // 工具参数增量,Gemini 不支持增量参数,忽略
1317
- return null;
1318
- }
1319
- }
1320
-
1321
- // message_delta 事件 - 流结束
1322
- if (claudeChunk.type === 'message_delta') {
1323
- const stopReason = claudeChunk.delta?.stop_reason;
1324
- const result = {
1325
- candidates: [{
1326
- finishReason: stopReason === 'end_turn' ? 'STOP' :
1327
- stopReason === 'max_tokens' ? 'MAX_TOKENS' :
1328
- stopReason === 'tool_use' ? 'STOP' :
1329
- 'OTHER'
1330
- }]
1331
- };
1332
-
1333
- // 添加 usage 信息
1334
- if (claudeChunk.usage) {
1335
- result.usageMetadata = {
1336
- promptTokenCount: claudeChunk.usage.input_tokens || 0,
1337
- candidatesTokenCount: claudeChunk.usage.output_tokens || 0,
1338
- totalTokenCount: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0),
1339
- cachedContentTokenCount: claudeChunk.usage.cache_read_input_tokens || 0,
1340
- promptTokensDetails: [{
1341
- modality: "TEXT",
1342
- tokenCount: claudeChunk.usage.input_tokens || 0
1343
- }],
1344
- candidatesTokensDetails: [{
1345
- modality: "TEXT",
1346
- tokenCount: claudeChunk.usage.output_tokens || 0
1347
- }]
1348
- };
1349
- }
1350
-
1351
- return result;
1352
- }
1353
- }
1354
-
1355
- // 向后兼容:处理字符串格式
1356
- if (typeof claudeChunk === 'string') {
1357
- return {
1358
- candidates: [{
1359
- content: {
1360
- role: "model",
1361
- parts: [{
1362
- text: claudeChunk
1363
- }]
1364
- }
1365
- }]
1366
- };
1367
- }
1368
-
1369
- return null;
1370
- }
1371
-
1372
- /**
1373
- * 处理Claude内容到Gemini parts
1374
- */
1375
- processClaudeContentToGeminiParts(content) {
1376
- if (!content) return [];
1377
-
1378
- if (typeof content === 'string') {
1379
- return [{ text: content }];
1380
- }
1381
-
1382
- if (Array.isArray(content)) {
1383
- const parts = [];
1384
-
1385
- content.forEach(block => {
1386
- if (!block || typeof block !== 'object' || !block.type) {
1387
- logger.warn("Skipping invalid content block.");
1388
- return;
1389
- }
1390
-
1391
- switch (block.type) {
1392
- case 'text':
1393
- if (typeof block.text === 'string') {
1394
- parts.push({ text: block.text });
1395
- }
1396
- break;
1397
-
1398
- case 'image':
1399
- if (block.source && typeof block.source === 'object' &&
1400
- block.source.type === 'base64' &&
1401
- typeof block.source.media_type === 'string' &&
1402
- typeof block.source.data === 'string') {
1403
- parts.push({
1404
- inlineData: {
1405
- mimeType: block.source.media_type,
1406
- data: block.source.data
1407
- }
1408
- });
1409
- }
1410
- break;
1411
-
1412
- case 'tool_use':
1413
- if (typeof block.name === 'string' &&
1414
- block.input && typeof block.input === 'object') {
1415
- parts.push({
1416
- functionCall: {
1417
- name: block.name,
1418
- args: block.input
1419
- }
1420
- });
1421
- }
1422
- break;
1423
-
1424
- case 'tool_result':
1425
- if (typeof block.tool_use_id === 'string') {
1426
- parts.push({
1427
- functionResponse: {
1428
- name: block.tool_use_id,
1429
- response: { content: block.content }
1430
- }
1431
- });
1432
- }
1433
- break;
1434
-
1435
- default:
1436
- if (typeof block.text === 'string') {
1437
- parts.push({ text: block.text });
1438
- }
1439
- }
1440
- });
1441
-
1442
- return parts;
1443
- }
1444
-
1445
- return [];
1446
- }
1447
-
1448
- /**
1449
- * 构建Gemini工具配置
1450
- */
1451
- buildGeminiToolConfigFromClaude(claudeToolChoice) {
1452
- if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) {
1453
- logger.warn("Invalid claudeToolChoice provided.");
1454
- return undefined;
1455
- }
1456
-
1457
- switch (claudeToolChoice.type) {
1458
- case 'auto':
1459
- return { functionCallingConfig: { mode: 'AUTO' } };
1460
- case 'none':
1461
- return { functionCallingConfig: { mode: 'NONE' } };
1462
- case 'tool':
1463
- if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') {
1464
- return {
1465
- functionCallingConfig: {
1466
- mode: 'ANY',
1467
- allowedFunctionNames: [claudeToolChoice.name]
1468
- }
1469
- };
1470
- }
1471
- logger.warn("Invalid tool name in claudeToolChoice of type 'tool'.");
1472
- return undefined;
1473
- default:
1474
- logger.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`);
1475
- return undefined;
1476
- }
1477
- }
1478
-
1479
- // =========================================================================
1480
- // Claude -> OpenAI Responses 转换
1481
- // =========================================================================
1482
-
1483
- /**
1484
- * Claude请求 -> OpenAI Responses请求
1485
- */
1486
- toOpenAIResponsesRequest(claudeRequest) {
1487
- const responsesRequest = {
1488
- model: claudeRequest.model,
1489
- instructions: '',
1490
- input: [],
1491
- stream: claudeRequest.stream || false,
1492
- max_output_tokens: claudeRequest.max_tokens,
1493
- temperature: claudeRequest.temperature,
1494
- top_p: claudeRequest.top_p
1495
- };
1496
-
1497
- // 处理系统指令
1498
- if (claudeRequest.system) {
1499
- if (Array.isArray(claudeRequest.system)) {
1500
- responsesRequest.instructions = claudeRequest.system.map(s => typeof s === 'string' ? s : s.text).join('\n');
1501
- } else {
1502
- responsesRequest.instructions = claudeRequest.system;
1503
- }
1504
- }
1505
-
1506
- // 处理 thinking 配置
1507
- if (claudeRequest.thinking && claudeRequest.thinking.type === 'enabled') {
1508
- responsesRequest.reasoning = {
1509
- effort: determineReasoningEffortFromBudget(claudeRequest.thinking.budget_tokens)
1510
- };
1511
- }
1512
-
1513
- // 处理消息
1514
- if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
1515
- claudeRequest.messages.forEach(msg => {
1516
- const role = msg.role;
1517
- const content = msg.content;
1518
-
1519
- if (Array.isArray(content)) {
1520
- // 检查是否包含 tool_result
1521
- const toolResult = content.find(c => c.type === 'tool_result');
1522
- if (toolResult) {
1523
- responsesRequest.input.push({
1524
- type: 'function_call_output',
1525
- call_id: toolResult.tool_use_id,
1526
- output: typeof toolResult.content === 'string' ? toolResult.content : JSON.stringify(toolResult.content)
1527
- });
1528
- return;
1529
- }
1530
-
1531
- // 检查是否包含 tool_use
1532
- const toolUse = content.find(c => c.type === 'tool_use');
1533
- if (toolUse) {
1534
- responsesRequest.input.push({
1535
- type: 'function_call',
1536
- call_id: toolUse.id,
1537
- name: toolUse.name,
1538
- arguments: typeof toolUse.input === 'string' ? toolUse.input : JSON.stringify(toolUse.input)
1539
- });
1540
- return;
1541
- }
1542
-
1543
- const responsesContent = content.map(c => {
1544
- if (c.type === 'text') {
1545
- return {
1546
- type: role === 'assistant' ? 'output_text' : 'input_text',
1547
- text: c.text
1548
- };
1549
- } else if (c.type === 'image') {
1550
- return {
1551
- type: 'input_image',
1552
- image_url: {
1553
- url: `data:${c.source.media_type};base64,${c.source.data}`
1554
- }
1555
- };
1556
- }
1557
- return null;
1558
- }).filter(Boolean);
1559
-
1560
- if (responsesContent.length > 0) {
1561
- responsesRequest.input.push({
1562
- type: 'message',
1563
- role: role,
1564
- content: responsesContent
1565
- });
1566
- }
1567
- } else if (typeof content === 'string') {
1568
- responsesRequest.input.push({
1569
- type: 'message',
1570
- role: role,
1571
- content: [{
1572
- type: role === 'assistant' ? 'output_text' : 'input_text',
1573
- text: content
1574
- }]
1575
- });
1576
- }
1577
- });
1578
- }
1579
-
1580
- // 处理工具
1581
- if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) {
1582
- responsesRequest.tools = claudeRequest.tools.map(tool => ({
1583
- type: 'function',
1584
- name: tool.name,
1585
- description: tool.description,
1586
- parameters: tool.input_schema || { type: 'object', properties: {} }
1587
- }));
1588
- }
1589
-
1590
- if (claudeRequest.tool_choice) {
1591
- if (claudeRequest.tool_choice.type === 'auto') {
1592
- responsesRequest.tool_choice = 'auto';
1593
- } else if (claudeRequest.tool_choice.type === 'any') {
1594
- responsesRequest.tool_choice = 'required';
1595
- } else if (claudeRequest.tool_choice.type === 'tool') {
1596
- responsesRequest.tool_choice = {
1597
- type: 'function',
1598
- function: { name: claudeRequest.tool_choice.name }
1599
- };
1600
- }
1601
- }
1602
-
1603
- return responsesRequest;
1604
- }
1605
-
1606
- /**
1607
- * Claude响应 -> OpenAI Responses响应
1608
- */
1609
- toOpenAIResponsesResponse(claudeResponse, model) {
1610
- const content = this.processClaudeResponseContent(claudeResponse.content);
1611
- const textContent = typeof content === 'string' ? content : JSON.stringify(content);
1612
-
1613
- let output = [];
1614
- output.push({
1615
- type: "message",
1616
- id: `msg_${uuidv4().replace(/-/g, '')}`,
1617
- summary: [],
1618
- role: "assistant",
1619
- status: "completed",
1620
- content: [{
1621
- annotations: [],
1622
- logprobs: [],
1623
- text: textContent,
1624
- type: "output_text"
1625
- }]
1626
- });
1627
-
1628
- return {
1629
- background: false,
1630
- created_at: Math.floor(Date.now() / 1000),
1631
- error: null,
1632
- id: `resp_${uuidv4().replace(/-/g, '')}`,
1633
- incomplete_details: null,
1634
- max_output_tokens: null,
1635
- max_tool_calls: null,
1636
- metadata: {},
1637
- model: model || claudeResponse.model,
1638
- object: "response",
1639
- output: output,
1640
- parallel_tool_calls: true,
1641
- previous_response_id: null,
1642
- prompt_cache_key: null,
1643
- reasoning: {},
1644
- safety_identifier: "user-" + uuidv4().replace(/-/g, ''),
1645
- service_tier: "default",
1646
- status: "completed",
1647
- store: false,
1648
- temperature: 1,
1649
- text: {
1650
- format: { type: "text" },
1651
- },
1652
- tool_choice: "auto",
1653
- tools: [],
1654
- top_logprobs: 0,
1655
- top_p: 1,
1656
- truncation: "disabled",
1657
- usage: {
1658
- input_tokens: claudeResponse.usage?.input_tokens || 0,
1659
- input_tokens_details: {
1660
- cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0
1661
- },
1662
- output_tokens: claudeResponse.usage?.output_tokens || 0,
1663
- output_tokens_details: {
1664
- reasoning_tokens: 0
1665
- },
1666
- total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0)
1667
- },
1668
- user: null
1669
- };
1670
- }
1671
-
1672
- /**
1673
- * Claude流式响应 -> OpenAI Responses流式响应
1674
- */
1675
- toOpenAIResponsesStreamChunk(claudeChunk, model, requestId = null) {
1676
- if (!claudeChunk) return [];
1677
-
1678
- const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
1679
- const events = [];
1680
-
1681
- // message_start 事件 - 流开始
1682
- if (claudeChunk.type === 'message_start') {
1683
- events.push(
1684
- generateResponseCreated(responseId, model || 'unknown'),
1685
- generateResponseInProgress(responseId),
1686
- generateOutputItemAdded(responseId),
1687
- generateContentPartAdded(responseId)
1688
- );
1689
- }
1690
-
1691
- // content_block_start 事件
1692
- if (claudeChunk.type === 'content_block_start') {
1693
- const contentBlock = claudeChunk.content_block;
1694
-
1695
- // 对于 tool_use 类型,添加工具调用项
1696
- if (contentBlock && contentBlock.type === 'tool_use') {
1697
- events.push({
1698
- item: {
1699
- id: contentBlock.id,
1700
- type: "function_call",
1701
- name: contentBlock.name,
1702
- arguments: "",
1703
- status: "in_progress"
1704
- },
1705
- output_index: claudeChunk.index || 0,
1706
- sequence_number: 2,
1707
- type: "response.output_item.added"
1708
- });
1709
- }
1710
- }
1711
-
1712
- // content_block_delta 事件
1713
- if (claudeChunk.type === 'content_block_delta') {
1714
- const delta = claudeChunk.delta;
1715
-
1716
- // 处理文本增量
1717
- if (delta && delta.type === 'text_delta') {
1718
- events.push({
1719
- delta: delta.text || "",
1720
- item_id: `msg_${uuidv4().replace(/-/g, '')}`,
1721
- output_index: claudeChunk.index || 0,
1722
- sequence_number: 3,
1723
- type: "response.output_text.delta"
1724
- });
1725
- }
1726
- // 处理推理内容增量
1727
- else if (delta && delta.type === 'thinking_delta') {
1728
- events.push({
1729
- delta: delta.thinking || "",
1730
- item_id: `thinking_${uuidv4().replace(/-/g, '')}`,
1731
- output_index: claudeChunk.index || 0,
1732
- sequence_number: 3,
1733
- type: "response.reasoning_summary_text.delta"
1734
- });
1735
- }
1736
- // 处理工具调用参数增量
1737
- else if (delta && delta.type === 'input_json_delta') {
1738
- events.push({
1739
- delta: delta.partial_json || "",
1740
- item_id: `call_${uuidv4().replace(/-/g, '')}`,
1741
- output_index: claudeChunk.index || 0,
1742
- sequence_number: 3,
1743
- type: "response.custom_tool_call_input.delta"
1744
- });
1745
- }
1746
- }
1747
-
1748
- // content_block_stop 事件
1749
- if (claudeChunk.type === 'content_block_stop') {
1750
- events.push({
1751
- item_id: `msg_${uuidv4().replace(/-/g, '')}`,
1752
- output_index: claudeChunk.index || 0,
1753
- sequence_number: 4,
1754
- type: "response.output_item.done"
1755
- });
1756
- }
1757
-
1758
- // message_delta 事件 - 流结束
1759
- if (claudeChunk.type === 'message_delta') {
1760
- // events.push(
1761
- // generateOutputTextDone(responseId),
1762
- // generateContentPartDone(responseId),
1763
- // generateOutputItemDone(responseId),
1764
- // generateResponseCompleted(responseId)
1765
- // );
1766
-
1767
- // 如果有 usage 信息,更新最后一个事件
1768
- if (claudeChunk.usage && events.length > 0) {
1769
- const lastEvent = events[events.length - 1];
1770
- if (lastEvent.response) {
1771
- lastEvent.response.usage = {
1772
- input_tokens: claudeChunk.usage.input_tokens || 0,
1773
- input_tokens_details: {
1774
- cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0
1775
- },
1776
- output_tokens: claudeChunk.usage.output_tokens || 0,
1777
- output_tokens_details: {
1778
- reasoning_tokens: 0
1779
- },
1780
- total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0)
1781
- };
1782
- }
1783
- }
1784
- }
1785
-
1786
- // message_stop 事件
1787
- if (claudeChunk.type === 'message_stop') {
1788
- events.push(
1789
- generateOutputTextDone(responseId),
1790
- generateContentPartDone(responseId),
1791
- generateOutputItemDone(responseId),
1792
- generateResponseCompleted(responseId)
1793
- );
1794
- }
1795
-
1796
- return events;
1797
- }
1798
-
1799
- // =========================================================================
1800
- // Claude -> Codex 转换
1801
- // =========================================================================
1802
-
1803
- /**
1804
- * 应用简单缩短规则缩短工具名称
1805
- */
1806
- _shortenNameIfNeeded(name) {
1807
- const limit = 64;
1808
- if (name.length <= limit) {
1809
- return name;
1810
- }
1811
- if (name.startsWith("mcp__")) {
1812
- const idx = name.lastIndexOf("__");
1813
- if (idx > 0) {
1814
- const cand = "mcp__" + name.substring(idx + 2);
1815
- if (cand.length > limit) {
1816
- return cand.substring(0, limit);
1817
- }
1818
- return cand;
1819
- }
1820
- }
1821
- return name.substring(0, limit);
1822
- }
1823
-
1824
- /**
1825
- * 构建短名称映射以确保请求内唯一性
1826
- */
1827
- _buildShortNameMap(names) {
1828
- const limit = 64;
1829
- const used = new Set();
1830
- const m = {};
1831
-
1832
- const baseCandidate = (n) => {
1833
- if (n.length <= limit) {
1834
- return n;
1835
- }
1836
- if (n.startsWith("mcp__")) {
1837
- const idx = n.lastIndexOf("__");
1838
- if (idx > 0) {
1839
- let cand = "mcp__" + n.substring(idx + 2);
1840
- if (cand.length > limit) {
1841
- cand = cand.substring(0, limit);
1842
- }
1843
- return cand;
1844
- }
1845
- }
1846
- return n.substring(0, limit);
1847
- };
1848
-
1849
- const makeUnique = (cand) => {
1850
- if (!used.has(cand)) {
1851
- return cand;
1852
- }
1853
- const base = cand;
1854
- for (let i = 1; ; i++) {
1855
- const suffix = "_" + i;
1856
- const allowed = limit - suffix.length;
1857
- let tmp = base;
1858
- if (tmp.length > (allowed < 0 ? 0 : allowed)) {
1859
- tmp = tmp.substring(0, allowed < 0 ? 0 : allowed);
1860
- }
1861
- tmp = tmp + suffix;
1862
- if (!used.has(tmp)) {
1863
- return tmp;
1864
- }
1865
- }
1866
- };
1867
-
1868
- for (const n of names) {
1869
- const cand = baseCandidate(n);
1870
- const uniq = makeUnique(cand);
1871
- used.add(uniq);
1872
- m[n] = uniq;
1873
- }
1874
- return m;
1875
- }
1876
-
1877
- /**
1878
- * 标准化工具参数,确保对象 Schema 包含 properties
1879
- */
1880
- _normalizeToolParameters(schema) {
1881
- if (!schema || typeof schema !== 'object') {
1882
- return { type: 'object', properties: {} };
1883
- }
1884
- const result = { ...schema };
1885
- if (!result.type) {
1886
- result.type = 'object';
1887
- }
1888
- if (result.type === 'object' && !result.properties) {
1889
- result.properties = {};
1890
- }
1891
- return result;
1892
- }
1893
-
1894
- /**
1895
- * Claude请求 -> Codex请求
1896
- */
1897
- toCodexRequest(claudeRequest) {
1898
- const codexRequest = {
1899
- model: claudeRequest.model,
1900
- instructions: '',
1901
- input: [],
1902
- stream: true,
1903
- store: false,
1904
- parallel_tool_calls: true,
1905
- metadata: claudeRequest.metadata || {},
1906
- reasoning: {
1907
- effort: claudeRequest.reasoning?.effort || 'medium',
1908
- summary: 'auto'
1909
- },
1910
- include: ['reasoning.encrypted_content']
1911
- };
1912
-
1913
- // 处理系统指令
1914
- if (claudeRequest.system) {
1915
- let instructions = '';
1916
- if (Array.isArray(claudeRequest.system)) {
1917
- instructions = claudeRequest.system.map(s => typeof s === 'string' ? s : s.text).join('\n');
1918
- } else {
1919
- instructions = claudeRequest.system;
1920
- }
1921
- codexRequest.instructions = instructions;
1922
-
1923
- // 处理 Codex 中的系统消息(作为 developer 角色添加到 input)
1924
- const systemParts = Array.isArray(claudeRequest.system) ? claudeRequest.system : [{ type: 'text', text: claudeRequest.system }];
1925
- const developerMessage = {
1926
- type: 'message',
1927
- role: 'developer',
1928
- content: []
1929
- };
1930
-
1931
- systemParts.forEach(part => {
1932
- if (part.type === 'text') {
1933
- developerMessage.content.push({
1934
- type: 'input_text',
1935
- text: part.text
1936
- });
1937
- } else if (typeof part === 'string') {
1938
- developerMessage.content.push({
1939
- type: 'input_text',
1940
- text: part
1941
- });
1942
- }
1943
- });
1944
-
1945
- if (developerMessage.content.length > 0) {
1946
- codexRequest.input.push(developerMessage);
1947
- }
1948
- }
1949
-
1950
- // 处理工具并构建短名称映射
1951
- let shortMap = {};
1952
- if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) {
1953
- const toolNames = claudeRequest.tools.map(t => t.name).filter(Boolean);
1954
- shortMap = this._buildShortNameMap(toolNames);
1955
-
1956
- codexRequest.tools = claudeRequest.tools.map(tool => {
1957
- // 特殊处理:将 Claude Web Search 工具映射到 Codex web_search
1958
- if (tool.type === "web_search_20250305") {
1959
- return { type: "web_search" };
1960
- }
1961
-
1962
- let name = tool.name;
1963
- if (shortMap[name]) {
1964
- name = shortMap[name];
1965
- } else {
1966
- name = this._shortenNameIfNeeded(name);
1967
- }
1968
-
1969
- const convertedTool = {
1970
- type: 'function',
1971
- name: name,
1972
- description: tool.description || '',
1973
- parameters: this._normalizeToolParameters(tool.input_schema),
1974
- strict: false
1975
- };
1976
-
1977
- // 移除 parameters.$schema
1978
- if (convertedTool.parameters && convertedTool.parameters.$schema) {
1979
- delete convertedTool.parameters.$schema;
1980
- }
1981
-
1982
- return convertedTool;
1983
- });
1984
- codexRequest.tool_choice = "auto";
1985
- }
1986
-
1987
- // 处理消息
1988
- if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
1989
- for (const msg of claudeRequest.messages) {
1990
- const role = msg.role;
1991
- const content = msg.content;
1992
-
1993
- let currentMessage = {
1994
- type: 'message',
1995
- role: role,
1996
- content: []
1997
- };
1998
-
1999
- const flushMessage = () => {
2000
- if (currentMessage.content.length > 0) {
2001
- codexRequest.input.push({ ...currentMessage });
2002
- currentMessage.content = [];
2003
- }
2004
- };
2005
-
2006
- const appendTextContent = (text) => {
2007
- const partType = role === 'assistant' ? 'output_text' : 'input_text';
2008
- currentMessage.content.push({
2009
- type: partType,
2010
- text: text
2011
- });
2012
- };
2013
-
2014
- const appendImageContent = (data, mediaType) => {
2015
- currentMessage.content.push({
2016
- type: 'input_image',
2017
- image_url: `data:${mediaType};base64,${data}`
2018
- });
2019
- };
2020
-
2021
- if (Array.isArray(content)) {
2022
- for (const block of content) {
2023
- switch (block.type) {
2024
- case 'text':
2025
- appendTextContent(block.text);
2026
- break;
2027
- case 'image':
2028
- if (block.source) {
2029
- const data = block.source.data || block.source.base64 || '';
2030
- const mediaType = block.source.media_type || block.source.mime_type || 'application/octet-stream';
2031
- if (data) {
2032
- appendImageContent(data, mediaType);
2033
- }
2034
- }
2035
- break;
2036
- case 'tool_use':
2037
- flushMessage();
2038
- let toolName = block.name;
2039
- if (shortMap[toolName]) {
2040
- toolName = shortMap[toolName];
2041
- } else {
2042
- toolName = this._shortenNameIfNeeded(toolName);
2043
- }
2044
- codexRequest.input.push({
2045
- type: 'function_call',
2046
- call_id: block.id,
2047
- name: toolName,
2048
- arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {})
2049
- });
2050
- break;
2051
- case 'tool_result':
2052
- flushMessage();
2053
- codexRequest.input.push({
2054
- type: 'function_call_output',
2055
- call_id: block.tool_use_id,
2056
- output: typeof block.content === 'string' ? block.content : JSON.stringify(block.content || "")
2057
- });
2058
- break;
2059
- }
2060
- }
2061
- } else if (typeof content === 'string') {
2062
- appendTextContent(content);
2063
- }
2064
- flushMessage();
2065
- }
2066
- }
2067
-
2068
- // 处理 thinking 转换
2069
- if (claudeRequest.thinking && claudeRequest.thinking.type === "enabled") {
2070
- const budgetTokens = claudeRequest.thinking.budget_tokens;
2071
- codexRequest.reasoning.effort = determineReasoningEffortFromBudget(budgetTokens);
2072
- } else if (claudeRequest.thinking && claudeRequest.thinking.type === "disabled") {
2073
- codexRequest.reasoning.effort = determineReasoningEffortFromBudget(0);
2074
- }
2075
-
2076
- // 注入 Codex 指令 (对应 末尾的特殊逻辑)
2077
- // 注意:这里需要检查是否需要注入 "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!"
2078
- // 通过 misc.GetCodexInstructionsEnabled() 判断,这里我们参考其逻辑
2079
- const shouldInjectInstructions = process.env.CODEX_INSTRUCTIONS_ENABLED === 'true'; // 假设环境变量控制
2080
- if (shouldInjectInstructions && codexRequest.input.length > 0) {
2081
- const firstInput = codexRequest.input[0];
2082
- const firstText = firstInput.content && firstInput.content[0] && firstInput.content[0].text;
2083
- const instructions = "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!";
2084
- if (firstText !== instructions) {
2085
- codexRequest.input.unshift({
2086
- type: 'message',
2087
- role: 'user',
2088
- content: [{
2089
- type: 'input_text',
2090
- text: instructions
2091
- }]
2092
- });
2093
- }
2094
- }
2095
-
2096
- return codexRequest;
2097
- }
2098
-
2099
- /**
2100
- * Claude请求 -> Grok请求
2101
- */
2102
- toGrokRequest(claudeRequest) {
2103
- // 先转换为 OpenAI 格式,因为 Grok 兼容 OpenAI 格式
2104
- const openaiRequest = this.toOpenAIRequest(claudeRequest);
2105
- return {
2106
- ...openaiRequest,
2107
- _isConverted: true
2108
- };
2109
- }
2110
-
2111
- /**
2112
- * Claude响应 -> Codex响应 (实际上是 Codex 转 Claude)
2113
- */
2114
- toCodexResponse(codexResponse, model) {
2115
- const content = [];
2116
- let stopReason = "end_turn";
2117
-
2118
- if (codexResponse.response?.output) {
2119
- codexResponse.response.output.forEach(item => {
2120
- if (item.type === 'message' && item.content) {
2121
- const textPart = item.content.find(c => c.type === 'output_text');
2122
- if (textPart) content.push({ type: 'text', text: textPart.text });
2123
- } else if (item.type === 'reasoning' && item.summary) {
2124
- const textPart = item.summary.find(c => c.type === 'summary_text');
2125
- if (textPart) content.push({ type: 'thinking', thinking: textPart.text });
2126
- } else if (item.type === 'function_call') {
2127
- stopReason = "tool_use";
2128
- content.push({
2129
- type: 'tool_use',
2130
- id: item.call_id,
2131
- name: item.name,
2132
- input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
2133
- });
2134
- }
2135
- });
2136
- }
2137
-
2138
- return {
2139
- id: codexResponse.response?.id || `msg_${uuidv4().replace(/-/g, '')}`,
2140
- type: "message",
2141
- role: "assistant",
2142
- model: model,
2143
- content: content,
2144
- stop_reason: stopReason,
2145
- usage: {
2146
- input_tokens: codexResponse.response?.usage?.input_tokens || 0,
2147
- output_tokens: codexResponse.response?.usage?.output_tokens || 0
2148
- }
2149
- };
2150
- }
2151
-
2152
- /**
2153
- * Claude流式响应 -> Codex流式响应 (实际上是 Codex 转 Claude)
2154
- */
2155
- toCodexStreamChunk(codexChunk, model) {
2156
- const type = codexChunk.type;
2157
- const resId = codexChunk.response?.id || 'default';
2158
-
2159
- if (type === 'response.created') {
2160
- return {
2161
- type: "message_start",
2162
- message: {
2163
- id: codexChunk.response.id,
2164
- type: "message",
2165
- role: "assistant",
2166
- content: [],
2167
- model: model,
2168
- usage: { input_tokens: 0, output_tokens: 0 }
2169
- }
2170
- };
2171
- }
2172
-
2173
- if (type === 'response.reasoning_summary_text.delta') {
2174
- return {
2175
- type: "content_block_delta",
2176
- index: 0,
2177
- delta: { type: "thinking_delta", thinking: codexChunk.delta }
2178
- };
2179
- }
2180
-
2181
- if (type === 'response.output_text.delta') {
2182
- return {
2183
- type: "content_block_delta",
2184
- index: 0,
2185
- delta: { type: "text_delta", text: codexChunk.delta }
2186
- };
2187
- }
2188
-
2189
- if (type === 'response.output_item.done' && codexChunk.item?.type === 'function_call') {
2190
- return [
2191
- {
2192
- type: "content_block_start",
2193
- index: 0,
2194
- content_block: {
2195
- type: "tool_use",
2196
- id: codexChunk.item.call_id,
2197
- name: codexChunk.item.name,
2198
- input: {}
2199
- }
2200
- },
2201
- {
2202
- type: "content_block_delta",
2203
- index: 0,
2204
- delta: {
2205
- type: "input_json_delta",
2206
- partial_json: typeof codexChunk.item.arguments === 'string' ? codexChunk.item.arguments : JSON.stringify(codexChunk.item.arguments)
2207
- }
2208
- },
2209
- {
2210
- type: "content_block_stop",
2211
- index: 0
2212
- }
2213
- ];
2214
- }
2215
-
2216
- if (type === 'response.completed') {
2217
- return [
2218
- {
2219
- type: "message_delta",
2220
- delta: { stop_reason: "end_turn" },
2221
- usage: {
2222
- input_tokens: codexChunk.response.usage?.input_tokens || 0,
2223
- output_tokens: codexChunk.response.usage?.output_tokens || 0
2224
- }
2225
- },
2226
- { type: "message_stop" }
2227
- ];
2228
- }
2229
-
2230
- return null;
2231
- }
2232
- }
2233
-
2234
- export default ClaudeConverter;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/strategies/CodexConverter.js DELETED
@@ -1,1327 +0,0 @@
1
- /**
2
- * Codex 转换器
3
- * 处理 OpenAI 协议与 Codex 协议之间的转换
4
- */
5
-
6
- import { v4 as uuidv4 } from 'uuid';
7
- import { BaseConverter } from '../BaseConverter.js';
8
- import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
9
- import {
10
- generateResponseCreated,
11
- generateResponseInProgress,
12
- generateOutputItemAdded,
13
- generateContentPartAdded,
14
- generateOutputTextDone,
15
- generateContentPartDone,
16
- generateOutputItemDone,
17
- generateResponseCompleted
18
- } from '../../providers/openai/openai-responses-core.mjs';
19
-
20
- export class CodexConverter extends BaseConverter {
21
- constructor() {
22
- super('codex');
23
- this.toolNameMap = new Map(); // 工具名称缩短映射: original -> short
24
- this.reverseToolNameMap = new Map(); // 反向映射: short -> original
25
- this.streamParams = new Map(); // 用于存储流式状态,key 为响应 ID 或临时标识
26
- }
27
-
28
- /**
29
- * 转换请求
30
- */
31
- convertRequest(data, targetProtocol) {
32
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
33
- }
34
-
35
- /**
36
- * 转换响应
37
- */
38
- convertResponse(data, targetProtocol, model) {
39
- switch (targetProtocol) {
40
- case MODEL_PROTOCOL_PREFIX.OPENAI:
41
- return this.toOpenAIResponse(data, model);
42
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
43
- return this.toOpenAIResponsesResponse(data, model);
44
- case MODEL_PROTOCOL_PREFIX.GEMINI:
45
- return this.toGeminiResponse(data, model);
46
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
47
- return this.toClaudeResponse(data, model);
48
- case MODEL_PROTOCOL_PREFIX.CODEX:
49
- return data; // Codex to Codex
50
- default:
51
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
52
- }
53
- }
54
-
55
- /**
56
- * 转换流式响应块
57
- */
58
- convertStreamChunk(chunk, targetProtocol, model, requestId) {
59
- switch (targetProtocol) {
60
- case MODEL_PROTOCOL_PREFIX.OPENAI:
61
- return this.toOpenAIStreamChunk(chunk, model);
62
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
63
- return this.toOpenAIResponsesStreamChunk(chunk, model);
64
- case MODEL_PROTOCOL_PREFIX.GEMINI:
65
- return this.toGeminiStreamChunk(chunk, model);
66
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
67
- return this.toClaudeStreamChunk(chunk, model, requestId);
68
- case MODEL_PROTOCOL_PREFIX.CODEX:
69
- return chunk; // Codex to Codex
70
- default:
71
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
72
- }
73
- }
74
-
75
- /**
76
- * 转换模型列表
77
- */
78
- convertModelList(data, targetProtocol) {
79
- return data;
80
- }
81
-
82
- /**
83
- * OpenAI Responses → Codex 请求转换
84
- */
85
- toOpenAIResponsesToCodexRequest(responsesRequest) {
86
- let codexRequest = { ...responsesRequest };
87
-
88
- // 保留监控相关字段
89
- if (responsesRequest._monitorRequestId) {
90
- codexRequest._monitorRequestId = responsesRequest._monitorRequestId;
91
- }
92
- if (responsesRequest._requestBaseUrl) {
93
- codexRequest._requestBaseUrl = responsesRequest._requestBaseUrl;
94
- }
95
-
96
- // 处理 input 字段,如果它是字符串,则转换为消息数组
97
- if (codexRequest.input && typeof codexRequest.input === 'string') {
98
- const inputText = codexRequest.input;
99
- codexRequest.input = [{
100
- type: "message",
101
- role: "user",
102
- content: [{
103
- type: "input_text",
104
- text: inputText
105
- }]
106
- }];
107
- }
108
-
109
- // 设置Codex特定的字段
110
- codexRequest.stream = true;
111
- codexRequest.store = false;
112
- codexRequest.parallel_tool_calls = true;
113
- codexRequest.include = ['reasoning.encrypted_content'];
114
- codexRequest.service_tier = responsesRequest.service_tier || 'default';
115
- if (codexRequest.service_tier !== 'priority') {
116
- delete codexRequest.service_tier;
117
- }
118
-
119
- // 删除Codex不支持的字段
120
- delete codexRequest.max_output_tokens;
121
- delete codexRequest.max_completion_tokens;
122
- delete codexRequest.temperature;
123
- delete codexRequest.top_p;
124
- delete codexRequest.user;
125
-
126
- // 添加 reasoning 配置
127
- codexRequest.reasoning = {
128
- "effort": responsesRequest.reasoning_effort || responsesRequest.reasoning?.effort || "medium",
129
- "summary": responsesRequest.reasoning?.summary || "auto"
130
- };
131
-
132
-
133
- // 确保 input 数组中的每个项都有 type: "message",并将系统角色转换为开发者角色
134
- if (codexRequest.input && Array.isArray(codexRequest.input)) {
135
- codexRequest.input = codexRequest.input.filter(item => {
136
- // 如��� instructions 已存在,过滤掉 input 中的 system/developer 消息以避免重复
137
- if (codexRequest.instructions && (item.role === 'system' || item.role === 'developer')) {
138
- return false;
139
- }
140
- return true;
141
- }).map(item => {
142
- // 如果没有 type 或者 type 不是 message,则添加 type: "message"
143
- if (!item.type || item.type !== 'message') {
144
- item = { type: "message", ...item };
145
- }
146
-
147
- // 将系统角色转换为开发者角色
148
- if (item.role === 'system') {
149
- item = { ...item, role: 'developer' };
150
- }
151
-
152
- return item;
153
- });
154
- }
155
-
156
- return codexRequest;
157
- }
158
-
159
- /**
160
- * OpenAI → Codex 请求转换
161
- */
162
- toOpenAIRequestToCodexRequest(data) {
163
- // 构建工具名称映射
164
- this.buildToolNameMap(data.tools || []);
165
-
166
- const codexRequest = {
167
- model: data.model,
168
- instructions: this.buildInstructions(data),
169
- input: this.convertMessages((data.messages || []).filter(m => m.role !== 'system' && m.role !== 'developer')),
170
- stream: true,
171
- store: false,
172
- metadata: data.metadata || {},
173
- reasoning: {
174
- effort: data.reasoning_effort || data.reasoning?.effort || 'medium',
175
- summary: data.reasoning?.summary || 'auto'
176
- },
177
- parallel_tool_calls: true,
178
- include: ['reasoning.encrypted_content']
179
- };
180
-
181
- // 保留监控相关字段
182
- if (data._monitorRequestId) {
183
- codexRequest._monitorRequestId = data._monitorRequestId;
184
- }
185
- if (data._requestBaseUrl) {
186
- codexRequest._requestBaseUrl = data._requestBaseUrl;
187
- }
188
-
189
- codexRequest.service_tier = data.service_tier || 'default';
190
- if (codexRequest.service_tier !== 'priority') {
191
- delete codexRequest.service_tier;
192
- }
193
-
194
- // 处理 OpenAI Responses 特有的 instructions 和 input 字段(如果存在)
195
- if (data.instructions && !codexRequest.instructions) {
196
- codexRequest.instructions = data.instructions;
197
- }
198
-
199
- if (data.input && Array.isArray(data.input) && codexRequest.input.length === 0) {
200
- // 如果是 OpenAI Responses 格式的 input
201
- for (const item of data.input) {
202
- if (item.type === 'message' && item.role !== 'system' && item.role !== 'developer') {
203
- codexRequest.input.push({
204
- type: 'message',
205
- role: item.role === 'system' ? 'developer' : item.role,
206
- content: Array.isArray(item.content) ? item.content.map(c => ({
207
- type: item.role === 'assistant' ? 'output_text' : 'input_text',
208
- text: c.text
209
- })) : [{
210
- type: item.role === 'assistant' ? 'output_text' : 'input_text',
211
- text: item.content
212
- }]
213
- });
214
- }
215
- }
216
- }
217
-
218
- if (data.tools && data.tools.length > 0) {
219
- codexRequest.tools = this.convertTools(data.tools);
220
- }
221
-
222
- if (data.tool_choice) {
223
- codexRequest.tool_choice = this.convertToolChoice(data.tool_choice);
224
- }
225
-
226
- if (data.response_format || data.text?.verbosity) {
227
- const textObj = {};
228
- if (data.response_format) {
229
- textObj.format = this.convertResponseFormat(data.response_format);
230
- }
231
- if (data.text?.verbosity) {
232
- textObj.verbosity = data.text.verbosity;
233
- }
234
- codexRequest.text = textObj;
235
- }
236
-
237
- // 在 input 开头注入特殊指令(如果配置允许)
238
- // 这里我们默认开启,因为这是为了确保 Codex 遵循指令
239
- if (codexRequest.input.length > 0 && codexRequest.instructions) {
240
- const firstMsg = codexRequest.input[0];
241
- const specialInstruction = "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!";
242
- const firstText = firstMsg.content?.[0]?.text;
243
-
244
- if (firstMsg.role === 'user' && firstText !== specialInstruction) {
245
- codexRequest.input.unshift({
246
- type: "message",
247
- role: "user",
248
- content: [{
249
- type: "input_text",
250
- text: specialInstruction
251
- }]
252
- });
253
- }
254
- }
255
-
256
- return codexRequest;
257
- }
258
-
259
- /**
260
- * 构建指令
261
- */
262
- buildInstructions(data) {
263
- // 首先检查显式的 instructions 字段 (OpenAI Responses)
264
- if (data.instructions) return data.instructions;
265
-
266
- const systemMessages = (data.messages || []).filter(m => m.role === 'system');
267
- if (systemMessages.length > 0) {
268
- return systemMessages.map(m => {
269
- if (typeof m.content === 'string') {
270
- return m.content;
271
- } else if (Array.isArray(m.content)) {
272
- const textPart = m.content.find(part => part.type === 'text');
273
- return textPart ? textPart.text : '';
274
- }
275
- return '';
276
- }).join('\n').trim();
277
- }
278
- return '';
279
- }
280
-
281
- /**
282
- * 转换消息
283
- */
284
- convertMessages(messages) {
285
- const input = [];
286
-
287
- for (const msg of messages) {
288
- const role = msg.role;
289
-
290
- if (role === 'tool' || role === 'tool_result') {
291
- input.push({
292
- type: 'function_call_output',
293
- call_id: msg.tool_call_id || msg.tool_use_id,
294
- output: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
295
- });
296
- } else {
297
- const codexMsg = {
298
- type: 'message',
299
- role: role === 'system' ? 'developer' : (role === 'model' ? 'assistant' : role),
300
- content: this.convertMessageContent(msg.content, role)
301
- };
302
-
303
- if (codexMsg.content.length > 0) {
304
- input.push(codexMsg);
305
- }
306
-
307
- if ((role === 'assistant' || role === 'model') && msg.tool_calls) {
308
- for (const toolCall of msg.tool_calls) {
309
- if (toolCall.type === 'function' || toolCall.function) {
310
- const func = toolCall.function || toolCall;
311
- const originalName = func.name;
312
- const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
313
- input.push({
314
- type: 'function_call',
315
- call_id: toolCall.id,
316
- name: shortName,
317
- arguments: typeof func.arguments === 'string' ? func.arguments : JSON.stringify(func.arguments)
318
- });
319
- }
320
- }
321
- }
322
-
323
- // 处理 Claude 格式的 tool_use
324
- if (role === 'assistant' && Array.isArray(msg.content)) {
325
- for (const part of msg.content) {
326
- if (part.type === 'tool_use') {
327
- const originalName = part.name;
328
- const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
329
- input.push({
330
- type: 'function_call',
331
- call_id: part.id,
332
- name: shortName,
333
- arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input)
334
- });
335
- }
336
- }
337
- }
338
- }
339
- }
340
-
341
- return input;
342
- }
343
-
344
- /**
345
- * 转换消息内容
346
- */
347
- convertMessageContent(content, role) {
348
- if (!content) return [];
349
-
350
- const isAssistant = role === 'assistant' || role === 'model';
351
-
352
- if (typeof content === 'string') {
353
- return [{
354
- type: isAssistant ? 'output_text' : 'input_text',
355
- text: content
356
- }];
357
- }
358
-
359
- if (Array.isArray(content)) {
360
- return content.map(part => {
361
- if (typeof part === 'string') {
362
- return {
363
- type: isAssistant ? 'output_text' : 'input_text',
364
- text: part
365
- };
366
- }
367
- if (part.type === 'text') {
368
- return {
369
- type: isAssistant ? 'output_text' : 'input_text',
370
- text: part.text
371
- };
372
- } else if ((part.type === 'image_url' || part.type === 'image') && !isAssistant) {
373
- let url = '';
374
- if (part.image_url) {
375
- url = typeof part.image_url === 'string' ? part.image_url : part.image_url.url;
376
- } else if (part.source && part.source.type === 'base64') {
377
- url = `data:${part.source.media_type};base64,${part.source.data}`;
378
- }
379
- return url ? {
380
- type: 'input_image',
381
- image_url: url
382
- } : null;
383
- }
384
- return null;
385
- }).filter(Boolean);
386
- }
387
-
388
- return [];
389
- }
390
-
391
- /**
392
- * 构建工具名称映射
393
- */
394
- buildToolNameMap(tools) {
395
- this.toolNameMap.clear();
396
- this.reverseToolNameMap.clear();
397
-
398
- const names = [];
399
- for (const t of tools) {
400
- if (t.type === 'function' && t.function?.name) {
401
- names.push(t.function.name);
402
- } else if (t.name) {
403
- names.push(t.name);
404
- }
405
- }
406
-
407
- if (names.length === 0) return;
408
-
409
- const limit = 64;
410
- const used = new Set();
411
-
412
- const baseCandidate = (n) => {
413
- if (n.length <= limit) return n;
414
- if (n.startsWith('mcp__')) {
415
- const idx = n.lastIndexOf('__');
416
- if (idx > 0) {
417
- let cand = 'mcp__' + n.slice(idx + 2);
418
- return cand.length > limit ? cand.slice(0, limit) : cand;
419
- }
420
- }
421
- return n.slice(0, limit);
422
- };
423
-
424
- for (const n of names) {
425
- let cand = baseCandidate(n);
426
- let uniq = cand;
427
- if (used.has(uniq)) {
428
- for (let i = 1; ; i++) {
429
- const suffix = '_' + i;
430
- const allowed = limit - suffix.length;
431
- const base = cand.slice(0, Math.max(0, allowed));
432
- const tmp = base + suffix;
433
- if (!used.has(tmp)) {
434
- uniq = tmp;
435
- break;
436
- }
437
- }
438
- }
439
- used.add(uniq);
440
- this.toolNameMap.set(n, uniq);
441
- this.reverseToolNameMap.set(uniq, n);
442
- }
443
- }
444
-
445
- /**
446
- * 转换工具
447
- */
448
- convertTools(tools) {
449
- return tools.map(tool => {
450
- // 处理 Claude 的 web_search
451
- if (tool.type === "web_search_20250305") {
452
- return { type: "web_search" };
453
- }
454
-
455
- if (tool.type !== 'function' && !tool.name) {
456
- return tool;
457
- }
458
-
459
- const func = tool.function || tool;
460
- const originalName = func.name;
461
- const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
462
-
463
- const result = {
464
- type: 'function',
465
- name: shortName,
466
- description: func.description,
467
- parameters: func.parameters || func.input_schema || { type: 'object', properties: {} },
468
- strict: func.strict !== undefined ? func.strict : false
469
- };
470
-
471
- // 清理参数
472
- if (result.parameters && result.parameters.$schema) {
473
- delete result.parameters.$schema;
474
- }
475
-
476
- return result;
477
- });
478
- }
479
-
480
- /**
481
- * 转换 tool_choice
482
- */
483
- convertToolChoice(toolChoice) {
484
- if (typeof toolChoice === 'string') {
485
- return toolChoice;
486
- }
487
-
488
- if (toolChoice.type === 'function') {
489
- const name = toolChoice.function?.name;
490
- const shortName = name ? (this.toolNameMap.get(name) || this.shortenToolName(name)) : '';
491
- return {
492
- type: 'function',
493
- name: shortName
494
- };
495
- }
496
-
497
- return toolChoice;
498
- }
499
-
500
- /**
501
- * 缩短工具名称
502
- */
503
- shortenToolName(name) {
504
- const limit = 64;
505
- if (name.length <= limit) return name;
506
- if (name.startsWith('mcp__')) {
507
- const idx = name.lastIndexOf('__');
508
- if (idx > 0) {
509
- let cand = 'mcp__' + name.slice(idx + 2);
510
- return cand.length > limit ? cand.slice(0, limit) : cand;
511
- }
512
- }
513
- return name.slice(0, limit);
514
- }
515
-
516
- /**
517
- * 获取原始工具名称
518
- */
519
- getOriginalToolName(shortName) {
520
- return this.reverseToolNameMap.get(shortName) || shortName;
521
- }
522
-
523
- /**
524
- * 转换响应格式
525
- */
526
- convertResponseFormat(responseFormat) {
527
- if (responseFormat.type === 'json_schema') {
528
- return {
529
- type: 'json_schema',
530
- name: responseFormat.json_schema?.name || 'response',
531
- schema: responseFormat.json_schema?.schema || {}
532
- };
533
- } else if (responseFormat.type === 'json_object') {
534
- return {
535
- type: 'json_object'
536
- };
537
- }
538
- return responseFormat;
539
- }
540
-
541
- /**
542
- * Codex → OpenAI 响应转换(非流式)
543
- */
544
- toOpenAIResponse(rawJSON, model) {
545
- const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
546
- if (root.type !== 'response.completed') {
547
- return null;
548
- }
549
-
550
- const response = root.response;
551
- const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
552
-
553
- const openaiResponse = {
554
- id: response.id || `chatcmpl-${Date.now()}`,
555
- object: 'chat.completion',
556
- created: unixTimestamp,
557
- model: response.model || model,
558
- choices: [{
559
- index: 0,
560
- message: {
561
- role: 'assistant',
562
- content: null,
563
- reasoning_content: null,
564
- tool_calls: null
565
- },
566
- finish_reason: null,
567
- native_finish_reason: null
568
- }],
569
- usage: {
570
- prompt_tokens: response.usage?.input_tokens || 0,
571
- completion_tokens: response.usage?.output_tokens || 0,
572
- total_tokens: response.usage?.total_tokens || 0
573
- }
574
- };
575
-
576
- if (response.usage?.output_tokens_details?.reasoning_tokens) {
577
- openaiResponse.usage.completion_tokens_details = {
578
- reasoning_tokens: response.usage.output_tokens_details.reasoning_tokens
579
- };
580
- }
581
-
582
- const output = response.output || [];
583
- let contentText = '';
584
- let reasoningText = '';
585
- const toolCalls = [];
586
-
587
- for (const item of output) {
588
- switch (item.type) {
589
- case 'reasoning':
590
- if (Array.isArray(item.summary)) {
591
- const summaryItem = item.summary.find(s => s.type === 'summary_text');
592
- if (summaryItem) reasoningText = summaryItem.text;
593
- }
594
- break;
595
- case 'message':
596
- if (Array.isArray(item.content)) {
597
- const contentItem = item.content.find(c => c.type === 'output_text');
598
- if (contentItem) contentText = contentItem.text;
599
- }
600
- break;
601
- case 'function_call':
602
- toolCalls.push({
603
- id: item.call_id || `call_${Date.now()}_${toolCalls.length}`,
604
- type: 'function',
605
- function: {
606
- name: this.getOriginalToolName(item.name),
607
- arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments)
608
- }
609
- });
610
- break;
611
- }
612
- }
613
-
614
- if (contentText) openaiResponse.choices[0].message.content = contentText;
615
- if (reasoningText) openaiResponse.choices[0].message.reasoning_content = reasoningText;
616
- if (toolCalls.length > 0) openaiResponse.choices[0].message.tool_calls = toolCalls;
617
-
618
- if (response.status === 'completed') {
619
- openaiResponse.choices[0].finish_reason = toolCalls.length > 0 ? 'tool_calls' : 'stop';
620
- openaiResponse.choices[0].native_finish_reason = 'stop';
621
- }
622
-
623
- return openaiResponse;
624
- }
625
-
626
- /**
627
- * Codex → OpenAI Responses 响应转换
628
- */
629
- toOpenAIResponsesResponse(rawJSON, model) {
630
- const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
631
- if (root.type !== 'response.completed') {
632
- return null;
633
- }
634
-
635
- const response = root.response;
636
- const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
637
-
638
- const output = [];
639
-
640
- if (response.output && Array.isArray(response.output)) {
641
- for (const item of response.output) {
642
- if (item.type === 'reasoning') {
643
- let reasoningText = '';
644
- if (Array.isArray(item.summary)) {
645
- const summaryItem = item.summary.find(s => s.type === 'summary_text');
646
- if (summaryItem) reasoningText = summaryItem.text;
647
- }
648
- if (reasoningText) {
649
- output.push({
650
- id: `msg_${uuidv4().replace(/-/g, '')}`,
651
- type: "message",
652
- role: "assistant",
653
- status: "completed",
654
- content: [{
655
- type: "reasoning",
656
- text: reasoningText
657
- }]
658
- });
659
- }
660
- } else if (item.type === 'message') {
661
- let contentText = '';
662
- if (Array.isArray(item.content)) {
663
- const contentItem = item.content.find(c => c.type === 'output_text');
664
- if (contentItem) contentText = contentItem.text;
665
- }
666
- if (contentText) {
667
- output.push({
668
- id: `msg_${uuidv4().replace(/-/g, '')}`,
669
- type: "message",
670
- role: "assistant",
671
- status: "completed",
672
- content: [{
673
- type: "output_text",
674
- text: contentText,
675
- annotations: []
676
- }]
677
- });
678
- }
679
- } else if (item.type === 'function_call') {
680
- output.push({
681
- id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
682
- type: "function_call",
683
- name: this.getOriginalToolName(item.name),
684
- arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
685
- status: "completed"
686
- });
687
- }
688
- }
689
- }
690
-
691
- return {
692
- id: response.id || `resp_${uuidv4().replace(/-/g, '')}`,
693
- object: "response",
694
- created_at: unixTimestamp,
695
- model: response.model || model,
696
- status: "completed",
697
- output: output,
698
- incomplete_details: response.incomplete_details || null,
699
- usage: {
700
- input_tokens: response.usage?.input_tokens || 0,
701
- output_tokens: response.usage?.output_tokens || 0,
702
- total_tokens: response.usage?.total_tokens || 0,
703
- output_tokens_details: {
704
- reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0
705
- }
706
- }
707
- };
708
- }
709
-
710
- /**
711
- * Codex → Gemini 响应转换
712
- */
713
- toGeminiResponse(rawJSON, model) {
714
- const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
715
- if (root.type !== 'response.completed') {
716
- return null;
717
- }
718
-
719
- const response = root.response;
720
- const parts = [];
721
-
722
- if (response.output && Array.isArray(response.output)) {
723
- for (const item of response.output) {
724
- if (item.type === 'reasoning') {
725
- let reasoningText = '';
726
- if (Array.isArray(item.summary)) {
727
- const summaryItem = item.summary.find(s => s.type === 'summary_text');
728
- if (summaryItem) reasoningText = summaryItem.text;
729
- }
730
- if (reasoningText) {
731
- parts.push({ text: reasoningText, thought: true });
732
- }
733
- } else if (item.type === 'message') {
734
- let contentText = '';
735
- if (Array.isArray(item.content)) {
736
- const contentItem = item.content.find(c => c.type === 'output_text');
737
- if (contentItem) contentText = contentItem.text;
738
- }
739
- if (contentText) {
740
- parts.push({ text: contentText });
741
- }
742
- } else if (item.type === 'function_call') {
743
- parts.push({
744
- functionCall: {
745
- name: this.getOriginalToolName(item.name),
746
- args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
747
- }
748
- });
749
- }
750
- }
751
- }
752
-
753
- return {
754
- candidates: [{
755
- content: {
756
- role: "model",
757
- parts: parts
758
- },
759
- finishReason: "STOP"
760
- }],
761
- usageMetadata: {
762
- promptTokenCount: response.usage?.input_tokens || 0,
763
- candidatesTokenCount: response.usage?.output_tokens || 0,
764
- totalTokenCount: response.usage?.total_tokens || 0
765
- },
766
- modelVersion: response.model || model,
767
- responseId: response.id
768
- };
769
- }
770
-
771
- /**
772
- * Codex → Claude 响应转换
773
- */
774
- toClaudeResponse(rawJSON, model) {
775
- const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
776
- if (root.type !== 'response.completed') {
777
- return null;
778
- }
779
-
780
- const response = root.response;
781
- const content = [];
782
- let stopReason = "end_turn";
783
-
784
- if (response.output && Array.isArray(response.output)) {
785
- for (const item of response.output) {
786
- if (item.type === 'reasoning') {
787
- let reasoningText = '';
788
- if (Array.isArray(item.summary)) {
789
- const summaryItem = item.summary.find(s => s.type === 'summary_text');
790
- if (summaryItem) reasoningText = summaryItem.text;
791
- }
792
- if (reasoningText) {
793
- content.push({ type: "thinking", thinking: reasoningText });
794
- }
795
- } else if (item.type === 'message') {
796
- let contentText = '';
797
- if (Array.isArray(item.content)) {
798
- const contentItem = item.content.find(c => c.type === 'output_text');
799
- if (contentItem) contentText = contentItem.text;
800
- }
801
- if (contentText) {
802
- content.push({ type: "text", text: contentText });
803
- }
804
- } else if (item.type === 'function_call') {
805
- stopReason = "tool_use";
806
- content.push({
807
- type: "tool_use",
808
- id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
809
- name: this.getOriginalToolName(item.name),
810
- input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
811
- });
812
- }
813
- }
814
- }
815
-
816
- return {
817
- id: response.id || `msg_${uuidv4().replace(/-/g, '')}`,
818
- type: "message",
819
- role: "assistant",
820
- model: response.model || model,
821
- content: content,
822
- stop_reason: stopReason,
823
- usage: {
824
- input_tokens: response.usage?.input_tokens || 0,
825
- output_tokens: response.usage?.output_tokens || 0
826
- }
827
- };
828
- }
829
-
830
- /**
831
- * Codex → OpenAI 流式响应块转换
832
- */
833
- toOpenAIStreamChunk(chunk, model) {
834
- const type = chunk.type;
835
- // 使用固定的 key 来存储当前流的状态
836
- const stateKey = 'openai_stream_current';
837
-
838
- if (!this.streamParams.has(stateKey)) {
839
- this.streamParams.set(stateKey, {
840
- model: model,
841
- createdAt: Math.floor(Date.now() / 1000),
842
- responseID: chunk.response?.id || `chatcmpl-${Date.now()}`,
843
- functionCallIndex: 0, // 初始值为 0,第一个 function_call 的 index 为 0
844
- isFirstChunk: true // 标记是否是第一个内容 chunk
845
- });
846
- }
847
- const state = this.streamParams.get(stateKey);
848
-
849
- // 构建模板时使用当前状态中的值
850
- const buildTemplate = () => ({
851
- id: state.responseID,
852
- object: 'chat.completion.chunk',
853
- created: state.createdAt,
854
- model: state.model,
855
- choices: [{
856
- index: 0,
857
- delta: {
858
- role: 'assistant',
859
- content: null,
860
- reasoning_content: null,
861
- tool_calls: null
862
- },
863
- finish_reason: null,
864
- native_finish_reason: null
865
- }]
866
- });
867
-
868
- if (type === 'response.created') {
869
- // 更新状态中的 responseID
870
- state.responseID = chunk.response.id;
871
- state.createdAt = chunk.response.created_at || state.createdAt;
872
- state.model = chunk.response.model || state.model;
873
- // 重置 functionCallIndex,确保每个新请求从 0 开始
874
- state.functionCallIndex = 0;
875
- state.isFirstChunk = true;
876
- // response.created 不发送 chunk,等待第一个内容 chunk
877
- return null;
878
- }
879
-
880
- if (type === 'response.reasoning_summary_text.delta') {
881
- const results = [];
882
- // 如果是第一个内容 chunk,先发送带 role 的 chunk
883
- if (state.isFirstChunk) {
884
- const firstTemplate = buildTemplate();
885
- firstTemplate.choices[0].delta = {
886
- role: 'assistant',
887
- content: null,
888
- reasoning_content: chunk.delta,
889
- tool_calls: null
890
- };
891
- results.push(firstTemplate);
892
- state.isFirstChunk = false;
893
- } else {
894
- const template = buildTemplate();
895
- template.choices[0].delta = {
896
- role: 'assistant',
897
- content: null,
898
- reasoning_content: chunk.delta,
899
- tool_calls: null
900
- };
901
- results.push(template);
902
- }
903
- return results.length === 1 ? results[0] : results;
904
- }
905
-
906
- if (type === 'response.reasoning_summary_text.done') {
907
- const template = buildTemplate();
908
- template.choices[0].delta = {
909
- role: 'assistant',
910
- content: null,
911
- reasoning_content: '\n\n',
912
- tool_calls: null
913
- };
914
- return template;
915
- }
916
-
917
- if (type === 'response.output_text.delta') {
918
- const results = [];
919
- // 如果是第一个内容 chunk,先发送带 role 的 chunk
920
- if (state.isFirstChunk) {
921
- const firstTemplate = buildTemplate();
922
- firstTemplate.choices[0].delta = {
923
- role: 'assistant',
924
- content: chunk.delta,
925
- reasoning_content: null,
926
- tool_calls: null
927
- };
928
- results.push(firstTemplate);
929
- state.isFirstChunk = false;
930
- } else {
931
- const template = buildTemplate();
932
- template.choices[0].delta = {
933
- role: 'assistant',
934
- content: chunk.delta,
935
- reasoning_content: null,
936
- tool_calls: null
937
- };
938
- results.push(template);
939
- }
940
- return results.length === 1 ? results[0] : results;
941
- }
942
-
943
- if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
944
- const currentIndex = state.functionCallIndex;
945
- state.functionCallIndex++; // 递增,为下一个 function_call 准备
946
- const template = buildTemplate();
947
- template.choices[0].delta = {
948
- role: 'assistant',
949
- content: null,
950
- reasoning_content: null,
951
- tool_calls: [{
952
- index: currentIndex,
953
- id: chunk.item.call_id,
954
- type: 'function',
955
- function: {
956
- name: this.getOriginalToolName(chunk.item.name),
957
- arguments: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments)
958
- }
959
- }]
960
- };
961
- return template;
962
- }
963
-
964
- if (type === 'response.completed') {
965
- const template = buildTemplate();
966
- const finishReason = state.functionCallIndex > 0 ? 'tool_calls' : 'stop';
967
- template.choices[0].delta = {
968
- role: null,
969
- content: null,
970
- reasoning_content: null,
971
- tool_calls: null
972
- };
973
- template.choices[0].finish_reason = finishReason;
974
- template.choices[0].native_finish_reason = finishReason;
975
- template.usage = {
976
- prompt_tokens: chunk.response.usage?.input_tokens || 0,
977
- completion_tokens: chunk.response.usage?.output_tokens || 0,
978
- total_tokens: chunk.response.usage?.total_tokens || 0
979
- };
980
- if (chunk.response.usage?.output_tokens_details?.reasoning_tokens) {
981
- template.usage.completion_tokens_details = {
982
- reasoning_tokens: chunk.response.usage.output_tokens_details.reasoning_tokens
983
- };
984
- }
985
- // 完成后清理状态
986
- this.streamParams.delete(stateKey);
987
- return template;
988
- }
989
-
990
- return null;
991
- }
992
-
993
- /**
994
- * Codex → OpenAI Responses 流式响应转换
995
- */
996
- toOpenAIResponsesStreamChunk(chunk, model) {
997
- if(true){
998
- return chunk;
999
- }
1000
-
1001
- const type = chunk.type;
1002
- const resId = chunk.response?.id || 'default';
1003
-
1004
- if (!this.streamParams.has(resId)) {
1005
- this.streamParams.set(resId, {
1006
- model: model,
1007
- createdAt: Math.floor(Date.now() / 1000),
1008
- responseID: resId,
1009
- functionCallIndex: -1,
1010
- eventsSent: new Set()
1011
- });
1012
- }
1013
- const state = this.streamParams.get(resId);
1014
- const events = [];
1015
-
1016
- if (type === 'response.created') {
1017
- state.responseID = chunk.response.id;
1018
- state.model = chunk.response.model || state.model;
1019
- events.push(
1020
- generateResponseCreated(state.responseID, state.model),
1021
- generateResponseInProgress(state.responseID)
1022
- );
1023
- return events;
1024
- }
1025
-
1026
- if (type === 'response.reasoning_summary_text.delta') {
1027
- events.push({
1028
- type: "response.reasoning_summary_text.delta",
1029
- response_id: state.responseID,
1030
- delta: chunk.delta
1031
- });
1032
- return events;
1033
- }
1034
-
1035
- if (type === 'response.output_text.delta') {
1036
- if (!state.eventsSent.has('output_item_added')) {
1037
- events.push(generateOutputItemAdded(state.responseID));
1038
- state.eventsSent.add('output_item_added');
1039
- }
1040
- if (!state.eventsSent.has('content_part_added')) {
1041
- events.push(generateContentPartAdded(state.responseID));
1042
- state.eventsSent.add('content_part_added');
1043
- }
1044
- events.push({
1045
- type: "response.output_text.delta",
1046
- response_id: state.responseID,
1047
- delta: chunk.delta
1048
- });
1049
- return events;
1050
- }
1051
-
1052
- if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
1053
- events.push({
1054
- type: "response.output_item.added",
1055
- response_id: state.responseID,
1056
- item: {
1057
- id: chunk.item.call_id,
1058
- type: "function_call",
1059
- name: this.getOriginalToolName(chunk.item.name),
1060
- arguments: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments),
1061
- status: "completed"
1062
- }
1063
- });
1064
- events.push({
1065
- type: "response.output_item.done",
1066
- response_id: state.responseID,
1067
- item_id: chunk.item.call_id
1068
- });
1069
- return events;
1070
- }
1071
-
1072
- if (type === 'response.completed') {
1073
- events.push(
1074
- generateOutputTextDone(state.responseID),
1075
- generateContentPartDone(state.responseID),
1076
- generateOutputItemDone(state.responseID)
1077
- );
1078
- const completedEvent = generateResponseCompleted(state.responseID);
1079
- completedEvent.response.usage = {
1080
- input_tokens: chunk.response.usage?.input_tokens || 0,
1081
- output_tokens: chunk.response.usage?.output_tokens || 0,
1082
- total_tokens: chunk.response.usage?.total_tokens || 0
1083
- };
1084
- events.push(completedEvent);
1085
- this.streamParams.delete(resId);
1086
- return events;
1087
- }
1088
-
1089
- return null;
1090
- }
1091
-
1092
- /**
1093
- * Codex → Gemini 流式响应转换
1094
- */
1095
- toGeminiStreamChunk(chunk, model) {
1096
- const type = chunk.type;
1097
- const resId = chunk.response?.id || 'default';
1098
-
1099
- if (!this.streamParams.has(resId)) {
1100
- this.streamParams.set(resId, {
1101
- model: model,
1102
- createdAt: Math.floor(Date.now() / 1000),
1103
- responseID: resId
1104
- });
1105
- }
1106
- const state = this.streamParams.get(resId);
1107
-
1108
- const template = {
1109
- candidates: [{
1110
- content: {
1111
- role: "model",
1112
- parts: []
1113
- }
1114
- }],
1115
- modelVersion: state.model,
1116
- responseId: state.responseID
1117
- };
1118
-
1119
- if (type === 'response.reasoning_summary_text.delta') {
1120
- template.candidates[0].content.parts.push({ text: chunk.delta, thought: true });
1121
- return template;
1122
- }
1123
-
1124
- if (type === 'response.output_text.delta') {
1125
- template.candidates[0].content.parts.push({ text: chunk.delta });
1126
- return template;
1127
- }
1128
-
1129
- if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
1130
- template.candidates[0].content.parts.push({
1131
- functionCall: {
1132
- name: this.getOriginalToolName(chunk.item.name),
1133
- args: typeof chunk.item.arguments === 'string' ? JSON.parse(chunk.item.arguments) : chunk.item.arguments
1134
- }
1135
- });
1136
- return template;
1137
- }
1138
-
1139
- if (type === 'response.completed') {
1140
- template.candidates[0].finishReason = "STOP";
1141
- template.usageMetadata = {
1142
- promptTokenCount: chunk.response.usage?.input_tokens || 0,
1143
- candidatesTokenCount: chunk.response.usage?.output_tokens || 0,
1144
- totalTokenCount: chunk.response.usage?.total_tokens || 0
1145
- };
1146
- this.streamParams.delete(resId);
1147
- return template;
1148
- }
1149
-
1150
- return null;
1151
- }
1152
-
1153
- /**
1154
- * Codex → Claude 流式响应转换
1155
- */
1156
- toClaudeStreamChunk(chunk, model, requestId) {
1157
- const type = chunk.type;
1158
-
1159
- // 使用 requestId 作为流状态的隔离 key(并发安全)。
1160
- // 每个请求在 handleStreamRequest 中生成唯一 requestId,
1161
- // 确保同一单例 converter 上的并发流状态完全独立。
1162
- const stateKey = requestId || chunk.response?.id || 'default';
1163
-
1164
- // response.created 携带 response.id,用它来初始化该请求的流状态
1165
- if (type === 'response.created') {
1166
- const resId = chunk.response.id;
1167
- this.streamParams.set(stateKey, {
1168
- model: model,
1169
- createdAt: Math.floor(Date.now() / 1000),
1170
- responseID: resId,
1171
- blockIndex: 0,
1172
- blockStarted: false,
1173
- currentBlockType: null,
1174
- });
1175
- const state = this.streamParams.get(stateKey);
1176
- return {
1177
- type: "message_start",
1178
- message: {
1179
- id: state.responseID,
1180
- type: "message",
1181
- role: "assistant",
1182
- content: [],
1183
- model: state.model,
1184
- usage: { input_tokens: 0, output_tokens: 0 }
1185
- }
1186
- };
1187
- }
1188
-
1189
- if (!this.streamParams.has(stateKey)) {
1190
- // 如果还没有状态(比如没有收到 response.created 就收到了其他事件),
1191
- // 用 chunk 中能拿到的信息初始化
1192
- this.streamParams.set(stateKey, {
1193
- model: model,
1194
- createdAt: Math.floor(Date.now() / 1000),
1195
- responseID: chunk.response?.id || stateKey,
1196
- blockIndex: 0,
1197
- blockStarted: false,
1198
- currentBlockType: null,
1199
- });
1200
- }
1201
- const state = this.streamParams.get(stateKey);
1202
-
1203
- // response.output_item.added 不产生 Claude 输出
1204
- if (type === 'response.output_item.added') {
1205
- return null;
1206
- }
1207
-
1208
- if (type === 'response.created') {
1209
- // 已在上方处理,不应到达此处
1210
- return null;
1211
- }
1212
-
1213
- if (type === 'response.reasoning_summary_text.delta') {
1214
- const events = [];
1215
- // If switching from a different block type, close the previous block first
1216
- if (state.blockStarted && state.currentBlockType !== 'thinking') {
1217
- events.push({ type: "content_block_stop", index: state.blockIndex });
1218
- state.blockIndex++;
1219
- state.blockStarted = false;
1220
- }
1221
- // Emit content_block_start on first delta for this thinking block
1222
- if (!state.blockStarted) {
1223
- events.push({
1224
- type: "content_block_start",
1225
- index: state.blockIndex,
1226
- content_block: { type: "thinking", thinking: "" }
1227
- });
1228
- state.blockStarted = true;
1229
- state.currentBlockType = 'thinking';
1230
- }
1231
- events.push({
1232
- type: "content_block_delta",
1233
- index: state.blockIndex,
1234
- delta: { type: "thinking_delta", thinking: chunk.delta }
1235
- });
1236
- return events;
1237
- }
1238
-
1239
- if (type === 'response.output_text.delta') {
1240
- const events = [];
1241
- // If switching from a different block type, close the previous block first
1242
- if (state.blockStarted && state.currentBlockType !== 'text') {
1243
- events.push({ type: "content_block_stop", index: state.blockIndex });
1244
- state.blockIndex++;
1245
- state.blockStarted = false;
1246
- }
1247
- // Emit content_block_start on first delta for this text block
1248
- if (!state.blockStarted) {
1249
- events.push({
1250
- type: "content_block_start",
1251
- index: state.blockIndex,
1252
- content_block: { type: "text", text: "" }
1253
- });
1254
- state.blockStarted = true;
1255
- state.currentBlockType = 'text';
1256
- }
1257
- events.push({
1258
- type: "content_block_delta",
1259
- index: state.blockIndex,
1260
- delta: { type: "text_delta", text: chunk.delta }
1261
- });
1262
- return events;
1263
- }
1264
-
1265
- if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
1266
- const events = [];
1267
- // Close any open text/thinking block before tool_use
1268
- if (state.blockStarted) {
1269
- events.push({ type: "content_block_stop", index: state.blockIndex });
1270
- state.blockIndex++;
1271
- state.blockStarted = false;
1272
- state.currentBlockType = null;
1273
- }
1274
- events.push(
1275
- {
1276
- type: "content_block_start",
1277
- index: state.blockIndex,
1278
- content_block: {
1279
- type: "tool_use",
1280
- id: chunk.item.call_id,
1281
- name: this.getOriginalToolName(chunk.item.name),
1282
- input: {}
1283
- }
1284
- },
1285
- {
1286
- type: "content_block_delta",
1287
- index: state.blockIndex,
1288
- delta: {
1289
- type: "input_json_delta",
1290
- partial_json: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments)
1291
- }
1292
- },
1293
- {
1294
- type: "content_block_stop",
1295
- index: state.blockIndex
1296
- }
1297
- );
1298
- state.blockIndex++;
1299
- return events;
1300
- }
1301
-
1302
- if (type === 'response.completed') {
1303
- const events = [];
1304
- // Close any open content block before ending the message
1305
- if (state.blockStarted) {
1306
- events.push({ type: "content_block_stop", index: state.blockIndex });
1307
- }
1308
- events.push(
1309
- {
1310
- type: "message_delta",
1311
- delta: { stop_reason: "end_turn" },
1312
- usage: {
1313
- input_tokens: chunk.response.usage?.input_tokens || 0,
1314
- output_tokens: chunk.response.usage?.output_tokens || 0
1315
- }
1316
- },
1317
- { type: "message_stop" }
1318
- );
1319
- // 清理该请求的流状态
1320
- this.streamParams.delete(stateKey);
1321
- return events;
1322
- }
1323
-
1324
- return null;
1325
- }
1326
-
1327
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/strategies/GeminiConverter.js DELETED
@@ -1,1529 +0,0 @@
1
- /**
2
- * Gemini转换器
3
- * 处理Gemini(Google)协议与其他协议之间的转换
4
- */
5
-
6
- import { v4 as uuidv4 } from 'uuid';
7
- import { BaseConverter } from '../BaseConverter.js';
8
- import {
9
- checkAndAssignOrDefault,
10
- OPENAI_DEFAULT_MAX_TOKENS,
11
- OPENAI_DEFAULT_TEMPERATURE,
12
- OPENAI_DEFAULT_TOP_P,
13
- CLAUDE_DEFAULT_MAX_TOKENS,
14
- CLAUDE_DEFAULT_TEMPERATURE,
15
- CLAUDE_DEFAULT_TOP_P
16
- } from '../utils.js';
17
- import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
18
- import {
19
- generateResponseCreated,
20
- generateResponseInProgress,
21
- generateOutputItemAdded,
22
- generateContentPartAdded,
23
- generateOutputTextDone,
24
- generateContentPartDone,
25
- generateOutputItemDone,
26
- generateResponseCompleted
27
- } from '../../providers/openai/openai-responses-core.mjs';
28
-
29
- /**
30
- * 修复 Gemini 返回的工具参数名称问题
31
- * Gemini 有时会使用不同的参数名称,需要映射到 Claude Code 期望的格式
32
- */
33
- function remapFunctionCallArgs(toolName, args) {
34
- if (!args || typeof args !== 'object') return args;
35
-
36
- const remappedArgs = { ...args };
37
- const toolNameLower = toolName.toLowerCase();
38
-
39
- // [IMPORTANT] Claude Code CLI 的 EnterPlanMode 工具禁止携带任何参数
40
- if (toolName === 'EnterPlanMode') {
41
- return {};
42
- }
43
-
44
- switch (toolNameLower) {
45
- case 'grep':
46
- case 'search':
47
- case 'search_code_definitions':
48
- case 'search_code_snippets':
49
- // [FIX] Gemini hallucination: maps parameter description to "description" field
50
- if (remappedArgs.description && !remappedArgs.pattern) {
51
- remappedArgs.pattern = remappedArgs.description;
52
- delete remappedArgs.description;
53
- }
54
-
55
- // Gemini uses "query", Claude Code expects "pattern"
56
- if (remappedArgs.query && !remappedArgs.pattern) {
57
- remappedArgs.pattern = remappedArgs.query;
58
- delete remappedArgs.query;
59
- }
60
-
61
- // [CRITICAL FIX] Claude Code uses "path" (string), NOT "paths" (array)!
62
- if (!remappedArgs.path) {
63
- if (remappedArgs.paths) {
64
- if (Array.isArray(remappedArgs.paths)) {
65
- remappedArgs.path = remappedArgs.paths[0] || '.';
66
- } else if (typeof remappedArgs.paths === 'string') {
67
- remappedArgs.path = remappedArgs.paths;
68
- } else {
69
- remappedArgs.path = '.';
70
- }
71
- delete remappedArgs.paths;
72
- } else {
73
- // Default to current directory if missing
74
- remappedArgs.path = '.';
75
- }
76
- }
77
- // Note: We keep "-n" and "output_mode" if present as they are valid in Grep schema
78
- break;
79
-
80
- case 'glob':
81
- // [FIX] Gemini hallucination: maps parameter description to "description" field
82
- if (remappedArgs.description && !remappedArgs.pattern) {
83
- remappedArgs.pattern = remappedArgs.description;
84
- delete remappedArgs.description;
85
- }
86
-
87
- // Gemini uses "query", Claude Code expects "pattern"
88
- if (remappedArgs.query && !remappedArgs.pattern) {
89
- remappedArgs.pattern = remappedArgs.query;
90
- delete remappedArgs.query;
91
- }
92
-
93
- // [CRITICAL FIX] Claude Code uses "path" (string), NOT "paths" (array)!
94
- // [NOTE] 与 grep 不同,glob 不添加默认 path(参考 Rust 代码)
95
- if (!remappedArgs.path) {
96
- if (remappedArgs.paths) {
97
- if (Array.isArray(remappedArgs.paths)) {
98
- remappedArgs.path = remappedArgs.paths[0] || '.';
99
- } else if (typeof remappedArgs.paths === 'string') {
100
- remappedArgs.path = remappedArgs.paths;
101
- } else {
102
- remappedArgs.path = '.';
103
- }
104
- delete remappedArgs.paths;
105
- }
106
- // [FIX] glob 不添加默认 path,与 Rust 代码保持一致
107
- }
108
- break;
109
-
110
- case 'read':
111
- // Gemini might use "path" vs "file_path"
112
- if (remappedArgs.path && !remappedArgs.file_path) {
113
- remappedArgs.file_path = remappedArgs.path;
114
- delete remappedArgs.path;
115
- }
116
- break;
117
-
118
- case 'ls':
119
- // LS tool: ensure "path" parameter exists
120
- if (!remappedArgs.path) {
121
- remappedArgs.path = '.';
122
- }
123
- break;
124
-
125
- default:
126
- // [NEW] [Issue #785] Generic Property Mapping for all tools
127
- // If a tool has "paths" (array of 1) but no "path", convert it.
128
- // [FIX] 与 Rust 代码保持一致:只在 paths.length === 1 时转换,不删除原始 paths
129
- if (!remappedArgs.path && remappedArgs.paths) {
130
- if (Array.isArray(remappedArgs.paths) && remappedArgs.paths.length === 1) {
131
- const pathValue = remappedArgs.paths[0];
132
- if (typeof pathValue === 'string') {
133
- remappedArgs.path = pathValue;
134
- // [FIX] Rust 代码中不删除 paths,这里也不删除
135
- }
136
- }
137
- }
138
- break;
139
- }
140
-
141
- return remappedArgs;
142
- }
143
-
144
- /**
145
- * [FIX] 规范化工具名称
146
- * Gemini 有时会返回 "search" 而不是 "Grep"
147
- */
148
- function normalizeToolName(name) {
149
- if (!name) return name;
150
-
151
- const nameLower = name.toLowerCase();
152
- if (nameLower === 'search') {
153
- return 'Grep';
154
- }
155
- return name;
156
- }
157
-
158
- /**
159
- * Gemini转换器类
160
- * 实现Gemini协议到其他协议的转换
161
- */
162
- export class GeminiConverter extends BaseConverter {
163
- constructor() {
164
- super('gemini');
165
- }
166
-
167
- /**
168
- * 转换请求
169
- */
170
- convertRequest(data, targetProtocol) {
171
- switch (targetProtocol) {
172
- case MODEL_PROTOCOL_PREFIX.OPENAI:
173
- return this.toOpenAIRequest(data);
174
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
175
- return this.toClaudeRequest(data);
176
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
177
- return this.toOpenAIResponsesRequest(data);
178
- case MODEL_PROTOCOL_PREFIX.CODEX:
179
- return this.toCodexRequest(data);
180
- case MODEL_PROTOCOL_PREFIX.GROK:
181
- return this.toGrokRequest(data);
182
- default:
183
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
184
- }
185
- }
186
-
187
- /**
188
- * 转换响应
189
- */
190
- convertResponse(data, targetProtocol, model) {
191
- switch (targetProtocol) {
192
- case MODEL_PROTOCOL_PREFIX.OPENAI:
193
- return this.toOpenAIResponse(data, model);
194
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
195
- return this.toClaudeResponse(data, model);
196
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
197
- return this.toOpenAIResponsesResponse(data, model);
198
- case MODEL_PROTOCOL_PREFIX.CODEX:
199
- return this.toCodexResponse(data, model);
200
- default:
201
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
202
- }
203
- }
204
-
205
- /**
206
- * 转换流式响应块
207
- */
208
- convertStreamChunk(chunk, targetProtocol, model) {
209
- switch (targetProtocol) {
210
- case MODEL_PROTOCOL_PREFIX.OPENAI:
211
- return this.toOpenAIStreamChunk(chunk, model);
212
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
213
- return this.toClaudeStreamChunk(chunk, model);
214
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
215
- return this.toOpenAIResponsesStreamChunk(chunk, model);
216
- case MODEL_PROTOCOL_PREFIX.CODEX:
217
- return this.toCodexStreamChunk(chunk, model);
218
- default:
219
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
220
- }
221
- }
222
-
223
- /**
224
- * 转换模型列表
225
- */
226
- convertModelList(data, targetProtocol) {
227
- switch (targetProtocol) {
228
- case MODEL_PROTOCOL_PREFIX.OPENAI:
229
- return this.toOpenAIModelList(data);
230
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
231
- return this.toClaudeModelList(data);
232
- default:
233
- return data;
234
- }
235
- }
236
-
237
- // =========================================================================
238
- // Gemini -> OpenAI 转换
239
- // =========================================================================
240
-
241
- /**
242
- * Gemini请求 -> OpenAI请求
243
- */
244
- toOpenAIRequest(geminiRequest) {
245
- const openaiRequest = {
246
- messages: [],
247
- model: geminiRequest.model,
248
- max_tokens: checkAndAssignOrDefault(geminiRequest.max_tokens, OPENAI_DEFAULT_MAX_TOKENS),
249
- temperature: checkAndAssignOrDefault(geminiRequest.temperature, OPENAI_DEFAULT_TEMPERATURE),
250
- top_p: checkAndAssignOrDefault(geminiRequest.top_p, OPENAI_DEFAULT_TOP_P),
251
- };
252
-
253
- // 处理系统指令
254
- if (geminiRequest.systemInstruction && Array.isArray(geminiRequest.systemInstruction.parts)) {
255
- const systemContent = this.processGeminiPartsToOpenAIContent(geminiRequest.systemInstruction.parts);
256
- if (systemContent) {
257
- openaiRequest.messages.push({
258
- role: 'system',
259
- content: systemContent
260
- });
261
- }
262
- }
263
-
264
- // 处理内容
265
- if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
266
- geminiRequest.contents.forEach(content => {
267
- if (content && Array.isArray(content.parts)) {
268
- const openaiContent = this.processGeminiPartsToOpenAIContent(content.parts);
269
- if (openaiContent && openaiContent.length > 0) {
270
- const openaiRole = content.role === 'model' ? 'assistant' : content.role;
271
- openaiRequest.messages.push({
272
- role: openaiRole,
273
- content: openaiContent
274
- });
275
- }
276
- }
277
- });
278
- }
279
-
280
- return openaiRequest;
281
- }
282
-
283
- /**
284
- * Gemini响应 -> OpenAI响应
285
- */
286
- toOpenAIResponse(geminiResponse, model) {
287
- const content = this.processGeminiResponseContent(geminiResponse);
288
-
289
- // 提取 tool_calls
290
- const toolCalls = [];
291
- let finishReason = "stop";
292
-
293
- if (geminiResponse && geminiResponse.candidates) {
294
- for (const candidate of geminiResponse.candidates) {
295
- if (candidate.content && candidate.content.parts) {
296
- for (const part of candidate.content.parts) {
297
- if (part.functionCall) {
298
- toolCalls.push({
299
- id: part.functionCall.id || `call_${uuidv4()}`,
300
- type: 'function',
301
- function: {
302
- name: part.functionCall.name,
303
- arguments: typeof part.functionCall.args === 'string'
304
- ? part.functionCall.args
305
- : JSON.stringify(part.functionCall.args)
306
- }
307
- });
308
- }
309
- }
310
- }
311
- }
312
- }
313
-
314
- // 如果有工具调用,设置 finish_reason 为 tool_calls
315
- if (toolCalls.length > 0) {
316
- finishReason = "tool_calls";
317
- }
318
-
319
- const message = {
320
- role: "assistant",
321
- content: content
322
- };
323
-
324
- // 只有在有 tool_calls 时才添加该字段
325
- if (toolCalls.length > 0) {
326
- message.tool_calls = toolCalls;
327
- }
328
-
329
- return {
330
- id: `chatcmpl-${uuidv4()}`,
331
- object: "chat.completion",
332
- created: Math.floor(Date.now() / 1000),
333
- model: model,
334
- choices: [{
335
- index: 0,
336
- message: message,
337
- finish_reason: finishReason,
338
- }],
339
- usage: geminiResponse.usageMetadata ? {
340
- prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
341
- completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
342
- total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0,
343
- cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0,
344
- prompt_tokens_details: {
345
- cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0
346
- },
347
- completion_tokens_details: {
348
- reasoning_tokens: geminiResponse.usageMetadata.thoughtsTokenCount || 0
349
- }
350
- } : {
351
- prompt_tokens: 0,
352
- completion_tokens: 0,
353
- total_tokens: 0,
354
- cached_tokens: 0,
355
- prompt_tokens_details: {
356
- cached_tokens: 0
357
- },
358
- completion_tokens_details: {
359
- reasoning_tokens: 0
360
- }
361
- },
362
- };
363
- }
364
-
365
- /**
366
- * Gemini流式响应 -> OpenAI流式响应
367
- */
368
- toOpenAIStreamChunk(geminiChunk, model) {
369
- if (!geminiChunk) return null;
370
-
371
- const candidate = geminiChunk.candidates?.[0];
372
- if (!candidate) return null;
373
-
374
- let content = '';
375
- const toolCalls = [];
376
-
377
- // 从parts中提取文本和tool calls
378
- const parts = candidate.content?.parts;
379
- if (parts && Array.isArray(parts)) {
380
- for (const part of parts) {
381
- if (part.text) {
382
- content += part.text;
383
- }
384
- if (part.functionCall) {
385
- toolCalls.push({
386
- index: toolCalls.length,
387
- id: part.functionCall.id || `call_${uuidv4()}`,
388
- type: 'function',
389
- function: {
390
- name: part.functionCall.name,
391
- arguments: typeof part.functionCall.args === 'string'
392
- ? part.functionCall.args
393
- : JSON.stringify(part.functionCall.args)
394
- }
395
- });
396
- }
397
- // thoughtSignature is ignored (internal Gemini data)
398
- }
399
- }
400
-
401
- // 处理finishReason
402
- let finishReason = null;
403
- if (candidate.finishReason) {
404
- const finishReasonMap = {
405
- 'FINISH_REASON_UNSPECIFIED': 'stop',
406
- 'STOP': 'stop',
407
- 'MAX_TOKENS': 'length',
408
- 'SAFETY': 'content_filter',
409
- 'RECITATION': 'content_filter',
410
- 'OTHER': 'stop',
411
- 'BLOCKLIST': 'content_filter',
412
- 'PROHIBITED_CONTENT': 'content_filter',
413
- 'SPII': 'content_filter',
414
- 'MALFORMED_FUNCTION_CALL': 'stop',
415
- 'MODEL_ARMOR': 'content_filter',
416
- };
417
- finishReason = finishReasonMap[candidate.finishReason] || 'stop';
418
- }
419
-
420
- // [FIX] 适配 Gemini 流式:Gemini 的最后一条流式消息通常不带 functionCall
421
- // 如果当前 chunk 包含工具调用,直接将其标记为 tool_calls
422
- if (toolCalls.length > 0) {
423
- finishReason = 'tool_calls';
424
- }
425
-
426
- // 构建delta对象
427
- const delta = {};
428
- if (content) delta.content = content;
429
- if (toolCalls.length > 0) delta.tool_calls = toolCalls;
430
-
431
- // Don't return empty delta chunks
432
- if (Object.keys(delta).length === 0 && !finishReason) {
433
- return null;
434
- }
435
-
436
- const chunk = {
437
- id: `chatcmpl-${uuidv4()}`,
438
- object: "chat.completion.chunk",
439
- created: Math.floor(Date.now() / 1000),
440
- model: model,
441
- choices: [{
442
- index: 0,
443
- delta: delta,
444
- finish_reason: finishReason,
445
- }],
446
- };
447
-
448
- if(geminiChunk.usageMetadata){
449
- chunk.usage = {
450
- prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
451
- completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
452
- total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
453
- cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
454
- prompt_tokens_details: {
455
- cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
456
- },
457
- completion_tokens_details: {
458
- reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
459
- }
460
- };
461
- }
462
-
463
- return chunk;
464
- }
465
-
466
- /**
467
- * Gemini模型列表 -> OpenAI模型列表
468
- */
469
- toOpenAIModelList(geminiModels) {
470
- return {
471
- object: "list",
472
- data: geminiModels.models.map(m => {
473
- const modelId = m.name.startsWith('models/') ? m.name.substring(7) : m.name;
474
- return {
475
- id: modelId,
476
- object: "model",
477
- created: Math.floor(Date.now() / 1000),
478
- owned_by: "google",
479
- display_name: m.displayName || modelId,
480
- };
481
- }),
482
- };
483
- }
484
-
485
- /**
486
- * 处理Gemini parts到OpenAI内容
487
- */
488
- processGeminiPartsToOpenAIContent(parts) {
489
- if (!parts || !Array.isArray(parts)) return '';
490
-
491
- const contentArray = [];
492
-
493
- parts.forEach(part => {
494
- if (!part) return;
495
-
496
- if (typeof part.text === 'string') {
497
- contentArray.push({
498
- type: 'text',
499
- text: part.text
500
- });
501
- }
502
-
503
- if (part.inlineData) {
504
- const { mimeType, data } = part.inlineData;
505
- if (mimeType && data) {
506
- contentArray.push({
507
- type: 'image_url',
508
- image_url: {
509
- url: `data:${mimeType};base64,${data}`
510
- }
511
- });
512
- }
513
- }
514
-
515
- if (part.fileData) {
516
- const { mimeType, fileUri } = part.fileData;
517
- if (mimeType && fileUri) {
518
- if (mimeType.startsWith('image/')) {
519
- contentArray.push({
520
- type: 'image_url',
521
- image_url: {
522
- url: fileUri
523
- }
524
- });
525
- } else if (mimeType.startsWith('audio/')) {
526
- contentArray.push({
527
- type: 'text',
528
- text: `[Audio file: ${fileUri}]`
529
- });
530
- }
531
- }
532
- }
533
- });
534
-
535
- return contentArray.length === 1 && contentArray[0].type === 'text'
536
- ? contentArray[0].text
537
- : contentArray;
538
- }
539
-
540
- /**
541
- * 处理Gemini响应内容
542
- */
543
- processGeminiResponseContent(geminiResponse) {
544
- if (!geminiResponse || !geminiResponse.candidates) return '';
545
-
546
- const contents = [];
547
-
548
- geminiResponse.candidates.forEach(candidate => {
549
- if (candidate.content && candidate.content.parts) {
550
- candidate.content.parts.forEach(part => {
551
- if (part.text) {
552
- contents.push(part.text);
553
- }
554
- });
555
- }
556
- });
557
-
558
- return contents.join('\n');
559
- }
560
-
561
- // =========================================================================
562
- // Gemini -> Claude 转换
563
- // =========================================================================
564
-
565
- /**
566
- * Gemini请求 -> Claude请求
567
- */
568
- toClaudeRequest(geminiRequest) {
569
- const claudeRequest = {
570
- model: geminiRequest.model || 'claude-3-opus',
571
- messages: [],
572
- max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, CLAUDE_DEFAULT_MAX_TOKENS),
573
- temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, CLAUDE_DEFAULT_TEMPERATURE),
574
- top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, CLAUDE_DEFAULT_TOP_P),
575
- };
576
-
577
- // 处理系统指令
578
- if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
579
- const systemText = geminiRequest.systemInstruction.parts
580
- .filter(p => p.text)
581
- .map(p => p.text)
582
- .join('\n');
583
- if (systemText) {
584
- claudeRequest.system = systemText;
585
- }
586
- }
587
-
588
- // 处理内容
589
- if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
590
- geminiRequest.contents.forEach(content => {
591
- if (!content || !content.parts) return;
592
-
593
- const role = content.role === 'model' ? 'assistant' : 'user';
594
- const claudeContent = this.processGeminiPartsToClaudeContent(content.parts);
595
-
596
- if (claudeContent.length > 0) {
597
- claudeRequest.messages.push({
598
- role: role,
599
- content: claudeContent
600
- });
601
- }
602
- });
603
- }
604
-
605
- // 处理工具
606
- if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
607
- claudeRequest.tools = geminiRequest.tools[0].functionDeclarations.map(func => ({
608
- name: func.name,
609
- description: func.description || '',
610
- input_schema: func.parameters || { type: 'object', properties: {} }
611
- }));
612
- }
613
-
614
- return claudeRequest;
615
- }
616
-
617
- /**
618
- * Gemini响应 -> Claude响应
619
- */
620
- toClaudeResponse(geminiResponse, model) {
621
- if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
622
- return {
623
- id: `msg_${uuidv4()}`,
624
- type: "message",
625
- role: "assistant",
626
- content: [],
627
- model: model,
628
- stop_reason: "end_turn",
629
- stop_sequence: null,
630
- usage: {
631
- input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0,
632
- output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0
633
- }
634
- };
635
- }
636
-
637
- const candidate = geminiResponse.candidates[0];
638
- const { content, hasToolUse } = this.processGeminiResponseToClaudeContent(geminiResponse);
639
- const finishReason = candidate.finishReason;
640
- let stopReason = "end_turn";
641
-
642
- // - 如果有工具调用,stop_reason 应该是 "tool_use"
643
- if (hasToolUse) {
644
- stopReason = 'tool_use';
645
- } else if (finishReason) {
646
- switch (finishReason) {
647
- case 'STOP':
648
- stopReason = 'end_turn';
649
- break;
650
- case 'MAX_TOKENS':
651
- stopReason = 'max_tokens';
652
- break;
653
- case 'SAFETY':
654
- stopReason = 'safety';
655
- break;
656
- case 'RECITATION':
657
- stopReason = 'recitation';
658
- break;
659
- case 'OTHER':
660
- stopReason = 'other';
661
- break;
662
- default:
663
- stopReason = 'end_turn';
664
- }
665
- }
666
-
667
- return {
668
- id: `msg_${uuidv4()}`,
669
- type: "message",
670
- role: "assistant",
671
- content: content,
672
- model: model,
673
- stop_reason: stopReason,
674
- stop_sequence: null,
675
- usage: {
676
- input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
677
- cache_creation_input_tokens: 0,
678
- cache_read_input_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0,
679
- output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0
680
- }
681
- };
682
- }
683
-
684
- /**
685
- * Gemini流式响应 -> Claude流式响应
686
- */
687
- toClaudeStreamChunk(geminiChunk, model) {
688
- if (!geminiChunk) return null;
689
-
690
- // 处理完整的Gemini chunk对象
691
- if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
692
- const candidate = geminiChunk.candidates?.[0];
693
-
694
- if (candidate) {
695
- const parts = candidate.content?.parts;
696
-
697
- // thinking 和 text 块
698
- if (parts && Array.isArray(parts)) {
699
- const results = [];
700
- let hasToolUse = false;
701
-
702
- for (const part of parts) {
703
- if (!part) continue;
704
-
705
- if (typeof part.text === 'string') {
706
- if (part.thought === true) {
707
- // [FIX] 这是一个 thinking 块
708
- const thinkingResult = {
709
- type: "content_block_delta",
710
- index: 0,
711
- delta: {
712
- type: "thinking_delta",
713
- thinking: part.text
714
- }
715
- };
716
- results.push(thinkingResult);
717
-
718
- // 如果有签名,发送 signature_delta
719
- // [FIX] 同时检查 thoughtSignature 和 thought_signature
720
- const rawSignature = part.thoughtSignature || part.thought_signature;
721
- if (rawSignature) {
722
- let signature = rawSignature;
723
- try {
724
- const decoded = Buffer.from(signature, 'base64').toString('utf-8');
725
- if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
726
- signature = decoded;
727
- }
728
- } catch (e) {
729
- // 解码失败,保持原样
730
- }
731
- results.push({
732
- type: "content_block_delta",
733
- index: 0,
734
- delta: {
735
- type: "signature_delta",
736
- signature: signature
737
- }
738
- });
739
- }
740
- } else {
741
- // 普通文本
742
- results.push({
743
- type: "content_block_delta",
744
- index: 0,
745
- delta: {
746
- type: "text_delta",
747
- text: part.text
748
- }
749
- });
750
- }
751
- }
752
-
753
- // [FIX] 处理 functionCall
754
- if (part.functionCall) {
755
- hasToolUse = true;
756
- // [FIX] 规范化工具名称和参数映射
757
- const toolName = normalizeToolName(part.functionCall.name);
758
- const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {});
759
-
760
- // 发送 tool_use 开始
761
- const toolId = part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`;
762
- results.push({
763
- type: "content_block_start",
764
- index: 0,
765
- content_block: {
766
- type: "tool_use",
767
- id: toolId,
768
- name: toolName,
769
- input: {}
770
- }
771
- });
772
- // 发送参数
773
- results.push({
774
- type: "content_block_delta",
775
- index: 0,
776
- delta: {
777
- type: "input_json_delta",
778
- partial_json: JSON.stringify(remappedArgs)
779
- }
780
- });
781
- }
782
- }
783
-
784
- // [FIX] 如果有工具���用,添加 message_delta 事件设置 stop_reason 为 tool_use
785
- if (hasToolUse && candidate.finishReason) {
786
- const messageDelta = {
787
- type: "message_delta",
788
- delta: {
789
- stop_reason: 'tool_use'
790
- }
791
- };
792
- if (geminiChunk.usageMetadata) {
793
- messageDelta.usage = {
794
- input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
795
- cache_creation_input_tokens: 0,
796
- cache_read_input_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
797
- output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0
798
- };
799
- }
800
- results.push(messageDelta);
801
- }
802
-
803
- // 如果有多个结果,返回数组;否则返回单个或 null
804
- if (results.length > 1) {
805
- return results;
806
- } else if (results.length === 1) {
807
- return results[0];
808
- }
809
- }
810
-
811
- // 处理finishReason
812
- if (candidate.finishReason) {
813
- const result = {
814
- type: "message_delta",
815
- delta: {
816
- stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' :
817
- candidate.finishReason === 'MAX_TOKENS' ? 'max_tokens' :
818
- candidate.finishReason.toLowerCase()
819
- }
820
- };
821
-
822
- // 添加 usage 信息
823
- if (geminiChunk.usageMetadata) {
824
- result.usage = {
825
- input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
826
- cache_creation_input_tokens: 0,
827
- cache_read_input_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
828
- output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
829
- prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
830
- completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
831
- total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
832
- cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
833
- };
834
- }
835
-
836
- return result;
837
- }
838
- }
839
- }
840
-
841
- // 向后兼容:处理字符串格式
842
- if (typeof geminiChunk === 'string') {
843
- return {
844
- type: "content_block_delta",
845
- index: 0,
846
- delta: {
847
- type: "text_delta",
848
- text: geminiChunk
849
- }
850
- };
851
- }
852
-
853
- return null;
854
- }
855
-
856
- /**
857
- * Gemini模型列表 -> Claude模型列表
858
- */
859
- toClaudeModelList(geminiModels) {
860
- return {
861
- models: geminiModels.models.map(m => ({
862
- name: m.name.startsWith('models/') ? m.name.substring(7) : m.name,
863
- description: "",
864
- })),
865
- };
866
- }
867
-
868
- /**
869
- * 处理Gemini parts到Claude内容
870
- */
871
- processGeminiPartsToClaudeContent(parts) {
872
- if (!parts || !Array.isArray(parts)) return [];
873
-
874
- const content = [];
875
-
876
- parts.forEach(part => {
877
- if (!part) return;
878
-
879
- // 处理 thinking 块
880
- // Gemini 使用 thought: true 和 thoughtSignature 表示思考内容
881
- // [FIX] 同时支持 thoughtSignature 和 thought_signature(Gemini CLI 可能使用下划线格式)
882
- if (part.text) {
883
- if (part.thought === true) {
884
- // 这是一个 thinking 块
885
- const thinkingBlock = {
886
- type: 'thinking',
887
- thinking: part.text
888
- };
889
- // 处理签名 - 可能是 Base64 编码的
890
- // [FIX] 同时检查 thoughtSignature 和 thought_signature
891
- const rawSignature = part.thoughtSignature || part.thought_signature;
892
- if (rawSignature) {
893
- let signature = rawSignature;
894
- // 尝试 Base64 解码
895
- try {
896
- const decoded = Buffer.from(signature, 'base64').toString('utf-8');
897
- // 检查解码后是否是有效的 UTF-8 字符串
898
- if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
899
- signature = decoded;
900
- }
901
- } catch (e) {
902
- // 解码失败,保持原样
903
- }
904
- thinkingBlock.signature = signature;
905
- }
906
- content.push(thinkingBlock);
907
- } else {
908
- // 普通文本
909
- content.push({
910
- type: 'text',
911
- text: part.text
912
- });
913
- }
914
- }
915
-
916
- if (part.inlineData) {
917
- content.push({
918
- type: 'image',
919
- source: {
920
- type: 'base64',
921
- media_type: part.inlineData.mimeType,
922
- data: part.inlineData.data
923
- }
924
- });
925
- }
926
-
927
- if (part.functionCall) {
928
- // [FIX] 规范化工具名称和参数映射
929
- const toolName = normalizeToolName(part.functionCall.name);
930
- const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {});
931
-
932
- // [FIX] 使用 Gemini 提供的 id,如果没有则生成
933
- const toolUseBlock = {
934
- type: 'tool_use',
935
- id: part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`,
936
- name: toolName,
937
- input: remappedArgs
938
- };
939
- // [FIX] 如果有签名,添加到 tool_use 块
940
- // [FIX] 同时检查 thoughtSignature 和 thought_signature
941
- const rawSignature = part.thoughtSignature || part.thought_signature;
942
- if (rawSignature) {
943
- let signature = rawSignature;
944
- try {
945
- const decoded = Buffer.from(signature, 'base64').toString('utf-8');
946
- if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
947
- signature = decoded;
948
- }
949
- } catch (e) {
950
- // 解码失败,保持原样
951
- }
952
- toolUseBlock.signature = signature;
953
- }
954
- content.push(toolUseBlock);
955
- }
956
-
957
- if (part.functionResponse) {
958
- // [FIX] 正确处理 functionResponse
959
- let responseContent = part.functionResponse.response;
960
- // 如果 response 是对象且有 result 字段,提取它
961
- if (responseContent && typeof responseContent === 'object' && responseContent.result !== undefined) {
962
- responseContent = responseContent.result;
963
- }
964
- content.push({
965
- type: 'tool_result',
966
- tool_use_id: part.functionResponse.name,
967
- content: typeof responseContent === 'string' ? responseContent : JSON.stringify(responseContent)
968
- });
969
- }
970
- });
971
-
972
- return content;
973
- }
974
-
975
- /**
976
- * 处理Gemini响应到Claude内容
977
- * @returns {{ content: Array, hasToolUse: boolean }}
978
- */
979
- processGeminiResponseToClaudeContent(geminiResponse) {
980
- if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
981
- return { content: [], hasToolUse: false };
982
- }
983
-
984
- const content = [];
985
- let hasToolUse = false;
986
-
987
- for (const candidate of geminiResponse.candidates) {
988
- if (candidate.finishReason && candidate.finishReason !== 'STOP') {
989
- if (candidate.finishMessage) {
990
- content.push({
991
- type: 'text',
992
- text: `Error: ${candidate.finishMessage}`
993
- });
994
- }
995
- continue;
996
- }
997
-
998
- if (candidate.content && candidate.content.parts) {
999
- for (const part of candidate.content.parts) {
1000
- // 处理 thinking 块
1001
- if (part.text) {
1002
- if (part.thought === true) {
1003
- // 这是一个 thinking 块
1004
- const thinkingBlock = {
1005
- type: 'thinking',
1006
- thinking: part.text
1007
- };
1008
- // 处理签名
1009
- // [FIX] 同时检查 thoughtSignature 和 thought_signature
1010
- const rawSignature = part.thoughtSignature || part.thought_signature;
1011
- if (rawSignature) {
1012
- let signature = rawSignature;
1013
- try {
1014
- const decoded = Buffer.from(signature, 'base64').toString('utf-8');
1015
- if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
1016
- signature = decoded;
1017
- }
1018
- } catch (e) {
1019
- // 解码失败,保持原样
1020
- }
1021
- thinkingBlock.signature = signature;
1022
- }
1023
- content.push(thinkingBlock);
1024
- } else {
1025
- // 普通文本
1026
- content.push({
1027
- type: 'text',
1028
- text: part.text
1029
- });
1030
- }
1031
- } else if (part.inlineData) {
1032
- content.push({
1033
- type: 'image',
1034
- source: {
1035
- type: 'base64',
1036
- media_type: part.inlineData.mimeType,
1037
- data: part.inlineData.data
1038
- }
1039
- });
1040
- } else if (part.functionCall) {
1041
- hasToolUse = true;
1042
- // [FIX] 规范化工具名称和参数映射
1043
- const toolName = normalizeToolName(part.functionCall.name);
1044
- const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {});
1045
-
1046
- // [FIX] 使用 Gemini 提供的 id
1047
- const toolUseBlock = {
1048
- type: 'tool_use',
1049
- id: part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`,
1050
- name: toolName,
1051
- input: remappedArgs
1052
- };
1053
- // 添加签名(如果存在)
1054
- // [FIX] 同时检查 thoughtSignature 和 thought_signature
1055
- const rawSignature = part.thoughtSignature || part.thought_signature;
1056
- if (rawSignature) {
1057
- let signature = rawSignature;
1058
- try {
1059
- const decoded = Buffer.from(signature, 'base64').toString('utf-8');
1060
- if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
1061
- signature = decoded;
1062
- }
1063
- } catch (e) {
1064
- // 解码失败,保持原样
1065
- }
1066
- toolUseBlock.signature = signature;
1067
- }
1068
- content.push(toolUseBlock);
1069
- }
1070
- }
1071
- }
1072
- }
1073
-
1074
- return { content, hasToolUse };
1075
- }
1076
-
1077
- // =========================================================================
1078
- // Gemini -> OpenAI Responses 转换
1079
- // =========================================================================
1080
-
1081
- /**
1082
- * Gemini请求 -> OpenAI Responses请求
1083
- */
1084
- toOpenAIResponsesRequest(geminiRequest) {
1085
- const responsesRequest = {
1086
- model: geminiRequest.model,
1087
- instructions: '',
1088
- input: [],
1089
- stream: geminiRequest.stream || false,
1090
- max_output_tokens: geminiRequest.generationConfig?.maxOutputTokens,
1091
- temperature: geminiRequest.generationConfig?.temperature,
1092
- top_p: geminiRequest.generationConfig?.topP
1093
- };
1094
-
1095
- // 处理系统指令
1096
- if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
1097
- responsesRequest.instructions = geminiRequest.systemInstruction.parts
1098
- .filter(p => p.text)
1099
- .map(p => p.text)
1100
- .join('\n');
1101
- }
1102
-
1103
- // 处理内容
1104
- if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
1105
- geminiRequest.contents.forEach(content => {
1106
- const role = content.role === 'model' ? 'assistant' : 'user';
1107
- const parts = content.parts || [];
1108
-
1109
- parts.forEach(part => {
1110
- if (part.text) {
1111
- responsesRequest.input.push({
1112
- type: 'message',
1113
- role: role,
1114
- content: [{
1115
- type: role === 'assistant' ? 'output_text' : 'input_text',
1116
- text: part.text
1117
- }]
1118
- });
1119
- }
1120
-
1121
- if (part.functionCall) {
1122
- responsesRequest.input.push({
1123
- type: 'function_call',
1124
- call_id: part.functionCall.id || `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`,
1125
- name: part.functionCall.name,
1126
- arguments: typeof part.functionCall.args === 'string'
1127
- ? part.functionCall.args
1128
- : JSON.stringify(part.functionCall.args)
1129
- });
1130
- }
1131
-
1132
- if (part.functionResponse) {
1133
- responsesRequest.input.push({
1134
- type: 'function_call_output',
1135
- call_id: part.functionResponse.name, // Gemini 通常使用 name 作为关联
1136
- output: typeof part.functionResponse.response?.result === 'string'
1137
- ? part.functionResponse.response.result
1138
- : JSON.stringify(part.functionResponse.response || {})
1139
- });
1140
- }
1141
-
1142
- if (part.inlineData) {
1143
- responsesRequest.input.push({
1144
- type: 'message',
1145
- role: role,
1146
- content: [{
1147
- type: 'input_image',
1148
- image_url: {
1149
- url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
1150
- }
1151
- }]
1152
- });
1153
- }
1154
- });
1155
- });
1156
- }
1157
-
1158
- // 处理工具
1159
- if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
1160
- responsesRequest.tools = geminiRequest.tools[0].functionDeclarations.map(fn => ({
1161
- type: 'function',
1162
- name: fn.name,
1163
- description: fn.description,
1164
- parameters: fn.parameters || fn.parametersJsonSchema || { type: 'object', properties: {} }
1165
- }));
1166
- }
1167
-
1168
- return responsesRequest;
1169
- }
1170
-
1171
- /**
1172
- * Gemini响应 -> OpenAI Responses响应
1173
- */
1174
- toOpenAIResponsesResponse(geminiResponse, model) {
1175
- const content = this.processGeminiResponseContent(geminiResponse);
1176
- const textContent = typeof content === 'string' ? content : JSON.stringify(content);
1177
-
1178
- let output = [];
1179
- output.push({
1180
- id: `msg_${uuidv4().replace(/-/g, '')}`,
1181
- summary: [],
1182
- type: "message",
1183
- role: "assistant",
1184
- status: "completed",
1185
- content: [{
1186
- annotations: [],
1187
- logprobs: [],
1188
- text: textContent,
1189
- type: "output_text"
1190
- }]
1191
- });
1192
-
1193
- return {
1194
- background: false,
1195
- created_at: Math.floor(Date.now() / 1000),
1196
- error: null,
1197
- id: `resp_${uuidv4().replace(/-/g, '')}`,
1198
- incomplete_details: null,
1199
- max_output_tokens: null,
1200
- max_tool_calls: null,
1201
- metadata: {},
1202
- model: model,
1203
- object: "response",
1204
- output: output,
1205
- parallel_tool_calls: true,
1206
- previous_response_id: null,
1207
- prompt_cache_key: null,
1208
- reasoning: {},
1209
- safety_identifier: "user-" + uuidv4().replace(/-/g, ''),
1210
- service_tier: "default",
1211
- status: "completed",
1212
- store: false,
1213
- temperature: 1,
1214
- text: {
1215
- format: { type: "text" },
1216
- },
1217
- tool_choice: "auto",
1218
- tools: [],
1219
- top_logprobs: 0,
1220
- top_p: 1,
1221
- truncation: "disabled",
1222
- usage: {
1223
- input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
1224
- input_tokens_details: {
1225
- cached_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0
1226
- },
1227
- output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
1228
- output_tokens_details: {
1229
- reasoning_tokens: geminiResponse.usageMetadata?.thoughtsTokenCount || 0
1230
- },
1231
- total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0
1232
- },
1233
- user: null
1234
- };
1235
- }
1236
-
1237
- /**
1238
- * Gemini流式响应 -> OpenAI Responses流式响应
1239
- */
1240
- toOpenAIResponsesStreamChunk(geminiChunk, model, requestId = null) {
1241
- if (!geminiChunk) return [];
1242
-
1243
- const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
1244
- const events = [];
1245
-
1246
- // 处理完整的Gemini chunk对象
1247
- if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
1248
- const candidate = geminiChunk.candidates?.[0];
1249
-
1250
- if (candidate) {
1251
- const parts = candidate.content?.parts;
1252
-
1253
- // 第一个chunk - 检测是否是开始(有role)
1254
- if (candidate.content?.role === 'model' && parts && parts.length > 0) {
1255
- // 只在第一次有内容时发送开始事件
1256
- const hasContent = parts.some(part => part && typeof part.text === 'string' && part.text.length > 0);
1257
- if (hasContent) {
1258
- events.push(
1259
- generateResponseCreated(responseId, model || 'unknown'),
1260
- generateResponseInProgress(responseId),
1261
- generateOutputItemAdded(responseId),
1262
- generateContentPartAdded(responseId)
1263
- );
1264
- }
1265
- }
1266
-
1267
- // 提取文本内容
1268
- if (parts && Array.isArray(parts)) {
1269
- const textParts = parts.filter(part => part && typeof part.text === 'string');
1270
- if (textParts.length > 0) {
1271
- const text = textParts.map(part => part.text).join('');
1272
- events.push({
1273
- delta: text,
1274
- item_id: `msg_${uuidv4().replace(/-/g, '')}`,
1275
- output_index: 0,
1276
- sequence_number: 3,
1277
- type: "response.output_text.delta"
1278
- });
1279
- }
1280
- }
1281
-
1282
- // 处理finishReason
1283
- if (candidate.finishReason) {
1284
- events.push(
1285
- generateOutputTextDone(responseId),
1286
- generateContentPartDone(responseId),
1287
- generateOutputItemDone(responseId),
1288
- generateResponseCompleted(responseId)
1289
- );
1290
-
1291
- // 如果有 usage 信息,更新最后一个事件
1292
- if (geminiChunk.usageMetadata && events.length > 0) {
1293
- const lastEvent = events[events.length - 1];
1294
- if (lastEvent.response) {
1295
- lastEvent.response.usage = {
1296
- input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
1297
- input_tokens_details: {
1298
- cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
1299
- },
1300
- output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
1301
- output_tokens_details: {
1302
- reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
1303
- },
1304
- total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0
1305
- };
1306
- }
1307
- }
1308
- }
1309
- }
1310
- }
1311
-
1312
- // 向后兼容:处理字符串格式
1313
- if (typeof geminiChunk === 'string') {
1314
- events.push({
1315
- delta: geminiChunk,
1316
- item_id: `msg_${uuidv4().replace(/-/g, '')}`,
1317
- output_index: 0,
1318
- sequence_number: 3,
1319
- type: "response.output_text.delta"
1320
- });
1321
- }
1322
-
1323
- return events;
1324
- }
1325
-
1326
- // =========================================================================
1327
- // Gemini -> Codex 转换
1328
- // =========================================================================
1329
-
1330
- /**
1331
- * Gemini请求 -> Codex请求
1332
- */
1333
- toCodexRequest(geminiRequest) {
1334
- // 使用 CodexConverter 进行转换,因为 CodexConverter.js 中已经实现了 OpenAI -> Codex 的逻辑
1335
- // 我们需要先将 Gemini 转为 OpenAI 格式,再转为 Codex 格式
1336
- const openaiRequest = this.toOpenAIRequest(geminiRequest);
1337
-
1338
- // 注意:这里我们直接在 GeminiConverter 中实现逻辑,避免循环依赖
1339
- const codexRequest = {
1340
- model: openaiRequest.model,
1341
- instructions: '',
1342
- input: [],
1343
- stream: geminiRequest.stream || false,
1344
- store: false,
1345
- reasoning: {
1346
- effort: 'medium',
1347
- summary: 'auto'
1348
- },
1349
- parallel_tool_calls: true,
1350
- include: ['reasoning.encrypted_content']
1351
- };
1352
-
1353
- // 处理系统指令
1354
- if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
1355
- codexRequest.instructions = geminiRequest.systemInstruction.parts
1356
- .filter(p => p.text)
1357
- .map(p => p.text)
1358
- .join('\n');
1359
- }
1360
-
1361
- // 处理内容
1362
- if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
1363
- const pendingCallIDs = [];
1364
-
1365
- geminiRequest.contents.forEach(content => {
1366
- const role = content.role === 'model' ? 'assistant' : 'user';
1367
- const parts = content.parts || [];
1368
-
1369
- parts.forEach(part => {
1370
- if (part.text) {
1371
- codexRequest.input.push({
1372
- type: 'message',
1373
- role: role,
1374
- content: [{
1375
- type: role === 'assistant' ? 'output_text' : 'input_text',
1376
- text: part.text
1377
- }]
1378
- });
1379
- }
1380
-
1381
- if (part.functionCall) {
1382
- const callId = `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`;
1383
- pendingCallIDs.push(callId);
1384
- codexRequest.input.push({
1385
- type: 'function_call',
1386
- call_id: callId,
1387
- name: part.functionCall.name,
1388
- arguments: typeof part.functionCall.args === 'string'
1389
- ? part.functionCall.args
1390
- : JSON.stringify(part.functionCall.args)
1391
- });
1392
- }
1393
-
1394
- if (part.functionResponse) {
1395
- const callId = pendingCallIDs.shift() || `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`;
1396
- codexRequest.input.push({
1397
- type: 'function_call_output',
1398
- call_id: callId,
1399
- output: typeof part.functionResponse.response?.result === 'string'
1400
- ? part.functionResponse.response.result
1401
- : JSON.stringify(part.functionResponse.response || {})
1402
- });
1403
- }
1404
- });
1405
- });
1406
- }
1407
-
1408
- // 处理工具
1409
- if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
1410
- codexRequest.tools = geminiRequest.tools[0].functionDeclarations.map(fn => ({
1411
- type: 'function',
1412
- name: fn.name,
1413
- description: fn.description,
1414
- parameters: fn.parameters || { type: 'object', properties: {} }
1415
- }));
1416
- }
1417
-
1418
- return codexRequest;
1419
- }
1420
-
1421
- /**
1422
- * Gemini请求 -> Grok请求
1423
- */
1424
- toGrokRequest(geminiRequest) {
1425
- // 先转换为 OpenAI 格式
1426
- const openaiRequest = this.toOpenAIRequest(geminiRequest);
1427
- return {
1428
- ...openaiRequest,
1429
- _isConverted: true
1430
- };
1431
- }
1432
-
1433
- /**
1434
- * Gemini响应 -> Codex响应 (实际上是 Codex 转 Gemini)
1435
- */
1436
- toCodexResponse(geminiResponse, model) {
1437
- // 这里实际上是实现 Codex -> Gemini 的非流式转换
1438
- // 为了保持接口一致,我们按照其他 Converter 的命名习惯
1439
- const parts = [];
1440
- if (geminiResponse.response?.output) {
1441
- geminiResponse.response.output.forEach(item => {
1442
- if (item.type === 'message' && item.content) {
1443
- const textPart = item.content.find(c => c.type === 'output_text');
1444
- if (textPart) parts.push({ text: textPart.text });
1445
- } else if (item.type === 'reasoning' && item.summary) {
1446
- const textPart = item.summary.find(c => c.type === 'summary_text');
1447
- if (textPart) parts.push({ text: textPart.text, thought: true });
1448
- } else if (item.type === 'function_call') {
1449
- parts.push({
1450
- functionCall: {
1451
- name: item.name,
1452
- args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
1453
- }
1454
- });
1455
- }
1456
- });
1457
- }
1458
-
1459
- return {
1460
- candidates: [{
1461
- content: {
1462
- role: 'model',
1463
- parts: parts
1464
- },
1465
- finishReason: 'STOP'
1466
- }],
1467
- usageMetadata: {
1468
- promptTokenCount: geminiResponse.response?.usage?.input_tokens || 0,
1469
- candidatesTokenCount: geminiResponse.response?.usage?.output_tokens || 0,
1470
- totalTokenCount: geminiResponse.response?.usage?.total_tokens || 0
1471
- },
1472
- modelVersion: model,
1473
- responseId: geminiResponse.response?.id
1474
- };
1475
- }
1476
-
1477
- /**
1478
- * Gemini流式响应 -> Codex流式响应 (实际上是 Codex 转 Gemini)
1479
- */
1480
- toCodexStreamChunk(codexChunk, model) {
1481
- const type = codexChunk.type;
1482
- const resId = codexChunk.response?.id || 'default';
1483
-
1484
- const template = {
1485
- candidates: [{
1486
- content: {
1487
- role: "model",
1488
- parts: []
1489
- }
1490
- }],
1491
- modelVersion: model,
1492
- responseId: resId
1493
- };
1494
-
1495
- if (type === 'response.reasoning_summary_text.delta') {
1496
- template.candidates[0].content.parts.push({ text: codexChunk.delta, thought: true });
1497
- return template;
1498
- }
1499
-
1500
- if (type === 'response.output_text.delta') {
1501
- template.candidates[0].content.parts.push({ text: codexChunk.delta });
1502
- return template;
1503
- }
1504
-
1505
- if (type === 'response.output_item.done' && codexChunk.item?.type === 'function_call') {
1506
- template.candidates[0].content.parts.push({
1507
- functionCall: {
1508
- name: codexChunk.item.name,
1509
- args: typeof codexChunk.item.arguments === 'string' ? JSON.parse(codexChunk.item.arguments) : codexChunk.item.arguments
1510
- }
1511
- });
1512
- return template;
1513
- }
1514
-
1515
- if (type === 'response.completed') {
1516
- template.candidates[0].finishReason = "STOP";
1517
- template.usageMetadata = {
1518
- promptTokenCount: codexChunk.response.usage?.input_tokens || 0,
1519
- candidatesTokenCount: codexChunk.response.usage?.output_tokens || 0,
1520
- totalTokenCount: codexChunk.response.usage?.total_tokens || 0
1521
- };
1522
- return template;
1523
- }
1524
-
1525
- return null;
1526
- }
1527
- }
1528
-
1529
- export default GeminiConverter;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/strategies/GrokConverter.js DELETED
@@ -1,1153 +0,0 @@
1
- /**
2
- * Grok转换器
3
- * 处理Grok协议与其他协议之间的转换
4
- */
5
-
6
- import { v4 as uuidv4 } from 'uuid';
7
- import logger from '../../utils/logger.js';
8
- import { BaseConverter } from '../BaseConverter.js';
9
- import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
10
-
11
- /**
12
- * Grok转换器类
13
- * 实现Grok协议到其他协议的转换
14
- */
15
- export class GrokConverter extends BaseConverter {
16
- // 静态属性,确保所有实例共享最新的基础 URL 和 UUID 配置
17
- static sharedRequestBaseUrl = "";
18
- static sharedUuid = null;
19
-
20
- constructor() {
21
- super('grok');
22
- // 用于跟踪每个请求的状态
23
- this.requestStates = new Map();
24
- }
25
-
26
- /**
27
- * 设置请求的基础 URL
28
- */
29
- setRequestBaseUrl(baseUrl) {
30
- if (baseUrl) {
31
- GrokConverter.sharedRequestBaseUrl = baseUrl;
32
- }
33
- }
34
-
35
- /**
36
- * 设置账号的 UUID
37
- */
38
- setUuid(uuid) {
39
- if (uuid) {
40
- GrokConverter.sharedUuid = uuid;
41
- }
42
- }
43
-
44
- /**
45
- * 为 assets.grok.com 域名的资源 URL 添加 uuid 参数,并转换为本地代理 URL
46
- */
47
- _appendSsoToken(url, state = null) {
48
- const requestBaseUrl = state?.requestBaseUrl || GrokConverter.sharedRequestBaseUrl;
49
- const uuid = state?.uuid || GrokConverter.sharedUuid;
50
-
51
- if (!url || !uuid) return url;
52
-
53
- // 检查是否为 assets.grok.com 域名或相对路径
54
- const isGrokAsset = url.includes('assets.grok.com') || (!url.startsWith('http') && !url.startsWith('data:'));
55
-
56
- if (!isGrokAsset) return url;
57
-
58
- // 构造完整的原始 URL
59
- let originalUrl = url;
60
- if (!url.startsWith('http')) {
61
- originalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
62
- }
63
-
64
- // 返回本地代理接口 URL
65
- // 使用 uuid 以提高安全性,防止 token 泄露在链接中
66
- const authParam = `uuid=${encodeURIComponent(uuid)}`;
67
-
68
- const proxyPath = `/api/grok/assets?url=${encodeURIComponent(originalUrl)}&${authParam}`;
69
- if (requestBaseUrl) {
70
- return `${requestBaseUrl}${proxyPath}`;
71
- }
72
- return proxyPath;
73
- }
74
-
75
- /**
76
- * 在文本中查找并替换所有 assets.grok.com 的资源链接为绝对代理链接
77
- */
78
- _processGrokAssetsInText(text, state = null) {
79
- const uuid = state?.uuid || GrokConverter.sharedUuid;
80
- if (!text || !uuid) return text;
81
-
82
- // 更宽松的正则匹配 assets.grok.com 的 URL
83
- const grokUrlRegex = /https?:\/\/assets\.grok\.com\/[^\s\)\"\'\>]+/g;
84
-
85
- return text.replace(grokUrlRegex, (url) => {
86
- return this._appendSsoToken(url, state);
87
- });
88
- }
89
-
90
- /**
91
- * 获取或初始化请求状态
92
- */
93
- _getState(requestId) {
94
- if (!this.requestStates.has(requestId)) {
95
- this.requestStates.set(requestId, {
96
- think_opened: false,
97
- image_think_active: false,
98
- video_think_active: false,
99
- role_sent: false,
100
- tool_buffer: "",
101
- last_is_thinking: false,
102
- fingerprint: "",
103
- content_buffer: "", // 用于缓存内容以解析工具调用
104
- has_tool_call: false,
105
- rollout_id: "",
106
- in_tool_call: false, // 是否处于 <tool_call> 块内
107
- requestBaseUrl: "",
108
- uuid: null,
109
- pending_text_buffer: "" // 用于处理流式输出中被截断的 URL
110
- });
111
- }
112
- return this.requestStates.get(requestId);
113
- }
114
-
115
- /**
116
- * 构建工具系统提示词 (build_tool_prompt)
117
- */
118
- buildToolPrompt(tools, toolChoice = "auto", parallelToolCalls = true) {
119
- if (!tools || tools.length === 0 || toolChoice === "none") {
120
- return "";
121
- }
122
-
123
- const lines = [
124
- "# Available Tools",
125
- "",
126
- "You have access to the following tools. To call a tool, output a <tool_call> block with a JSON object containing \"name\" and \"arguments\".",
127
- "",
128
- "Format:",
129
- "<tool_call>",
130
- '{"name": "function_name", "arguments": {"param": "value"}}',
131
- "</tool_call>",
132
- "",
133
- ];
134
-
135
- if (parallelToolCalls) {
136
- lines.push("You may make multiple tool calls in a single response by using multiple <tool_call> blocks.");
137
- lines.push("");
138
- }
139
-
140
- lines.push("## Tool Definitions");
141
- lines.push("");
142
- for (const tool of tools) {
143
- if (tool.type !== "function") continue;
144
- const func = tool.function || {};
145
- lines.push(`### ${func.name}`);
146
- if (func.description) lines.push(func.description);
147
- if (func.parameters) lines.push(`Parameters: ${JSON.stringify(func.parameters)}`);
148
- lines.push("");
149
- }
150
-
151
- if (toolChoice === "required") {
152
- lines.push("IMPORTANT: You MUST call at least one tool in your response. Do not respond with only text.");
153
- } else if (typeof toolChoice === 'object' && toolChoice.function?.name) {
154
- lines.push(`IMPORTANT: You MUST call the tool "${toolChoice.function.name}" in your response.`);
155
- } else {
156
- lines.push("Decide whether to call a tool based on the user's request. If you don't need a tool, respond normally with text only.");
157
- }
158
-
159
- lines.push("");
160
- lines.push("When you call a tool, you may include text before or after the <tool_call> blocks, but the tool call blocks must be valid JSON.");
161
-
162
- return lines.join("\n");
163
- }
164
-
165
- /**
166
- * 格式化工具历史 (format_tool_history)
167
- */
168
- formatToolHistory(messages) {
169
- const result = [];
170
- for (const msg of messages) {
171
- const role = msg.role;
172
- const content = msg.content;
173
- const toolCalls = msg.tool_calls;
174
-
175
- if (role === "assistant" && toolCalls && toolCalls.length > 0) {
176
- const parts = [];
177
- if (content) parts.push(typeof content === 'string' ? content : JSON.stringify(content));
178
- for (const tc of toolCalls) {
179
- const func = tc.function || {};
180
- parts.push(`<tool_call>{"name":"${func.name}","arguments":${func.arguments || "{}"}}</tool_call>`);
181
- }
182
- result.push({ role: "assistant", content: parts.join("\n") });
183
- } else if (role === "tool") {
184
- const toolName = msg.name || "unknown";
185
- const callId = msg.tool_call_id || "";
186
- const contentStr = typeof content === 'string' ? content : JSON.stringify(content);
187
- result.push({
188
- role: "user",
189
- content: `tool (${toolName}, ${callId}): ${contentStr}`
190
- });
191
- } else {
192
- result.push(msg);
193
- }
194
- }
195
- return result;
196
- }
197
-
198
- /**
199
- * 解析工具调用 (parse_tool_calls)
200
- */
201
- parseToolCalls(content) {
202
- if (!content) return { text: content, toolCalls: null };
203
-
204
- const toolCallRegex = /<tool_call>\s*(.*?)\s*<\/tool_call>/gs;
205
- const matches = [...content.matchAll(toolCallRegex)];
206
-
207
- if (matches.length === 0) return { text: content, toolCalls: null };
208
-
209
- const toolCalls = [];
210
- for (const match of matches) {
211
- try {
212
- const parsed = JSON.parse(match[1].trim());
213
- if (parsed.name) {
214
- let args = parsed.arguments || {};
215
- const argumentsStr = typeof args === 'string' ? args : JSON.stringify(args);
216
-
217
- toolCalls.push({
218
- id: `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`,
219
- type: "function",
220
- function: {
221
- name: parsed.name,
222
- arguments: argumentsStr
223
- }
224
- });
225
- }
226
- } catch (e) {
227
- // 忽略解析失败的块
228
- }
229
- }
230
-
231
- if (toolCalls.length === 0) return { text: content, toolCalls: null };
232
-
233
- // 提取文本内容
234
- let text = content;
235
- for (const match of matches) {
236
- text = text.replace(match[0], "");
237
- }
238
- text = text.trim() || null;
239
-
240
- return { text, toolCalls };
241
- }
242
-
243
- /**
244
- * 转换请求
245
- */
246
- convertRequest(data, targetProtocol) {
247
- switch (targetProtocol) {
248
- default:
249
- return data;
250
- }
251
- }
252
-
253
- /**
254
- * 转换响应
255
- */
256
- convertResponse(data, targetProtocol, model) {
257
- switch (targetProtocol) {
258
- case MODEL_PROTOCOL_PREFIX.OPENAI:
259
- return this.toOpenAIResponse(data, model);
260
- case MODEL_PROTOCOL_PREFIX.GEMINI:
261
- return this.toGeminiResponse(data, model);
262
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
263
- return this.toOpenAIResponsesResponse(data, model);
264
- case MODEL_PROTOCOL_PREFIX.CODEX:
265
- return this.toCodexResponse(data, model);
266
- default:
267
- return data;
268
- }
269
- }
270
-
271
- /**
272
- * 转换流式响应块
273
- */
274
- convertStreamChunk(chunk, targetProtocol, model) {
275
- switch (targetProtocol) {
276
- case MODEL_PROTOCOL_PREFIX.OPENAI:
277
- return this.toOpenAIStreamChunk(chunk, model);
278
- case MODEL_PROTOCOL_PREFIX.GEMINI:
279
- return this.toGeminiStreamChunk(chunk, model);
280
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
281
- return this.toOpenAIResponsesStreamChunk(chunk, model);
282
- case MODEL_PROTOCOL_PREFIX.CODEX:
283
- return this.toCodexStreamChunk(chunk, model);
284
- default:
285
- return chunk;
286
- }
287
- }
288
-
289
- /**
290
- * 转换模型列表
291
- */
292
- convertModelList(data, targetProtocol) {
293
- switch (targetProtocol) {
294
- case MODEL_PROTOCOL_PREFIX.OPENAI:
295
- return this.toOpenAIModelList(data);
296
- case MODEL_PROTOCOL_PREFIX.GEMINI:
297
- return this.toGeminiModelList(data);
298
- default:
299
- return data;
300
- }
301
- }
302
-
303
- /**
304
- * 构建工具覆盖配置 (build_tool_overrides)
305
- */
306
- buildToolOverrides(tools) {
307
- if (!tools || !Array.isArray(tools)) {
308
- return {};
309
- }
310
-
311
- const toolOverrides = {};
312
- for (const tool of tools) {
313
- if (tool.type !== "function") continue;
314
- const func = tool.function || {};
315
- const name = func.name;
316
- if (!name) continue;
317
-
318
- toolOverrides[name] = {
319
- "enabled": true,
320
- "description": func.description || "",
321
- "parameters": func.parameters || {}
322
- };
323
- }
324
-
325
- return toolOverrides;
326
- }
327
-
328
- /**
329
- * 递归收集响应中的图片 URL
330
- */
331
- _collectImages(obj) {
332
- const urls = [];
333
- const seen = new Set();
334
-
335
- const add = (url) => {
336
- if (!url || seen.has(url)) return;
337
- seen.add(url);
338
- urls.push(url);
339
- };
340
-
341
- const walk = (value) => {
342
- if (value && typeof value === 'object') {
343
- if (Array.isArray(value)) {
344
- value.forEach(walk);
345
- } else {
346
- for (const [key, item] of Object.entries(value)) {
347
- if (key === "generatedImageUrls" || key === "imageUrls" || key === "imageURLs") {
348
- if (Array.isArray(item)) {
349
- item.forEach(url => typeof url === 'string' && add(url));
350
- } else if (typeof item === 'string') {
351
- add(item);
352
- }
353
- continue;
354
- }
355
- walk(item);
356
- }
357
- }
358
- }
359
- };
360
-
361
- walk(obj);
362
- return urls;
363
- }
364
-
365
- /**
366
- * 渲染图片为 Markdown
367
- */
368
- _renderImage(url, imageId = "image", state = null) {
369
- let finalUrl = url;
370
- if (!url.startsWith('http')) {
371
- finalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
372
- }
373
- finalUrl = this._appendSsoToken(finalUrl, state);
374
- return `![${imageId}](${finalUrl})`;
375
- }
376
-
377
- /**
378
- * 渲染视频为 Markdown/HTML (render_video)
379
- */
380
- _renderVideo(videoUrl, thumbnailImageUrl = "", state = null) {
381
- let finalVideoUrl = videoUrl;
382
- if (!videoUrl.startsWith('http')) {
383
- finalVideoUrl = `https://assets.grok.com${videoUrl.startsWith('/') ? '' : '/'}${videoUrl}`;
384
- }
385
-
386
- let finalThumbUrl = thumbnailImageUrl;
387
- if (thumbnailImageUrl && !thumbnailImageUrl.startsWith('http')) {
388
- finalThumbUrl = `https://assets.grok.com${thumbnailImageUrl.startsWith('/') ? '' : '/'}${thumbnailImageUrl}`;
389
- }
390
-
391
- const defaultThumb = 'https://assets.grok.com/favicon.ico';
392
- return `\n[![video](${finalThumbUrl || defaultThumb})](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`;
393
- }
394
-
395
- /**
396
- * 提取工具卡片文本 (extract_tool_text)
397
- */
398
- _extractToolText(raw, rolloutId = "") {
399
- if (!raw) return "";
400
-
401
- const nameMatch = raw.match(/<xai:tool_name>(.*?)<\/xai:tool_name>/s);
402
- const argsMatch = raw.match(/<xai:tool_args>(.*?)<\/xai:tool_args>/s);
403
-
404
- let name = nameMatch ? nameMatch[1].replace(/<!\[CDATA\[(.*?)\]\]>/gs, "$1").trim() : "";
405
- let args = argsMatch ? argsMatch[1].replace(/<!\[CDATA\[(.*?)\]\]>/gs, "$1").trim() : "";
406
-
407
- let payload = null;
408
- if (args) {
409
- try {
410
- payload = JSON.parse(args);
411
- } catch (e) {
412
- payload = null;
413
- }
414
- }
415
-
416
- let label = name;
417
- let text = args;
418
- const prefix = rolloutId ? `[${rolloutId}]` : "";
419
-
420
- if (name === "web_search") {
421
- label = `${prefix}[WebSearch]`;
422
- if (payload && typeof payload === 'object') {
423
- text = payload.query || payload.q || "";
424
- }
425
- } else if (name === "search_images") {
426
- label = `${prefix}[SearchImage]`;
427
- if (payload && typeof payload === 'object') {
428
- text = payload.image_description || payload.description || payload.query || "";
429
- }
430
- } else if (name === "chatroom_send") {
431
- label = `${prefix}[AgentThink]`;
432
- if (payload && typeof payload === 'object') {
433
- text = payload.message || "";
434
- }
435
- }
436
-
437
- if (label && text) return `${label} ${text}`.trim();
438
- if (label) return label;
439
- if (text) return text;
440
- return raw.replace(/<[^>]+>/g, "").trim();
441
- }
442
-
443
- /**
444
- * 过滤特殊标签
445
- */
446
- _filterToken(token, requestId = "") {
447
- if (!token) return token;
448
-
449
- let filtered = token;
450
-
451
- // 移除 xai:tool_usage_card 及其内容,不显示工具调用的过程输出
452
- filtered = filtered.replace(/<xai:tool_usage_card[^>]*>.*?<\/xai:tool_usage_card>/gs, "");
453
- filtered = filtered.replace(/<xai:tool_usage_card[^>]*\/>/gs, "");
454
-
455
- // 移除其他内部标签
456
- const tagsToFilter = ["rolloutId", "responseId", "isThinking"];
457
- for (const tag of tagsToFilter) {
458
- const pattern = new RegExp(`<${tag}[^>]*>.*?<\\/${tag}>|<${tag}[^>]*\\/>`, 'gs');
459
- filtered = filtered.replace(pattern, "");
460
- }
461
-
462
- return filtered;
463
- }
464
-
465
- /**
466
- * Grok响应 -> OpenAI响应
467
- */
468
- toOpenAIResponse(grokResponse, model) {
469
- if (!grokResponse) return null;
470
-
471
- const responseId = grokResponse.responseId || `chatcmpl-${uuidv4()}`;
472
- let content = grokResponse.message || "";
473
- const modelHash = grokResponse.llmInfo?.modelHash || "";
474
-
475
- const state = this._getState(this._formatResponseId(responseId));
476
- if (grokResponse._requestBaseUrl) {
477
- state.requestBaseUrl = grokResponse._requestBaseUrl;
478
- }
479
- if (grokResponse._uuid) {
480
- state.uuid = grokResponse._uuid;
481
- }
482
-
483
- // 过滤内容并处理其中的 Grok 资源链接
484
- content = this._filterToken(content, responseId);
485
- content = this._processGrokAssetsInText(content, state);
486
-
487
- // 收集图片并追加
488
- const imageUrls = this._collectImages(grokResponse);
489
- if (imageUrls.length > 0) {
490
- content += "\n";
491
- for (const url of imageUrls) {
492
- content += this._renderImage(url, "image", state) + "\n";
493
- }
494
- }
495
-
496
- // 处理视频 (非流式模式)
497
- if (grokResponse.finalVideoUrl) {
498
- content += this._renderVideo(grokResponse.finalVideoUrl, grokResponse.finalThumbnailUrl, state);
499
- }
500
-
501
- // 解析工具调用
502
- const { text, toolCalls } = this.parseToolCalls(content);
503
-
504
- const result = {
505
- id: responseId,
506
- object: "chat.completion",
507
- created: Math.floor(Date.now() / 1000),
508
- model: model,
509
- system_fingerprint: modelHash,
510
- choices: [{
511
- index: 0,
512
- message: {
513
- role: "assistant",
514
- content: text,
515
- },
516
- finish_reason: toolCalls ? "tool_calls" : "stop",
517
- }],
518
- usage: {
519
- prompt_tokens: 0,
520
- completion_tokens: 0,
521
- total_tokens: 0,
522
- },
523
- };
524
-
525
- if (toolCalls) {
526
- result.choices[0].message.tool_calls = toolCalls;
527
- }
528
-
529
- return result;
530
- }
531
-
532
- _formatResponseId(id) {
533
- if (!id) return `chatcmpl-${uuidv4()}`;
534
- if (id.startsWith('chatcmpl-')) return id;
535
- return `chatcmpl-${id}`;
536
- }
537
-
538
- /**
539
- * Grok流式响应块 -> OpenAI流式响应块
540
- */
541
- toOpenAIStreamChunk(grokChunk, model) {
542
- if (!grokChunk || !grokChunk.result || !grokChunk.result.response) {
543
- return null;
544
- }
545
-
546
- const resp = grokChunk.result.response;
547
- const rawResponseId = resp.responseId || "";
548
- const responseId = this._formatResponseId(rawResponseId);
549
- const state = this._getState(responseId);
550
-
551
- // 从响应块中同步 uuid 和基础 URL
552
- if (resp._requestBaseUrl) {
553
- state.requestBaseUrl = resp._requestBaseUrl;
554
- }
555
- if (resp._uuid) {
556
- state.uuid = resp._uuid;
557
- }
558
-
559
- if (resp.llmInfo?.modelHash && !state.fingerprint) {
560
- state.fingerprint = resp.llmInfo.modelHash;
561
- }
562
- if (resp.rolloutId) {
563
- state.rollout_id = String(resp.rolloutId);
564
- }
565
-
566
- const chunks = [];
567
-
568
- // 0. 发送角色信息(仅第一次)
569
- if (!state.role_sent) {
570
- chunks.push({
571
- id: responseId,
572
- object: "chat.completion.chunk",
573
- created: Math.floor(Date.now() / 1000),
574
- model: model,
575
- system_fingerprint: state.fingerprint,
576
- choices: [{
577
- index: 0,
578
- delta: { role: "assistant", content: "" },
579
- finish_reason: null
580
- }]
581
- });
582
- state.role_sent = true;
583
- }
584
-
585
- // 处理结束标志
586
- if (resp.isDone) {
587
- let finalContent = "";
588
- // 处理剩余的缓冲区
589
- if (state.pending_text_buffer) {
590
- finalContent += this._processGrokAssetsInText(state.pending_text_buffer, state);
591
- state.pending_text_buffer = "";
592
- }
593
-
594
- // 处理 buffer 中的工具调用
595
- const { text, toolCalls } = this.parseToolCalls(state.content_buffer);
596
-
597
- if (toolCalls) {
598
- chunks.push({
599
- id: responseId,
600
- object: "chat.completion.chunk",
601
- created: Math.floor(Date.now() / 1000),
602
- model: model,
603
- system_fingerprint: state.fingerprint,
604
- choices: [{
605
- index: 0,
606
- delta: {
607
- content: (finalContent + (text || "")).trim() || null,
608
- tool_calls: toolCalls
609
- },
610
- finish_reason: "tool_calls"
611
- }]
612
- });
613
- } else {
614
- chunks.push({
615
- id: responseId,
616
- object: "chat.completion.chunk",
617
- created: Math.floor(Date.now() / 1000),
618
- model: model,
619
- system_fingerprint: state.fingerprint,
620
- choices: [{
621
- index: 0,
622
- delta: { content: finalContent || null },
623
- finish_reason: "stop"
624
- }]
625
- });
626
- }
627
-
628
- // 清理状态
629
- this.requestStates.delete(responseId);
630
- return chunks;
631
- }
632
-
633
- let deltaContent = "";
634
- let deltaReasoning = "";
635
-
636
- // 1. 处理图片生成进度
637
- if (resp.streamingImageGenerationResponse) {
638
- const img = resp.streamingImageGenerationResponse;
639
- state.image_think_active = true;
640
- /*
641
- if (!state.think_opened) {
642
- deltaReasoning += "<think>\n";
643
- state.think_opened = true;
644
- }
645
- */
646
- const idx = (img.imageIndex || 0) + 1;
647
- const progress = img.progress || 0;
648
- deltaReasoning += `正在生成第${idx}张图片中,当前进度${progress}%\n`;
649
- }
650
-
651
- // 2. 处理视频生成进度 (VideoStreamProcessor)
652
- if (resp.streamingVideoGenerationResponse) {
653
- const vid = resp.streamingVideoGenerationResponse;
654
- state.video_think_active = true;
655
- /*
656
- if (!state.think_opened) {
657
- deltaReasoning += "<think>\n";
658
- state.think_opened = true;
659
- }
660
- */
661
- const progress = vid.progress || 0;
662
- deltaReasoning += `正在生成视频中,当前进度${progress}%\n`;
663
-
664
- if (progress === 100 && vid.videoUrl) {
665
- /*
666
- if (state.think_opened) {
667
- deltaContent += "\n</think>\n";
668
- state.think_opened = false;
669
- }
670
- */
671
- state.video_think_active = false;
672
- deltaContent += this._renderVideo(vid.videoUrl, vid.thumbnailImageUrl, state);
673
- }
674
- }
675
-
676
- // 3. 处理模型响应(通常包含完整消息或图片)
677
- if (resp.modelResponse) {
678
- const mr = resp.modelResponse;
679
- /*
680
- if ((state.image_think_active || state.video_think_active) && state.think_opened) {
681
- deltaContent += "\n</think>\n";
682
- state.think_opened = false;
683
- }
684
- */
685
- state.image_think_active = false;
686
- state.video_think_active = false;
687
-
688
- const imageUrls = this._collectImages(mr);
689
- for (const url of imageUrls) {
690
- deltaContent += this._renderImage(url, "image", state) + "\n";
691
- }
692
-
693
- if (mr.metadata?.llm_info?.modelHash) {
694
- state.fingerprint = mr.metadata.llm_info.modelHash;
695
- }
696
- }
697
-
698
- // 4. 处理卡片附件
699
- if (resp.cardAttachment) {
700
- const card = resp.cardAttachment;
701
- if (card.jsonData) {
702
- try {
703
- const cardData = JSON.parse(card.jsonData);
704
- let original = cardData.image?.original;
705
- const title = cardData.image?.title || "image";
706
- if (original) {
707
- // 确保是绝对路径
708
- if (!original.startsWith('http')) {
709
- original = `https://assets.grok.com${original.startsWith('/') ? '' : '/'}${original}`;
710
- }
711
- original = this._appendSsoToken(original, state);
712
- deltaContent += `![${title}](${original})\n`;
713
- }
714
- } catch (e) {
715
- // 忽略 JSON 解析错误
716
- }
717
- }
718
- }
719
-
720
- // 5. 处理普通 Token 和 思考状态
721
- if (resp.token !== undefined && resp.token !== null) {
722
- const token = resp.token;
723
- const filtered = this._filterToken(token, responseId);
724
- const isThinking = !!resp.isThinking;
725
- const inThink = isThinking || state.image_think_active || state.video_think_active;
726
-
727
- if (inThink) {
728
- deltaReasoning += filtered;
729
- } else {
730
- // 将新 token 加入待处理缓冲区,解决 URL 被截断的问题
731
- state.pending_text_buffer += filtered;
732
-
733
- let outputFromBuffer = "";
734
-
735
- // 启发式逻辑:检查缓冲区是否包含完整的 URL
736
- if (state.pending_text_buffer.includes("https://assets.grok.com")) {
737
- const lastUrlIndex = state.pending_text_buffer.lastIndexOf("https://assets.grok.com");
738
- const textAfterUrl = state.pending_text_buffer.slice(lastUrlIndex);
739
-
740
- // 检查 URL 是否结束(空格、右括号、引号、换行、大于号等)
741
- const terminatorMatch = textAfterUrl.match(/[\s\)\"\'\>\n]/);
742
- if (terminatorMatch) {
743
- // URL 已结束,可以安全地处理并输出缓冲区
744
- outputFromBuffer = this._processGrokAssetsInText(state.pending_text_buffer, state);
745
- state.pending_text_buffer = "";
746
- } else if (state.pending_text_buffer.length > 1000) {
747
- // 缓冲区过长,强制处理输出,避免过度延迟
748
- outputFromBuffer = this._processGrokAssetsInText(state.pending_text_buffer, state);
749
- state.pending_text_buffer = "";
750
- }
751
- } else {
752
- // 不包含 Grok URL,直接输出
753
- outputFromBuffer = state.pending_text_buffer;
754
- state.pending_text_buffer = "";
755
- }
756
-
757
- if (outputFromBuffer) {
758
- // 工具调用抑制逻辑:不向客户端输出 <tool_call> 块及其内容
759
- let outputToken = outputFromBuffer;
760
-
761
- // 简单的状态切换检测
762
- if (outputToken.includes('<tool_call>')) {
763
- state.in_tool_call = true;
764
- state.has_tool_call = true;
765
- // 移除标签之后的部分(如果有)
766
- outputToken = outputToken.split('<tool_call>')[0];
767
- } else if (state.in_tool_call && outputToken.includes('</tool_call>')) {
768
- state.in_tool_call = false;
769
- // 只保留标签之后的部分
770
- outputToken = outputToken.split('</tool_call>')[1] || "";
771
- } else if (state.in_tool_call) {
772
- // 处于块内,完全抑制
773
- outputToken = "";
774
- }
775
-
776
- deltaContent += outputToken;
777
- }
778
-
779
- // 将内容加入 buffer 用于最终解析工具调用
780
- state.content_buffer += filtered;
781
- }
782
- state.last_is_thinking = isThinking;
783
- }
784
-
785
- if (deltaContent || deltaReasoning) {
786
- const delta = {};
787
- if (deltaContent) delta.content = deltaContent;
788
- if (deltaReasoning) delta.reasoning_content = deltaReasoning;
789
-
790
- chunks.push({
791
- id: responseId,
792
- object: "chat.completion.chunk",
793
- created: Math.floor(Date.now() / 1000),
794
- model: model,
795
- system_fingerprint: state.fingerprint,
796
- choices: [{
797
- index: 0,
798
- delta: delta,
799
- finish_reason: null
800
- }]
801
- });
802
- }
803
-
804
- return chunks.length > 0 ? chunks : null;
805
- }
806
-
807
- /**
808
- * Grok响应 -> Gemini响应
809
- */
810
- toGeminiResponse(grokResponse, model) {
811
- const openaiRes = this.toOpenAIResponse(grokResponse, model);
812
- if (!openaiRes) return null;
813
-
814
- const choice = openaiRes.choices[0];
815
- const message = choice.message;
816
- const parts = [];
817
-
818
- if (message.reasoning_content) {
819
- parts.push({ text: message.reasoning_content, thought: true });
820
- }
821
-
822
- if (message.content) {
823
- parts.push({ text: message.content });
824
- }
825
-
826
- if (message.tool_calls) {
827
- for (const tc of message.tool_calls) {
828
- parts.push({
829
- functionCall: {
830
- name: tc.function.name,
831
- args: typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments
832
- }
833
- });
834
- }
835
- }
836
-
837
- return {
838
- candidates: [{
839
- content: {
840
- role: 'model',
841
- parts: parts
842
- },
843
- finishReason: choice.finish_reason === 'tool_calls' ? 'STOP' : (choice.finish_reason === 'length' ? 'MAX_TOKENS' : 'STOP')
844
- }],
845
- usageMetadata: {
846
- promptTokenCount: openaiRes.usage.prompt_tokens,
847
- candidatesTokenCount: openaiRes.usage.completion_tokens,
848
- totalTokenCount: openaiRes.usage.total_tokens
849
- }
850
- };
851
- }
852
-
853
- /**
854
- * Grok流式响应块 -> Gemini流式响应块
855
- */
856
- toGeminiStreamChunk(grokChunk, model) {
857
- const openaiChunks = this.toOpenAIStreamChunk(grokChunk, model);
858
- if (!openaiChunks) return null;
859
-
860
- const geminiChunks = [];
861
- for (const oachunk of openaiChunks) {
862
- const choice = oachunk.choices[0];
863
- const delta = choice.delta;
864
- const parts = [];
865
-
866
- if (delta.reasoning_content) {
867
- parts.push({ text: delta.reasoning_content, thought: true });
868
- }
869
- if (delta.content) {
870
- parts.push({ text: delta.content });
871
- }
872
- if (delta.tool_calls) {
873
- for (const tc of delta.tool_calls) {
874
- parts.push({
875
- functionCall: {
876
- name: tc.function.name,
877
- args: typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments
878
- }
879
- });
880
- }
881
- }
882
-
883
- if (parts.length > 0 || choice.finish_reason) {
884
- const gchunk = {
885
- candidates: [{
886
- content: {
887
- role: 'model',
888
- parts: parts
889
- }
890
- }]
891
- };
892
- if (choice.finish_reason) {
893
- gchunk.candidates[0].finishReason = choice.finish_reason === 'length' ? 'MAX_TOKENS' : 'STOP';
894
- }
895
- geminiChunks.push(gchunk);
896
- }
897
- }
898
-
899
- return geminiChunks.length > 0 ? geminiChunks : null;
900
- }
901
-
902
- /**
903
- * Grok响应 -> OpenAI Responses响应
904
- */
905
- toOpenAIResponsesResponse(grokResponse, model) {
906
- const openaiRes = this.toOpenAIResponse(grokResponse, model);
907
- if (!openaiRes) return null;
908
-
909
- const choice = openaiRes.choices[0];
910
- const message = choice.message;
911
- const output = [];
912
-
913
- const content = [];
914
- if (message.content) {
915
- content.push({
916
- type: "output_text",
917
- text: message.content
918
- });
919
- }
920
-
921
- output.push({
922
- id: `msg_${uuidv4().replace(/-/g, '')}`,
923
- type: "message",
924
- role: "assistant",
925
- status: "completed",
926
- content: content
927
- });
928
-
929
- if (message.tool_calls) {
930
- for (const tc of message.tool_calls) {
931
- output.push({
932
- id: tc.id,
933
- type: "function_call",
934
- name: tc.function.name,
935
- arguments: tc.function.arguments,
936
- status: "completed"
937
- });
938
- }
939
- }
940
-
941
- return {
942
- id: `resp_${uuidv4().replace(/-/g, '')}`,
943
- object: "response",
944
- created_at: Math.floor(Date.now() / 1000),
945
- status: "completed",
946
- model: model,
947
- output: output,
948
- usage: {
949
- input_tokens: openaiRes.usage.prompt_tokens,
950
- output_tokens: openaiRes.usage.completion_tokens,
951
- total_tokens: openaiRes.usage.total_tokens
952
- }
953
- };
954
- }
955
-
956
- /**
957
- * Grok流式响应块 -> OpenAI Responses流式响应块
958
- */
959
- toOpenAIResponsesStreamChunk(grokChunk, model) {
960
- const openaiChunks = this.toOpenAIStreamChunk(grokChunk, model);
961
- if (!openaiChunks) return null;
962
-
963
- const events = [];
964
- for (const oachunk of openaiChunks) {
965
- const choice = oachunk.choices[0];
966
- const delta = choice.delta;
967
-
968
- if (delta.role === 'assistant') {
969
- events.push({ type: "response.created", response: { id: oachunk.id, model: model } });
970
- }
971
-
972
- if (delta.reasoning_content) {
973
- events.push({
974
- type: "response.reasoning_summary_text.delta",
975
- delta: delta.reasoning_content,
976
- response_id: oachunk.id
977
- });
978
- }
979
-
980
- if (delta.content) {
981
- events.push({
982
- type: "response.output_text.delta",
983
- delta: delta.content,
984
- response_id: oachunk.id
985
- });
986
- }
987
-
988
- if (delta.tool_calls) {
989
- for (const tc of delta.tool_calls) {
990
- if (tc.function?.name) {
991
- events.push({
992
- type: "response.output_item.added",
993
- item: { id: tc.id, type: "function_call", name: tc.function.name, arguments: "" },
994
- response_id: oachunk.id
995
- });
996
- }
997
- if (tc.function?.arguments) {
998
- events.push({
999
- type: "response.custom_tool_call_input.delta",
1000
- delta: tc.function.arguments,
1001
- item_id: tc.id,
1002
- response_id: oachunk.id
1003
- });
1004
- }
1005
- }
1006
- }
1007
-
1008
- if (choice.finish_reason) {
1009
- events.push({ type: "response.completed", response: { id: oachunk.id, status: "completed" } });
1010
- }
1011
- }
1012
-
1013
- return events;
1014
- }
1015
-
1016
- /**
1017
- * Grok响应 -> Codex响应
1018
- */
1019
- toCodexResponse(grokResponse, model) {
1020
- const openaiRes = this.toOpenAIResponse(grokResponse, model);
1021
- if (!openaiRes) return null;
1022
-
1023
- const choice = openaiRes.choices[0];
1024
- const message = choice.message;
1025
- const output = [];
1026
-
1027
- if (message.content) {
1028
- output.push({
1029
- type: "message",
1030
- role: "assistant",
1031
- content: [{ type: "output_text", text: message.content }]
1032
- });
1033
- }
1034
-
1035
- if (message.reasoning_content) {
1036
- output.push({
1037
- type: "reasoning",
1038
- summary: [{ type: "summary_text", text: message.reasoning_content }]
1039
- });
1040
- }
1041
-
1042
- if (message.tool_calls) {
1043
- for (const tc of message.tool_calls) {
1044
- output.push({
1045
- type: "function_call",
1046
- call_id: tc.id,
1047
- name: tc.function.name,
1048
- arguments: tc.function.arguments
1049
- });
1050
- }
1051
- }
1052
-
1053
- return {
1054
- response: {
1055
- id: openaiRes.id,
1056
- output: output,
1057
- usage: {
1058
- input_tokens: openaiRes.usage.prompt_tokens,
1059
- output_tokens: openaiRes.usage.completion_tokens,
1060
- total_tokens: openaiRes.usage.total_tokens
1061
- }
1062
- }
1063
- };
1064
- }
1065
-
1066
- /**
1067
- * Grok流式响应块 -> Codex流式响应块
1068
- */
1069
- toCodexStreamChunk(grokChunk, model) {
1070
- const openaiChunks = this.toOpenAIStreamChunk(grokChunk, model);
1071
- if (!openaiChunks) return null;
1072
-
1073
- const codexChunks = [];
1074
- for (const oachunk of openaiChunks) {
1075
- const choice = oachunk.choices[0];
1076
- const delta = choice.delta;
1077
-
1078
- if (delta.role === 'assistant') {
1079
- codexChunks.push({ type: "response.created", response: { id: oachunk.id } });
1080
- }
1081
-
1082
- if (delta.reasoning_content) {
1083
- codexChunks.push({
1084
- type: "response.reasoning_summary_text.delta",
1085
- delta: delta.reasoning_content,
1086
- response: { id: oachunk.id }
1087
- });
1088
- }
1089
-
1090
- if (delta.content) {
1091
- codexChunks.push({
1092
- type: "response.output_text.delta",
1093
- delta: delta.content,
1094
- response: { id: oachunk.id }
1095
- });
1096
- }
1097
-
1098
- if (delta.tool_calls) {
1099
- for (const tc of delta.tool_calls) {
1100
- if (tc.function?.arguments) {
1101
- codexChunks.push({
1102
- type: "response.custom_tool_call_input.delta",
1103
- delta: tc.function.arguments,
1104
- item_id: tc.id,
1105
- response: { id: oachunk.id }
1106
- });
1107
- }
1108
- }
1109
- }
1110
-
1111
- if (choice.finish_reason) {
1112
- codexChunks.push({ type: "response.completed", response: { id: oachunk.id, usage: oachunk.usage } });
1113
- }
1114
- }
1115
-
1116
- return codexChunks.length > 0 ? codexChunks : null;
1117
- }
1118
-
1119
- /**
1120
- * Grok模型列表 -> OpenAI模型列表
1121
- */
1122
- toOpenAIModelList(grokModels) {
1123
- const models = Array.isArray(grokModels) ? grokModels : (grokModels?.models || grokModels?.data || []);
1124
- return {
1125
- object: "list",
1126
- data: models.map(m => ({
1127
- id: m.id || m.name || (typeof m === 'string' ? m : ''),
1128
- object: "model",
1129
- created: Math.floor(Date.now() / 1000),
1130
- owned_by: "xai",
1131
- display_name: m.display_name || m.name || m.id || (typeof m === 'string' ? m : ''),
1132
- })),
1133
- };
1134
- }
1135
-
1136
- /**
1137
- * Grok模���列表 -> Gemini模型列表
1138
- */
1139
- toGeminiModelList(grokModels) {
1140
- const models = Array.isArray(grokModels) ? grokModels : (grokModels?.models || grokModels?.data || []);
1141
- return {
1142
- models: models.map(m => ({
1143
- name: `models/${m.id || m.name || (typeof m === 'string' ? m : '')}`,
1144
- version: "1.0",
1145
- displayName: m.display_name || m.name || m.id || (typeof m === 'string' ? m : ''),
1146
- description: m.description || `Grok model: ${m.name || m.id || (typeof m === 'string' ? m : '')}`,
1147
- inputTokenLimit: 131072,
1148
- outputTokenLimit: 8192,
1149
- supportedGenerationMethods: ["generateContent", "streamGenerateContent"]
1150
- }))
1151
- };
1152
- }
1153
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/strategies/OpenAIConverter.js DELETED
@@ -1,1769 +0,0 @@
1
- /**
2
- * OpenAI转换器
3
- * 处理OpenAI协议与其他协议之间的转换
4
- */
5
-
6
- import { v4 as uuidv4 } from 'uuid';
7
- import logger from '../../utils/logger.js';
8
- import { BaseConverter } from '../BaseConverter.js';
9
- import { CodexConverter } from './CodexConverter.js';
10
- import {
11
- extractAndProcessSystemMessages as extractSystemMessages,
12
- extractTextFromMessageContent as extractText,
13
- safeParseJSON,
14
- checkAndAssignOrDefault,
15
- extractThinkingFromOpenAIText,
16
- mapFinishReason,
17
- cleanJsonSchemaProperties as cleanJsonSchema,
18
- CLAUDE_DEFAULT_MAX_TOKENS,
19
- CLAUDE_DEFAULT_TEMPERATURE,
20
- CLAUDE_DEFAULT_TOP_P,
21
- GEMINI_DEFAULT_MAX_TOKENS,
22
- GEMINI_DEFAULT_TEMPERATURE,
23
- GEMINI_DEFAULT_TOP_P,
24
- OPENAI_DEFAULT_INPUT_TOKEN_LIMIT,
25
- OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT
26
- } from '../utils.js';
27
- import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
28
- import {
29
- generateResponseCreated,
30
- generateResponseInProgress,
31
- generateOutputItemAdded,
32
- generateContentPartAdded,
33
- generateOutputTextDone,
34
- generateContentPartDone,
35
- generateOutputItemDone,
36
- generateResponseCompleted
37
- } from '../../providers/openai/openai-responses-core.mjs';
38
-
39
- /**
40
- * OpenAI转换器类
41
- * 实现OpenAI协议到其他协议的转换
42
- */
43
- export class OpenAIConverter extends BaseConverter {
44
- constructor() {
45
- super('openai');
46
- // 创建 CodexConverter 实例用于委托
47
- this.codexConverter = new CodexConverter();
48
- }
49
-
50
- /**
51
- * 转换请求
52
- */
53
- convertRequest(data, targetProtocol) {
54
- switch (targetProtocol) {
55
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
56
- return this.toClaudeRequest(data);
57
- case MODEL_PROTOCOL_PREFIX.GEMINI:
58
- return this.toGeminiRequest(data);
59
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
60
- return this.toOpenAIResponsesRequest(data);
61
- case MODEL_PROTOCOL_PREFIX.CODEX:
62
- return this.toCodexRequest(data);
63
- case MODEL_PROTOCOL_PREFIX.GROK:
64
- return this.toGrokRequest(data);
65
- default:
66
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
67
- }
68
- }
69
-
70
- /**
71
- * 转换响应
72
- */
73
- convertResponse(data, targetProtocol, model) {
74
- // OpenAI作为源格式时,通常不需要转换响应
75
- // 因为其他协议会转换到OpenAI格式
76
- switch (targetProtocol) {
77
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
78
- return this.toClaudeResponse(data, model);
79
- case MODEL_PROTOCOL_PREFIX.GEMINI:
80
- return this.toGeminiResponse(data, model);
81
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
82
- return this.toOpenAIResponsesResponse(data, model);
83
- case MODEL_PROTOCOL_PREFIX.GROK:
84
- return this.toGrokResponse(data, model);
85
- default:
86
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
87
- }
88
- }
89
-
90
- /**
91
- * 转换流式响应块
92
- */
93
- convertStreamChunk(chunk, targetProtocol, model) {
94
- switch (targetProtocol) {
95
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
96
- return this.toClaudeStreamChunk(chunk, model);
97
- case MODEL_PROTOCOL_PREFIX.GEMINI:
98
- return this.toGeminiStreamChunk(chunk, model);
99
- case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
100
- return this.toOpenAIResponsesStreamChunk(chunk, model);
101
- case MODEL_PROTOCOL_PREFIX.GROK:
102
- return this.toGrokStreamChunk(chunk, model);
103
- default:
104
- throw new Error(`Unsupported target protocol: ${targetProtocol}`);
105
- }
106
- }
107
-
108
- /**
109
- * 转换模型列表
110
- */
111
- convertModelList(data, targetProtocol) {
112
- switch (targetProtocol) {
113
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
114
- return this.toClaudeModelList(data);
115
- case MODEL_PROTOCOL_PREFIX.GEMINI:
116
- return this.toGeminiModelList(data);
117
- default:
118
- return this.ensureDisplayName(data);
119
- }
120
- }
121
-
122
- /**
123
- * Ensure display_name field exists in OpenAI model list
124
- */
125
- ensureDisplayName(openaiModels) {
126
- if (!openaiModels || !openaiModels.data) {
127
- return openaiModels;
128
- }
129
-
130
- return {
131
- ...openaiModels,
132
- data: openaiModels.data.map(model => ({
133
- ...model,
134
- display_name: model.display_name || model.id,
135
- })),
136
- };
137
- }
138
-
139
- // =========================================================================
140
- // OpenAI -> Claude 转换
141
- // =========================================================================
142
-
143
- /**
144
- * OpenAI请求 -> Claude请求
145
- */
146
- toClaudeRequest(openaiRequest) {
147
- const messages = openaiRequest.messages || [];
148
- const { systemInstruction, nonSystemMessages } = extractSystemMessages(messages);
149
-
150
- const claudeMessages = [];
151
-
152
- for (const message of nonSystemMessages) {
153
- const role = message.role === 'assistant' ? 'assistant' : 'user';
154
- let content = [];
155
-
156
- if (message.role === 'tool') {
157
- // 工具结果消息
158
- let toolContent = message.content;
159
- if (typeof toolContent === 'object' && toolContent !== null) {
160
- toolContent = JSON.stringify(toolContent);
161
- }
162
- content.push({
163
- type: 'tool_result',
164
- tool_use_id: message.tool_call_id || message.tool_use_id,
165
- content: toolContent
166
- });
167
- claudeMessages.push({ role: 'user', content: content });
168
- } else if (message.role === 'assistant' && (message.tool_calls?.length || message.function_calls?.length)) {
169
- // 助手工具调用消息 - 支持tool_calls和function_calls
170
- const calls = message.tool_calls || message.function_calls || [];
171
- const toolUseBlocks = calls.map(tc => ({
172
- type: 'tool_use',
173
- id: tc.id,
174
- name: tc.function.name,
175
- input: safeParseJSON(tc.function.arguments)
176
- }));
177
- claudeMessages.push({ role: 'assistant', content: toolUseBlocks });
178
- } else {
179
- // 普通消息
180
- if (typeof message.content === 'string') {
181
- if (message.content) {
182
- content.push({ type: 'text', text: message.content.trim() });
183
- }
184
- } else if (Array.isArray(message.content)) {
185
- message.content.forEach(item => {
186
- if (!item) return;
187
- switch (item.type) {
188
- case 'text':
189
- if (item.text) {
190
- content.push({ type: 'text', text: item.text.trim() });
191
- }
192
- break;
193
- case 'image_url':
194
- if (item.image_url) {
195
- const imageUrl = typeof item.image_url === 'string'
196
- ? item.image_url
197
- : item.image_url.url;
198
- if (imageUrl.startsWith('data:')) {
199
- const [header, data] = imageUrl.split(',');
200
- const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg';
201
- content.push({
202
- type: 'image',
203
- source: {
204
- type: 'base64',
205
- media_type: mediaType,
206
- data: data
207
- }
208
- });
209
- } else {
210
- content.push({ type: 'text', text: `[Image: ${imageUrl}]` });
211
- }
212
- }
213
- break;
214
- case 'audio':
215
- if (item.audio_url) {
216
- const audioUrl = typeof item.audio_url === 'string'
217
- ? item.audio_url
218
- : item.audio_url.url;
219
- content.push({ type: 'text', text: `[Audio: ${audioUrl}]` });
220
- }
221
- break;
222
- case 'input_audio':
223
- // OpenAI 官方 input_audio 格式
224
- if (item.input_audio) {
225
- // Claude 不直接支持音频输入,转换为文本描述
226
- content.push({ type: 'text', text: `[Audio Input: ${item.input_audio.format || 'audio'}]` });
227
- }
228
- break;
229
- case 'tool_use':
230
- content.push({
231
- type: 'tool_use',
232
- id: item.id,
233
- name: item.name,
234
- input: typeof item.input === 'string' ? safeParseJSON(item.input) : (item.input || {})
235
- });
236
- break;
237
- case 'tool_result': {
238
- let resultContent = item.content;
239
- if (typeof resultContent === 'object' && resultContent !== null) {
240
- resultContent = JSON.stringify(resultContent);
241
- }
242
- content.push({
243
- type: 'tool_result',
244
- tool_use_id: item.tool_use_id || item.id,
245
- content: resultContent
246
- });
247
- break;
248
- }
249
- }
250
- });
251
- }
252
- if (content.length > 0) {
253
- claudeMessages.push({ role: role, content: content });
254
- }
255
- }
256
- }
257
- // 合并相邻相同 role 的消息
258
- const mergedClaudeMessages = [];
259
- for (let i = 0; i < claudeMessages.length; i++) {
260
- const currentMessage = claudeMessages[i];
261
-
262
- if (mergedClaudeMessages.length === 0) {
263
- mergedClaudeMessages.push(currentMessage);
264
- } else {
265
- const lastMessage = mergedClaudeMessages[mergedClaudeMessages.length - 1];
266
-
267
- // 如果当前消息的 role 与上一条消息的 role 相同,则合并 content 数组
268
- if (lastMessage.role === currentMessage.role) {
269
- lastMessage.content = lastMessage.content.concat(currentMessage.content);
270
- } else {
271
- mergedClaudeMessages.push(currentMessage);
272
- }
273
- }
274
- }
275
-
276
- // 清理最后一条 assistant 消息的尾部空白
277
- if (mergedClaudeMessages.length > 0) {
278
- const lastMessage = mergedClaudeMessages[mergedClaudeMessages.length - 1];
279
- if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
280
- // 从后往前找到最后一个 text 类型的内容块
281
- for (let i = lastMessage.content.length - 1; i >= 0; i--) {
282
- const contentBlock = lastMessage.content[i];
283
- if (contentBlock.type === 'text' && contentBlock.text) {
284
- // 移除尾部空白字符
285
- contentBlock.text = contentBlock.text.trimEnd();
286
- break;
287
- }
288
- }
289
- }
290
- }
291
-
292
-
293
- const claudeRequest = {
294
- model: openaiRequest.model,
295
- messages: mergedClaudeMessages,
296
- max_tokens: checkAndAssignOrDefault(openaiRequest.max_tokens, CLAUDE_DEFAULT_MAX_TOKENS),
297
- temperature: checkAndAssignOrDefault(openaiRequest.temperature, CLAUDE_DEFAULT_TEMPERATURE),
298
- top_p: checkAndAssignOrDefault(openaiRequest.top_p, CLAUDE_DEFAULT_TOP_P),
299
- };
300
-
301
- if (systemInstruction) {
302
- claudeRequest.system = extractText(systemInstruction.parts[0].text);
303
- }
304
-
305
- if (openaiRequest.tools?.length) {
306
- claudeRequest.tools = openaiRequest.tools
307
- .filter(t => t && ((t.function && t.function.name) || t.name))
308
- .map(t => {
309
- if (t.function) {
310
- return {
311
- name: t.function.name,
312
- description: t.function.description || '',
313
- input_schema: t.function.parameters || { type: 'object', properties: {} }
314
- };
315
- }
316
- return {
317
- name: t.name,
318
- description: t.description || '',
319
- input_schema: t.input_schema || { type: 'object', properties: {} }
320
- };
321
- });
322
- if (claudeRequest.tools.length > 0) {
323
- claudeRequest.tool_choice = this.buildClaudeToolChoice(openaiRequest.tool_choice);
324
- }
325
- }
326
-
327
- // Optional passthrough: request-side "thinking" controls for Claude/Kiro.
328
- // OpenAI-compatible clients can provide these via `extra_body.anthropic.thinking`.
329
- // We intentionally keep normalization minimal here; provider implementations
330
- // (e.g. Kiro) clamp budgets and apply defaults.
331
- const extThinking = openaiRequest?.extra_body?.anthropic?.thinking;
332
- if (extThinking && typeof extThinking === 'object' && !Array.isArray(extThinking)) {
333
- const type = String(extThinking.type || '').toLowerCase().trim();
334
- if (type === 'enabled') {
335
- const thinkingCfg = { type: 'enabled' };
336
- if (extThinking.budget_tokens !== undefined) {
337
- const n = parseInt(extThinking.budget_tokens, 10);
338
- if (Number.isFinite(n)) {
339
- thinkingCfg.budget_tokens = n;
340
- }
341
- }
342
- claudeRequest.thinking = thinkingCfg;
343
- } else if (type === 'adaptive') {
344
- const effortRaw = typeof extThinking.effort === 'string' ? extThinking.effort : '';
345
- const effort = effortRaw.toLowerCase().trim();
346
- const normalizedEffort = (effort === 'low' || effort === 'medium' || effort === 'high') ? effort : 'high';
347
- claudeRequest.thinking = { type: 'adaptive', effort: normalizedEffort };
348
- } else if (type === 'disabled') {
349
- // Explicitly disabled: omit thinking config.
350
- }
351
- }
352
-
353
- return claudeRequest;
354
- }
355
-
356
- /**
357
- * OpenAI响应 -> Claude响应
358
- */
359
- toClaudeResponse(openaiResponse, model) {
360
- if (!openaiResponse || !openaiResponse.choices || openaiResponse.choices.length === 0) {
361
- return {
362
- id: `msg_${uuidv4()}`,
363
- type: "message",
364
- role: "assistant",
365
- content: [],
366
- model: model,
367
- stop_reason: "end_turn",
368
- stop_sequence: null,
369
- usage: {
370
- input_tokens: openaiResponse?.usage?.prompt_tokens || 0,
371
- output_tokens: openaiResponse?.usage?.completion_tokens || 0
372
- }
373
- };
374
- }
375
-
376
- const choice = openaiResponse.choices[0];
377
- const contentList = [];
378
-
379
- // 处理工具调用 - 支持tool_calls和function_calls
380
- const toolCalls = choice.message?.tool_calls || choice.message?.function_calls || [];
381
- for (const toolCall of toolCalls.filter(tc => tc && typeof tc === 'object')) {
382
- if (toolCall.function) {
383
- const func = toolCall.function;
384
- const argStr = func.arguments || "{}";
385
- let argObj;
386
- try {
387
- argObj = typeof argStr === 'string' ? JSON.parse(argStr) : argStr;
388
- } catch (e) {
389
- argObj = {};
390
- }
391
- contentList.push({
392
- type: "tool_use",
393
- id: toolCall.id || "",
394
- name: func.name || "",
395
- input: argObj,
396
- });
397
- }
398
- }
399
-
400
- // 处理reasoning_content(推理内容)
401
- const reasoningContent = choice.message?.reasoning_content || "";
402
- if (reasoningContent) {
403
- contentList.push({
404
- type: "thinking",
405
- thinking: reasoningContent
406
- });
407
- }
408
-
409
- // 处理文本内容
410
- const contentText = choice.message?.content || "";
411
- if (contentText) {
412
- const extractedContent = extractThinkingFromOpenAIText(contentText);
413
- if (Array.isArray(extractedContent)) {
414
- contentList.push(...extractedContent);
415
- } else {
416
- contentList.push({ type: "text", text: extractedContent });
417
- }
418
- }
419
-
420
- // 映射结束原因
421
- const stopReason = mapFinishReason(
422
- choice.finish_reason || "stop",
423
- "openai",
424
- "anthropic"
425
- );
426
-
427
- return {
428
- id: `msg_${uuidv4()}`,
429
- type: "message",
430
- role: "assistant",
431
- content: contentList,
432
- model: model,
433
- stop_reason: stopReason,
434
- stop_sequence: null,
435
- usage: {
436
- input_tokens: openaiResponse.usage?.prompt_tokens || 0,
437
- cache_creation_input_tokens: 0,
438
- cache_read_input_tokens: openaiResponse.usage?.prompt_tokens_details?.cached_tokens || 0,
439
- output_tokens: openaiResponse.usage?.completion_tokens || 0
440
- }
441
- };
442
- }
443
-
444
- /**
445
- * OpenAI流式响应 -> Claude流式响应
446
- *
447
- * 这个方法实现了与 ClaudeConverter.toOpenAIStreamChunk 相反的转换逻辑
448
- * 将 OpenAI 的流式 chunk 转换为 Claude 的流式事件
449
- */
450
- toClaudeStreamChunk(openaiChunk, model) {
451
- if (!openaiChunk) return null;
452
-
453
- // 处理 OpenAI chunk 对象
454
- if (typeof openaiChunk === 'object' && !Array.isArray(openaiChunk)) {
455
- const choice = openaiChunk.choices?.[0];
456
- if (!choice) {
457
- return null;
458
- }
459
-
460
- const delta = choice.delta;
461
- const finishReason = choice.finish_reason;
462
- const events = [];
463
-
464
- // 注释部分是为了兼容claude code,但是不兼容cherry studio
465
- // 1. 处理 role (对应 message_start)
466
- // if (delta?.role === "assistant") {
467
- // events.push({
468
- // type: "message_start",
469
- // message: {
470
- // id: openaiChunk.id || `msg_${uuidv4()}`,
471
- // type: "message",
472
- // role: "assistant",
473
- // content: [],
474
- // model: model || openaiChunk.model || "unknown",
475
- // stop_reason: null,
476
- // stop_sequence: null,
477
- // usage: {
478
- // input_tokens: openaiChunk.usage?.prompt_tokens || 0,
479
- // output_tokens: 0
480
- // }
481
- // }
482
- // });
483
- // events.push({
484
- // type: "content_block_start",
485
- // index: 0,
486
- // content_block: {
487
- // type: "text",
488
- // text: ""
489
- // }
490
- // });
491
- // }
492
-
493
- // 2. 处理 tool_calls (对应 content_block_start 和 content_block_delta)
494
- // if (delta?.tool_calls) {
495
- // const toolCalls = delta.tool_calls;
496
- // for (const toolCall of toolCalls) {
497
- // // 如果有 function.name,说明是工具调用开始
498
- // if (toolCall.function?.name) {
499
- // events.push({
500
- // type: "content_block_start",
501
- // index: toolCall.index || 0,
502
- // content_block: {
503
- // type: "tool_use",
504
- // id: toolCall.id || `tool_${uuidv4()}`,
505
- // name: toolCall.function.name,
506
- // input: {}
507
- // }
508
- // });
509
- // }
510
-
511
- // // 如果有 function.arguments,说明是参数增量
512
- // if (toolCall.function?.arguments) {
513
- // events.push({
514
- // type: "content_block_delta",
515
- // index: toolCall.index || 0,
516
- // delta: {
517
- // type: "input_json_delta",
518
- // partial_json: toolCall.function.arguments
519
- // }
520
- // });
521
- // }
522
- // }
523
- // }
524
-
525
- // 3. 处理 reasoning_content (对应 thinking 类型的 content_block)
526
- if (delta?.reasoning_content) {
527
- // 注意:这里可能需要先发送 content_block_start,但由于状态管理复杂,
528
- // 我们假设调用方会处理这个逻辑
529
- events.push({
530
- type: "content_block_delta",
531
- index: 0,
532
- delta: {
533
- type: "thinking_delta",
534
- thinking: delta.reasoning_content
535
- }
536
- });
537
- }
538
-
539
- // 4. 处理普通文本 content (对应 text 类型的 content_block)
540
- if (delta?.content) {
541
- events.push({
542
- type: "content_block_delta",
543
- index: 0,
544
- delta: {
545
- type: "text_delta",
546
- text: delta.content
547
- }
548
- });
549
- }
550
-
551
- // 5. 处理 finish_reason (对应 message_delta 和 message_stop)
552
- if (finishReason) {
553
- // 映射 finish_reason
554
- const stopReason = finishReason === "stop" ? "end_turn" :
555
- finishReason === "length" ? "max_tokens" :
556
- "end_turn";
557
-
558
- events.push({
559
- type: "content_block_stop",
560
- index: 0
561
- });
562
- // 发送 message_delta
563
- events.push({
564
- type: "message_delta",
565
- delta: {
566
- stop_reason: stopReason,
567
- stop_sequence: null
568
- },
569
- usage: {
570
- input_tokens: openaiChunk.usage?.prompt_tokens || 0,
571
- cache_creation_input_tokens: 0,
572
- cache_read_input_tokens: openaiChunk.usage?.prompt_tokens_details?.cached_tokens || 0,
573
- output_tokens: openaiChunk.usage?.completion_tokens || 0
574
- }
575
- });
576
-
577
- // 发送 message_stop
578
- events.push({
579
- type: "message_stop"
580
- });
581
- }
582
-
583
- return events.length > 0 ? events : null;
584
- }
585
-
586
- // 向后兼容:处理字符串格式
587
- if (typeof openaiChunk === 'string') {
588
- return {
589
- type: "content_block_delta",
590
- index: 0,
591
- delta: {
592
- type: "text_delta",
593
- text: openaiChunk
594
- }
595
- };
596
- }
597
-
598
- return null;
599
- }
600
-
601
- /**
602
- * OpenAI模型列表 -> Claude模型列表
603
- */
604
- toClaudeModelList(openaiModels) {
605
- return {
606
- models: openaiModels.data.map(m => ({
607
- name: m.id,
608
- description: "",
609
- })),
610
- };
611
- }
612
-
613
- /**
614
- * 将 OpenAI 模型列表转换为 Gemini 模型列表
615
- */
616
- toGeminiModelList(openaiModels) {
617
- const models = openaiModels.data || [];
618
- return {
619
- models: models.map(m => ({
620
- name: `models/${m.id}`,
621
- version: m.version || "1.0.0",
622
- displayName: m.displayName || m.id,
623
- description: m.description || `A generative model for text and chat generation. ID: ${m.id}`,
624
- inputTokenLimit: m.inputTokenLimit || OPENAI_DEFAULT_INPUT_TOKEN_LIMIT,
625
- outputTokenLimit: m.outputTokenLimit || OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT,
626
- supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
627
- }))
628
- };
629
- }
630
-
631
- /**
632
- * 构建Claude工具选择
633
- */
634
- buildClaudeToolChoice(toolChoice) {
635
- if (typeof toolChoice === 'string') {
636
- const mapping = { auto: 'auto', none: 'none', required: 'any' };
637
- return { type: mapping[toolChoice] };
638
- }
639
- if (typeof toolChoice === 'object') {
640
- // Claude 原生格式:{ type, name }
641
- if (toolChoice.type && toolChoice.name) {
642
- return { type: toolChoice.type, name: toolChoice.name };
643
- }
644
- // OpenAI 格式:{ function: { name } }
645
- if (toolChoice.function) {
646
- return { type: 'tool', name: toolChoice.function.name };
647
- }
648
- }
649
- return undefined;
650
- }
651
-
652
- // =========================================================================
653
- // OpenAI -> Gemini 转换
654
- // =========================================================================
655
-
656
- // Gemini Openai thought signature constant
657
- static GEMINI_OPENAI_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
658
- /**
659
- * OpenAI请求 -> Gemini请求
660
- */
661
- toGeminiRequest(openaiRequest) {
662
- const messages = openaiRequest.messages || [];
663
- const model = openaiRequest.model || '';
664
-
665
- // 构建 tool_call_id -> function_name 映射
666
- const tcID2Name = {};
667
- for (const message of messages) {
668
- if (message.role === 'assistant' && message.tool_calls) {
669
- for (const tc of message.tool_calls) {
670
- if (tc.type === 'function' && tc.id && tc.function?.name) {
671
- tcID2Name[tc.id] = tc.function.name;
672
- }
673
- }
674
- }
675
- // Claude 格式:content 数组中的 tool_use
676
- if (message.role === 'assistant' && Array.isArray(message.content)) {
677
- for (const item of message.content) {
678
- if (item && item.type === 'tool_use' && item.id && item.name) {
679
- tcID2Name[item.id] = item.name;
680
- }
681
- }
682
- }
683
- }
684
-
685
- // 构建 tool_call_id -> response 映射
686
- const toolResponses = {};
687
- for (const message of messages) {
688
- if (message.role === 'tool' && message.tool_call_id) {
689
- toolResponses[message.tool_call_id] = message.content;
690
- }
691
- // Claude 格式:user content 数组中的 tool_result
692
- if (message.role === 'user' && Array.isArray(message.content)) {
693
- for (const item of message.content) {
694
- if (item && item.type === 'tool_result' && item.tool_use_id) {
695
- toolResponses[item.tool_use_id] = item.content;
696
- }
697
- }
698
- }
699
- }
700
-
701
- const processedMessages = [];
702
- let systemInstruction = null;
703
-
704
- for (let i = 0; i < messages.length; i++) {
705
- const message = messages[i];
706
- const role = message.role;
707
- const content = message.content;
708
-
709
- if (role === 'system' || role === 'developer') {
710
- // system -> system_instruction
711
- if (messages.length > 1) {
712
- if (typeof content === 'string') {
713
- systemInstruction = {
714
- role: 'user',
715
- parts: [{ text: content }]
716
- };
717
- } else if (Array.isArray(content)) {
718
- const parts = content
719
- .filter(item => item.type === 'text' && item.text)
720
- .map(item => ({ text: item.text }));
721
- if (parts.length > 0) {
722
- systemInstruction = {
723
- role: 'user',
724
- parts: parts
725
- };
726
- }
727
- } else if (typeof content === 'object' && content.type === 'text') {
728
- systemInstruction = {
729
- role: 'user',
730
- parts: [{ text: content.text }]
731
- };
732
- }
733
- } else {
734
- // 只有一条 system 消息时,作为 user 消息处理
735
- const node = { role: 'user', parts: [] };
736
- if (typeof content === 'string') {
737
- node.parts.push({ text: content });
738
- } else if (Array.isArray(content)) {
739
- for (const item of content) {
740
- if (item.type === 'text' && item.text) {
741
- node.parts.push({ text: item.text });
742
- }
743
- }
744
- }
745
- if (node.parts.length > 0) {
746
- processedMessages.push(node);
747
- }
748
- }
749
- } else if (role === 'user') {
750
- // user -> user content
751
- const node = { role: 'user', parts: [] };
752
- if (typeof content === 'string') {
753
- node.parts.push({ text: content });
754
- } else if (Array.isArray(content)) {
755
- for (const item of content) {
756
- if (!item) continue;
757
- switch (item.type) {
758
- case 'text':
759
- if (item.text) {
760
- node.parts.push({ text: item.text });
761
- }
762
- break;
763
- case 'image_url':
764
- if (item.image_url) {
765
- const imageUrl = typeof item.image_url === 'string'
766
- ? item.image_url
767
- : item.image_url.url;
768
- if (imageUrl && imageUrl.startsWith('data:')) {
769
- const commaIndex = imageUrl.indexOf(',');
770
- if (commaIndex > 5) {
771
- const header = imageUrl.substring(5, commaIndex);
772
- const semicolonIndex = header.indexOf(';');
773
- if (semicolonIndex > 0) {
774
- const mimeType = header.substring(0, semicolonIndex);
775
- const data = imageUrl.substring(commaIndex + 1);
776
- node.parts.push({
777
- inlineData: {
778
- mimeType: mimeType,
779
- data: data
780
- },
781
- thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
782
- });
783
- }
784
- }
785
- } else if (imageUrl) {
786
- node.parts.push({
787
- fileData: {
788
- mimeType: 'image/jpeg',
789
- fileUri: imageUrl
790
- }
791
- });
792
- }
793
- }
794
- break;
795
- case 'file':
796
- if (item.file) {
797
- const filename = item.file.filename || '';
798
- const fileData = item.file.file_data || '';
799
- const ext = filename.includes('.')
800
- ? filename.split('.').pop().toLowerCase()
801
- : '';
802
- const mimeTypes = {
803
- 'pdf': 'application/pdf',
804
- 'txt': 'text/plain',
805
- 'html': 'text/html',
806
- 'css': 'text/css',
807
- 'js': 'application/javascript',
808
- 'json': 'application/json',
809
- 'xml': 'application/xml',
810
- 'csv': 'text/csv',
811
- 'md': 'text/markdown',
812
- 'py': 'text/x-python',
813
- 'java': 'text/x-java',
814
- 'c': 'text/x-c',
815
- 'cpp': 'text/x-c++',
816
- 'h': 'text/x-c',
817
- 'hpp': 'text/x-c++',
818
- 'go': 'text/x-go',
819
- 'rs': 'text/x-rust',
820
- 'ts': 'text/typescript',
821
- 'tsx': 'text/typescript',
822
- 'jsx': 'text/javascript',
823
- 'png': 'image/png',
824
- 'jpg': 'image/jpeg',
825
- 'jpeg': 'image/jpeg',
826
- 'gif': 'image/gif',
827
- 'webp': 'image/webp',
828
- 'svg': 'image/svg+xml',
829
- 'mp3': 'audio/mpeg',
830
- 'wav': 'audio/wav',
831
- 'mp4': 'video/mp4',
832
- 'webm': 'video/webm'
833
- };
834
- const mimeType = mimeTypes[ext];
835
- if (mimeType && fileData) {
836
- node.parts.push({
837
- inlineData: {
838
- mimeType: mimeType,
839
- data: fileData
840
- }
841
- });
842
- }
843
- }
844
- break;
845
- }
846
- }
847
- }
848
- if (node.parts.length > 0) {
849
- processedMessages.push(node);
850
- }
851
- } else if (role === 'assistant') {
852
- // assistant -> model content
853
- const node = { role: 'model', parts: [] };
854
-
855
- // 处理文本内容
856
- const functionCallIds = [];
857
- if (typeof content === 'string' && content) {
858
- node.parts.push({ text: content });
859
- } else if (Array.isArray(content)) {
860
- for (const item of content) {
861
- if (!item) continue;
862
- if (item.type === 'text' && item.text) {
863
- node.parts.push({ text: item.text });
864
- } else if (item.type === 'tool_use') {
865
- // Claude 格式 tool_use -> Gemini functionCall
866
- const fid = item.id || '';
867
- const fname = item.name || '';
868
- const argsObj = typeof item.input === 'string' ? (() => { try { return JSON.parse(item.input); } catch(e) { return {}; } })() : (item.input || {});
869
- node.parts.push({
870
- functionCall: {
871
- name: fname,
872
- args: argsObj
873
- },
874
- thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
875
- });
876
- if (fid) functionCallIds.push(fid);
877
- } else if (item.type === 'image_url' && item.image_url) {
878
- const imageUrl = typeof item.image_url === 'string'
879
- ? item.image_url
880
- : item.image_url.url;
881
- if (imageUrl && imageUrl.startsWith('data:')) {
882
- const commaIndex = imageUrl.indexOf(',');
883
- if (commaIndex > 5) {
884
- const header = imageUrl.substring(5, commaIndex);
885
- const semicolonIndex = header.indexOf(';');
886
- if (semicolonIndex > 0) {
887
- const mimeType = header.substring(0, semicolonIndex);
888
- const data = imageUrl.substring(commaIndex + 1);
889
- node.parts.push({
890
- inlineData: {
891
- mimeType: mimeType,
892
- data: data
893
- },
894
- thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
895
- });
896
- }
897
- }
898
- }
899
- }
900
- }
901
- }
902
-
903
- // 处理 OpenAI 格式 tool_calls -> functionCall
904
- if (message.tool_calls && Array.isArray(message.tool_calls)) {
905
- for (const tc of message.tool_calls) {
906
- if (tc.type !== 'function') continue;
907
- const fid = tc.id || '';
908
- const fname = tc.function?.name || '';
909
- const fargs = tc.function?.arguments || '{}';
910
-
911
- let argsObj;
912
- try {
913
- argsObj = typeof fargs === 'string' ? JSON.parse(fargs) : fargs;
914
- } catch (e) {
915
- argsObj = {};
916
- }
917
-
918
- node.parts.push({
919
- functionCall: {
920
- name: fname,
921
- args: argsObj
922
- },
923
- thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
924
- });
925
-
926
- if (fid) {
927
- functionCallIds.push(fid);
928
- }
929
- }
930
- }
931
-
932
- // 添加 model 消息
933
- if (node.parts.length > 0) {
934
- processedMessages.push(node);
935
- }
936
-
937
- // 添加对应的 functionResponse(作为 user 消息)
938
- if (functionCallIds.length > 0) {
939
- const toolNode = { role: 'user', parts: [] };
940
- for (const fid of functionCallIds) {
941
- const name = tcID2Name[fid];
942
- if (name) {
943
- let resp = toolResponses[fid] || '{}';
944
- if (typeof resp !== 'string') {
945
- resp = JSON.stringify(resp);
946
- }
947
- toolNode.parts.push({
948
- functionResponse: {
949
- name: name,
950
- response: {
951
- result: resp
952
- }
953
- }
954
- });
955
- }
956
- }
957
- if (toolNode.parts.length > 0) {
958
- processedMessages.push(toolNode);
959
- }
960
- }
961
- } else if (role === 'tool') {
962
- // 处理独立的 tool role 消息(OpenAI 格式)
963
- // 转换为 Gemini 的 functionResponse 格式
964
- const toolNode = { role: 'user', parts: [] };
965
-
966
- // 从 tool_call_id 查找对应的函数名
967
- const toolCallId = message.tool_call_id;
968
- const functionName = tcID2Name[toolCallId];
969
-
970
- if (functionName) {
971
- let responseContent = message.content;
972
- if (typeof responseContent !== 'string') {
973
- responseContent = JSON.stringify(responseContent);
974
- }
975
-
976
- toolNode.parts.push({
977
- functionResponse: {
978
- name: functionName,
979
- response: {
980
- result: responseContent
981
- }
982
- }
983
- });
984
-
985
- if (toolNode.parts.length > 0) {
986
- processedMessages.push(toolNode);
987
- }
988
- }
989
- }
990
- // 其他 role 类型跳过
991
- }
992
-
993
- // 构建 Gemini 请求
994
- const geminiRequest = {
995
- contents: processedMessages.filter(item => item.parts && item.parts.length > 0)
996
- };
997
-
998
- // 添加 model
999
- if (model) {
1000
- geminiRequest.model = model;
1001
- }
1002
-
1003
- // 添加 system_instruction
1004
- if (systemInstruction) {
1005
- geminiRequest.system_instruction = systemInstruction;
1006
- }
1007
-
1008
- // 处理 reasoning_effort -> thinkingConfig
1009
- if (openaiRequest.reasoning_effort) {
1010
- const effort = String(openaiRequest.reasoning_effort).toLowerCase().trim();
1011
- if (this.modelSupportsThinking(model)) {
1012
- if (this.isGemini3Model(model)) {
1013
- // Gemini 3 模型使用 thinkingLevel
1014
- if (effort === 'none') {
1015
- // 不添加 thinkingConfig
1016
- } else if (effort === 'auto') {
1017
- geminiRequest.generationConfig = geminiRequest.generationConfig || {};
1018
- geminiRequest.generationConfig.thinkingConfig = {
1019
- includeThoughts: true
1020
- };
1021
- } else {
1022
- const level = this.validateGemini3ThinkingLevel(model, effort);
1023
- if (level) {
1024
- geminiRequest.generationConfig = geminiRequest.generationConfig || {};
1025
- geminiRequest.generationConfig.thinkingConfig = {
1026
- thinkingLevel: level
1027
- };
1028
- }
1029
- }
1030
- } else if (!this.modelUsesThinkingLevels(model)) {
1031
- // 使用 thinkingBudget 的模型
1032
- geminiRequest.generationConfig = geminiRequest.generationConfig || {};
1033
- geminiRequest.generationConfig.thinkingConfig = this.applyReasoningEffortToGemini(effort);
1034
- }
1035
- }
1036
- }
1037
-
1038
- // 处理 extra_body.google.thinking_config(Cherry Studio 扩展)
1039
- if (!openaiRequest.reasoning_effort && openaiRequest.extra_body?.google?.thinking_config) {
1040
- const tc = openaiRequest.extra_body.google.thinking_config;
1041
- if (this.modelSupportsThinking(model) && !this.modelUsesThinkingLevels(model)) {
1042
- geminiRequest.generationConfig = geminiRequest.generationConfig || {};
1043
- geminiRequest.generationConfig.thinkingConfig = geminiRequest.generationConfig.thinkingConfig || {};
1044
-
1045
- let setBudget = false;
1046
- let budget = 0;
1047
-
1048
- if (tc.thinkingBudget !== undefined) {
1049
- budget = parseInt(tc.thinkingBudget, 10);
1050
- geminiRequest.generationConfig.thinkingConfig.thinkingBudget = budget;
1051
- setBudget = true;
1052
- } else if (tc.thinking_budget !== undefined) {
1053
- budget = parseInt(tc.thinking_budget, 10);
1054
- geminiRequest.generationConfig.thinkingConfig.thinkingBudget = budget;
1055
- setBudget = true;
1056
- }
1057
-
1058
- if (tc.includeThoughts !== undefined) {
1059
- geminiRequest.generationConfig.thinkingConfig.includeThoughts = tc.includeThoughts;
1060
- } else if (tc.include_thoughts !== undefined) {
1061
- geminiRequest.generationConfig.thinkingConfig.includeThoughts = tc.include_thoughts;
1062
- } else if (setBudget && budget !== 0) {
1063
- geminiRequest.generationConfig.thinkingConfig.includeThoughts = true;
1064
- }
1065
- }
1066
- }
1067
-
1068
- // 处理 modalities -> responseModalities
1069
- if (openaiRequest.modalities && Array.isArray(openaiRequest.modalities)) {
1070
- const responseMods = [];
1071
- for (const m of openaiRequest.modalities) {
1072
- const mod = String(m).toLowerCase();
1073
- if (mod === 'text') {
1074
- responseMods.push('TEXT');
1075
- } else if (mod === 'image') {
1076
- responseMods.push('IMAGE');
1077
- }
1078
- }
1079
- if (responseMods.length > 0) {
1080
- geminiRequest.generationConfig = geminiRequest.generationConfig || {};
1081
- geminiRequest.generationConfig.responseModalities = responseMods;
1082
- }
1083
- }
1084
-
1085
- // 处理 image_config(OpenRouter 风格)
1086
- if (openaiRequest.image_config) {
1087
- const imgCfg = openaiRequest.image_config;
1088
- if (imgCfg.aspect_ratio) {
1089
- geminiRequest.generationConfig = geminiRequest.generationConfig || {};
1090
- geminiRequest.generationConfig.imageConfig = geminiRequest.generationConfig.imageConfig || {};
1091
- geminiRequest.generationConfig.imageConfig.aspectRatio = imgCfg.aspect_ratio;
1092
- }
1093
- if (imgCfg.image_size) {
1094
- geminiRequest.generationConfig = geminiRequest.generationConfig || {};
1095
- geminiRequest.generationConfig.imageConfig = geminiRequest.generationConfig.imageConfig || {};
1096
- geminiRequest.generationConfig.imageConfig.imageSize = imgCfg.image_size;
1097
- }
1098
- }
1099
-
1100
- // 处理 tools -> functionDeclarations
1101
- if (openaiRequest.tools?.length) {
1102
- const functionDeclarations = [];
1103
- let hasGoogleSearch = false;
1104
-
1105
- for (const t of openaiRequest.tools) {
1106
- if (!t || typeof t !== 'object') continue;
1107
-
1108
- if (t.type === 'function' && t.function) {
1109
- const func = t.function;
1110
- let fnDecl = {
1111
- name: String(func.name || ''),
1112
- description: String(func.description || '')
1113
- };
1114
-
1115
- // 处理 parameters -> parametersJsonSchema
1116
- if (func.parameters) {
1117
- fnDecl.parametersJsonSchema = cleanJsonSchema(func.parameters);
1118
- } else {
1119
- fnDecl.parametersJsonSchema = {
1120
- type: 'object',
1121
- properties: {}
1122
- };
1123
- }
1124
-
1125
- functionDeclarations.push(fnDecl);
1126
- } else if (t.name) {
1127
- functionDeclarations.push({
1128
- name: String(t.name),
1129
- description: String(t.description || ''),
1130
- parametersJsonSchema: cleanJsonSchema(t.input_schema || { type: 'object', properties: {} })
1131
- });
1132
- }
1133
-
1134
- // 处�� google_search 工具
1135
- if (t.google_search) {
1136
- hasGoogleSearch = true;
1137
- }
1138
- }
1139
-
1140
- if (functionDeclarations.length > 0 || hasGoogleSearch) {
1141
- geminiRequest.tools = [{}];
1142
- if (functionDeclarations.length > 0) {
1143
- geminiRequest.tools[0].functionDeclarations = functionDeclarations;
1144
- }
1145
- if (hasGoogleSearch) {
1146
- const googleSearchTool = openaiRequest.tools.find(t => t.google_search);
1147
- geminiRequest.tools[0].googleSearch = googleSearchTool.google_search;
1148
- }
1149
- }
1150
- }
1151
-
1152
- // 处理 tool_choice
1153
- if (openaiRequest.tool_choice) {
1154
- geminiRequest.toolConfig = this.buildGeminiToolConfig(openaiRequest.tool_choice);
1155
- }
1156
-
1157
- // 构建 generationConfig
1158
- const config = this.buildGeminiGenerationConfig(openaiRequest, model);
1159
- if (Object.keys(config).length) {
1160
- geminiRequest.generationConfig = {
1161
- ...config,
1162
- ...(geminiRequest.generationConfig || {})
1163
- };
1164
- }
1165
-
1166
- // 添加默认安全设置
1167
- geminiRequest.safetySettings = this.getDefaultSafetySettings();
1168
-
1169
- return geminiRequest;
1170
- }
1171
-
1172
- /**
1173
- * 检查模型是否支持 thinking
1174
- */
1175
- modelSupportsThinking(model) {
1176
- if (!model) return false;
1177
- const m = model.toLowerCase();
1178
- return m.includes('2.5') || m.includes('thinking') || m.includes('2.0-flash-thinking');
1179
- }
1180
-
1181
- /**
1182
- * 检查是否是 Gemini 3 模型
1183
- */
1184
- isGemini3Model(model) {
1185
- if (!model) return false;
1186
- const m = model.toLowerCase();
1187
- return m.includes('gemini-3') || m.includes('gemini3');
1188
- }
1189
-
1190
- /**
1191
- * 检查模型是否使用 thinking levels(而不是 budget)
1192
- */
1193
- modelUsesThinkingLevels(model) {
1194
- if (!model) return false;
1195
- // Gemini 3 模型使用 levels,其他使用 budget
1196
- return this.isGemini3Model(model);
1197
- }
1198
-
1199
- /**
1200
- * 验证 Gemini 3 thinking level
1201
- */
1202
- validateGemini3ThinkingLevel(model, effort) {
1203
- const validLevels = ['low', 'medium', 'high'];
1204
- if (validLevels.includes(effort)) {
1205
- return effort.toUpperCase();
1206
- }
1207
- return null;
1208
- }
1209
-
1210
- /**
1211
- * 将 reasoning_effort 转换为 Gemini thinkingConfig
1212
- */
1213
- applyReasoningEffortToGemini(effort) {
1214
- const effortToBudget = {
1215
- 'low': 1024,
1216
- 'medium': 8192,
1217
- 'high': 24576
1218
- };
1219
- const budget = effortToBudget[effort] || effortToBudget['medium'];
1220
- return {
1221
- thinkingBudget: budget,
1222
- includeThoughts: true
1223
- };
1224
- }
1225
-
1226
- /**
1227
- * 获取默认安全设置
1228
- */
1229
- getDefaultSafetySettings() {
1230
- return [
1231
- { category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
1232
- { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
1233
- { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
1234
- { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
1235
- { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
1236
- ];
1237
- }
1238
-
1239
- /**
1240
- * 处理OpenAI内容到Gemini parts
1241
- */
1242
- processOpenAIContentToGeminiParts(content) {
1243
- if (!content) return [];
1244
- if (typeof content === 'string') return [{ text: content }];
1245
-
1246
- if (Array.isArray(content)) {
1247
- const parts = [];
1248
-
1249
- for (const item of content) {
1250
- if (!item) continue;
1251
-
1252
- if (item.type === 'text' && item.text) {
1253
- parts.push({ text: item.text });
1254
- } else if (item.type === 'image_url' && item.image_url) {
1255
- const imageUrl = typeof item.image_url === 'string'
1256
- ? item.image_url
1257
- : item.image_url.url;
1258
-
1259
- if (imageUrl.startsWith('data:')) {
1260
- const [header, data] = imageUrl.split(',');
1261
- const mimeType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg';
1262
- parts.push({ inlineData: { mimeType, data } });
1263
- } else {
1264
- parts.push({
1265
- fileData: { mimeType: 'image/jpeg', fileUri: imageUrl }
1266
- });
1267
- }
1268
- }
1269
- }
1270
-
1271
- return parts;
1272
- }
1273
-
1274
- return [];
1275
- }
1276
-
1277
- /**
1278
- * 构建Gemini工具配置
1279
- */
1280
- buildGeminiToolConfig(toolChoice) {
1281
- if (typeof toolChoice === 'string' && ['none', 'auto'].includes(toolChoice)) {
1282
- return { functionCallingConfig: { mode: toolChoice.toUpperCase() } };
1283
- }
1284
- if (typeof toolChoice === 'object' && toolChoice.function) {
1285
- return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [toolChoice.function.name] } };
1286
- }
1287
- return null;
1288
- }
1289
-
1290
- /**
1291
- * 构建Gemini生成配置
1292
- */
1293
- buildGeminiGenerationConfig({ temperature, max_tokens, top_p, stop, tools, response_format }, model) {
1294
- const config = {};
1295
- config.temperature = checkAndAssignOrDefault(temperature, GEMINI_DEFAULT_TEMPERATURE);
1296
- config.maxOutputTokens = checkAndAssignOrDefault(max_tokens, GEMINI_DEFAULT_MAX_TOKENS);
1297
- config.topP = checkAndAssignOrDefault(top_p, GEMINI_DEFAULT_TOP_P);
1298
- if (stop !== undefined) config.stopSequences = Array.isArray(stop) ? stop : [stop];
1299
-
1300
- // Handle response_format
1301
- if (response_format) {
1302
- if (response_format.type === 'json_object') {
1303
- config.responseMimeType = 'application/json';
1304
- } else if (response_format.type === 'json_schema' && response_format.json_schema) {
1305
- config.responseMimeType = 'application/json';
1306
- if (response_format.json_schema.schema) {
1307
- config.responseSchema = response_format.json_schema.schema;
1308
- }
1309
- }
1310
- }
1311
-
1312
- // Gemini 2.5 and thinking models require responseModalities: ["TEXT"]
1313
- // But this parameter cannot be added when using tools (causes 400 error)
1314
- const hasTools = tools && Array.isArray(tools) && tools.length > 0;
1315
- if (!hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) {
1316
- logger.info(`[OpenAI->Gemini] Adding responseModalities: ["TEXT"] for model: ${model}`);
1317
- config.responseModalities = ["TEXT"];
1318
- } else if (hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) {
1319
- logger.info(`[OpenAI->Gemini] Skipping responseModalities for model ${model} because tools are present`);
1320
- }
1321
-
1322
- return config;
1323
- }
1324
- /**
1325
- * 将OpenAI响应转换为Gemini响应格式
1326
- */
1327
- toGeminiResponse(openaiResponse, model) {
1328
- if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) {
1329
- return { candidates: [], usageMetadata: {} };
1330
- }
1331
-
1332
- const choice = openaiResponse.choices[0];
1333
- const message = choice.message || {};
1334
- const parts = [];
1335
-
1336
- // 处理文本内容
1337
- if (message.content) {
1338
- parts.push({ text: message.content });
1339
- }
1340
-
1341
- // 处理工具调用
1342
- if (message.tool_calls && message.tool_calls.length > 0) {
1343
- for (const toolCall of message.tool_calls) {
1344
- if (toolCall.type === 'function') {
1345
- parts.push({
1346
- functionCall: {
1347
- name: toolCall.function.name,
1348
- args: typeof toolCall.function.arguments === 'string'
1349
- ? JSON.parse(toolCall.function.arguments)
1350
- : toolCall.function.arguments
1351
- }
1352
- });
1353
- }
1354
- }
1355
- }
1356
-
1357
- // 映射finish_reason
1358
- const finishReasonMap = {
1359
- 'stop': 'STOP',
1360
- 'length': 'MAX_TOKENS',
1361
- 'tool_calls': 'STOP',
1362
- 'content_filter': 'SAFETY'
1363
- };
1364
-
1365
- return {
1366
- candidates: [{
1367
- content: {
1368
- role: 'model',
1369
- parts: parts
1370
- },
1371
- finishReason: finishReasonMap[choice.finish_reason] || 'STOP'
1372
- }],
1373
- usageMetadata: openaiResponse.usage ? {
1374
- promptTokenCount: openaiResponse.usage.prompt_tokens || 0,
1375
- candidatesTokenCount: openaiResponse.usage.completion_tokens || 0,
1376
- totalTokenCount: openaiResponse.usage.total_tokens || 0,
1377
- cachedContentTokenCount: openaiResponse.usage.prompt_tokens_details?.cached_tokens || 0,
1378
- promptTokensDetails: [{
1379
- modality: "TEXT",
1380
- tokenCount: openaiResponse.usage.prompt_tokens || 0
1381
- }],
1382
- candidatesTokensDetails: [{
1383
- modality: "TEXT",
1384
- tokenCount: openaiResponse.usage.completion_tokens || 0
1385
- }],
1386
- thoughtsTokenCount: openaiResponse.usage.completion_tokens_details?.reasoning_tokens || 0
1387
- } : {}
1388
- };
1389
- }
1390
-
1391
- /**
1392
- * 将OpenAI流式响应块转换为Gemini流式响应格式
1393
- */
1394
- toGeminiStreamChunk(openaiChunk, model) {
1395
- if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) {
1396
- return null;
1397
- }
1398
-
1399
- const choice = openaiChunk.choices[0];
1400
- const delta = choice.delta || {};
1401
- const parts = [];
1402
-
1403
- // 处理文本内容
1404
- if (delta.content) {
1405
- parts.push({ text: delta.content });
1406
- }
1407
-
1408
- // 处理工具调用
1409
- if (delta.tool_calls && delta.tool_calls.length > 0) {
1410
- for (const toolCall of delta.tool_calls) {
1411
- if (toolCall.function) {
1412
- const functionCall = {
1413
- name: toolCall.function.name || '',
1414
- args: {}
1415
- };
1416
-
1417
- if (toolCall.function.arguments) {
1418
- try {
1419
- functionCall.args = typeof toolCall.function.arguments === 'string'
1420
- ? JSON.parse(toolCall.function.arguments)
1421
- : toolCall.function.arguments;
1422
- } catch (e) {
1423
- // 部分参数,保持为字符串
1424
- functionCall.args = { partial: toolCall.function.arguments };
1425
- }
1426
- }
1427
-
1428
- parts.push({ functionCall });
1429
- }
1430
- }
1431
- }
1432
-
1433
- const result = {
1434
- candidates: [{
1435
- content: {
1436
- role: 'model',
1437
- parts: parts
1438
- }
1439
- }]
1440
- };
1441
-
1442
- return result;
1443
- }
1444
-
1445
- // =========================================================================
1446
- // OpenAI -> Grok 转换
1447
- // =========================================================================
1448
-
1449
- /**
1450
- * OpenAI请求 -> Grok请求
1451
- */
1452
- toGrokRequest(openaiRequest) {
1453
- // 我们需要 GrokConverter 来处理复杂的仿真逻辑
1454
- const { ConverterFactory } = (import.meta.url ? { ConverterFactory: null } : { ConverterFactory: null }); // 这是一个占位,实际会从全局获取
1455
-
1456
- // 直接返回结构化数据,由 GrokApiService.buildPayload 最终处理
1457
- // 这样可以保留原始的 messages, tools, tool_choice 以进行高质量仿真
1458
- return {
1459
- ...openaiRequest,
1460
- // 保持原始结构以便 GrokApiService 处理
1461
- _isConverted: true
1462
- };
1463
- }
1464
-
1465
- /**
1466
- * OpenAI响应 -> Grok响应(通常不使用)
1467
- */
1468
- toGrokResponse(openaiResponse, model) {
1469
- return openaiResponse;
1470
- }
1471
-
1472
- /**
1473
- * OpenAI流式响应 -> Grok流式响应(通常不使用)
1474
- */
1475
- toGrokStreamChunk(openaiChunk, model) {
1476
- return openaiChunk;
1477
- }
1478
-
1479
- /**
1480
- * OpenAI模型列表 -> Grok模型列表(通常不使用)
1481
- */
1482
- toGrokModelList(openaiModels) {
1483
- return openaiModels;
1484
- }
1485
-
1486
- /**
1487
- * 将 OpenAI 模型列表转换为 Gemini 模型列表
1488
- */
1489
- toCodexRequest(openaiRequest) {
1490
- return this.codexConverter.toOpenAIRequestToCodexRequest(openaiRequest);
1491
- }
1492
-
1493
- /**
1494
- * 将OpenAI请求转换为OpenAI Responses格式
1495
- */
1496
- toOpenAIResponsesRequest(openaiRequest) {
1497
- const responsesRequest = {
1498
- model: openaiRequest.model,
1499
- instructions: '',
1500
- input: [],
1501
- stream: openaiRequest.stream || false,
1502
- max_output_tokens: openaiRequest.max_tokens,
1503
- temperature: openaiRequest.temperature,
1504
- top_p: openaiRequest.top_p,
1505
- parallel_tool_calls: openaiRequest.parallel_tool_calls,
1506
- tool_choice: openaiRequest.tool_choice
1507
- };
1508
-
1509
- const { systemInstruction, nonSystemMessages } = extractSystemMessages(openaiRequest.messages || []);
1510
-
1511
- if (systemInstruction) {
1512
- responsesRequest.instructions = extractText(systemInstruction.parts[0].text);
1513
- }
1514
-
1515
- if (openaiRequest.reasoning_effort) {
1516
- responsesRequest.reasoning = {
1517
- effort: openaiRequest.reasoning_effort
1518
- };
1519
- }
1520
-
1521
- // 转换messages到input
1522
- for (const msg of nonSystemMessages) {
1523
- if (msg.role === 'tool') {
1524
- responsesRequest.input.push({
1525
- type: 'function_call_output',
1526
- call_id: msg.tool_call_id,
1527
- output: msg.content
1528
- });
1529
- } else if (msg.role === 'assistant' && msg.tool_calls?.length) {
1530
- for (const tc of msg.tool_calls) {
1531
- responsesRequest.input.push({
1532
- type: 'function_call',
1533
- call_id: tc.id,
1534
- name: tc.function.name,
1535
- arguments: tc.function.arguments
1536
- });
1537
- }
1538
- } else {
1539
- let content = [];
1540
- if (typeof msg.content === 'string') {
1541
- content.push({
1542
- type: msg.role === 'assistant' ? 'output_text' : 'input_text',
1543
- text: msg.content
1544
- });
1545
- } else if (Array.isArray(msg.content)) {
1546
- msg.content.forEach(c => {
1547
- if (c.type === 'text') {
1548
- content.push({
1549
- type: msg.role === 'assistant' ? 'output_text' : 'input_text',
1550
- text: c.text
1551
- });
1552
- } else if (c.type === 'image_url') {
1553
- content.push({
1554
- type: 'input_image',
1555
- image_url: c.image_url
1556
- });
1557
- }
1558
- });
1559
- }
1560
-
1561
- if (content.length > 0) {
1562
- responsesRequest.input.push({
1563
- type: 'message',
1564
- role: msg.role,
1565
- content: content
1566
- });
1567
- }
1568
- }
1569
- }
1570
-
1571
- // 处理工具
1572
- if (openaiRequest.tools) {
1573
- responsesRequest.tools = openaiRequest.tools.map(t => ({
1574
- type: t.type || 'function',
1575
- name: t.function?.name,
1576
- description: t.function?.description,
1577
- parameters: t.function?.parameters
1578
- }));
1579
- }
1580
-
1581
- return responsesRequest;
1582
- }
1583
-
1584
- /**
1585
- * 将OpenAI响应转换为OpenAI Responses格式
1586
- */
1587
- toOpenAIResponsesResponse(openaiResponse, model) {
1588
- if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) {
1589
- return {
1590
- id: `resp_${Date.now()}`,
1591
- object: 'response',
1592
- created_at: Math.floor(Date.now() / 1000),
1593
- status: 'completed',
1594
- model: model || 'unknown',
1595
- output: [],
1596
- usage: {
1597
- input_tokens: 0,
1598
- output_tokens: 0,
1599
- total_tokens: 0
1600
- }
1601
- };
1602
- }
1603
-
1604
- const choice = openaiResponse.choices[0];
1605
- const message = choice.message || {};
1606
- const output = [];
1607
-
1608
- // 构建message输出
1609
- const messageContent = [];
1610
- if (message.content) {
1611
- messageContent.push({
1612
- type: 'output_text',
1613
- text: message.content
1614
- });
1615
- }
1616
-
1617
- output.push({
1618
- type: 'message',
1619
- id: `msg_${Date.now()}`,
1620
- status: 'completed',
1621
- role: 'assistant',
1622
- content: messageContent
1623
- });
1624
-
1625
- return {
1626
- id: openaiResponse.id || `resp_${Date.now()}`,
1627
- object: 'response',
1628
- created_at: openaiResponse.created || Math.floor(Date.now() / 1000),
1629
- status: choice.finish_reason === 'stop' ? 'completed' : 'in_progress',
1630
- model: model || openaiResponse.model || 'unknown',
1631
- output: output,
1632
- usage: openaiResponse.usage ? {
1633
- input_tokens: openaiResponse.usage.prompt_tokens || 0,
1634
- input_tokens_details: {
1635
- cached_tokens: openaiResponse.usage.prompt_tokens_details?.cached_tokens || 0
1636
- },
1637
- output_tokens: openaiResponse.usage.completion_tokens || 0,
1638
- output_tokens_details: {
1639
- reasoning_tokens: openaiResponse.usage.completion_tokens_details?.reasoning_tokens || 0
1640
- },
1641
- total_tokens: openaiResponse.usage.total_tokens || 0
1642
- } : {
1643
- input_tokens: 0,
1644
- input_tokens_details: {
1645
- cached_tokens: 0
1646
- },
1647
- output_tokens: 0,
1648
- output_tokens_details: {
1649
- reasoning_tokens: 0
1650
- },
1651
- total_tokens: 0
1652
- }
1653
- };
1654
- }
1655
-
1656
- /**
1657
- * 将OpenAI流式响应转换为OpenAI Responses流式格式
1658
- * 参考 ClaudeConverter.toOpenAIResponsesStreamChunk 的实现逻辑
1659
- */
1660
- toOpenAIResponsesStreamChunk(openaiChunk, model, requestId = null) {
1661
- if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) {
1662
- return [];
1663
- }
1664
-
1665
- const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
1666
- const choice = openaiChunk.choices[0];
1667
- const delta = choice.delta || {};
1668
- const events = [];
1669
-
1670
- // 第一个chunk - role为assistant时调用 getOpenAIResponsesStreamChunkBegin
1671
- if (delta.role === 'assistant') {
1672
- events.push(
1673
- generateResponseCreated(responseId, model || openaiChunk.model || 'unknown'),
1674
- generateResponseInProgress(responseId),
1675
- generateOutputItemAdded(responseId),
1676
- generateContentPartAdded(responseId)
1677
- );
1678
- }
1679
-
1680
- // 处理 reasoning_content(推理内容)
1681
- if (delta.reasoning_content) {
1682
- events.push({
1683
- delta: delta.reasoning_content,
1684
- item_id: `thinking_${uuidv4().replace(/-/g, '')}`,
1685
- output_index: 0,
1686
- sequence_number: 3,
1687
- type: "response.reasoning_summary_text.delta"
1688
- });
1689
- }
1690
-
1691
- // 处理 tool_calls(工具调用)
1692
- if (delta.tool_calls && delta.tool_calls.length > 0) {
1693
- for (const toolCall of delta.tool_calls) {
1694
- const outputIndex = toolCall.index || 0;
1695
-
1696
- // 如果有 function.name,说明是工具调用开始
1697
- if (toolCall.function && toolCall.function.name) {
1698
- events.push({
1699
- item: {
1700
- id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`,
1701
- type: "function_call",
1702
- name: toolCall.function.name,
1703
- arguments: "",
1704
- status: "in_progress"
1705
- },
1706
- output_index: outputIndex,
1707
- sequence_number: 2,
1708
- type: "response.output_item.added"
1709
- });
1710
- }
1711
-
1712
- // 如果有 function.arguments,说明是参数增量
1713
- if (toolCall.function && toolCall.function.arguments) {
1714
- events.push({
1715
- delta: toolCall.function.arguments,
1716
- item_id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`,
1717
- output_index: outputIndex,
1718
- sequence_number: 3,
1719
- type: "response.custom_tool_call_input.delta"
1720
- });
1721
- }
1722
- }
1723
- }
1724
-
1725
- // 处理普通文本内容
1726
- if (delta.content) {
1727
- events.push({
1728
- delta: delta.content,
1729
- item_id: `msg_${uuidv4().replace(/-/g, '')}`,
1730
- output_index: 0,
1731
- sequence_number: 3,
1732
- type: "response.output_text.delta"
1733
- });
1734
- }
1735
-
1736
- // 处理完成状态 - 调用 getOpenAIResponsesStreamChunkEnd
1737
- if (choice.finish_reason) {
1738
- events.push(
1739
- generateOutputTextDone(responseId),
1740
- generateContentPartDone(responseId),
1741
- generateOutputItemDone(responseId),
1742
- generateResponseCompleted(responseId)
1743
- );
1744
-
1745
- // 如果有 usage 信息,更新最后一个事件
1746
- if (openaiChunk.usage && events.length > 0) {
1747
- const lastEvent = events[events.length - 1];
1748
- if (lastEvent.response) {
1749
- lastEvent.response.usage = {
1750
- input_tokens: openaiChunk.usage.prompt_tokens || 0,
1751
- input_tokens_details: {
1752
- cached_tokens: openaiChunk.usage.prompt_tokens_details?.cached_tokens || 0
1753
- },
1754
- output_tokens: openaiChunk.usage.completion_tokens || 0,
1755
- output_tokens_details: {
1756
- reasoning_tokens: openaiChunk.usage.completion_tokens_details?.reasoning_tokens || 0
1757
- },
1758
- total_tokens: openaiChunk.usage.total_tokens || 0
1759
- };
1760
- }
1761
- }
1762
- }
1763
-
1764
- return events;
1765
- }
1766
-
1767
- }
1768
-
1769
- export default OpenAIConverter;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/strategies/OpenAIResponsesConverter.js DELETED
@@ -1,1032 +0,0 @@
1
- /**
2
- * OpenAI Responses API 转换器
3
- * 处理 OpenAI Responses API 格式与其他协议之间的转换
4
- */
5
-
6
- import { v4 as uuidv4 } from 'uuid';
7
- import { BaseConverter } from '../BaseConverter.js';
8
- import { CodexConverter } from './CodexConverter.js';
9
- import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
10
- import {
11
- extractAndProcessSystemMessages as extractSystemMessages,
12
- extractTextFromMessageContent as extractText,
13
- CLAUDE_DEFAULT_MAX_TOKENS,
14
- GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
15
- GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT
16
- } from '../utils.js';
17
- import {
18
- generateResponseCreated,
19
- generateResponseInProgress,
20
- generateOutputItemAdded,
21
- generateContentPartAdded,
22
- generateOutputTextDone,
23
- generateContentPartDone,
24
- generateOutputItemDone,
25
- generateResponseCompleted
26
- } from '../../providers/openai/openai-responses-core.mjs';
27
-
28
- /**
29
- * OpenAI Responses API 转换器类
30
- * 支持 OpenAI Responses 格式与 OpenAI、Claude、Gemini 之间的转换
31
- */
32
- export class OpenAIResponsesConverter extends BaseConverter {
33
- constructor() {
34
- super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
35
- this.codexConverter = new CodexConverter();
36
- }
37
-
38
- // =============================================================================
39
- // 请求转换
40
- // =============================================================================
41
-
42
- /**
43
- * 转换请求到目标协议
44
- */
45
- convertRequest(data, toProtocol) {
46
- switch (toProtocol) {
47
- case MODEL_PROTOCOL_PREFIX.OPENAI:
48
- return this.toOpenAIRequest(data);
49
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
50
- return this.toClaudeRequest(data);
51
- case MODEL_PROTOCOL_PREFIX.GEMINI:
52
- return this.toGeminiRequest(data);
53
- case MODEL_PROTOCOL_PREFIX.CODEX:
54
- return this.toCodexRequest(data);
55
- case MODEL_PROTOCOL_PREFIX.GROK:
56
- return this.toGrokRequest(data);
57
- default:
58
- throw new Error(`Unsupported target protocol: ${toProtocol}`);
59
- }
60
- }
61
-
62
- /**
63
- * 转换响应到目标协议
64
- */
65
- convertResponse(data, toProtocol, model) {
66
- switch (toProtocol) {
67
- case MODEL_PROTOCOL_PREFIX.OPENAI:
68
- return this.toOpenAIResponse(data, model);
69
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
70
- return this.toClaudeResponse(data, model);
71
- case MODEL_PROTOCOL_PREFIX.GEMINI:
72
- return this.toGeminiResponse(data, model);
73
- case MODEL_PROTOCOL_PREFIX.CODEX:
74
- return this.toCodexResponse(data, model);
75
- default:
76
- throw new Error(`Unsupported target protocol: ${toProtocol}`);
77
- }
78
- }
79
-
80
- /**
81
- * 转换流式响应块到目标协议
82
- */
83
- convertStreamChunk(chunk, toProtocol, model) {
84
- switch (toProtocol) {
85
- case MODEL_PROTOCOL_PREFIX.OPENAI:
86
- return this.toOpenAIStreamChunk(chunk, model);
87
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
88
- return this.toClaudeStreamChunk(chunk, model);
89
- case MODEL_PROTOCOL_PREFIX.GEMINI:
90
- return this.toGeminiStreamChunk(chunk, model);
91
- case MODEL_PROTOCOL_PREFIX.CODEX:
92
- return this.toCodexStreamChunk(chunk, model);
93
- default:
94
- throw new Error(`Unsupported target protocol: ${toProtocol}`);
95
- }
96
- }
97
-
98
- /**
99
- * 转换模型列表到目标协议
100
- */
101
- convertModelList(data, targetProtocol) {
102
- switch (targetProtocol) {
103
- case MODEL_PROTOCOL_PREFIX.OPENAI:
104
- return this.toOpenAIModelList(data);
105
- case MODEL_PROTOCOL_PREFIX.CLAUDE:
106
- return this.toClaudeModelList(data);
107
- case MODEL_PROTOCOL_PREFIX.GEMINI:
108
- return this.toGeminiModelList(data);
109
- default:
110
- return data;
111
- }
112
- }
113
-
114
- // =============================================================================
115
- // 转换到 OpenAI 格式
116
- // =============================================================================
117
-
118
- /**
119
- * 将 OpenAI Responses 请求转换为标准 OpenAI 请求
120
- */
121
- toOpenAIRequest(responsesRequest) {
122
- const openaiRequest = {
123
- model: responsesRequest.model,
124
- messages: [],
125
- stream: responsesRequest.stream || false
126
- };
127
-
128
- // 复制其他参数
129
- if (responsesRequest.temperature !== undefined) {
130
- openaiRequest.temperature = responsesRequest.temperature;
131
- }
132
- if (responsesRequest.max_output_tokens !== undefined) {
133
- openaiRequest.max_tokens = responsesRequest.max_output_tokens;
134
- } else if (responsesRequest.max_tokens !== undefined) {
135
- openaiRequest.max_tokens = responsesRequest.max_tokens;
136
- }
137
- if (responsesRequest.top_p !== undefined) {
138
- openaiRequest.top_p = responsesRequest.top_p;
139
- }
140
- if (responsesRequest.parallel_tool_calls !== undefined) {
141
- openaiRequest.parallel_tool_calls = responsesRequest.parallel_tool_calls;
142
- }
143
-
144
- // OpenAI Responses API 使用 instructions 和 input 字段
145
- // 需要转换为标准的 messages 格式
146
- if (responsesRequest.instructions) {
147
- // instructions 作为系统消息
148
- openaiRequest.messages.push({
149
- role: 'system',
150
- content: responsesRequest.instructions
151
- });
152
- }
153
-
154
- // input 包含用户消息和历史对话
155
- if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
156
- responsesRequest.input.forEach(item => {
157
- const itemType = item.type || (item.role ? 'message' : '');
158
-
159
- switch (itemType) {
160
- case 'message':
161
- // 提取消息内容
162
- let content = '';
163
- if (Array.isArray(item.content)) {
164
- content = item.content
165
- .filter(c => c.type === 'input_text' || c.type === 'output_text')
166
- .map(c => c.text)
167
- .join('\n');
168
- } else if (typeof item.content === 'string') {
169
- content = item.content;
170
- }
171
-
172
- if (content || (item.role === 'assistant' || item.role === 'developer')) {
173
- openaiRequest.messages.push({
174
- role: item.role === 'developer' ? 'assistant' : item.role,
175
- content: content
176
- });
177
- }
178
- break;
179
-
180
- case 'function_call':
181
- openaiRequest.messages.push({
182
- role: 'assistant',
183
- tool_calls: [{
184
- id: item.call_id,
185
- type: 'function',
186
- function: {
187
- name: item.name,
188
- arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments)
189
- }
190
- }]
191
- });
192
- break;
193
-
194
- case 'function_call_output':
195
- openaiRequest.messages.push({
196
- role: 'tool',
197
- tool_call_id: item.call_id,
198
- content: item.output
199
- });
200
- break;
201
- }
202
- });
203
- }
204
-
205
- // 如果有标准的 messages 字段,也支持
206
- if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
207
- responsesRequest.messages.forEach(msg => {
208
- openaiRequest.messages.push({
209
- role: msg.role,
210
- content: msg.content
211
- });
212
- });
213
- }
214
-
215
- // 处理工具
216
- if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
217
- openaiRequest.tools = responsesRequest.tools
218
- .map(tool => {
219
- if (tool.type && tool.type !== 'function') {
220
- return null;
221
- }
222
-
223
- const name = tool.name || (tool.function && tool.function.name);
224
- const description = tool.description || (tool.function && tool.function.description);
225
- const parameters = tool.parameters || (tool.function && tool.function.parameters) || tool.parametersJsonSchema || { type: 'object', properties: {} };
226
-
227
- // 如果没有名称,则该工具无效,稍后过滤掉
228
- if (!name) {
229
- return null;
230
- }
231
-
232
- return {
233
- type: 'function',
234
- function: {
235
- name: name,
236
- description: description,
237
- parameters: parameters
238
- }
239
- };
240
- })
241
- .filter(tool => tool !== null);
242
- }
243
-
244
- if (responsesRequest.tool_choice) {
245
- openaiRequest.tool_choice = responsesRequest.tool_choice;
246
- }
247
-
248
- return openaiRequest;
249
- }
250
-
251
- /**
252
- * 将 OpenAI Responses 响应转换为标准 OpenAI 响应
253
- */
254
- toOpenAIResponse(responsesResponse, model) {
255
- const choices = [];
256
- let usage = {
257
- prompt_tokens: 0,
258
- completion_tokens: 0,
259
- total_tokens: 0,
260
- prompt_tokens_details: { cached_tokens: 0 },
261
- completion_tokens_details: { reasoning_tokens: 0 }
262
- };
263
-
264
- if (responsesResponse.output && Array.isArray(responsesResponse.output)) {
265
- responsesResponse.output.forEach((item, index) => {
266
- if (item.type === 'message') {
267
- const content = item.content
268
- ?.filter(c => c.type === 'output_text')
269
- .map(c => c.text)
270
- .join('') || '';
271
-
272
- choices.push({
273
- index: index,
274
- message: {
275
- role: 'assistant',
276
- content: content
277
- },
278
- finish_reason: responsesResponse.status === 'completed' ? 'stop' : null
279
- });
280
- } else if (item.type === 'function_call') {
281
- choices.push({
282
- index: index,
283
- message: {
284
- role: 'assistant',
285
- tool_calls: [{
286
- id: item.call_id,
287
- type: 'function',
288
- function: {
289
- name: item.name,
290
- arguments: item.arguments
291
- }
292
- }]
293
- },
294
- finish_reason: 'tool_calls'
295
- });
296
- }
297
- });
298
- }
299
-
300
- if (responsesResponse.usage) {
301
- usage = {
302
- prompt_tokens: responsesResponse.usage.input_tokens || 0,
303
- completion_tokens: responsesResponse.usage.output_tokens || 0,
304
- total_tokens: responsesResponse.usage.total_tokens || 0,
305
- prompt_tokens_details: {
306
- cached_tokens: responsesResponse.usage.input_tokens_details?.cached_tokens || 0
307
- },
308
- completion_tokens_details: {
309
- reasoning_tokens: responsesResponse.usage.output_tokens_details?.reasoning_tokens || 0
310
- }
311
- };
312
- }
313
-
314
- return {
315
- id: responsesResponse.id || `chatcmpl-${Date.now()}`,
316
- object: 'chat.completion',
317
- created: responsesResponse.created_at || Math.floor(Date.now() / 1000),
318
- model: model || responsesResponse.model,
319
- choices: choices.length > 0 ? choices : [{
320
- index: 0,
321
- message: {
322
- role: 'assistant',
323
- content: ''
324
- },
325
- finish_reason: 'stop'
326
- }],
327
- usage: usage
328
- };
329
- }
330
-
331
- /**
332
- * 将 OpenAI Responses 流式块转换为标准 OpenAI 流式块
333
- */
334
- toOpenAIStreamChunk(responsesChunk, model) {
335
- const resId = responsesChunk.response?.id || responsesChunk.id || `chatcmpl-${Date.now()}`;
336
- const created = responsesChunk.response?.created_at || responsesChunk.created || Math.floor(Date.now() / 1000);
337
-
338
- const delta = {};
339
- let finish_reason = null;
340
-
341
- if (responsesChunk.type === 'response.output_text.delta') {
342
- delta.content = responsesChunk.delta;
343
- } else if (responsesChunk.type === 'response.function_call_arguments.delta') {
344
- delta.tool_calls = [{
345
- index: responsesChunk.output_index || 0,
346
- function: {
347
- arguments: responsesChunk.delta
348
- }
349
- }];
350
- } else if (responsesChunk.type === 'response.output_item.added' && responsesChunk.item?.type === 'function_call') {
351
- delta.tool_calls = [{
352
- index: responsesChunk.output_index || 0,
353
- id: responsesChunk.item.call_id,
354
- type: 'function',
355
- function: {
356
- name: responsesChunk.item.name,
357
- arguments: ''
358
- }
359
- }];
360
- } else if (responsesChunk.type === 'response.completed') {
361
- finish_reason = 'stop';
362
- }
363
-
364
- return {
365
- id: resId,
366
- object: 'chat.completion.chunk',
367
- created: created,
368
- model: model || responsesChunk.response?.model || responsesChunk.model,
369
- choices: [{
370
- index: 0,
371
- delta: delta,
372
- finish_reason: finish_reason
373
- }]
374
- };
375
- }
376
-
377
- // =============================================================================
378
- // 转换到 Claude 格式
379
- // =============================================================================
380
-
381
- /**
382
- * 将 OpenAI Responses 请求转换为 Claude 请求
383
- */
384
- toClaudeRequest(responsesRequest) {
385
- const claudeRequest = {
386
- model: responsesRequest.model,
387
- messages: [],
388
- max_tokens: responsesRequest.max_output_tokens || responsesRequest.max_tokens || CLAUDE_DEFAULT_MAX_TOKENS,
389
- stream: responsesRequest.stream || false
390
- };
391
-
392
- // 处理 instructions 作为系统消息
393
- if (responsesRequest.instructions) {
394
- claudeRequest.system = responsesRequest.instructions;
395
- }
396
-
397
- // 处理 reasoning effort
398
- if (responsesRequest.reasoning?.effort) {
399
- const effort = String(responsesRequest.reasoning.effort || '').toLowerCase().trim();
400
- let budgetTokens = 20000;
401
- if (effort === 'low') budgetTokens = 2048;
402
- else if (effort === 'medium') budgetTokens = 8192;
403
- else if (effort === 'high') budgetTokens = 20000;
404
- claudeRequest.thinking = {
405
- type: 'enabled',
406
- budget_tokens: budgetTokens
407
- };
408
- }
409
-
410
- // 处理 input 数组中的消息
411
- if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
412
- responsesRequest.input.forEach(item => {
413
- const itemType = item.type || (item.role ? 'message' : '');
414
-
415
- switch (itemType) {
416
- case 'message':
417
- const content = [];
418
- if (Array.isArray(item.content)) {
419
- item.content.forEach(c => {
420
- if (c.type === 'input_text' || c.type === 'output_text') {
421
- content.push({ type: 'text', text: c.text });
422
- } else if (c.type === 'input_image') {
423
- const url = c.image_url?.url || c.url;
424
- if (url && url.startsWith('data:')) {
425
- const [mediaInfo, data] = url.split(';base64,');
426
- const mediaType = mediaInfo.replace('data:', '');
427
- content.push({
428
- type: 'image',
429
- source: {
430
- type: 'base64',
431
- media_type: mediaType,
432
- data: data
433
- }
434
- });
435
- }
436
- }
437
- });
438
- } else if (typeof item.content === 'string') {
439
- content.push({ type: 'text', text: item.content });
440
- }
441
-
442
- if (content.length > 0) {
443
- claudeRequest.messages.push({
444
- role: item.role === 'assistant' ? 'assistant' : 'user',
445
- content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
446
- });
447
- }
448
- break;
449
-
450
- case 'function_call':
451
- claudeRequest.messages.push({
452
- role: 'assistant',
453
- content: [{
454
- type: 'tool_use',
455
- id: item.call_id || `toolu_${uuidv4().replace(/-/g, '')}`,
456
- name: item.name,
457
- input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
458
- }]
459
- });
460
- break;
461
-
462
- case 'function_call_output':
463
- claudeRequest.messages.push({
464
- role: 'user',
465
- content: [{
466
- type: 'tool_result',
467
- tool_use_id: item.call_id,
468
- content: item.output
469
- }]
470
- });
471
- break;
472
- }
473
- });
474
- }
475
-
476
- // 处理工具
477
- if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
478
- claudeRequest.tools = responsesRequest.tools.map(tool => ({
479
- name: tool.name,
480
- description: tool.description,
481
- input_schema: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
482
- }));
483
- }
484
-
485
- if (responsesRequest.tool_choice) {
486
- if (typeof responsesRequest.tool_choice === 'string') {
487
- if (responsesRequest.tool_choice === 'auto') {
488
- claudeRequest.tool_choice = { type: 'auto' };
489
- } else if (responsesRequest.tool_choice === 'required') {
490
- claudeRequest.tool_choice = { type: 'any' };
491
- }
492
- } else if (responsesRequest.tool_choice.type === 'function') {
493
- claudeRequest.tool_choice = {
494
- type: 'tool',
495
- name: responsesRequest.tool_choice.function.name
496
- };
497
- }
498
- }
499
-
500
- return claudeRequest;
501
- }
502
-
503
- /**
504
- * 将 OpenAI Responses 响应转换为 Claude 响应
505
- */
506
- toClaudeResponse(responsesResponse, model) {
507
- const content = [];
508
- let stop_reason = 'end_turn';
509
-
510
- if (responsesResponse.output && Array.isArray(responsesResponse.output)) {
511
- responsesResponse.output.forEach(item => {
512
- if (item.type === 'message') {
513
- const text = item.content
514
- ?.filter(c => c.type === 'output_text')
515
- .map(c => c.text)
516
- .join('') || '';
517
- if (text) {
518
- content.push({ type: 'text', text: text });
519
- }
520
- } else if (item.type === 'function_call') {
521
- content.push({
522
- type: 'tool_use',
523
- id: item.call_id,
524
- name: item.name,
525
- input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
526
- });
527
- stop_reason = 'tool_use';
528
- }
529
- });
530
- }
531
-
532
- return {
533
- id: responsesResponse.id || `msg_${Date.now()}`,
534
- type: 'message',
535
- role: 'assistant',
536
- content: content,
537
- model: model || responsesResponse.model,
538
- stop_reason: stop_reason,
539
- usage: {
540
- input_tokens: responsesResponse.usage?.input_tokens || 0,
541
- output_tokens: responsesResponse.usage?.output_tokens || 0,
542
- total_tokens: responsesResponse.usage?.total_tokens || 0
543
- }
544
- };
545
- }
546
-
547
- /**
548
- * 将 OpenAI Responses 流式块转换为 Claude 流式块
549
- */
550
- toClaudeStreamChunk(responsesChunk, model) {
551
- if (responsesChunk.type === 'response.created') {
552
- return {
553
- type: 'message_start',
554
- message: {
555
- id: responsesChunk.response.id,
556
- type: 'message',
557
- role: 'assistant',
558
- content: [],
559
- model: model || responsesChunk.response.model
560
- }
561
- };
562
- }
563
-
564
- if (responsesChunk.type === 'response.output_text.delta') {
565
- return {
566
- type: 'content_block_delta',
567
- index: 0,
568
- delta: {
569
- type: 'text_delta',
570
- text: responsesChunk.delta
571
- }
572
- };
573
- }
574
-
575
- if (responsesChunk.type === 'response.function_call_arguments.delta') {
576
- return {
577
- type: 'content_block_delta',
578
- index: responsesChunk.output_index || 0,
579
- delta: {
580
- type: 'input_json_delta',
581
- partial_json: responsesChunk.delta
582
- }
583
- };
584
- }
585
-
586
- if (responsesChunk.type === 'response.output_item.added' && responsesChunk.item?.type === 'function_call') {
587
- return {
588
- type: 'content_block_start',
589
- index: responsesChunk.output_index || 0,
590
- content_block: {
591
- type: 'tool_use',
592
- id: responsesChunk.item.call_id,
593
- name: responsesChunk.item.name,
594
- input: {}
595
- }
596
- };
597
- }
598
-
599
- if (responsesChunk.type === 'response.completed') {
600
- return {
601
- type: 'message_stop'
602
- };
603
- }
604
-
605
- return null;
606
- }
607
-
608
- // =============================================================================
609
- // 转换到 Gemini 格式
610
- // =============================================================================
611
-
612
- /**
613
- * 将 OpenAI Responses 请求转换为 Gemini 请求
614
- */
615
- toGeminiRequest(responsesRequest) {
616
- const geminiRequest = {
617
- contents: [],
618
- generationConfig: {}
619
- };
620
-
621
- // 处理 instructions 作为系统指令
622
- if (responsesRequest.instructions) {
623
- geminiRequest.systemInstruction = {
624
- parts: [{
625
- text: responsesRequest.instructions
626
- }]
627
- };
628
- }
629
-
630
- // 处理 input 数组中的消息
631
- if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
632
- responsesRequest.input.forEach(item => {
633
- const itemType = item.type || (item.role ? 'message' : '');
634
-
635
- switch (itemType) {
636
- case 'message':
637
- const parts = [];
638
- if (Array.isArray(item.content)) {
639
- item.content.forEach(c => {
640
- if (c.type === 'input_text' || c.type === 'output_text') {
641
- parts.push({ text: c.text });
642
- } else if (c.type === 'input_image') {
643
- const url = c.image_url?.url || c.url;
644
- if (url && url.startsWith('data:')) {
645
- const [mediaInfo, data] = url.split(';base64,');
646
- const mimeType = mediaInfo.replace('data:', '');
647
- parts.push({
648
- inlineData: {
649
- mimeType: mimeType,
650
- data: data
651
- }
652
- });
653
- }
654
- }
655
- });
656
- } else if (typeof item.content === 'string') {
657
- parts.push({ text: item.content });
658
- }
659
-
660
- if (parts.length > 0) {
661
- geminiRequest.contents.push({
662
- role: item.role === 'assistant' ? 'model' : 'user',
663
- parts: parts
664
- });
665
- }
666
- break;
667
-
668
- case 'function_call':
669
- geminiRequest.contents.push({
670
- role: 'model',
671
- parts: [{
672
- functionCall: {
673
- name: item.name,
674
- args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
675
- }
676
- }]
677
- });
678
- break;
679
-
680
- case 'function_call_output':
681
- geminiRequest.contents.push({
682
- role: 'user', // Gemini function response role is user or tool? usually user/model
683
- parts: [{
684
- functionResponse: {
685
- name: item.name,
686
- response: { content: item.output }
687
- }
688
- }]
689
- });
690
- break;
691
- }
692
- });
693
- }
694
-
695
- // 设置生成配置
696
- if (responsesRequest.temperature !== undefined) {
697
- geminiRequest.generationConfig.temperature = responsesRequest.temperature;
698
- }
699
- if (responsesRequest.max_output_tokens !== undefined) {
700
- geminiRequest.generationConfig.maxOutputTokens = responsesRequest.max_output_tokens;
701
- } else if (responsesRequest.max_tokens !== undefined) {
702
- geminiRequest.generationConfig.maxOutputTokens = responsesRequest.max_tokens;
703
- }
704
- if (responsesRequest.top_p !== undefined) {
705
- geminiRequest.generationConfig.topP = responsesRequest.top_p;
706
- }
707
-
708
- // 处理工具
709
- if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
710
- geminiRequest.tools = [{
711
- functionDeclarations: responsesRequest.tools
712
- .filter(tool => !tool.type || tool.type === 'function')
713
- .map(tool => ({
714
- name: tool.name,
715
- description: tool.description,
716
- parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
717
- }))
718
- }];
719
- }
720
-
721
- return geminiRequest;
722
- }
723
-
724
- /**
725
- * 将 OpenAI Responses 响应转换为 Gemini 响应
726
- */
727
- toGeminiResponse(responsesResponse, model) {
728
- const parts = [];
729
- let finishReason = 'STOP';
730
-
731
- if (responsesResponse.output && Array.isArray(responsesResponse.output)) {
732
- responsesResponse.output.forEach(item => {
733
- if (item.type === 'message') {
734
- const text = item.content
735
- ?.filter(c => c.type === 'output_text')
736
- .map(c => c.text)
737
- .join('') || '';
738
- if (text) {
739
- parts.push({ text: text });
740
- }
741
- } else if (item.type === 'function_call') {
742
- parts.push({
743
- functionCall: {
744
- name: item.name,
745
- args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
746
- }
747
- });
748
- }
749
- });
750
- }
751
-
752
- return {
753
- candidates: [{
754
- content: {
755
- parts: parts,
756
- role: 'model'
757
- },
758
- finishReason: finishReason,
759
- index: 0
760
- }],
761
- usageMetadata: {
762
- promptTokenCount: responsesResponse.usage?.input_tokens || 0,
763
- candidatesTokenCount: responsesResponse.usage?.output_tokens || 0,
764
- totalTokenCount: responsesResponse.usage?.total_tokens || 0
765
- }
766
- };
767
- }
768
-
769
- /**
770
- * 将 OpenAI Responses 流式块转换为 Gemini 流式块
771
- */
772
- toGeminiStreamChunk(responsesChunk, model) {
773
- if (responsesChunk.type === 'response.output_text.delta') {
774
- return {
775
- candidates: [{
776
- content: {
777
- parts: [{ text: responsesChunk.delta }],
778
- role: 'model'
779
- },
780
- index: 0
781
- }]
782
- };
783
- }
784
-
785
- if (responsesChunk.type === 'response.function_call_arguments.delta') {
786
- // Gemini 不太支持流式 functionCall 参数,这里只能简单映射
787
- return {
788
- candidates: [{
789
- content: {
790
- parts: [{
791
- functionCall: {
792
- name: '', // 无法在 delta 中获取名称
793
- args: responsesChunk.delta
794
- }
795
- }],
796
- role: 'model'
797
- },
798
- index: 0
799
- }]
800
- };
801
- }
802
-
803
- return null;
804
- }
805
-
806
- /**
807
- * OpenAI Responses → Codex 请求转换
808
- */
809
- toCodexRequest(responsesRequest) {
810
- return this.codexConverter.toOpenAIResponsesToCodexRequest(responsesRequest);
811
- }
812
-
813
- /**
814
- * OpenAI Responses → Grok 请求转换
815
- */
816
- toGrokRequest(responsesRequest) {
817
- // 先转换为 OpenAI 格式
818
- const openaiRequest = this.toOpenAIRequest(responsesRequest);
819
- return {
820
- ...openaiRequest,
821
- _isConverted: true
822
- };
823
- }
824
-
825
- // =============================================================================
826
- // 辅助方法
827
- // =============================================================================
828
-
829
- /**
830
- * 映射完成原因
831
- */
832
- mapFinishReason(reason) {
833
- const reasonMap = {
834
- 'stop': 'STOP',
835
- 'length': 'MAX_TOKENS',
836
- 'content_filter': 'SAFETY',
837
- 'end_turn': 'STOP'
838
- };
839
- return reasonMap[reason] || 'STOP';
840
- }
841
-
842
- /**
843
- * 将 OpenAI Responses 模型列表转换为标准 OpenAI 模型列表
844
- */
845
- toOpenAIModelList(responsesModels) {
846
- // OpenAI Responses 格式的模型列表已经是标准 OpenAI 格式
847
- // 如果输入已经是标准格式,直接返回
848
- if (responsesModels.object === 'list' && responsesModels.data) {
849
- return responsesModels;
850
- }
851
-
852
- // 如果是其他格式,转换为标准格式
853
- return {
854
- object: "list",
855
- data: (responsesModels.models || responsesModels.data || []).map(m => ({
856
- id: m.id || m.name,
857
- object: "model",
858
- created: m.created || Math.floor(Date.now() / 1000),
859
- owned_by: m.owned_by || "openai",
860
- })),
861
- };
862
- }
863
-
864
- /**
865
- * 将 OpenAI Responses 模型列表转换为 Claude 模型列表
866
- */
867
- toClaudeModelList(responsesModels) {
868
- const models = responsesModels.data || responsesModels.models || [];
869
- return {
870
- models: models.map(m => ({
871
- name: m.id || m.name,
872
- description: m.description || "",
873
- })),
874
- };
875
- }
876
-
877
- /**
878
- * 将 OpenAI Responses 模型列表转换为 Gemini 模型列表
879
- */
880
- toGeminiModelList(responsesModels) {
881
- const models = responsesModels.data || responsesModels.models || [];
882
- return {
883
- models: models.map(m => ({
884
- name: `models/${m.id || m.name}`,
885
- version: m.version || "1.0.0",
886
- displayName: m.displayName || m.id || m.name,
887
- description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`,
888
- inputTokenLimit: m.inputTokenLimit || GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
889
- outputTokenLimit: m.outputTokenLimit || GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT,
890
- supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
891
- }))
892
- };
893
- }
894
-
895
-
896
- /**
897
- * OpenAI Responses → Codex 响应转换 (实际上是 Codex 转 OpenAI Responses)
898
- */
899
- toCodexResponse(codexResponse, model) {
900
- const output = [];
901
- const responseData = codexResponse.response || codexResponse;
902
-
903
- if (responseData.output && Array.isArray(responseData.output)) {
904
- responseData.output.forEach(item => {
905
- if (item.type === 'message' && item.content) {
906
- const content = item.content.map(c => ({
907
- type: c.type === 'output_text' ? 'output_text' : 'input_text',
908
- text: c.text,
909
- annotations: []
910
- }));
911
- output.push({
912
- id: item.id || `msg_${uuidv4().replace(/-/g, '')}`,
913
- type: "message",
914
- role: item.role || "assistant",
915
- status: item.status || "completed",
916
- content: content
917
- });
918
- } else if (item.type === 'reasoning') {
919
- output.push({
920
- id: item.id || `rs_${uuidv4().replace(/-/g, '')}`,
921
- type: "reasoning",
922
- status: item.status || "completed",
923
- summary: item.summary || []
924
- });
925
- } else if (item.type === 'function_call') {
926
- output.push({
927
- id: item.id || `fc_${uuidv4().replace(/-/g, '')}`,
928
- call_id: item.call_id,
929
- type: "function_call",
930
- name: item.name,
931
- arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
932
- status: item.status || "completed"
933
- });
934
- }
935
- });
936
- }
937
-
938
- return {
939
- id: responseData.id || `resp_${uuidv4().replace(/-/g, '')}`,
940
- object: "response",
941
- created_at: responseData.created_at || Math.floor(Date.now() / 1000),
942
- model: model || responseData.model,
943
- status: responseData.status || "completed",
944
- output: output,
945
- usage: {
946
- input_tokens: responseData.usage?.input_tokens || 0,
947
- output_tokens: responseData.usage?.output_tokens || 0,
948
- total_tokens: responseData.usage?.total_tokens || 0
949
- }
950
- };
951
- }
952
-
953
- /**
954
- * OpenAI Responses → Codex 流式响应转换 (实际上是 Codex 转 OpenAI Responses)
955
- */
956
- toCodexStreamChunk(codexChunk, model) {
957
- const type = codexChunk.type;
958
- const resId = codexChunk.response?.id || 'default';
959
- const events = [];
960
-
961
- if (type === 'response.created') {
962
- events.push(
963
- generateResponseCreated(resId, model || codexChunk.response?.model),
964
- generateResponseInProgress(resId)
965
- );
966
- return events;
967
- }
968
-
969
- if (type === 'response.reasoning_summary_text.delta') {
970
- events.push({
971
- type: "response.reasoning_summary_text.delta",
972
- response_id: resId,
973
- item_id: codexChunk.item_id,
974
- output_index: codexChunk.output_index,
975
- summary_index: codexChunk.summary_index,
976
- delta: codexChunk.delta
977
- });
978
- return events;
979
- }
980
-
981
- if (type === 'response.output_text.delta') {
982
- events.push({
983
- type: "response.output_text.delta",
984
- response_id: resId,
985
- item_id: codexChunk.item_id,
986
- output_index: codexChunk.output_index,
987
- content_index: codexChunk.content_index,
988
- delta: codexChunk.delta
989
- });
990
- return events;
991
- }
992
-
993
- if (type === 'response.function_call_arguments.delta') {
994
- events.push({
995
- type: "response.function_call_arguments.delta",
996
- response_id: resId,
997
- item_id: codexChunk.item_id,
998
- output_index: codexChunk.output_index,
999
- delta: codexChunk.delta
1000
- });
1001
- return events;
1002
- }
1003
-
1004
- if (type === 'response.output_item.added') {
1005
- events.push({
1006
- type: "response.output_item.added",
1007
- response_id: resId,
1008
- output_index: codexChunk.output_index,
1009
- item: codexChunk.item
1010
- });
1011
- return events;
1012
- }
1013
-
1014
- if (type === 'response.completed') {
1015
- const completedEvent = generateResponseCompleted(resId);
1016
- completedEvent.response = {
1017
- ...completedEvent.response,
1018
- ...codexChunk.response
1019
- };
1020
- events.push(completedEvent);
1021
- return events;
1022
- }
1023
-
1024
- // 透传其他 response.* 事件
1025
- if (type && type.startsWith('response.')) {
1026
- return [codexChunk];
1027
- }
1028
-
1029
- return null;
1030
- }
1031
-
1032
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/converters/utils.js DELETED
@@ -1,369 +0,0 @@
1
- /**
2
- * 转换器公共工具函数模块
3
- * 提供各种协议转换所需的通用辅助函数
4
- */
5
-
6
- import { v4 as uuidv4 } from 'uuid';
7
- import logger from '../utils/logger.js';
8
-
9
- // =============================================================================
10
- // 常量定义
11
- // =============================================================================
12
-
13
- // 通用默认值
14
- export const DEFAULT_MAX_TOKENS = 8192;
15
- export const DEFAULT_TEMPERATURE = 1;
16
- export const DEFAULT_TOP_P = 0.95;
17
-
18
- // =============================================================================
19
- // OpenAI 相关常量
20
- // =============================================================================
21
- export const OPENAI_DEFAULT_MAX_TOKENS = 128000;
22
- export const OPENAI_DEFAULT_TEMPERATURE = 1;
23
- export const OPENAI_DEFAULT_TOP_P = 0.95;
24
- export const OPENAI_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
25
- export const OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT = 128000;
26
-
27
- // =============================================================================
28
- // Claude 相关常量
29
- // =============================================================================
30
- export const CLAUDE_DEFAULT_MAX_TOKENS = 200000;
31
- export const CLAUDE_DEFAULT_TEMPERATURE = 1;
32
- export const CLAUDE_DEFAULT_TOP_P = 0.95;
33
-
34
- // =============================================================================
35
- // Gemini 相关常量
36
- // =============================================================================
37
- export const GEMINI_DEFAULT_MAX_TOKENS = 65534;
38
- export const GEMINI_DEFAULT_TEMPERATURE = 1;
39
- export const GEMINI_DEFAULT_TOP_P = 0.95;
40
- export const GEMINI_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
41
- export const GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT = 65534;
42
-
43
- // =============================================================================
44
- // OpenAI Responses 相关常量
45
- // =============================================================================
46
- export const OPENAI_RESPONSES_DEFAULT_MAX_TOKENS = 128000;
47
- export const OPENAI_RESPONSES_DEFAULT_TEMPERATURE = 1;
48
- export const OPENAI_RESPONSES_DEFAULT_TOP_P = 0.95;
49
- export const OPENAI_RESPONSES_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
50
- export const OPENAI_RESPONSES_DEFAULT_OUTPUT_TOKEN_LIMIT = 128000;
51
-
52
- // =============================================================================
53
- // 通用辅助函数
54
- // =============================================================================
55
-
56
- /**
57
- * 判断值是否为 undefined 或 0,并返回默认值
58
- * @param {*} value - 要检查的值
59
- * @param {*} defaultValue - 默认值
60
- * @returns {*} 处理后的值
61
- */
62
- export function checkAndAssignOrDefault(value, defaultValue) {
63
- if (value !== undefined && value !== 0) {
64
- return value;
65
- }
66
- return defaultValue;
67
- }
68
-
69
- /**
70
- * 生成唯一ID
71
- * @param {string} prefix - ID前缀
72
- * @returns {string} 生成的ID
73
- */
74
- export function generateId(prefix = '') {
75
- return prefix ? `${prefix}_${uuidv4()}` : uuidv4();
76
- }
77
-
78
- /**
79
- * 安全解析JSON字符串
80
- * @param {string} str - JSON字符串
81
- * @returns {*} 解析后的对象或原始字符串
82
- */
83
- export function safeParseJSON(str) {
84
- if (!str) {
85
- return str;
86
- }
87
- let cleanedStr = str;
88
-
89
- // 处理可能被截断的转义序列
90
- if (cleanedStr.endsWith('\\') && !cleanedStr.endsWith('\\\\')) {
91
- cleanedStr = cleanedStr.substring(0, cleanedStr.length - 1);
92
- } else if (cleanedStr.endsWith('\\u') || cleanedStr.endsWith('\\u0') || cleanedStr.endsWith('\\u00')) {
93
- const idx = cleanedStr.lastIndexOf('\\u');
94
- cleanedStr = cleanedStr.substring(0, idx);
95
- }
96
-
97
- try {
98
- return JSON.parse(cleanedStr || '{}');
99
- } catch (e) {
100
- return str;
101
- }
102
- }
103
-
104
- /**
105
- * 提取消息内容中的文本
106
- * @param {string|Array} content - 消息内容
107
- * @returns {string} 提取的文本
108
- */
109
- export function extractTextFromMessageContent(content) {
110
- if (typeof content === 'string') {
111
- return content;
112
- }
113
- if (Array.isArray(content)) {
114
- return content
115
- .filter(part => part.type === 'text' && part.text)
116
- .map(part => part.text)
117
- .join('\n');
118
- }
119
- return '';
120
- }
121
-
122
- /**
123
- * 提取并处理系统消息
124
- * @param {Array} messages - 消息数组
125
- * @returns {{systemInstruction: Object|null, nonSystemMessages: Array}}
126
- */
127
- export function extractAndProcessSystemMessages(messages) {
128
- const systemContents = [];
129
- const nonSystemMessages = [];
130
-
131
- for (const message of messages) {
132
- if (message.role === 'system') {
133
- systemContents.push(extractTextFromMessageContent(message.content));
134
- } else {
135
- nonSystemMessages.push(message);
136
- }
137
- }
138
-
139
- let systemInstruction = null;
140
- if (systemContents.length > 0) {
141
- systemInstruction = {
142
- parts: [{
143
- text: systemContents.join('\n')
144
- }]
145
- };
146
- }
147
- return { systemInstruction, nonSystemMessages };
148
- }
149
-
150
- /**
151
- * 清理JSON Schema属性(移除Gemini不支持的属性)
152
- * Google Gemini API 只支持有限的 JSON Schema 属性,不支持以下属性:
153
- * - exclusiveMinimum, exclusiveMaximum, minimum, maximum
154
- * - minLength, maxLength, minItems, maxItems
155
- * - pattern, format, default, const
156
- * - additionalProperties, $schema, $ref, $id
157
- * - allOf, anyOf, oneOf, not
158
- * @param {Object} schema - JSON Schema
159
- * @returns {Object} 清理后的JSON Schema
160
- */
161
- export function cleanJsonSchemaProperties(schema) {
162
- if (!schema || typeof schema !== 'object') {
163
- return schema;
164
- }
165
-
166
- // 如果是数组,递归处理每个元素
167
- if (Array.isArray(schema)) {
168
- return schema.map(item => cleanJsonSchemaProperties(item));
169
- }
170
-
171
- // Gemini 支持的 JSON Schema 属性白名单
172
- const allowedKeys = [
173
- "type",
174
- "description",
175
- "properties",
176
- "required",
177
- "enum",
178
- "items",
179
- "nullable"
180
- ];
181
-
182
- const sanitized = {};
183
- for (const [key, value] of Object.entries(schema)) {
184
- if (allowedKeys.includes(key)) {
185
- // 对于需要递归处理的属性
186
- if (key === 'properties' && typeof value === 'object' && value !== null) {
187
- const cleanProperties = {};
188
- for (const [propName, propSchema] of Object.entries(value)) {
189
- cleanProperties[propName] = cleanJsonSchemaProperties(propSchema);
190
- }
191
- sanitized[key] = cleanProperties;
192
- } else if (key === 'items') {
193
- sanitized[key] = cleanJsonSchemaProperties(value);
194
- } else if (key === 'type') {
195
- // Google Gemini API 不支持数组形式的 type (如 ["string", "null"])
196
- // 必须是单个字符串,且通常需要大写 (STRING, NUMBER, OBJECT, ARRAY, BOOLEAN, INTEGER)
197
- if (Array.isArray(value)) {
198
- // 如果包含 null,设置 nullable 为 true
199
- if (value.includes('null')) {
200
- sanitized.nullable = true;
201
- }
202
- // 取第一个非 null 类型
203
- const actualType = value.find(t => t !== 'null');
204
- if (actualType) {
205
- sanitized[key] = actualType.toUpperCase();
206
- }
207
- } else if (typeof value === 'string') {
208
- sanitized[key] = value.toUpperCase();
209
- }
210
- } else {
211
- sanitized[key] = value;
212
- }
213
- }
214
- // 其他属性(如 exclusiveMinimum, minimum, maximum, pattern 等)被忽略
215
- }
216
-
217
- return sanitized;
218
- }
219
-
220
- /**
221
- * 映射结束原因
222
- * @param {string} reason - 结束原因
223
- * @param {string} sourceFormat - 源格式
224
- * @param {string} targetFormat - 目标格式
225
- * @returns {string} 映射后的结束原因
226
- */
227
- export function mapFinishReason(reason, sourceFormat, targetFormat) {
228
- const reasonMappings = {
229
- openai: {
230
- anthropic: {
231
- stop: "end_turn",
232
- length: "max_tokens",
233
- content_filter: "stop_sequence",
234
- tool_calls: "tool_use"
235
- }
236
- },
237
- gemini: {
238
- anthropic: {
239
- STOP: "end_turn",
240
- MAX_TOKENS: "max_tokens",
241
- SAFETY: "stop_sequence",
242
- RECITATION: "stop_sequence",
243
- stop: "end_turn",
244
- length: "max_tokens",
245
- safety: "stop_sequence",
246
- recitation: "stop_sequence",
247
- other: "end_turn"
248
- }
249
- }
250
- };
251
-
252
- try {
253
- return reasonMappings[sourceFormat][targetFormat][reason] || "end_turn";
254
- } catch (e) {
255
- return "end_turn";
256
- }
257
- }
258
-
259
- /**
260
- * 根据budget_tokens智能判断OpenAI reasoning_effort等级
261
- * @param {number|null} budgetTokens - Anthropic thinking的budget_tokens值
262
- * @returns {string} OpenAI reasoning_effort等级
263
- */
264
- export function determineReasoningEffortFromBudget(budgetTokens) {
265
- if (budgetTokens === null || budgetTokens === undefined) {
266
- logger.info("No budget_tokens provided, defaulting to reasoning_effort='high'");
267
- return "high";
268
- }
269
-
270
- const LOW_THRESHOLD = 50;
271
- const HIGH_THRESHOLD = 200;
272
-
273
- logger.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`);
274
-
275
- let effort;
276
- if (budgetTokens <= LOW_THRESHOLD) {
277
- effort = "low";
278
- } else if (budgetTokens <= HIGH_THRESHOLD) {
279
- effort = "medium";
280
- } else {
281
- effort = "high";
282
- }
283
-
284
- logger.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`);
285
- return effort;
286
- }
287
-
288
- /**
289
- * 从OpenAI文本中提取thinking内容
290
- * @param {string} text - 文本内容
291
- * @returns {string|Array} 提取后的内容
292
- */
293
- export function extractThinkingFromOpenAIText(text) {
294
- const thinkingPattern = /<thinking>\s*(.*?)\s*<\/thinking>/gs;
295
- const matches = [...text.matchAll(thinkingPattern)];
296
-
297
- const contentBlocks = [];
298
- let lastEnd = 0;
299
-
300
- for (const match of matches) {
301
- const beforeText = text.substring(lastEnd, match.index).trim();
302
- if (beforeText) {
303
- contentBlocks.push({
304
- type: "text",
305
- text: beforeText
306
- });
307
- }
308
-
309
- const thinkingText = match[1].trim();
310
- if (thinkingText) {
311
- contentBlocks.push({
312
- type: "thinking",
313
- thinking: thinkingText
314
- });
315
- }
316
-
317
- lastEnd = match.index + match[0].length;
318
- }
319
-
320
- const afterText = text.substring(lastEnd).trim();
321
- if (afterText) {
322
- contentBlocks.push({
323
- type: "text",
324
- text: afterText
325
- });
326
- }
327
-
328
- if (contentBlocks.length === 0) {
329
- return text;
330
- }
331
-
332
- if (contentBlocks.length === 1 && contentBlocks[0].type === "text") {
333
- return contentBlocks[0].text;
334
- }
335
-
336
- return contentBlocks;
337
- }
338
-
339
- // =============================================================================
340
- // 工具状态管理器(单例模式)
341
- // =============================================================================
342
-
343
- /**
344
- * 全局工具状态管理器
345
- */
346
- class ToolStateManager {
347
- constructor() {
348
- if (ToolStateManager.instance) {
349
- return ToolStateManager.instance;
350
- }
351
- ToolStateManager.instance = this;
352
- this._toolMappings = {};
353
- return this;
354
- }
355
-
356
- storeToolMapping(funcName, toolId) {
357
- this._toolMappings[funcName] = toolId;
358
- }
359
-
360
- getToolId(funcName) {
361
- return this._toolMappings[funcName] || null;
362
- }
363
-
364
- clearMappings() {
365
- this._toolMappings = {};
366
- }
367
- }
368
-
369
- export const toolStateManager = new ToolStateManager();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/core/config-manager.js DELETED
@@ -1,249 +0,0 @@
1
- import * as fs from 'fs';
2
- import { promises as pfs } from 'fs';
3
- import { INPUT_SYSTEM_PROMPT_FILE, MODEL_PROVIDER } from '../utils/common.js';
4
- import logger from '../utils/logger.js';
5
-
6
- export let CONFIG = {}; // Make CONFIG exportable
7
- export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable
8
-
9
- const ALL_MODEL_PROVIDERS = Object.values(MODEL_PROVIDER);
10
-
11
- function normalizeConfiguredProviders(config) {
12
- const fallbackProvider = MODEL_PROVIDER.GEMINI_CLI;
13
- const dedupedProviders = [];
14
-
15
- const addProvider = (value) => {
16
- if (typeof value !== 'string') {
17
- return;
18
- }
19
- const trimmed = value.trim();
20
- if (!trimmed) {
21
- return;
22
- }
23
- const matched = ALL_MODEL_PROVIDERS.find((provider) => provider.toLowerCase() === trimmed.toLowerCase());
24
- if (!matched) {
25
- logger.warn(`[Config Warning] Unknown model provider '${trimmed}'. This entry will be ignored.`);
26
- return;
27
- }
28
- if (!dedupedProviders.includes(matched)) {
29
- dedupedProviders.push(matched);
30
- }
31
- };
32
-
33
- const rawValue = config.MODEL_PROVIDER;
34
- if (Array.isArray(rawValue)) {
35
- rawValue.forEach((entry) => addProvider(typeof entry === 'string' ? entry : String(entry)));
36
- } else if (typeof rawValue === 'string') {
37
- rawValue.split(',').forEach(addProvider);
38
- } else if (rawValue != null) {
39
- addProvider(String(rawValue));
40
- }
41
-
42
- if (dedupedProviders.length === 0) {
43
- dedupedProviders.push(fallbackProvider);
44
- }
45
-
46
- config.DEFAULT_MODEL_PROVIDERS = dedupedProviders;
47
- config.MODEL_PROVIDER = dedupedProviders[0];
48
- }
49
-
50
- /**
51
- * Initializes the server configuration from config.json and command-line arguments.
52
- * @param {string[]} args - Command-line arguments.
53
- * @param {string} [configFilePath='configs/config.json'] - Path to the configuration file.
54
- * @returns {Object} The initialized configuration object.
55
- */
56
- export async function initializeConfig(args = process.argv.slice(2), configFilePath = 'configs/config.json') {
57
- const defaultConfig = {
58
- REQUIRED_API_KEY: "123456",
59
- SERVER_PORT: 3000,
60
- HOST: '0.0.0.0',
61
- MODEL_PROVIDER: MODEL_PROVIDER.GEMINI_CLI,
62
- SYSTEM_PROMPT_FILE_PATH: INPUT_SYSTEM_PROMPT_FILE, // Default value
63
- SYSTEM_PROMPT_MODE: 'append',
64
- PROXY_URL: null, // HTTP/HTTPS/SOCKS5 代理地址,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
65
- PROXY_ENABLED_PROVIDERS: [], // 启用代理的提供商列表,如 ['gemini-cli-oauth', 'claude-kiro-oauth']
66
- PROMPT_LOG_BASE_NAME: "prompt_log",
67
- PROMPT_LOG_MODE: "none",
68
- REQUEST_MAX_RETRIES: 3,
69
- REQUEST_BASE_DELAY: 1000,
70
- CREDENTIAL_SWITCH_MAX_RETRIES: 5, // 坏凭证切换最大重试次数(用于认证错误后切换凭证)
71
- CRON_NEAR_MINUTES: 15,
72
- CRON_REFRESH_TOKEN: false,
73
- LOGIN_EXPIRY: 3600, // 登录过期时间(秒),默认1小时
74
- LOGIN_MAX_ATTEMPTS: 5, // 最大失败重试次数
75
- LOGIN_LOCKOUT_DURATION: 1800, // 锁定持续时间(秒),默认30分钟
76
- LOGIN_MIN_INTERVAL: 5000, // 两次尝试之间的最小间隔(毫秒),默认1秒
77
- PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径
78
- MAX_ERROR_COUNT: 10, // 提供商最大错误次数
79
- providerFallbackChain: {}, // 跨类型 Fallback 链配置
80
- LOG_ENABLED: true,
81
- LOG_OUTPUT_MODE: "all",
82
- LOG_LEVEL: "info",
83
- LOG_DIR: "logs",
84
- LOG_INCLUDE_REQUEST_ID: true,
85
- LOG_INCLUDE_TIMESTAMP: true,
86
- LOG_MAX_FILE_SIZE: 10485760,
87
- LOG_MAX_FILES: 10,
88
- TLS_SIDECAR_ENABLED: false, // 启用 Go uTLS sidecar(需要编译 tls-sidecar 二进制)
89
- TLS_SIDECAR_ENABLED_PROVIDERS: [], // 启用 TLS Sidecar 的提供商列表
90
- TLS_SIDECAR_PORT: 9090, // sidecar 监听端口
91
- TLS_SIDECAR_BINARY_PATH: null, // 自定义二进制路径(默认自动搜索)
92
- TLS_SIDECAR_PROXY_URL: null // TLS Sidecar 专用的上游代理地址
93
- };
94
-
95
- let currentConfig = { ...defaultConfig };
96
-
97
- try {
98
- const configData = fs.readFileSync(configFilePath, 'utf8');
99
- const loadedConfig = JSON.parse(configData);
100
- Object.assign(currentConfig, loadedConfig);
101
- logger.info('[Config] Loaded configuration from configs/config.json');
102
- } catch (error) {
103
- if (error.code !== 'ENOENT') {
104
- logger.error('[Config Error] Failed to load configs/config.json:', error.message);
105
- } else {
106
- logger.info('[Config] configs/config.json not found, using default configuration.');
107
- }
108
- }
109
-
110
-
111
- // CLI argument definitions: { flag, configKey, type, validValues? }
112
- // type: 'string' | 'int' | 'bool' | 'enum'
113
- const cliArgDefs = [
114
- { flag: '--api-key', configKey: 'REQUIRED_API_KEY', type: 'string' },
115
- { flag: '--log-prompts', configKey: 'PROMPT_LOG_MODE', type: 'enum', validValues: ['console', 'file'] },
116
- { flag: '--port', configKey: 'SERVER_PORT', type: 'int' },
117
- { flag: '--model-provider', configKey: 'MODEL_PROVIDER', type: 'string' },
118
- { flag: '--system-prompt-file', configKey: 'SYSTEM_PROMPT_FILE_PATH', type: 'string' },
119
- { flag: '--system-prompt-mode', configKey: 'SYSTEM_PROMPT_MODE', type: 'enum', validValues: ['overwrite', 'append'] },
120
- { flag: '--host', configKey: 'HOST', type: 'string' },
121
- { flag: '--prompt-log-base-name', configKey: 'PROMPT_LOG_BASE_NAME', type: 'string' },
122
- { flag: '--cron-near-minutes', configKey: 'CRON_NEAR_MINUTES', type: 'int' },
123
- { flag: '--cron-refresh-token', configKey: 'CRON_REFRESH_TOKEN', type: 'bool' },
124
- { flag: '--provider-pools-file', configKey: 'PROVIDER_POOLS_FILE_PATH', type: 'string' },
125
- { flag: '--max-error-count', configKey: 'MAX_ERROR_COUNT', type: 'int' },
126
- { flag: '--login-max-attempts', configKey: 'LOGIN_MAX_ATTEMPTS', type: 'int' },
127
- { flag: '--login-lockout-duration', configKey: 'LOGIN_LOCKOUT_DURATION', type: 'int' },
128
- { flag: '--login-min-interval', configKey: 'LOGIN_MIN_INTERVAL', type: 'int' },
129
- ];
130
-
131
- // Parse command-line arguments using definitions
132
- const flagMap = new Map(cliArgDefs.map(def => [def.flag, def]));
133
- for (let i = 0; i < args.length; i++) {
134
- const def = flagMap.get(args[i]);
135
- if (!def) continue;
136
-
137
- if (i + 1 >= args.length) {
138
- logger.warn(`[Config Warning] ${def.flag} flag requires a value.`);
139
- continue;
140
- }
141
-
142
- const rawValue = args[++i];
143
- switch (def.type) {
144
- case 'string':
145
- currentConfig[def.configKey] = rawValue;
146
- break;
147
- case 'int':
148
- currentConfig[def.configKey] = parseInt(rawValue, 10);
149
- break;
150
- case 'bool':
151
- currentConfig[def.configKey] = rawValue.toLowerCase() === 'true';
152
- break;
153
- case 'enum':
154
- if (def.validValues.includes(rawValue)) {
155
- currentConfig[def.configKey] = rawValue;
156
- } else {
157
- logger.warn(`[Config Warning] Invalid value for ${def.flag}. Expected one of: ${def.validValues.join(', ')}.`);
158
- }
159
- break;
160
- }
161
- }
162
-
163
- normalizeConfiguredProviders(currentConfig);
164
-
165
- if (!currentConfig.SYSTEM_PROMPT_FILE_PATH) {
166
- currentConfig.SYSTEM_PROMPT_FILE_PATH = INPUT_SYSTEM_PROMPT_FILE;
167
- }
168
- currentConfig.SYSTEM_PROMPT_CONTENT = await getSystemPromptFileContent(currentConfig.SYSTEM_PROMPT_FILE_PATH);
169
-
170
- // 加载号池配置
171
- if (!currentConfig.PROVIDER_POOLS_FILE_PATH) {
172
- currentConfig.PROVIDER_POOLS_FILE_PATH = 'configs/provider_pools.json';
173
- }
174
- if (currentConfig.PROVIDER_POOLS_FILE_PATH) {
175
- try {
176
- const poolsData = await pfs.readFile(currentConfig.PROVIDER_POOLS_FILE_PATH, 'utf8');
177
- currentConfig.providerPools = JSON.parse(poolsData);
178
- logger.info(`[Config] Loaded provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}`);
179
- } catch (error) {
180
- logger.error(`[Config Error] Failed to load provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}: ${error.message}`);
181
- currentConfig.providerPools = {};
182
- }
183
- } else {
184
- currentConfig.providerPools = {};
185
- }
186
-
187
- // Set PROMPT_LOG_FILENAME based on the determined config
188
- if (currentConfig.PROMPT_LOG_MODE === 'file') {
189
- const now = new Date();
190
- const pad = (num) => String(num).padStart(2, '0');
191
- const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
192
- PROMPT_LOG_FILENAME = `${currentConfig.PROMPT_LOG_BASE_NAME}-${timestamp}.log`;
193
- } else {
194
- PROMPT_LOG_FILENAME = ''; // Clear if not logging to file
195
- }
196
-
197
- // Assign to the exported CONFIG
198
- Object.assign(CONFIG, currentConfig);
199
-
200
- // Initialize logger
201
- logger.initialize({
202
- enabled: CONFIG.LOG_ENABLED ?? true,
203
- outputMode: CONFIG.LOG_OUTPUT_MODE || "all",
204
- logLevel: CONFIG.LOG_LEVEL || "info",
205
- logDir: CONFIG.LOG_DIR || "logs",
206
- includeRequestId: CONFIG.LOG_INCLUDE_REQUEST_ID ?? true,
207
- includeTimestamp: CONFIG.LOG_INCLUDE_TIMESTAMP ?? true,
208
- maxFileSize: CONFIG.LOG_MAX_FILE_SIZE || 10485760,
209
- maxFiles: CONFIG.LOG_MAX_FILES || 10
210
- });
211
-
212
- // Cleanup old logs periodically
213
- logger.cleanupOldLogs();
214
-
215
- return CONFIG;
216
- }
217
-
218
- /**
219
- * Gets system prompt content from the specified file path.
220
- * @param {string} filePath - Path to the system prompt file.
221
- * @returns {Promise<string|null>} File content, or null if the file does not exist, is empty, or an error occurs.
222
- */
223
- export async function getSystemPromptFileContent(filePath) {
224
- try {
225
- await pfs.access(filePath, pfs.constants.F_OK);
226
- } catch (error) {
227
- if (error.code === 'ENOENT') {
228
- logger.warn(`[System Prompt] Specified system prompt file not found: ${filePath}`);
229
- } else {
230
- logger.error(`[System Prompt] Error accessing system prompt file ${filePath}: ${error.message}`);
231
- }
232
- return null;
233
- }
234
-
235
- try {
236
- const content = await pfs.readFile(filePath, 'utf8');
237
- if (!content.trim()) {
238
- return null;
239
- }
240
- logger.info(`[System Prompt] Loaded system prompt from ${filePath}`);
241
- return content;
242
- } catch (error) {
243
- logger.error(`[System Prompt] Error reading system prompt file ${filePath}: ${error.message}`);
244
- return null;
245
- }
246
- }
247
-
248
- export { ALL_MODEL_PROVIDERS };
249
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/core/master.js DELETED
@@ -1,395 +0,0 @@
1
- /**
2
- * 主进程 (Master Process)
3
- *
4
- * 负责管理子进程的生命周期,包括:
5
- * - 启动子进程
6
- * - 监控子进程状态
7
- * - 处理子进程重启请求
8
- * - 提供 IPC 通信
9
- *
10
- * 使用方式:
11
- * node src/core/master.js [原有的命令行参数]
12
- */
13
-
14
- import { fork } from 'child_process';
15
- import logger from '../utils/logger.js';
16
- import * as http from 'http';
17
- import * as path from 'path';
18
- import { fileURLToPath } from 'url';
19
- import { isRetryableNetworkError } from '../utils/common.js';
20
-
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = path.dirname(__filename);
23
-
24
- // 子进程实例
25
- let workerProcess = null;
26
-
27
- // 子进程状态
28
- let workerStatus = {
29
- pid: null,
30
- startTime: null,
31
- restartCount: 0,
32
- lastRestartTime: null,
33
- isRestarting: false
34
- };
35
-
36
- // 配置
37
- const config = {
38
- workerScript: path.join(__dirname, '../services/api-server.js'),
39
- maxRestartAttempts: 10,
40
- restartDelay: 1000, // 重启延迟(毫秒)
41
- masterPort: parseInt(process.env.MASTER_PORT) || 3100, // 主进程管理端口
42
- args: process.argv.slice(2) // 传递给子进程的参数
43
- };
44
-
45
- /**
46
- * 启动子进程
47
- */
48
- function startWorker() {
49
- if (workerProcess) {
50
- logger.info('[Master] Worker process already running, PID:', workerProcess.pid);
51
- return;
52
- }
53
-
54
- logger.info('[Master] Starting worker process...');
55
- logger.info('[Master] Worker script:', config.workerScript);
56
- logger.info('[Master] Worker args:', config.args.join(' '));
57
-
58
- workerProcess = fork(config.workerScript, config.args, {
59
- stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
60
- env: {
61
- ...process.env,
62
- IS_WORKER_PROCESS: 'true'
63
- }
64
- });
65
-
66
- workerStatus.pid = workerProcess.pid;
67
- workerStatus.startTime = new Date().toISOString();
68
-
69
- logger.info('[Master] Worker process started, PID:', workerProcess.pid);
70
-
71
- // 监听子进程消息
72
- workerProcess.on('message', (message) => {
73
- logger.info('[Master] Received message from worker:', message);
74
- handleWorkerMessage(message);
75
- });
76
-
77
- // 监听子进程退出
78
- workerProcess.on('exit', (code, signal) => {
79
- logger.info(`[Master] Worker process exited with code ${code}, signal ${signal}`);
80
- workerProcess = null;
81
- workerStatus.pid = null;
82
-
83
- // 如果不是主动重启导致的退出,尝试自动重启
84
- if (!workerStatus.isRestarting && code !== 0) {
85
- logger.info('[Master] Worker crashed, attempting auto-restart...');
86
- scheduleRestart();
87
- }
88
- });
89
-
90
- // 监听子进程错误
91
- workerProcess.on('error', (error) => {
92
- logger.error('[Master] Worker process error:', error.message);
93
- });
94
- }
95
-
96
- /**
97
- * 停止子进程
98
- * @param {boolean} graceful - 是否优雅关闭
99
- * @returns {Promise<void>}
100
- */
101
- function stopWorker(graceful = true) {
102
- return new Promise((resolve) => {
103
- if (!workerProcess) {
104
- logger.info('[Master] No worker process to stop');
105
- resolve();
106
- return;
107
- }
108
-
109
- logger.info('[Master] Stopping worker process, PID:', workerProcess.pid);
110
-
111
- const timeout = setTimeout(() => {
112
- if (workerProcess) {
113
- logger.info('[Master] Force killing worker process...');
114
- workerProcess.kill('SIGKILL');
115
- }
116
- resolve();
117
- }, 5000); // 5秒超时后强制杀死
118
-
119
- workerProcess.once('exit', () => {
120
- clearTimeout(timeout);
121
- workerProcess = null;
122
- workerStatus.pid = null;
123
- logger.info('[Master] Worker process stopped');
124
- resolve();
125
- });
126
-
127
- if (graceful) {
128
- // 发送优雅关闭信号
129
- workerProcess.send({ type: 'shutdown' });
130
- workerProcess.kill('SIGTERM');
131
- } else {
132
- workerProcess.kill('SIGKILL');
133
- }
134
- });
135
- }
136
-
137
- /**
138
- * 重启子进程
139
- * @returns {Promise<Object>}
140
- */
141
- async function restartWorker() {
142
- if (workerStatus.isRestarting) {
143
- logger.info('[Master] Restart already in progress');
144
- return { success: false, message: 'Restart already in progress' };
145
- }
146
-
147
- workerStatus.isRestarting = true;
148
- workerStatus.restartCount++;
149
- workerStatus.lastRestartTime = new Date().toISOString();
150
-
151
- logger.info('[Master] Restarting worker process...');
152
-
153
- try {
154
- await stopWorker(true);
155
-
156
- // 等待一小段时间确保端口释放
157
- await new Promise(resolve => setTimeout(resolve, config.restartDelay));
158
-
159
- startWorker();
160
- workerStatus.isRestarting = false;
161
-
162
- return {
163
- success: true,
164
- message: 'Worker restarted successfully',
165
- pid: workerStatus.pid,
166
- restartCount: workerStatus.restartCount
167
- };
168
- } catch (error) {
169
- workerStatus.isRestarting = false;
170
- logger.error('[Master] Failed to restart worker:', error.message);
171
- return {
172
- success: false,
173
- message: 'Failed to restart worker: ' + error.message
174
- };
175
- }
176
- }
177
-
178
- /**
179
- * 计划重启(用于崩溃后自动重启)
180
- */
181
- function scheduleRestart() {
182
- if (workerStatus.restartCount >= config.maxRestartAttempts) {
183
- logger.error('[Master] Max restart attempts reached, giving up');
184
- return;
185
- }
186
-
187
- const delay = Math.min(config.restartDelay * Math.pow(2, workerStatus.restartCount), 30000);
188
- logger.info(`[Master] Scheduling restart in ${delay}ms...`);
189
-
190
- setTimeout(() => {
191
- restartWorker();
192
- }, delay);
193
- }
194
-
195
- /**
196
- * 处理来自子进程的消息
197
- * @param {Object} message - 消息对象
198
- */
199
- function handleWorkerMessage(message) {
200
- if (!message || !message.type) return;
201
-
202
- switch (message.type) {
203
- case 'ready':
204
- logger.info('[Master] Worker is ready');
205
- break;
206
- case 'restart_request':
207
- logger.info('[Master] Worker requested restart');
208
- restartWorker();
209
- break;
210
- case 'status':
211
- logger.info('[Master] Worker status:', message.data);
212
- break;
213
- default:
214
- logger.info('[Master] Unknown message type:', message.type);
215
- }
216
- }
217
-
218
- /**
219
- * 获取状态信息
220
- * @returns {Object}
221
- */
222
- function getStatus() {
223
- return {
224
- master: {
225
- pid: process.pid,
226
- uptime: process.uptime(),
227
- memoryUsage: process.memoryUsage()
228
- },
229
- worker: {
230
- pid: workerStatus.pid,
231
- startTime: workerStatus.startTime,
232
- restartCount: workerStatus.restartCount,
233
- lastRestartTime: workerStatus.lastRestartTime,
234
- isRestarting: workerStatus.isRestarting,
235
- isRunning: workerProcess !== null
236
- }
237
- };
238
- }
239
-
240
- /**
241
- * 创建主进程管理 HTTP 服务器
242
- */
243
- function createMasterServer() {
244
- const server = http.createServer(async (req, res) => {
245
- const url = new URL(req.url, `http://${req.headers.host}`);
246
- const path = url.pathname;
247
- const method = req.method;
248
-
249
- // 设置 CORS 头
250
- res.setHeader('Access-Control-Allow-Origin', '*');
251
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
252
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
253
-
254
- if (method === 'OPTIONS') {
255
- res.writeHead(204);
256
- res.end();
257
- return;
258
- }
259
-
260
- // 状态端点
261
- if (method === 'GET' && path === '/master/status') {
262
- res.writeHead(200, { 'Content-Type': 'application/json' });
263
- res.end(JSON.stringify(getStatus()));
264
- return;
265
- }
266
-
267
- // 重启端点
268
- if (method === 'POST' && path === '/master/restart') {
269
- logger.info('[Master] Restart requested via API');
270
- const result = await restartWorker();
271
- res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' });
272
- res.end(JSON.stringify(result));
273
- return;
274
- }
275
-
276
- // 停止端点
277
- if (method === 'POST' && path === '/master/stop') {
278
- logger.info('[Master] Stop requested via API');
279
- await stopWorker(true);
280
- res.writeHead(200, { 'Content-Type': 'application/json' });
281
- res.end(JSON.stringify({ success: true, message: 'Worker stopped' }));
282
- return;
283
- }
284
-
285
- // 启动端点
286
- if (method === 'POST' && path === '/master/start') {
287
- logger.info('[Master] Start requested via API');
288
- if (workerProcess) {
289
- res.writeHead(400, { 'Content-Type': 'application/json' });
290
- res.end(JSON.stringify({ success: false, message: 'Worker already running' }));
291
- return;
292
- }
293
- startWorker();
294
- res.writeHead(200, { 'Content-Type': 'application/json' });
295
- res.end(JSON.stringify({ success: true, message: 'Worker started', pid: workerStatus.pid }));
296
- return;
297
- }
298
-
299
- // 健康检查
300
- if (method === 'GET' && path === '/master/health') {
301
- res.writeHead(200, { 'Content-Type': 'application/json' });
302
- res.end(JSON.stringify({
303
- status: 'healthy',
304
- workerRunning: workerProcess !== null,
305
- timestamp: new Date().toISOString()
306
- }));
307
- return;
308
- }
309
-
310
- // 404
311
- res.writeHead(404, { 'Content-Type': 'application/json' });
312
- res.end(JSON.stringify({ error: 'Not Found' }));
313
- });
314
-
315
- server.listen(config.masterPort, () => {
316
- logger.info(`[Master] Management server listening on port ${config.masterPort}`);
317
- logger.info(`[Master] Available endpoints:`);
318
- logger.info(` GET /master/status - Get master and worker status`);
319
- logger.info(` GET /master/health - Health check`);
320
- logger.info(` POST /master/restart - Restart worker process`);
321
- logger.info(` POST /master/stop - Stop worker process`);
322
- logger.info(` POST /master/start - Start worker process`);
323
- });
324
-
325
- return server;
326
- }
327
-
328
- /**
329
- * 处理进程信号
330
- */
331
- function setupSignalHandlers() {
332
- // 优雅关闭
333
- process.on('SIGTERM', async () => {
334
- logger.info('[Master] Received SIGTERM, shutting down...');
335
- await stopWorker(true);
336
- process.exit(0);
337
- });
338
-
339
- process.on('SIGINT', async () => {
340
- logger.info('[Master] Received SIGINT, shutting down...');
341
- await stopWorker(true);
342
- process.exit(0);
343
- });
344
-
345
- // 未捕获的异常
346
- process.on('uncaughtException', (error) => {
347
- logger.error('[Master] Uncaught exception:', error);
348
-
349
- // 检查是否为可重试的网络错误
350
- if (isRetryableNetworkError(error)) {
351
- logger.warn('[Master] Network error detected, continuing operation...');
352
- return; // 不退出程序,继续运行
353
- }
354
-
355
- // 对于其他严重错误,记录但不退出(由主进程管理子进程)
356
- logger.error('[Master] Fatal error detected in master process');
357
- });
358
-
359
- process.on('unhandledRejection', (reason, promise) => {
360
- logger.error('[Master] Unhandled rejection at:', promise, 'reason:', reason);
361
-
362
- // 检查是否为可重试的网络错误
363
- if (reason && isRetryableNetworkError(reason)) {
364
- logger.warn('[Master] Network error in promise rejection, continuing operation...');
365
- return; // 不退出程序,继续运行
366
- }
367
- });
368
- }
369
-
370
- /**
371
- * 主函数
372
- */
373
- async function main() {
374
- logger.info('='.repeat(50));
375
- logger.info('[Master] AIClient2API Master Process');
376
- logger.info('[Master] PID:', process.pid);
377
- logger.info('[Master] Node version:', process.version);
378
- logger.info('[Master] Working directory:', process.cwd());
379
- logger.info('='.repeat(50));
380
-
381
- // 设置信号处理
382
- setupSignalHandlers();
383
-
384
- // 创建管理服务器
385
- createMasterServer();
386
-
387
- // 启动子进程
388
- startWorker();
389
- }
390
-
391
- // 启动主进程
392
- main().catch(error => {
393
- logger.error('[Master] Failed to start:', error);
394
- process.exit(1);
395
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/core/plugin-manager.js DELETED
@@ -1,549 +0,0 @@
1
- /**
2
- * 插件管理器 - 可插拔插件系统核心
3
- *
4
- * 功能:
5
- * 1. 插件注册与加载
6
- * 2. 生命周期管理(init/destroy)
7
- * 3. 扩展点管理(中间件、路由、钩子)
8
- * 4. 插件配置管理
9
- */
10
-
11
- import { promises as fs } from 'fs';
12
- import logger from '../utils/logger.js';
13
- import { existsSync } from 'fs';
14
- import path from 'path';
15
-
16
- // 插件配置文件路径
17
- const PLUGINS_CONFIG_FILE = path.join(process.cwd(), 'configs', 'plugins.json');
18
-
19
- // 默认禁用的插件列表
20
- const DEFAULT_DISABLED_PLUGINS = ['api-potluck', 'ai-monitor'];
21
-
22
- /**
23
- * 插件类型常量
24
- */
25
- export const PLUGIN_TYPE = {
26
- AUTH: 'auth', // 认证插件,参与认证流程
27
- MIDDLEWARE: 'middleware' // 普通中间件,不参与认证
28
- };
29
-
30
- /**
31
- * 插件接口定义(JSDoc 类型)
32
- * @typedef {Object} Plugin
33
- * @property {string} name - 插件名称(唯一标识)
34
- * @property {string} version - 插件版本
35
- * @property {string} [description] - 插件描述
36
- * @property {string} [type] - 插件类型:'auth'(认证插件)或 'middleware'(普通中间件,默认)
37
- * @property {boolean} [enabled] - 是否启用(默认 true)
38
- * @property {number} [_priority] - 优先级,数字越小越先执行(默认 100)
39
- * @property {boolean} [_builtin] - 是否为内置插件(内置插件最后执行)
40
- * @property {Function} [init] - 初始化钩子 (config) => Promise<void>
41
- * @property {Function} [destroy] - 销毁钩子 () => Promise<void>
42
- * @property {Function} [middleware] - 请求中间件 (req, res, requestUrl, config) => Promise<{handled: boolean, data?: Object}>
43
- * @property {Function} [authenticate] - 认证方法(仅 type='auth' 时有效)(req, res, requestUrl, config) => Promise<{handled: boolean, authorized: boolean|null, error?: Object, data?: Object}>
44
- * @property {Array<{method: string, path: string|RegExp, handler: Function}>} [routes] - 路由定义
45
- * @property {string[]} [staticPaths] - 静态文件路径(相对于 static 目录)
46
- * @property {Object} [hooks] - 钩子函数
47
- * @property {Function} [hooks.onBeforeRequest] - 请求前钩子 (req, config) => Promise<void>
48
- * @property {Function} [hooks.onAfterResponse] - 响应后钩子 (req, res, config) => Promise<void>
49
- * @property {Function} [hooks.onContentGenerated] - 内容生成后钩子 (config) => Promise<void>
50
- */
51
-
52
- /**
53
- * 插件管理器类
54
- */
55
- class PluginManager {
56
- constructor() {
57
- /** @type {Map<string, Plugin>} */
58
- this.plugins = new Map();
59
- /** @type {Object} */
60
- this.pluginsConfig = { plugins: {} };
61
- /** @type {boolean} */
62
- this.initialized = false;
63
- }
64
-
65
- /**
66
- * 加载插件配置文件
67
- * 永远生成默认配置,如果本地文件存在则合并,但不覆盖 enabled 字段
68
- */
69
- async loadConfig() {
70
- try {
71
- // 1. 永远生成默认配置
72
- const defaultConfig = await this.generateDefaultConfig();
73
-
74
- // 2. 如果本地文件存在,读取并合并
75
- if (existsSync(PLUGINS_CONFIG_FILE)) {
76
- const content = await fs.readFile(PLUGINS_CONFIG_FILE, 'utf8');
77
- const localConfig = JSON.parse(content);
78
-
79
- // 3. 合并配置:遍历默认配置中的所有插件
80
- for (const [pluginName, defaultPluginConfig] of Object.entries(defaultConfig.plugins)) {
81
- const localPluginConfig = localConfig.plugins?.[pluginName];
82
-
83
- if (localPluginConfig) {
84
- // 本地配置存在,合并但保留本地的 enabled 字段
85
- defaultConfig.plugins[pluginName] = {
86
- ...defaultPluginConfig,
87
- ...localPluginConfig,
88
- enabled: localPluginConfig.enabled // 保留本地的 enabled 字段
89
- };
90
- }
91
- // 如果本地配置不存在该插件,使用默认配置
92
- }
93
- }
94
-
95
- this.pluginsConfig = defaultConfig;
96
- await this.saveConfig();
97
- } catch (error) {
98
- logger.error('[PluginManager] Failed to load config:', error.message);
99
- this.pluginsConfig = { plugins: {} };
100
- }
101
- }
102
-
103
- /**
104
- * 扫描 plugins 目录生成默认配置
105
- * @returns {Promise<Object>} 默认插件配置
106
- */
107
- async generateDefaultConfig() {
108
- const defaultConfig = { plugins: {} };
109
- const pluginsDir = path.join(process.cwd(), 'src', 'plugins');
110
-
111
- try {
112
- if (!existsSync(pluginsDir)) {
113
- return defaultConfig;
114
- }
115
-
116
- const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
117
-
118
- for (const entry of entries) {
119
- if (!entry.isDirectory()) continue;
120
-
121
- const pluginPath = path.join(pluginsDir, entry.name, 'index.js');
122
- if (!existsSync(pluginPath)) continue;
123
-
124
- try {
125
- // 动态导入插件以获取其元信息
126
- const pluginModule = await import(`file://${pluginPath}`);
127
- const plugin = pluginModule.default || pluginModule;
128
-
129
- if (plugin && plugin.name) {
130
- // 检查是否在默认禁用列表中
131
- const enabled = !DEFAULT_DISABLED_PLUGINS.includes(plugin.name);
132
- defaultConfig.plugins[plugin.name] = {
133
- enabled: enabled,
134
- description: plugin.description || ''
135
- };
136
- logger.info(`[PluginManager] Found plugin for default config: ${plugin.name}`);
137
- }
138
- } catch (importError) {
139
- // 如果导入失败,使用目录名作为插件名
140
- // 检查是否在默认禁用列表中
141
- const enabled = !DEFAULT_DISABLED_PLUGINS.includes(entry.name);
142
- defaultConfig.plugins[entry.name] = {
143
- enabled: enabled,
144
- description: ''
145
- };
146
- logger.warn(`[PluginManager] Could not import plugin ${entry.name}, using directory name:`, importError.message);
147
- }
148
- }
149
- } catch (error) {
150
- logger.error('[PluginManager] Failed to scan plugins directory:', error.message);
151
- }
152
-
153
- return defaultConfig;
154
- }
155
-
156
- /**
157
- * 保存插件配置文件
158
- */
159
- async saveConfig() {
160
- try {
161
- const dir = path.dirname(PLUGINS_CONFIG_FILE);
162
- if (!existsSync(dir)) {
163
- await fs.mkdir(dir, { recursive: true });
164
- }
165
- await fs.writeFile(PLUGINS_CONFIG_FILE, JSON.stringify(this.pluginsConfig, null, 2), 'utf8');
166
- } catch (error) {
167
- logger.error('[PluginManager] Failed to save config:', error.message);
168
- }
169
- }
170
-
171
- /**
172
- * 注册插件
173
- * @param {Plugin} plugin - 插件对象
174
- */
175
- register(plugin) {
176
- if (!plugin.name) {
177
- throw new Error('Plugin must have a name');
178
- }
179
- if (this.plugins.has(plugin.name)) {
180
- logger.warn(`[PluginManager] Plugin "${plugin.name}" is already registered, skipping`);
181
- return;
182
- }
183
- this.plugins.set(plugin.name, plugin);
184
- logger.info(`[PluginManager] Registered plugin: ${plugin.name} v${plugin.version || '1.0.0'}`);
185
- }
186
-
187
- /**
188
- * 初始化所有已启用的插件
189
- * @param {Object} config - 服务器配置
190
- */
191
- async initAll(config) {
192
- await this.loadConfig();
193
-
194
- for (const [name, plugin] of this.plugins) {
195
- const pluginConfig = this.pluginsConfig.plugins[name] || {};
196
- const enabled = pluginConfig.enabled !== false; // 默认启用
197
-
198
- if (!enabled) {
199
- logger.info(`[PluginManager] Plugin "${name}" is disabled, skipping init`);
200
- continue;
201
- }
202
-
203
- try {
204
- if (typeof plugin.init === 'function') {
205
- await plugin.init(config);
206
- logger.info(`[PluginManager] Initialized plugin: ${name}`);
207
- }
208
- plugin._enabled = true;
209
- } catch (error) {
210
- logger.error(`[PluginManager] Failed to init plugin "${name}":`, error.message);
211
- plugin._enabled = false;
212
- }
213
- }
214
-
215
- this.initialized = true;
216
- }
217
-
218
- /**
219
- * 销毁所有插件
220
- */
221
- async destroyAll() {
222
- for (const [name, plugin] of this.plugins) {
223
- if (!plugin._enabled) continue;
224
-
225
- try {
226
- if (typeof plugin.destroy === 'function') {
227
- await plugin.destroy();
228
- logger.info(`[PluginManager] Destroyed plugin: ${name}`);
229
- }
230
- } catch (error) {
231
- logger.error(`[PluginManager] Failed to destroy plugin "${name}":`, error.message);
232
- }
233
- }
234
- this.initialized = false;
235
- }
236
-
237
- /**
238
- * 检查插件是否启用
239
- * @param {string} name - 插件名称
240
- * @returns {boolean}
241
- */
242
- isEnabled(name) {
243
- const plugin = this.plugins.get(name);
244
- return plugin && plugin._enabled === true;
245
- }
246
-
247
- /**
248
- * 获取所有启用的插件(按优先级排序)
249
- * 优先级数字越小越先执行,内置插件(_builtin: true)最后执行
250
- * @returns {Plugin[]}
251
- */
252
- getEnabledPlugins() {
253
- return Array.from(this.plugins.values())
254
- .filter(p => p._enabled)
255
- .sort((a, b) => {
256
- // 内置插件排在最后
257
- const aBuiltin = a._builtin ? 1 : 0;
258
- const bBuiltin = b._builtin ? 1 : 0;
259
- if (aBuiltin !== bBuiltin) return aBuiltin - bBuiltin;
260
-
261
- // 按优先级排序(数字越小越先执行)
262
- const aPriority = a._priority || 100;
263
- const bPriority = b._priority || 100;
264
- return aPriority - bPriority;
265
- });
266
- }
267
-
268
- /**
269
- * 获取所有认证插件(按优先级排序)
270
- * @returns {Plugin[]}
271
- */
272
- getAuthPlugins() {
273
- return this.getEnabledPlugins().filter(p =>
274
- p.type === PLUGIN_TYPE.AUTH && typeof p.authenticate === 'function'
275
- );
276
- }
277
-
278
- /**
279
- * 获取所有普通中间件插件(按优先级排序)
280
- * @returns {Plugin[]}
281
- */
282
- getMiddlewarePlugins() {
283
- return this.getEnabledPlugins().filter(p =>
284
- p.type !== PLUGIN_TYPE.AUTH && typeof p.middleware === 'function'
285
- );
286
- }
287
-
288
- /**
289
- * 执行认证流程
290
- * 只有 type='auth' 的插件会参与认证
291
- *
292
- * 认证插件返回值说明:
293
- * - { handled: true } - 请求已被处理(如发送了错误响应),停止后续处理
294
- * - { authorized: true, data: {...} } - 认证成功,可附带数据
295
- * - { authorized: false } - 认证失败,已发送错误响应
296
- * - { authorized: null } - 此插件不处理该请求,继续下一个认证插件
297
- *
298
- * @param {http.IncomingMessage} req - HTTP 请求
299
- * @param {http.ServerResponse} res - HTTP 响应
300
- * @param {URL} requestUrl - 解析后的 URL
301
- * @param {Object} config - 服务器配置
302
- * @returns {Promise<{handled: boolean, authorized: boolean}>}
303
- */
304
- async executeAuth(req, res, requestUrl, config) {
305
- const authPlugins = this.getAuthPlugins();
306
-
307
- for (const plugin of authPlugins) {
308
- try {
309
- const result = await plugin.authenticate(req, res, requestUrl, config);
310
-
311
- if (!result) continue;
312
-
313
- // 如果请求已被处理(如发送了错误响应),停止执行
314
- if (result.handled) {
315
- return { handled: true, authorized: false };
316
- }
317
-
318
- // 如果认证失败,停止执行
319
- if (result.authorized === false) {
320
- return { handled: true, authorized: false };
321
- }
322
-
323
- // 如果认证成功,合并数据并返回
324
- if (result.authorized === true) {
325
- if (result.data) {
326
- Object.assign(config, result.data);
327
- }
328
- return { handled: false, authorized: true };
329
- }
330
-
331
- // authorized === null 表示此插件不处理,继续下一个
332
- } catch (error) {
333
- logger.error(`[PluginManager] Auth error in plugin "${plugin.name}":`, error.message);
334
- }
335
- }
336
-
337
- // 没有任何认证插件处理,返回未授权
338
- return { handled: false, authorized: false };
339
- }
340
-
341
- /**
342
- * 执行普通中间件
343
- * 只有 type!='auth' 的插件会执行
344
- *
345
- * 中间件返回值说明:
346
- * - { handled: true } - 请求已被处理,停止后续处理
347
- * - { handled: false, data: {...} } - 继续处理,可附带数据
348
- * - null/undefined - 继续执行下一个中间件
349
- *
350
- * @param {http.IncomingMessage} req - HTTP 请求
351
- * @param {http.ServerResponse} res - HTTP 响应
352
- * @param {URL} requestUrl - 解析后的 URL
353
- * @param {Object} config - 服务器配置
354
- * @returns {Promise<{handled: boolean}>}
355
- */
356
- async executeMiddleware(req, res, requestUrl, config) {
357
- const middlewarePlugins = this.getMiddlewarePlugins();
358
-
359
- for (const plugin of middlewarePlugins) {
360
- try {
361
- const result = await plugin.middleware(req, res, requestUrl, config);
362
-
363
- if (!result) continue;
364
-
365
- // 如果请求已被处理,停止执行
366
- if (result.handled) {
367
- return { handled: true };
368
- }
369
-
370
- // 合并数据
371
- if (result.data) {
372
- Object.assign(config, result.data);
373
- }
374
- } catch (error) {
375
- logger.error(`[PluginManager] Middleware error in plugin "${plugin.name}":`, error.message);
376
- }
377
- }
378
-
379
- return { handled: false };
380
- }
381
-
382
- /**
383
- * 执行所有插件的路由处理
384
- * @param {string} method - HTTP 方法
385
- * @param {string} path - 请求路径
386
- * @param {http.IncomingMessage} req - HTTP 请求
387
- * @param {http.ServerResponse} res - HTTP 响应
388
- * @returns {Promise<boolean>} - 是否已处理
389
- */
390
- async executeRoutes(method, path, req, res) {
391
- for (const plugin of this.getEnabledPlugins()) {
392
- if (!Array.isArray(plugin.routes)) continue;
393
-
394
- for (const route of plugin.routes) {
395
- const methodMatch = route.method === '*' || route.method.toUpperCase() === method;
396
- if (!methodMatch) continue;
397
-
398
- let pathMatch = false;
399
- if (route.path instanceof RegExp) {
400
- pathMatch = route.path.test(path);
401
- } else if (typeof route.path === 'string') {
402
- pathMatch = path === route.path || path.startsWith(route.path + '/');
403
- }
404
-
405
- if (pathMatch) {
406
- try {
407
- const handled = await route.handler(method, path, req, res);
408
- if (handled) return true;
409
- } catch (error) {
410
- logger.error(`[PluginManager] Route error in plugin "${plugin.name}":`, error.message);
411
- }
412
- }
413
- }
414
- }
415
- return false;
416
- }
417
-
418
- /**
419
- * 获取所有插件的静态文件路径
420
- * @returns {string[]}
421
- */
422
- getStaticPaths() {
423
- const paths = [];
424
- for (const plugin of this.getEnabledPlugins()) {
425
- if (Array.isArray(plugin.staticPaths)) {
426
- paths.push(...plugin.staticPaths);
427
- }
428
- }
429
- return paths;
430
- }
431
-
432
- /**
433
- * 检查路径是否是插件静态文件
434
- * @param {string} path - 请求路径
435
- * @returns {boolean}
436
- */
437
- isPluginStaticPath(path) {
438
- const staticPaths = this.getStaticPaths();
439
- return staticPaths.some(sp => path === sp || path === '/' + sp);
440
- }
441
-
442
- /**
443
- * 执行钩子函数
444
- * @param {string} hookName - 钩子名称
445
- * @param {...any} args - 钩子参数
446
- */
447
- async executeHook(hookName, ...args) {
448
- for (const plugin of this.getEnabledPlugins()) {
449
- if (!plugin.hooks || typeof plugin.hooks[hookName] !== 'function') continue;
450
-
451
- try {
452
- await plugin.hooks[hookName](...args);
453
- } catch (error) {
454
- logger.error(`[PluginManager] Hook "${hookName}" error in plugin "${plugin.name}":`, error.message);
455
- }
456
- }
457
- }
458
-
459
- /**
460
- * 获取插件列表(用于 API)
461
- * @returns {Object[]}
462
- */
463
- getPluginList() {
464
- const list = [];
465
- for (const [name, plugin] of this.plugins) {
466
- const pluginConfig = this.pluginsConfig.plugins[name] || {};
467
- list.push({
468
- name: plugin.name,
469
- version: plugin.version || '1.0.0',
470
- description: plugin.description || pluginConfig.description || '',
471
- enabled: plugin._enabled === true,
472
- hasMiddleware: typeof plugin.middleware === 'function',
473
- hasRoutes: Array.isArray(plugin.routes) && plugin.routes.length > 0,
474
- hasHooks: plugin.hooks && Object.keys(plugin.hooks).length > 0
475
- });
476
- }
477
- return list;
478
- }
479
-
480
- /**
481
- * 启用/禁用插件
482
- * @param {string} name - 插件名称
483
- * @param {boolean} enabled - 是否启用
484
- */
485
- async setPluginEnabled(name, enabled) {
486
- if (!this.pluginsConfig.plugins[name]) {
487
- this.pluginsConfig.plugins[name] = {};
488
- }
489
- this.pluginsConfig.plugins[name].enabled = enabled;
490
- await this.saveConfig();
491
-
492
- const plugin = this.plugins.get(name);
493
- if (plugin) {
494
- plugin._enabled = enabled;
495
- }
496
- }
497
- }
498
-
499
- // 单例实例
500
- const pluginManager = new PluginManager();
501
-
502
- /**
503
- * 自动发现并加载插件
504
- * 扫描 src/plugins/ 目录下的所有插件
505
- */
506
- export async function discoverPlugins() {
507
- const pluginsDir = path.join(process.cwd(), 'src', 'plugins');
508
-
509
- try {
510
- if (!existsSync(pluginsDir)) {
511
- await fs.mkdir(pluginsDir, { recursive: true });
512
- logger.info('[PluginManager] Created plugins directory');
513
- }
514
-
515
- const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
516
-
517
- for (const entry of entries) {
518
- if (!entry.isDirectory()) continue;
519
-
520
- const pluginPath = path.join(pluginsDir, entry.name, 'index.js');
521
- if (!existsSync(pluginPath)) continue;
522
-
523
- try {
524
- // 动态导入插件
525
- const pluginModule = await import(`file://${pluginPath}`);
526
- const plugin = pluginModule.default || pluginModule;
527
-
528
- if (plugin && plugin.name) {
529
- pluginManager.register(plugin);
530
- }
531
- } catch (error) {
532
- logger.error(`[PluginManager] Failed to load plugin from ${entry.name}:`, error.message);
533
- }
534
- }
535
- } catch (error) {
536
- logger.error('[PluginManager] Failed to discover plugins:', error.message);
537
- }
538
- }
539
-
540
- /**
541
- * 获取插件管理器实例
542
- * @returns {PluginManager}
543
- */
544
- export function getPluginManager() {
545
- return pluginManager;
546
- }
547
-
548
- // 导出类和实例
549
- export { PluginManager, pluginManager };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/handlers/request-handler.js DELETED
@@ -1,271 +0,0 @@
1
- import deepmerge from 'deepmerge';
2
- import logger from '../utils/logger.js';
3
- import { handleError, getClientIp } from '../utils/common.js';
4
- import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js';
5
- import { handleAPIRequests } from '../services/api-manager.js';
6
- import { getApiService, getProviderStatus } from '../services/service-manager.js';
7
- import { getProviderPoolManager } from '../services/service-manager.js';
8
- import { MODEL_PROVIDER } from '../utils/common.js';
9
- import { getRegisteredProviders } from '../providers/adapter.js';
10
- import { countTokensAnthropic } from '../utils/token-utils.js';
11
- import { PROMPT_LOG_FILENAME } from '../core/config-manager.js';
12
- import { getPluginManager } from '../core/plugin-manager.js';
13
- import { randomUUID } from 'crypto';
14
- import { handleGrokAssetsProxy } from '../utils/grok-assets-proxy.js';
15
-
16
- /**
17
- * Generate a short unique request ID (8 characters)
18
- */
19
- function generateRequestId() {
20
- return randomUUID().slice(0, 8);
21
- }
22
-
23
- /**
24
- * Parse request body as JSON
25
- */
26
- function parseRequestBody(req) {
27
- return new Promise((resolve, reject) => {
28
- let body = '';
29
- req.on('data', chunk => { body += chunk.toString(); });
30
- req.on('end', () => {
31
- try {
32
- resolve(body ? JSON.parse(body) : {});
33
- } catch (e) {
34
- reject(new Error('Invalid JSON in request body'));
35
- }
36
- });
37
- req.on('error', reject);
38
- });
39
- }
40
-
41
- /**
42
- * Main request handler. It authenticates the request, determines the endpoint type,
43
- * and delegates to the appropriate specialized handler function.
44
- * @param {Object} config - The server configuration
45
- * @param {Object} providerPoolManager - The provider pool manager instance
46
- * @returns {Function} - The request handler function
47
- */
48
- export function createRequestHandler(config, providerPoolManager) {
49
- return async function requestHandler(req, res) {
50
- // Generate unique request ID and set it in logger context
51
- const clientIp = getClientIp(req);
52
- const requestId = `${clientIp}:${generateRequestId()}`;
53
-
54
- return logger.runWithContext(requestId, async () => {
55
- // Deep copy the config for each request to allow dynamic modification
56
- const currentConfig = deepmerge({}, config);
57
-
58
- // 计算当前请求的基础 URL
59
- const protocol = req.socket.encrypted || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
60
- const host = req.headers.host;
61
- currentConfig.requestBaseUrl = `${protocol}://${host}`;
62
-
63
- const requestUrl = new URL(req.url, `http://${req.headers.host}`);
64
- let path = requestUrl.pathname;
65
- const method = req.method;
66
-
67
- try {
68
- // Set CORS headers for all requests
69
- res.setHeader('Access-Control-Allow-Origin', '*');
70
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
71
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-goog-api-key, Model-Provider, X-Requested-With, Accept, Origin');
72
- res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours cache for preflight
73
-
74
- // Handle CORS preflight requests
75
- if (method === 'OPTIONS') {
76
- res.writeHead(204);
77
- res.end();
78
- return;
79
- }
80
-
81
- // Serve static files for UI (除了登录页面需要认证)
82
- // 检查是否是插件静态文件
83
- const pluginManager = getPluginManager();
84
- const isPluginStatic = pluginManager.isPluginStaticPath(path);
85
- if (path.startsWith('/static/') || path === '/' || path === '/favicon.ico' || path === '/index.html' || path.startsWith('/app/') || path.startsWith('/components/') || path === '/login.html' || isPluginStatic) {
86
- const served = await serveStaticFiles(path, res);
87
- if (served) return;
88
- }
89
-
90
- // 执行插件路由
91
- const pluginRouteHandled = await pluginManager.executeRoutes(method, path, req, res);
92
- if (pluginRouteHandled) return;
93
-
94
- const uiHandled = await handleUIApiRequests(method, path, req, res, currentConfig, providerPoolManager);
95
- if (uiHandled) return;
96
-
97
- // logger.info(`\n${new Date().toLocaleString()}`);
98
- logger.info(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`);
99
-
100
- // Health check endpoint
101
- if (method === 'GET' && path === '/health') {
102
- res.writeHead(200, { 'Content-Type': 'application/json' });
103
- res.end(JSON.stringify({
104
- status: 'healthy',
105
- timestamp: new Date().toISOString(),
106
- provider: currentConfig.MODEL_PROVIDER
107
- }));
108
- return true;
109
- }
110
-
111
- // Grok assets proxy endpoint
112
- if (method === 'GET' && path === '/api/grok/assets') {
113
- await handleGrokAssetsProxy(req, res, currentConfig, providerPoolManager);
114
- return true;
115
- }
116
-
117
- // providers health endpoint
118
- // url params: provider[string], customName[string], unhealthRatioThreshold[float]
119
- // 支持provider, customName过滤记录
120
- // 支持unhealthRatioThreshold控制不健康比例的阈值, 当unhealthyRatio超过阈值返回summaryHealthy: false
121
- if (method === 'GET' && path === '/provider_health') {
122
- try {
123
- const provider = requestUrl.searchParams.get('provider');
124
- const customName = requestUrl.searchParams.get('customName');
125
- let unhealthRatioThreshold = requestUrl.searchParams.get('unhealthRatioThreshold');
126
- unhealthRatioThreshold = unhealthRatioThreshold === null ? 0.0001 : parseFloat(unhealthRatioThreshold);
127
- let provideStatus = await getProviderStatus(currentConfig, { provider, customName });
128
- let summaryHealth = true;
129
- if (!isNaN(unhealthRatioThreshold)) {
130
- summaryHealth = provideStatus.unhealthyRatio <= unhealthRatioThreshold;
131
- }
132
- res.writeHead(200, { 'Content-Type': 'application/json' });
133
- res.end(JSON.stringify({
134
- timestamp: new Date().toISOString(),
135
- items: provideStatus.providerPoolsSlim,
136
- count: provideStatus.count,
137
- unhealthyCount: provideStatus.unhealthyCount,
138
- unhealthyRatio: provideStatus.unhealthyRatio,
139
- unhealthySummeryMessage: provideStatus.unhealthySummeryMessage,
140
- summaryHealth
141
- }));
142
- return true;
143
- } catch (error) {
144
- logger.info(`[Server] req provider_health error: ${error.message}`);
145
- handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER);
146
- return;
147
- }
148
- }
149
-
150
-
151
- // Handle API requests
152
- // Allow overriding MODEL_PROVIDER via request header
153
- const modelProviderHeader = req.headers['model-provider'];
154
- if (modelProviderHeader) {
155
- const registeredProviders = getRegisteredProviders();
156
- if (registeredProviders.includes(modelProviderHeader)) {
157
- currentConfig.MODEL_PROVIDER = modelProviderHeader;
158
- logger.info(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`);
159
- } else {
160
- logger.warn(`[Config] Provider ${modelProviderHeader} in header is not available.`);
161
- res.writeHead(400, { 'Content-Type': 'application/json' });
162
- res.end(JSON.stringify({ error: { message: `Provider ${modelProviderHeader} is not available.` } }));
163
- return;
164
- }
165
- }
166
-
167
- // Check if the first path segment matches a MODEL_PROVIDER and switch if it does
168
- const pathSegments = path.split('/').filter(segment => segment.length > 0);
169
-
170
- if (pathSegments.length > 0) {
171
- const firstSegment = pathSegments[0];
172
- const registeredProviders = getRegisteredProviders();
173
- const isValidProvider = registeredProviders.includes(firstSegment);
174
- const isAutoMode = firstSegment === MODEL_PROVIDER.AUTO;
175
-
176
- if (firstSegment && (isValidProvider || isAutoMode)) {
177
- currentConfig.MODEL_PROVIDER = firstSegment;
178
- logger.info(`[Config] MODEL_PROVIDER overridden by path segment to: ${currentConfig.MODEL_PROVIDER}`);
179
- pathSegments.shift();
180
- path = '/' + pathSegments.join('/');
181
- requestUrl.pathname = path;
182
- } else if (firstSegment && Object.values(MODEL_PROVIDER).includes(firstSegment)) {
183
- // 如果在 MODEL_PROVIDER 中但没注册适配器,拦截并报错
184
- logger.warn(`[Config] Provider ${firstSegment} is recognized but no adapter is registered.`);
185
- res.writeHead(400, { 'Content-Type': 'application/json' });
186
- res.end(JSON.stringify({ error: { message: `Provider ${firstSegment} is not available.` } }));
187
- return;
188
- } else if (firstSegment && !isValidProvider) {
189
- logger.info(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`);
190
- }
191
- }
192
-
193
- // 1. 执行认证流程(只有 type='auth' 的插件参与)
194
- const authResult = await pluginManager.executeAuth(req, res, requestUrl, currentConfig);
195
- if (authResult.handled) {
196
- // 认证插件已处理请求(如发送了错误响应)
197
- return;
198
- }
199
- if (!authResult.authorized) {
200
- // 没有认证插件授权,返回 401
201
- res.writeHead(401, { 'Content-Type': 'application/json' });
202
- res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing.' } }));
203
- return;
204
- }
205
-
206
- // 2. 执行普通中间件(type!='auth' 的插件)
207
- const middlewareResult = await pluginManager.executeMiddleware(req, res, requestUrl, currentConfig);
208
- if (middlewareResult.handled) {
209
- // 中间件已处理请求
210
- return;
211
- }
212
-
213
- // Handle count_tokens requests (Anthropic API compatible)
214
- if (path.includes('/count_tokens') && method === 'POST') {
215
- try {
216
- const body = await parseRequestBody(req);
217
- logger.info(`[Server] Handling count_tokens request for model: ${body.model}`);
218
-
219
- // Use common utility method directly
220
- try {
221
- const result = countTokensAnthropic(body);
222
- res.writeHead(200, { 'Content-Type': 'application/json' });
223
- res.end(JSON.stringify(result));
224
- } catch (tokenError) {
225
- logger.warn(`[Server] Common countTokens failed, falling back: ${tokenError.message}`);
226
- // Last resort: return 0
227
- res.writeHead(200, { 'Content-Type': 'application/json' });
228
- res.end(JSON.stringify({ input_tokens: 0 }));
229
- }
230
- return true;
231
- } catch (error) {
232
- logger.error(`[Server] count_tokens error: ${error.message}`);
233
- handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER);
234
- return;
235
- }
236
- }
237
-
238
- // 获取或选择 API Service 实例
239
- let apiService;
240
- // try {
241
- // apiService = await getApiService(currentConfig);
242
- // } catch (error) {
243
- // handleError(res, { statusCode: 500, message: `Failed to get API service: ${error.message}` }, currentConfig.MODEL_PROVIDER);
244
- // const poolManager = getProviderPoolManager();
245
- // if (poolManager) {
246
- // poolManager.markProviderUnhealthy(currentConfig.MODEL_PROVIDER, {
247
- // uuid: currentConfig.uuid
248
- // });
249
- // }
250
- // return;
251
- // }
252
-
253
- try {
254
- // Handle API requests
255
- const apiHandled = await handleAPIRequests(method, path, req, res, currentConfig, apiService, providerPoolManager, PROMPT_LOG_FILENAME);
256
- if (apiHandled) return;
257
-
258
- // Fallback for unmatched routes
259
- res.writeHead(404, { 'Content-Type': 'application/json' });
260
- res.end(JSON.stringify({ error: { message: 'Not Found' } }));
261
- } catch (error) {
262
- handleError(res, error, currentConfig.MODEL_PROVIDER);
263
- }
264
- } finally {
265
- // Clear request context after request is complete
266
- logger.clearRequestContext(requestId);
267
- }
268
- });
269
- };
270
-
271
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/plugins/ai-monitor/index.js DELETED
@@ -1,145 +0,0 @@
1
- import logger from '../../utils/logger.js';
2
-
3
- /**
4
- * AI 接口监控插件
5
- * 功能:
6
- * 1. 捕获 AI 接口的请求参数(转换前和转换后)
7
- * 2. 捕获 AI 接口的响应结果(转换前和转换后,流式响应聚合输出)
8
- */
9
- const aiMonitorPlugin = {
10
- name: 'ai-monitor',
11
- version: '1.0.0',
12
- description: 'AI 接口监控插件 - 捕获请求和响应参数(全链路协议转换监控,流式聚合输出,用于调试和分析)',
13
- type: 'middleware',
14
- _priority: 100,
15
-
16
- // 用于存储流式响应的中间状态
17
- streamCache: new Map(),
18
-
19
- async init(config) {
20
- logger.info('[AI Monitor Plugin] Initialized');
21
- },
22
-
23
- /**
24
- * 中间件:初始化请求上下文
25
- */
26
- async middleware(req, res, requestUrl, config) {
27
- const aiPaths = ['/v1/chat/completions', '/v1/responses', '/v1/messages', '/v1beta/models'];
28
- const isAiPath = aiPaths.some(path => requestUrl.pathname.includes(path));
29
-
30
- if (isAiPath && req.method === 'POST') {
31
- // 在监控插件中生成请求标识,并存入 config 以供全链路追踪
32
- const requestId = Date.now() + Math.random().toString(36).substring(2, 10);
33
- config._monitorRequestId = requestId;
34
- }
35
-
36
- return { handled: false };
37
- },
38
-
39
- hooks: {
40
- /**
41
- * 请求转换后的钩子
42
- */
43
- async onContentGenerated(config) {
44
- const { originalRequestBody, processedRequestBody, fromProvider, toProvider, model, _monitorRequestId, isStream } = config;
45
- if (!originalRequestBody) return;
46
-
47
- setImmediate(() => {
48
- const hasConversion = JSON.stringify(originalRequestBody) !== JSON.stringify(processedRequestBody);
49
- logger.info(`[AI Monitor][${_monitorRequestId}] >>> Req Protocol: ${fromProvider}${hasConversion ? ' -> ' + toProvider : ''} | Model: ${model}`);
50
-
51
- if (hasConversion) {
52
- logger.info(`[AI Monitor][${_monitorRequestId}] [Req Original]: ${JSON.stringify(originalRequestBody)}`);
53
- logger.info(`[AI Monitor][${_monitorRequestId}] [Req Processed]: ${JSON.stringify(processedRequestBody)}`);
54
- } else {
55
- logger.info(`[AI Monitor][${_monitorRequestId}] [Req]: ${JSON.stringify(originalRequestBody)}`);
56
- }
57
- });
58
-
59
- // 处理流式响应的聚合输出
60
- if (isStream && _monitorRequestId) {
61
- setTimeout(() => {
62
- const cache = aiMonitorPlugin.streamCache.get(_monitorRequestId);
63
- if (cache) {
64
- const hasConversion = JSON.stringify(cache.nativeChunks) !== JSON.stringify(cache.convertedChunks);
65
- logger.info(`[AI Monitor][${_monitorRequestId}] <<< Stream Response Aggregated: ${hasConversion ? cache.toProvider + ' -> ' : ''}${cache.fromProvider}`);
66
-
67
- if (hasConversion) {
68
- logger.info(`[AI Monitor][${_monitorRequestId}] [Res Native Full]: ${JSON.stringify(cache.nativeChunks)}`);
69
- logger.info(`[AI Monitor][${_monitorRequestId}] [Res Converted Full]: ${JSON.stringify(cache.convertedChunks)}`);
70
- } else {
71
- logger.info(`[AI Monitor][${_monitorRequestId}] [Res Full]: ${JSON.stringify(cache.nativeChunks)}`);
72
- }
73
-
74
- aiMonitorPlugin.streamCache.delete(_monitorRequestId);
75
- }
76
- }, 2000); // 等待流传输完成
77
- }
78
- },
79
-
80
- /**
81
- * 非流式响应转换监控
82
- */
83
- async onUnaryResponse({ nativeResponse, clientResponse, fromProvider, toProvider, requestId }) {
84
- setImmediate(() => {
85
- const reqId = requestId || 'N/A';
86
- const hasConversion = JSON.stringify(nativeResponse) !== JSON.stringify(clientResponse);
87
- logger.info(`[AI Monitor][${reqId}] <<< Res Protocol: ${hasConversion ? toProvider + ' -> ' : ''}${fromProvider} (Unary)`);
88
-
89
- if (hasConversion) {
90
- logger.info(`[AI Monitor][${reqId}] [Res Native]: ${JSON.stringify(nativeResponse)}`);
91
- logger.info(`[AI Monitor][${reqId}] [Res Converted]: ${JSON.stringify(clientResponse)}`);
92
- } else {
93
- logger.info(`[AI Monitor][${reqId}] [Res]: ${JSON.stringify(nativeResponse)}`);
94
- }
95
- });
96
- },
97
-
98
- /**
99
- * 流式响应分块转换监控 - 聚合数据
100
- */
101
- async onStreamChunk({ nativeChunk, chunkToSend, fromProvider, toProvider, requestId }) {
102
- if (!requestId) return;
103
-
104
- if (!aiMonitorPlugin.streamCache.has(requestId)) {
105
- aiMonitorPlugin.streamCache.set(requestId, {
106
- nativeChunks: [],
107
- convertedChunks: [],
108
- fromProvider,
109
- toProvider
110
- });
111
- }
112
-
113
- const cache = aiMonitorPlugin.streamCache.get(requestId);
114
-
115
- // 过滤 null 值,并判断是否为数组类型
116
- if (nativeChunk != null) {
117
- if (Array.isArray(nativeChunk)) {
118
- cache.nativeChunks.push(...nativeChunk.filter(item => item != null));
119
- } else {
120
- cache.nativeChunks.push(nativeChunk);
121
- }
122
- }
123
-
124
- if (chunkToSend != null) {
125
- if (Array.isArray(chunkToSend)) {
126
- cache.convertedChunks.push(...chunkToSend.filter(item => item != null));
127
- } else {
128
- cache.convertedChunks.push(chunkToSend);
129
- }
130
- }
131
- },
132
-
133
- /**
134
- * 内部请求转换监控
135
- */
136
- async onInternalRequestConverted({ requestId, internalRequest, converterName }) {
137
- setImmediate(() => {
138
- const reqId = requestId || 'N/A';
139
- logger.info(`[AI Monitor][${reqId}] >>> Internal Req Converted [${converterName}]: ${JSON.stringify(internalRequest)}`);
140
- });
141
- }
142
- }
143
- };
144
-
145
- export default aiMonitorPlugin;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/plugins/api-potluck/api-routes.js DELETED
@@ -1,452 +0,0 @@
1
- /**
2
- * API 大锅饭 - 管理 API 路由
3
- * 提供 Key 管理的 RESTful API
4
- */
5
-
6
- import {
7
- createKey,
8
- listKeys,
9
- getKey,
10
- deleteKey,
11
- updateKeyLimit,
12
- resetKeyUsage,
13
- toggleKey,
14
- updateKeyName,
15
- regenerateKey,
16
- getStats,
17
- validateKey,
18
- KEY_PREFIX,
19
- applyDailyLimitToAllKeys,
20
- getAllKeyIds
21
- } from './key-manager.js';
22
- import logger from '../../utils/logger.js';
23
-
24
- /**
25
- * 解析请求体
26
- * @param {http.IncomingMessage} req
27
- * @returns {Promise<Object>}
28
- */
29
- function parseRequestBody(req) {
30
- return new Promise((resolve, reject) => {
31
- let body = '';
32
- req.on('data', chunk => {
33
- body += chunk.toString();
34
- });
35
- req.on('end', () => {
36
- try {
37
- resolve(body ? JSON.parse(body) : {});
38
- } catch (error) {
39
- reject(new Error('JSON 格式无效'));
40
- }
41
- });
42
- req.on('error', reject);
43
- });
44
- }
45
-
46
- /**
47
- * 发送 JSON 响应
48
- * @param {http.ServerResponse} res
49
- * @param {number} statusCode
50
- * @param {Object} data
51
- */
52
- function sendJson(res, statusCode, data) {
53
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
54
- res.end(JSON.stringify(data));
55
- }
56
-
57
- /**
58
- * 验证管理员 Token
59
- * @param {http.IncomingMessage} req
60
- * @returns {Promise<boolean>}
61
- */
62
- async function checkAdminAuth(req) {
63
- const authHeader = req.headers.authorization;
64
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
65
- return false;
66
- }
67
-
68
- // 动态导入 ui-manager 中的 token 验证逻辑
69
- try {
70
- const { existsSync, readFileSync } = await import('fs');
71
- const { promises: fs } = await import('fs');
72
- const path = await import('path');
73
-
74
- const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json');
75
-
76
- if (!existsSync(TOKEN_STORE_FILE)) {
77
- return false;
78
- }
79
-
80
- const content = readFileSync(TOKEN_STORE_FILE, 'utf8');
81
- const tokenStore = JSON.parse(content);
82
- const token = authHeader.substring(7);
83
- const tokenInfo = tokenStore.tokens[token];
84
-
85
- if (!tokenInfo) {
86
- return false;
87
- }
88
-
89
- // 检查是否过期
90
- if (Date.now() > tokenInfo.expiryTime) {
91
- return false;
92
- }
93
-
94
- return true;
95
- } catch (error) {
96
- logger.error('[API Potluck] Auth check error:', error.message);
97
- return false;
98
- }
99
- }
100
-
101
- /**
102
- * 处理 Potluck 管理 API 请求
103
- * @param {string} method - HTTP 方法
104
- * @param {string} path - 请求路径
105
- * @param {http.IncomingMessage} req - HTTP 请求对象
106
- * @param {http.ServerResponse} res - HTTP 响应对象
107
- * @returns {Promise<boolean>} - 是否处理了请求
108
- */
109
- export async function handlePotluckApiRoutes(method, path, req, res) {
110
- // 只处理 /api/potluck 开头的请求
111
- if (!path.startsWith('/api/potluck')) {
112
- return false;
113
- }
114
- logger.info('[API Potluck] Handling request:', method, path);
115
-
116
- // 验证管理员权限
117
- const isAuthed = await checkAdminAuth(req);
118
- if (!isAuthed) {
119
- sendJson(res, 401, {
120
- success: false,
121
- error: { message: '未授权:请先登录', code: 'UNAUTHORIZED' }
122
- });
123
- return true;
124
- }
125
-
126
- try {
127
- // GET /api/potluck/stats - 获取统计信息
128
- if (method === 'GET' && path === '/api/potluck/stats') {
129
- const stats = await getStats();
130
- sendJson(res, 200, { success: true, data: stats });
131
- return true;
132
- }
133
-
134
- // GET /api/potluck/keys - 获取所有 Key 列表
135
- if (method === 'GET' && path === '/api/potluck/keys') {
136
- const keys = await listKeys();
137
- const stats = await getStats();
138
- sendJson(res, 200, {
139
- success: true,
140
- data: { keys, stats }
141
- });
142
- return true;
143
- }
144
-
145
- // POST /api/potluck/keys/apply-limit - 批量应用每日限额到所有 Key
146
- if (method === 'POST' && path === '/api/potluck/keys/apply-limit') {
147
- const body = await parseRequestBody(req);
148
- const { dailyLimit } = body;
149
-
150
- if (dailyLimit === undefined || typeof dailyLimit !== 'number' || dailyLimit < 1) {
151
- sendJson(res, 400, { success: false, error: { message: 'dailyLimit 必须是一个正数' } });
152
- return true;
153
- }
154
-
155
- const result = await applyDailyLimitToAllKeys(dailyLimit);
156
- sendJson(res, 200, {
157
- success: true,
158
- message: `已将每日限额 ${dailyLimit} 应用到 ${result.updated}/${result.total} 个 Key`,
159
- data: result
160
- });
161
- return true;
162
- }
163
-
164
- // POST /api/potluck/keys - 创建新 Key
165
- if (method === 'POST' && path === '/api/potluck/keys') {
166
- const body = await parseRequestBody(req);
167
- const { name, dailyLimit } = body;
168
- const keyData = await createKey(name, dailyLimit);
169
- sendJson(res, 201, {
170
- success: true,
171
- message: 'API Key 创建成功',
172
- data: keyData
173
- });
174
- return true;
175
- }
176
-
177
- // 处理带 keyId 的路由
178
- const keyIdMatch = path.match(/^\/api\/potluck\/keys\/([^\/]+)(\/.*)?$/);
179
- if (keyIdMatch) {
180
- const keyId = decodeURIComponent(keyIdMatch[1]);
181
- const subPath = keyIdMatch[2] || '';
182
-
183
- // GET /api/potluck/keys/:keyId - 获取单个 Key 详情
184
- if (method === 'GET' && !subPath) {
185
- const keyData = await getKey(keyId);
186
- if (!keyData) {
187
- sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
188
- return true;
189
- }
190
- sendJson(res, 200, { success: true, data: keyData });
191
- return true;
192
- }
193
-
194
- // DELETE /api/potluck/keys/:keyId - 删除 Key
195
- if (method === 'DELETE' && !subPath) {
196
- const deleted = await deleteKey(keyId);
197
- if (!deleted) {
198
- sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
199
- return true;
200
- }
201
- sendJson(res, 200, { success: true, message: 'Key 删除成功' });
202
- return true;
203
- }
204
-
205
- // PUT /api/potluck/keys/:keyId/limit - 更新每日限额
206
- if (method === 'PUT' && subPath === '/limit') {
207
- const body = await parseRequestBody(req);
208
- const { dailyLimit } = body;
209
-
210
- if (typeof dailyLimit !== 'number' || dailyLimit < 0) {
211
- sendJson(res, 400, {
212
- success: false,
213
- error: { message: '无效的每日限额值' }
214
- });
215
- return true;
216
- }
217
-
218
- const keyData = await updateKeyLimit(keyId, dailyLimit);
219
- if (!keyData) {
220
- sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
221
- return true;
222
- }
223
- sendJson(res, 200, {
224
- success: true,
225
- message: '每日限额更新成功',
226
- data: keyData
227
- });
228
- return true;
229
- }
230
-
231
- // POST /api/potluck/keys/:keyId/reset - 重置当天调用次数
232
- if (method === 'POST' && subPath === '/reset') {
233
- const keyData = await resetKeyUsage(keyId);
234
- if (!keyData) {
235
- sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
236
- return true;
237
- }
238
- sendJson(res, 200, {
239
- success: true,
240
- message: '使用量重置成功',
241
- data: keyData
242
- });
243
- return true;
244
- }
245
-
246
- // POST /api/potluck/keys/:keyId/toggle - 切换启用/禁用状态
247
- if (method === 'POST' && subPath === '/toggle') {
248
- const keyData = await toggleKey(keyId);
249
- if (!keyData) {
250
- sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
251
- return true;
252
- }
253
- sendJson(res, 200, {
254
- success: true,
255
- message: `Key 已成功${keyData.enabled ? '启用' : '禁用'}`,
256
- data: keyData
257
- });
258
- return true;
259
- }
260
-
261
- // PUT /api/potluck/keys/:keyId/name - 更新 Key 名称
262
- if (method === 'PUT' && subPath === '/name') {
263
- const body = await parseRequestBody(req);
264
- const { name } = body;
265
-
266
- if (!name || typeof name !== 'string') {
267
- sendJson(res, 400, {
268
- success: false,
269
- error: { message: '无效的名称值' }
270
- });
271
- return true;
272
- }
273
-
274
- const keyData = await updateKeyName(keyId, name);
275
- if (!keyData) {
276
- sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
277
- return true;
278
- }
279
- sendJson(res, 200, {
280
- success: true,
281
- message: '名称更新成功',
282
- data: keyData
283
- });
284
- return true;
285
- }
286
-
287
- // POST /api/potluck/keys/:keyId/regenerate - 重新生成 Key
288
- if (method === 'POST' && subPath === '/regenerate') {
289
- const result = await regenerateKey(keyId);
290
- if (!result) {
291
- sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
292
- return true;
293
- }
294
- sendJson(res, 200, {
295
- success: true,
296
- message: 'Key 重新生成成功',
297
- data: {
298
- oldKey: result.oldKey,
299
- newKey: result.newKey,
300
- keyData: result.keyData
301
- }
302
- });
303
- return true;
304
- }
305
- }
306
-
307
- // 未匹配的 potluck 路由
308
- sendJson(res, 404, { success: false, error: { message: '未找到 Potluck API 端点' } });
309
- return true;
310
-
311
- } catch (error) {
312
- logger.error('[API Potluck] API error:', error);
313
- sendJson(res, 500, {
314
- success: false,
315
- error: { message: error.message || '内部服务器错误' }
316
- });
317
- return true;
318
- }
319
- }
320
-
321
- /**
322
- * 从请求中提取 Potluck API Key
323
- * @param {http.IncomingMessage} req - HTTP 请求对象
324
- * @returns {string|null}
325
- */
326
- function extractApiKeyFromRequest(req) {
327
- // 1. 检查 Authorization header
328
- const authHeader = req.headers['authorization'];
329
- if (authHeader && authHeader.startsWith('Bearer ')) {
330
- const token = authHeader.substring(7);
331
- if (token.startsWith(KEY_PREFIX)) {
332
- return token;
333
- }
334
- }
335
-
336
- // 2. 检查 x-api-key header
337
- const xApiKey = req.headers['x-api-key'];
338
- if (xApiKey && xApiKey.startsWith(KEY_PREFIX)) {
339
- return xApiKey;
340
- }
341
-
342
- return null;
343
- }
344
-
345
- /**
346
- * 处理用户端 API 请求 - 用户通过自己的 API Key 查询使用量
347
- * @param {string} method - HTTP 方法
348
- * @param {string} path - 请求路径
349
- * @param {http.IncomingMessage} req - HTTP 请求对象
350
- * @param {http.ServerResponse} res - HTTP 响应对象
351
- * @returns {Promise<boolean>} - 是否处理了请求
352
- */
353
- export async function handlePotluckUserApiRoutes(method, path, req, res) {
354
- // 只处理 /api/potluckuser 开头的请求
355
- if (!path.startsWith('/api/potluckuser')) {
356
- return false;
357
- }
358
- logger.info('[API Potluck User] Handling request:', method, path);
359
-
360
- try {
361
- // 从请求中提取 API Key
362
- const apiKey = extractApiKeyFromRequest(req);
363
-
364
- if (!apiKey) {
365
- sendJson(res, 401, {
366
- success: false,
367
- error: {
368
- message: '需要 API Key。请在 Authorization 标头 (Bearer maki_xxx) 或 x-api-key 标头中提供您的 API Key。',
369
- code: 'API_KEY_REQUIRED'
370
- }
371
- });
372
- return true;
373
- }
374
-
375
- // 验证 API Key
376
- const validation = await validateKey(apiKey);
377
-
378
- if (!validation.valid && validation.reason !== 'quota_exceeded') {
379
- const errorMessages = {
380
- 'invalid_format': 'API Key 格式无效',
381
- 'not_found': '未找到 API Key',
382
- 'disabled': 'API Key 已禁用'
383
- };
384
-
385
- sendJson(res, 401, {
386
- success: false,
387
- error: {
388
- message: errorMessages[validation.reason] || '无效的 API Key',
389
- code: validation.reason
390
- }
391
- });
392
- return true;
393
- }
394
-
395
- // GET /api/potluckuser/usage - 获取当前用户的使用量信息
396
- if (method === 'GET' && path === '/api/potluckuser/usage') {
397
- const keyData = await getKey(apiKey);
398
-
399
- if (!keyData) {
400
- sendJson(res, 404, {
401
- success: false,
402
- error: { message: '未找到 Key', code: 'KEY_NOT_FOUND' }
403
- });
404
- return true;
405
- }
406
-
407
- // 计算使用百分比
408
- const usagePercent = keyData.dailyLimit > 0
409
- ? Math.round((keyData.todayUsage / keyData.dailyLimit) * 100)
410
- : 0;
411
-
412
- // 返回用户友好的使用量信息(隐藏敏感信息)
413
- sendJson(res, 200, {
414
- success: true,
415
- data: {
416
- name: keyData.name,
417
- enabled: keyData.enabled,
418
- usage: {
419
- today: keyData.todayUsage,
420
- limit: keyData.dailyLimit,
421
- remaining: Math.max(0, keyData.dailyLimit - keyData.todayUsage),
422
- percent: usagePercent,
423
- resetDate: keyData.lastResetDate
424
- },
425
- total: keyData.totalUsage,
426
- lastUsedAt: keyData.lastUsedAt,
427
- createdAt: keyData.createdAt,
428
- usageHistory: keyData.usageHistory || {},
429
- // 显示部分遮蔽的 Key ID
430
-
431
- maskedKey: `${apiKey.substring(0, 12)}...${apiKey.substring(apiKey.length - 4)}`
432
- }
433
- });
434
- return true;
435
- }
436
-
437
- // 未匹配的用户端路由
438
- sendJson(res, 404, {
439
- success: false,
440
- error: { message: '未找到用户 API 端点' }
441
- });
442
- return true;
443
-
444
- } catch (error) {
445
- logger.error('[API Potluck] User API error:', error);
446
- sendJson(res, 500, {
447
- success: false,
448
- error: { message: error.message || '内部服务器错误' }
449
- });
450
- return true;
451
- }
452
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/plugins/api-potluck/index.js DELETED
@@ -1,208 +0,0 @@
1
- /**
2
- * API 大锅饭插件 - 标准插件格式
3
- *
4
- * 功能:
5
- * 1. API Key 管理(创建、删除、启用/禁用)
6
- * 2. 每日配额限制
7
- * 3. 用量统计
8
- * 4. 管理 API 接口
9
- */
10
-
11
- import {
12
- createKey,
13
- listKeys,
14
- getKey,
15
- deleteKey,
16
- updateKeyLimit,
17
- resetKeyUsage,
18
- toggleKey,
19
- updateKeyName,
20
- validateKey,
21
- incrementUsage,
22
- getStats,
23
- KEY_PREFIX,
24
- setConfigGetter
25
- } from './key-manager.js';
26
-
27
- import {
28
- extractPotluckKey,
29
- isPotluckRequest,
30
- sendPotluckError
31
- } from './middleware.js';
32
-
33
- import logger from '../../utils/logger.js';
34
-
35
- import { handlePotluckApiRoutes, handlePotluckUserApiRoutes } from './api-routes.js';
36
-
37
- /**
38
- * 插件定义
39
- */
40
- const apiPotluckPlugin = {
41
- name: 'api-potluck',
42
- version: '1.0.2',
43
- description: 'API 大锅饭 - Key 管理和用量统计插件<br>管理端:<a href="potluck.html" target="_blank">potluck.html</a><br>用户端:<a href="potluck-user.html" target="_blank">potluck-user.html</a>',
44
-
45
- // 插件类型:认证插件
46
- type: 'auth',
47
-
48
- // 优先级:数字越小越先执行,默认认证插件优先级为 9999
49
- _priority: 10,
50
-
51
- /**
52
- * 初始化钩子
53
- * @param {Object} config - 服务器配置
54
- */
55
- async init(config) {
56
- logger.info('[API Potluck Plugin] Initializing...');
57
- },
58
-
59
- /**
60
- * 销毁钩子
61
- */
62
- async destroy() {
63
- logger.info('[API Potluck Plugin] Destroying...');
64
- },
65
-
66
- /**
67
- * 静态文件路径
68
- */
69
- staticPaths: ['potluck.html', 'potluck-user.html'],
70
-
71
- /**
72
- * 路由定义
73
- */
74
- routes: [
75
- {
76
- method: '*',
77
- path: '/api/potluckuser',
78
- handler: handlePotluckUserApiRoutes
79
- },
80
- {
81
- method: '*',
82
- path: '/api/potluck',
83
- handler: handlePotluckApiRoutes
84
- }
85
- ],
86
-
87
- /**
88
- * 认证方法 - 处理 Potluck Key 认证
89
- * @param {http.IncomingMessage} req - HTTP 请求
90
- * @param {http.ServerResponse} res - HTTP 响应
91
- * @param {URL} requestUrl - 解析后的 URL
92
- * @param {Object} config - 服务器配置
93
- * @returns {Promise<{handled: boolean, authorized: boolean|null, error?: Object, data?: Object}>}
94
- */
95
- async authenticate(req, res, requestUrl, config) {
96
- const apiKey = extractPotluckKey(req, requestUrl);
97
-
98
- if (!apiKey) {
99
- // 不是 potluck 请求,返回 null 让其他认证插件处理
100
- return { handled: false, authorized: null };
101
- }
102
-
103
- // 验证 Key
104
- const validation = await validateKey(apiKey);
105
-
106
- if (!validation.valid) {
107
- const errorMessages = {
108
- 'invalid_format': 'Invalid API key format',
109
- 'not_found': 'API key not found',
110
- 'disabled': 'API key has been disabled',
111
- 'quota_exceeded': 'Quota exceeded for this API key'
112
- };
113
-
114
- const statusCodes = {
115
- 'invalid_format': 401,
116
- 'not_found': 401,
117
- 'disabled': 403,
118
- 'quota_exceeded': 429
119
- };
120
-
121
- const error = {
122
- statusCode: statusCodes[validation.reason] || 401,
123
- message: errorMessages[validation.reason] || 'Authentication failed',
124
- code: validation.reason,
125
- keyData: validation.keyData
126
- };
127
-
128
- // 发送错误响应
129
- sendPotluckError(res, error);
130
- return { handled: true, authorized: false, error };
131
- }
132
-
133
- // 认证成功,返回数据供后续使用
134
- logger.info(`[API Potluck Plugin] Authorized with key: ${apiKey.substring(0, 12)}...`);
135
- return {
136
- handled: false,
137
- authorized: true,
138
- data: {
139
- potluckApiKey: apiKey,
140
- potluckKeyData: validation.keyData
141
- }
142
- };
143
- },
144
-
145
- /**
146
- * 钩子函数
147
- */
148
- hooks: {
149
- /**
150
- * 内容生成后钩子 - 记录用量
151
- * @param {Object} hookContext - 钩子上下文,包含请求和模型信息
152
- */
153
- async onContentGenerated(hookContext) {
154
- if (hookContext.potluckApiKey) {
155
- try {
156
- // 传入提供商和模型信息
157
- await incrementUsage(
158
- hookContext.potluckApiKey,
159
- hookContext.toProvider,
160
- hookContext.model
161
- );
162
- } catch (e) {
163
- // 静默失败,不影响主流程
164
- logger.error('[API Potluck Plugin] Failed to record usage:', e.message);
165
- }
166
- }
167
- }
168
-
169
- },
170
-
171
- // 导出内部函数供外部使用(可选)
172
- exports: {
173
- createKey,
174
- listKeys,
175
- getKey,
176
- deleteKey,
177
- updateKeyLimit,
178
- resetKeyUsage,
179
- toggleKey,
180
- updateKeyName,
181
- validateKey,
182
- incrementUsage,
183
- getStats,
184
- KEY_PREFIX,
185
- extractPotluckKey,
186
- isPotluckRequest
187
- }
188
- };
189
-
190
- export default apiPotluckPlugin;
191
-
192
- // 也导出命名导出,方便直接引用
193
- export {
194
- createKey,
195
- listKeys,
196
- getKey,
197
- deleteKey,
198
- updateKeyLimit,
199
- resetKeyUsage,
200
- toggleKey,
201
- updateKeyName,
202
- validateKey,
203
- incrementUsage,
204
- getStats,
205
- KEY_PREFIX,
206
- extractPotluckKey,
207
- isPotluckRequest
208
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/plugins/api-potluck/key-manager.js DELETED
@@ -1,486 +0,0 @@
1
- /**
2
- * API 大锅饭 - Key 管理模块
3
- * 使用内存缓存 + 写锁 + 定期持久化,解决并发安全问题
4
- */
5
-
6
- import { promises as fs } from 'fs';
7
- import logger from '../../utils/logger.js';
8
- import { existsSync, readFileSync, writeFileSync } from 'fs';
9
- import path from 'path';
10
- import crypto from 'crypto';
11
-
12
- // 配置文件路径
13
- const KEYS_STORE_FILE = path.join(process.cwd(), 'configs', 'api-potluck-keys.json');
14
- const KEY_PREFIX = 'maki_';
15
-
16
- // 默认配置
17
- const DEFAULT_CONFIG = {
18
- defaultDailyLimit: 500,
19
- persistInterval: 5000
20
- };
21
-
22
- // 配置获取函数(由外部注入)
23
- let configGetter = null;
24
-
25
- /**
26
- * 设置配置获取函数
27
- * @param {Function} getter - 返回配置对象的函数
28
- */
29
- export function setConfigGetter(getter) {
30
- configGetter = getter;
31
- }
32
-
33
- /**
34
- * 获取当前配置
35
- */
36
- function getConfig() {
37
- if (configGetter) {
38
- return configGetter();
39
- }
40
- return DEFAULT_CONFIG;
41
- }
42
-
43
- // 内存缓存
44
- let keyStore = null;
45
- let isDirty = false;
46
- let isWriting = false;
47
- let persistTimer = null;
48
- let currentPersistInterval = DEFAULT_CONFIG.persistInterval;
49
-
50
- /**
51
- * 初始化:从文件加载数据到内存
52
- */
53
- function ensureLoaded() {
54
- if (keyStore !== null) return;
55
- try {
56
- if (existsSync(KEYS_STORE_FILE)) {
57
- const content = readFileSync(KEYS_STORE_FILE, 'utf8');
58
- keyStore = JSON.parse(content);
59
- } else {
60
- keyStore = { keys: {} };
61
- syncWriteToFile();
62
- }
63
- } catch (error) {
64
- logger.error('[API Potluck] Failed to load key store:', error.message);
65
- keyStore = { keys: {} };
66
- }
67
-
68
- // 获取配置的持久化间隔
69
- const config = getConfig();
70
- currentPersistInterval = config.persistInterval || DEFAULT_CONFIG.persistInterval;
71
-
72
- // 启动定期持久化
73
- if (!persistTimer) {
74
- persistTimer = setInterval(persistIfDirty, currentPersistInterval);
75
- // 进程退出时保存
76
- process.on('beforeExit', () => persistIfDirty());
77
- process.on('SIGINT', () => { persistIfDirty(); process.exit(0); });
78
- process.on('SIGTERM', () => { persistIfDirty(); process.exit(0); });
79
- }
80
- }
81
-
82
- /**
83
- * 同步写入文件(仅初始化时使用)
84
- */
85
- function syncWriteToFile() {
86
- try {
87
- const dir = path.dirname(KEYS_STORE_FILE);
88
- if (!existsSync(dir)) {
89
- require('fs').mkdirSync(dir, { recursive: true });
90
- }
91
- writeFileSync(KEYS_STORE_FILE, JSON.stringify(keyStore, null, 2), 'utf8');
92
- } catch (error) {
93
- logger.error('[API Potluck] Sync write failed:', error.message);
94
- }
95
- }
96
-
97
- /**
98
- * 异步持久化(带写锁)
99
- */
100
- async function persistIfDirty() {
101
- if (!isDirty || isWriting || keyStore === null) return;
102
- isWriting = true;
103
- try {
104
- const dir = path.dirname(KEYS_STORE_FILE);
105
- if (!existsSync(dir)) {
106
- await fs.mkdir(dir, { recursive: true });
107
- }
108
- // 写入临时文件再重命名,防止写入中断导致文件损坏
109
- const tempFile = KEYS_STORE_FILE + '.tmp';
110
- await fs.writeFile(tempFile, JSON.stringify(keyStore, null, 2), 'utf8');
111
- await fs.rename(tempFile, KEYS_STORE_FILE);
112
- isDirty = false;
113
- } catch (error) {
114
- logger.error('[API Potluck] Persist failed:', error.message);
115
- } finally {
116
- isWriting = false;
117
- }
118
- }
119
-
120
- /**
121
- * 标记数据已修改
122
- */
123
- function markDirty() {
124
- isDirty = true;
125
- }
126
-
127
- /**
128
- * 生成随机 API Key(确保不重复)
129
- */
130
- function generateApiKey() {
131
- ensureLoaded();
132
- let apiKey;
133
- let attempts = 0;
134
- const maxAttempts = 10;
135
-
136
- do {
137
- apiKey = `${KEY_PREFIX}${crypto.randomBytes(16).toString('hex')}`;
138
- attempts++;
139
- if (attempts >= maxAttempts) {
140
- throw new Error('Failed to generate unique API key after multiple attempts');
141
- }
142
- } while (keyStore.keys[apiKey]);
143
-
144
- return apiKey;
145
- }
146
-
147
- /**
148
- * 获取今天的日期字符串 (YYYY-MM-DD)
149
- */
150
- function getTodayDateString() {
151
- const now = new Date();
152
- return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
153
- }
154
-
155
- /**
156
- * 检查并重置过期的每日计数
157
- */
158
- function checkAndResetDailyCount(keyData) {
159
- const today = getTodayDateString();
160
- if (keyData.lastResetDate !== today) {
161
- keyData.todayUsage = 0;
162
- keyData.lastResetDate = today;
163
- }
164
- return keyData;
165
- }
166
-
167
- /**
168
- * 创建新的 API Key
169
-
170
- * @param {string} name - Key 名称
171
- * @param {number} [dailyLimit] - 每日限额,不传则使用配置的默认值
172
- */
173
- export async function createKey(name = '', dailyLimit = null) {
174
- ensureLoaded();
175
- const config = getConfig();
176
- const actualDailyLimit = dailyLimit ?? config.defaultDailyLimit ?? DEFAULT_CONFIG.defaultDailyLimit;
177
-
178
- const apiKey = generateApiKey();
179
- const now = new Date().toISOString();
180
- const today = getTodayDateString();
181
-
182
- const keyData = {
183
- id: apiKey,
184
- name: name || `Key-${Object.keys(keyStore.keys).length + 1}`,
185
- createdAt: now,
186
- dailyLimit: actualDailyLimit,
187
- todayUsage: 0,
188
- totalUsage: 0,
189
- lastResetDate: today,
190
- lastUsedAt: null,
191
- enabled: true
192
- };
193
-
194
- keyStore.keys[apiKey] = keyData;
195
- markDirty();
196
- await persistIfDirty(); // 创建操作立即持久化
197
-
198
- logger.info(`[API Potluck] Created key: ${apiKey.substring(0, 12)}...`);
199
- return keyData;
200
- }
201
-
202
- /**
203
- * 获取所有 Key 列表
204
- */
205
- export async function listKeys() {
206
- ensureLoaded();
207
- const keys = [];
208
- for (const [keyId, keyData] of Object.entries(keyStore.keys)) {
209
- const updated = checkAndResetDailyCount({ ...keyData });
210
- keys.push({
211
- ...updated,
212
- maskedKey: `${keyId.substring(0, 12)}...${keyId.substring(keyId.length - 4)}`
213
- });
214
- }
215
- return keys;
216
- }
217
-
218
- /**
219
- * 获取单个 Key 详情
220
- */
221
- export async function getKey(keyId) {
222
- ensureLoaded();
223
- const keyData = keyStore.keys[keyId];
224
- if (!keyData) return null;
225
- return checkAndResetDailyCount({ ...keyData });
226
- }
227
-
228
- /**
229
- * 删除 Key
230
- */
231
- export async function deleteKey(keyId) {
232
- ensureLoaded();
233
- if (!keyStore.keys[keyId]) return false;
234
- delete keyStore.keys[keyId];
235
- markDirty();
236
- await persistIfDirty(); // 删除操作立即持久化
237
- logger.info(`[API Potluck] Deleted key: ${keyId.substring(0, 12)}...`);
238
- return true;
239
- }
240
-
241
- /**
242
- * 更新 Key 的每日限额
243
- */
244
- export async function updateKeyLimit(keyId, newLimit) {
245
- ensureLoaded();
246
- if (!keyStore.keys[keyId]) return null;
247
- keyStore.keys[keyId].dailyLimit = newLimit;
248
- markDirty();
249
- return keyStore.keys[keyId];
250
- }
251
-
252
- /**
253
- * 重置 Key 的当天调用次数
254
- */
255
- export async function resetKeyUsage(keyId) {
256
- ensureLoaded();
257
- if (!keyStore.keys[keyId]) return null;
258
- keyStore.keys[keyId].todayUsage = 0;
259
- keyStore.keys[keyId].lastResetDate = getTodayDateString();
260
- markDirty();
261
- return keyStore.keys[keyId];
262
- }
263
-
264
- /**
265
- * 切换 Key 的启用/禁用状态
266
- */
267
- export async function toggleKey(keyId) {
268
- ensureLoaded();
269
- if (!keyStore.keys[keyId]) return null;
270
- keyStore.keys[keyId].enabled = !keyStore.keys[keyId].enabled;
271
- markDirty();
272
- return keyStore.keys[keyId];
273
- }
274
-
275
- /**
276
- * 更新 Key 名称
277
- */
278
- export async function updateKeyName(keyId, newName) {
279
- ensureLoaded();
280
- if (!keyStore.keys[keyId]) return null;
281
- keyStore.keys[keyId].name = newName;
282
- markDirty();
283
- return keyStore.keys[keyId];
284
- }
285
-
286
- /**
287
- * 重新生成 API Key(保留原有数据,更换 Key ID)
288
- * @param {string} oldKeyId - 原 Key ID
289
- * @returns {Promise<{oldKey: string, newKey: string, keyData: Object}|null>}
290
- */
291
- export async function regenerateKey(oldKeyId) {
292
- ensureLoaded();
293
- const oldKeyData = keyStore.keys[oldKeyId];
294
- if (!oldKeyData) return null;
295
-
296
- // 生成新的唯一 Key
297
- const newKeyId = generateApiKey();
298
-
299
- // 复制数据到新 Key
300
- const newKeyData = {
301
- ...oldKeyData,
302
- id: newKeyId,
303
- regeneratedAt: new Date().toISOString(),
304
- regeneratedFrom: oldKeyId.substring(0, 12) + '...'
305
- };
306
-
307
- // 删除旧 Key,添加新 Key
308
- delete keyStore.keys[oldKeyId];
309
- keyStore.keys[newKeyId] = newKeyData;
310
-
311
- markDirty();
312
- await persistIfDirty(); // 立即持久化
313
-
314
- logger.info(`[API Potluck] Regenerated key: ${oldKeyId.substring(0, 12)}... -> ${newKeyId.substring(0, 12)}...`);
315
-
316
- return {
317
- oldKey: oldKeyId,
318
- newKey: newKeyId,
319
- keyData: newKeyData
320
- };
321
- }
322
-
323
- /**
324
- * 验证 API Key 是否有效且有配额
325
- */
326
- export async function validateKey(apiKey) {
327
- ensureLoaded();
328
- if (!apiKey || !apiKey.startsWith(KEY_PREFIX)) {
329
- return { valid: false, reason: 'invalid_format' };
330
- }
331
- const keyData = keyStore.keys[apiKey];
332
- if (!keyData) return { valid: false, reason: 'not_found' };
333
- if (!keyData.enabled) return { valid: false, reason: 'disabled' };
334
-
335
- // 直接在内存中检查和重置
336
- checkAndResetDailyCount(keyData);
337
-
338
- // 检查每日限额
339
- if (keyData.todayUsage < keyData.dailyLimit) {
340
- return { valid: true, keyData };
341
- }
342
-
343
- return { valid: false, reason: 'quota_exceeded', keyData };
344
- }
345
-
346
- /**
347
- * 增加 Key 的使用次数(原子操作,直接修改内存)
348
- * @param {string} apiKey - API Key
349
- * @param {string} provider - 使用的提供商
350
- * @param {string} model - 使用的模型
351
- */
352
- export async function incrementUsage(apiKey, provider = 'unknown', model = 'unknown') {
353
- ensureLoaded();
354
- const keyData = keyStore.keys[apiKey];
355
- if (!keyData) return null;
356
-
357
- checkAndResetDailyCount(keyData);
358
-
359
- // 消耗每日限额
360
- if (keyData.todayUsage < keyData.dailyLimit) {
361
- keyData.todayUsage += 1;
362
- } else {
363
- // 每日限额用尽
364
- return null;
365
- }
366
-
367
- keyData.totalUsage += 1;
368
- keyData.lastUsedAt = new Date().toISOString();
369
-
370
- // 记录个人按天统计 (每个 Key 独立)
371
- const today = getTodayDateString();
372
- if (!keyData.usageHistory) keyData.usageHistory = {};
373
- if (!keyData.usageHistory[today]) {
374
- keyData.usageHistory[today] = { providers: {}, models: {} };
375
- }
376
-
377
- // 确保 provider 和 model 是字符串
378
- const pName = String(provider || 'unknown');
379
- const mName = String(model || 'unknown');
380
-
381
- const userHistory = keyData.usageHistory[today];
382
- userHistory.providers[pName] = (userHistory.providers[pName] || 0) + 1;
383
- userHistory.models[mName] = (userHistory.models[mName] || 0) + 1;
384
-
385
- // 清理该 Key 的过期历史 (保留 7 天)
386
- const userDates = Object.keys(keyData.usageHistory).sort();
387
- if (userDates.length > 7) {
388
- const dropDates = userDates.slice(0, userDates.length - 7);
389
- dropDates.forEach(d => delete keyData.usageHistory[d]);
390
- }
391
-
392
- markDirty();
393
-
394
- return {
395
- ...keyData,
396
- usedBonus: false
397
- };
398
- }
399
-
400
- /**
401
- * 获取统计信息
402
- */
403
- export async function getStats() {
404
- ensureLoaded();
405
- const keys = Object.values(keyStore.keys);
406
- let enabledKeys = 0, todayTotalUsage = 0, totalUsage = 0;
407
- const aggregatedHistory = {};
408
-
409
- for (const key of keys) {
410
- checkAndResetDailyCount(key);
411
- if (key.enabled) enabledKeys++;
412
- todayTotalUsage += key.todayUsage;
413
- totalUsage += key.totalUsage;
414
-
415
- // 汇总每个 Key 的历史数据
416
- if (key.usageHistory) {
417
- Object.entries(key.usageHistory).forEach(([date, history]) => {
418
- if (!aggregatedHistory[date]) {
419
- aggregatedHistory[date] = { providers: {}, models: {} };
420
- }
421
-
422
- // 汇总提供商
423
- if (history.providers) {
424
- Object.entries(history.providers).forEach(([p, count]) => {
425
- aggregatedHistory[date].providers[p] = (aggregatedHistory[date].providers[p] || 0) + count;
426
- });
427
- }
428
-
429
- // 汇总模型
430
- if (history.models) {
431
- Object.entries(history.models).forEach(([m, count]) => {
432
- aggregatedHistory[date].models[m] = (aggregatedHistory[date].models[m] || 0) + count;
433
- });
434
- }
435
- });
436
- }
437
- }
438
-
439
- return {
440
- totalKeys: keys.length,
441
- enabledKeys,
442
- disabledKeys: keys.length - enabledKeys,
443
- todayTotalUsage,
444
- totalUsage,
445
- usageHistory: aggregatedHistory
446
- };
447
- }
448
-
449
-
450
- /**
451
- * 批量更新所有 Key 的每日限额
452
- * @param {number} newLimit - 新的每日限额
453
- * @returns {Promise<{total: number, updated: number}>}
454
- */
455
- export async function applyDailyLimitToAllKeys(newLimit) {
456
- ensureLoaded();
457
- const keys = Object.values(keyStore.keys);
458
- let updated = 0;
459
-
460
- for (const keyData of keys) {
461
- if (keyData.dailyLimit !== newLimit) {
462
- keyData.dailyLimit = newLimit;
463
- updated++;
464
- }
465
- }
466
-
467
- if (updated > 0) {
468
- markDirty();
469
- await persistIfDirty();
470
- }
471
-
472
- logger.info(`[API Potluck] Applied daily limit ${newLimit} to ${updated}/${keys.length} keys`);
473
- return { total: keys.length, updated };
474
- }
475
-
476
- /**
477
- * 获取所有 Key ID 列表
478
- * @returns {string[]}
479
- */
480
- export function getAllKeyIds() {
481
- ensureLoaded();
482
- return Object.keys(keyStore.keys);
483
- }
484
-
485
- // 导出常量
486
- export { KEY_PREFIX };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/plugins/api-potluck/middleware.js DELETED
@@ -1,166 +0,0 @@
1
- /**
2
- * API 大锅饭 - 中间件模块
3
- * 负责请求拦截和配额检查
4
- */
5
-
6
- import { validateKey, incrementUsage, KEY_PREFIX } from './key-manager.js';
7
- import logger from '../../utils/logger.js';
8
-
9
- /**
10
- * 从请求中提取 Potluck API Key
11
- * 支持多种认证方式:
12
- * 1. Authorization: Bearer maki_xxx
13
- * 2. x-api-key: maki_xxx
14
- * 3. x-goog-api-key: maki_xxx
15
- * 4. URL query: ?key=maki_xxx
16
- *
17
- * @param {http.IncomingMessage} req - HTTP 请求对象
18
- * @param {URL} requestUrl - 解析后的 URL 对象
19
- * @returns {string|null} 提取到的 API Key,如果不是 potluck key 则返回 null
20
- */
21
- export function extractPotluckKey(req, requestUrl) {
22
- // 1. 检查 Authorization header
23
- const authHeader = req.headers['authorization'];
24
- if (authHeader && authHeader.startsWith('Bearer ')) {
25
- const token = authHeader.substring(7);
26
- if (token.startsWith(KEY_PREFIX)) {
27
- return token;
28
- }
29
- }
30
-
31
- // 2. 检查 x-api-key header (Claude style)
32
- const xApiKey = req.headers['x-api-key'];
33
- if (xApiKey && xApiKey.startsWith(KEY_PREFIX)) {
34
- return xApiKey;
35
- }
36
-
37
- // 3. 检查 x-goog-api-key header (Gemini style)
38
- const googApiKey = req.headers['x-goog-api-key'];
39
- if (googApiKey && googApiKey.startsWith(KEY_PREFIX)) {
40
- return googApiKey;
41
- }
42
-
43
- // 4. 检查 URL query parameter
44
- const queryKey = requestUrl.searchParams.get('key');
45
- if (queryKey && queryKey.startsWith(KEY_PREFIX)) {
46
- return queryKey;
47
- }
48
-
49
- return null;
50
- }
51
-
52
- /**
53
- * 检查请求是否使用 Potluck Key
54
- * @param {http.IncomingMessage} req - HTTP 请求对象
55
- * @param {URL} requestUrl - 解析后的 URL 对象
56
- * @returns {boolean}
57
- */
58
- export function isPotluckRequest(req, requestUrl) {
59
- return extractPotluckKey(req, requestUrl) !== null;
60
- }
61
-
62
- /**
63
- * Potluck 认证中间件
64
- * 验证 Potluck API Key 并检查配额
65
- *
66
- * @param {http.IncomingMessage} req - HTTP 请求对象
67
- * @param {URL} requestUrl - 解析后的 URL 对象
68
- * @returns {Promise<{authorized: boolean, error?: Object, keyData?: Object, apiKey?: string}>}
69
- */
70
- export async function potluckAuthMiddleware(req, requestUrl) {
71
- const apiKey = extractPotluckKey(req, requestUrl);
72
-
73
- if (!apiKey) {
74
- // 不是 potluck 请求,返回 null 让原有逻辑处理
75
- return { authorized: null };
76
- }
77
-
78
- // 验证 Key
79
- const validation = await validateKey(apiKey);
80
-
81
- if (!validation.valid) {
82
- const errorMessages = {
83
- 'invalid_format': 'Invalid API key format',
84
- 'not_found': 'API key not found',
85
- 'disabled': 'API key has been disabled',
86
- 'quota_exceeded': 'Quota exceeded for this API key'
87
- };
88
-
89
- const statusCodes = {
90
- 'invalid_format': 401,
91
- 'not_found': 401,
92
- 'disabled': 403,
93
- 'quota_exceeded': 429
94
- };
95
-
96
- return {
97
- authorized: false,
98
- error: {
99
- statusCode: statusCodes[validation.reason] || 401,
100
- message: errorMessages[validation.reason] || 'Authentication failed',
101
- code: validation.reason,
102
- keyData: validation.keyData
103
- }
104
- };
105
- }
106
-
107
- return {
108
- authorized: true,
109
- keyData: validation.keyData,
110
- apiKey: apiKey
111
- };
112
- }
113
-
114
- /**
115
- * 记录 Potluck 请求使用
116
- * 在请求成功处理后调用
117
- *
118
- * @param {string} apiKey - API Key
119
- * @returns {Promise<Object|null>}
120
- */
121
- export async function recordPotluckUsage(apiKey) {
122
- if (!apiKey || !apiKey.startsWith(KEY_PREFIX)) {
123
- return null;
124
- }
125
- return incrementUsage(apiKey);
126
- }
127
-
128
- /**
129
- * 创建 Potluck 错误响应
130
- * @param {http.ServerResponse} res - HTTP 响应对象
131
- * @param {Object} error - 错误信息
132
- */
133
- export function sendPotluckError(res, error) {
134
- const response = {
135
- error: {
136
- message: error.message,
137
- code: error.code,
138
- type: 'potluck_error'
139
- }
140
- };
141
-
142
- // 如果是配额超限,添加额外信息
143
- if (error.code === 'quota_exceeded' && error.keyData) {
144
- response.error.quota = {
145
- used: error.keyData.todayUsage,
146
- limit: error.keyData.dailyLimit,
147
- resetDate: error.keyData.lastResetDate
148
- };
149
- }
150
-
151
- // 检查响应流是否已关闭
152
- if (res.writableEnded || res.destroyed) {
153
- logger.warn('[API Potluck] Response already ended, skipping error response');
154
- return;
155
- }
156
-
157
- if (!res.headersSent) {
158
- res.writeHead(error.statusCode, { 'Content-Type': 'application/json' });
159
- }
160
-
161
- try {
162
- res.end(JSON.stringify(response));
163
- } catch (writeError) {
164
- logger.error('[API Potluck] Failed to write error response:', writeError.message);
165
- }
166
- }