File size: 73,637 Bytes
feccc69
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
 
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
0ed5879
 
feccc69
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
0ed5879
 
feccc69
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
0ed5879
 
 
 
 
feccc69
 
0ed5879
 
 
 
 
 
 
 
 
 
 
feccc69
 
 
0ed5879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
 
 
feccc69
 
0ed5879
 
feccc69
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
 
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
0ed5879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fdd5251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcdd4aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feccc69
 
 
 
 
 
 
3df9d54
fcdd4aa
feccc69
 
 
0ed5879
fdd5251
fcdd4aa
 
feccc69
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
fdd5251
fcdd4aa
0ed5879
feccc69
 
 
fdd5251
 
 
 
 
 
 
 
fcdd4aa
 
 
 
 
 
 
 
0ed5879
 
 
 
 
 
 
 
feccc69
 
 
 
 
 
 
 
 
 
 
 
0ed5879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcdd4aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed5879
feccc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
# DS2API 接口文档1

语言 / Language: [中文](API.md) | [English](API.en.md)

本文档描述当前 Go 代码库的实际 API 行为。

文档导航:[总览](README.MD) / [架构说明](docs/ARCHITECTURE.md) / [部署指南](docs/DEPLOY.md) / [测试指南](docs/TESTING.md) / [上下文兼容流程](docs/prompt-compatibility.md)

---

## 目录

- [基础信息](#基础信息)
- [配置最佳实践](#配置最佳实践)
- [鉴权规则](#鉴权规则)
- [路由总览](#路由总览)
- [健康检查](#健康检查)
- [OpenAI 兼容接口](#openai-兼容接口)
- [Claude 兼容接口](#claude-兼容接口)
- [Gemini 兼容接口](#gemini-兼容接口)
- [Ollama 兼容接口](#ollama-兼容接口)
- [Admin 接口](#admin-接口)
- [错误响应格式](#错误响应格式)
- [cURL 示例](#curl-示例)

---

## 基础信息

| 项目 | 说明 |
| --- | --- |
| Base URL | `http://localhost:5001` 或你的部署域名 |
| 默认 Content-Type | `application/json` |
| 健康检查 | `GET /healthz`、`GET /readyz` |
| CORS | 已启用(统一覆盖 `/v1/*`、`/anthropic/*`、`/v1beta/models/*`、`/api/*`、`/admin/*`;浏览器有 `Origin` 时回显该 Origin,否则为 `*`;默认允许 `Content-Type`, `Authorization`, `X-API-Key`, `X-Ds2-Target-Account`, `X-Ds2-Source`, `X-Vercel-Protection-Bypass`, `X-Goog-Api-Key`, `Anthropic-Version`, `Anthropic-Beta`,并会放行预检里声明的第三方请求头,如 `x-stainless-*`;Vercel 上 `/v1/chat/completions` 的 Node Runtime 也对齐相同行为;内部专用头 `X-Ds2-Internal-Token` 仍被拦截) |

- 所有 JSON 请求体都必须是合法 UTF-8;非法字节序列会在入站阶段被拒绝为 `400 invalid json`。

### 3.0 接口适配层说明

- OpenAI / Claude / Gemini 三套协议已统一挂在同一 `chi` 路由树上,由 `internal/server/router.go` 负责装配。
- 适配器层职责收敛为:**请求归一化 → DeepSeek 调用 → 协议形态渲染**,减少历史版本中“同能力多处实现”的分叉。
- 从协议请求到纯文本 web-chat prompt 的完整链路(统一标准请求、思考格式注入、历史拆分、完成载荷组装、响应归档)见 [docs/prompt-compatibility.md](docs/prompt-compatibility.md),该文是这条流程的源文档;本文只描述各接口的可见字段与响应形态。
- Tool Calling 的解析策略在 Go 与 Node Runtime 间保持一致:推荐模型输出半角管道符 DSML 外壳 `<|DSML|tool_calls>` → `<|DSML|invoke name="...">` → `<|DSML|parameter name="...">`;兼容层也接受 DSML wrapper 别名 `<dsml|tool_calls>`、`<|tool_calls>`、常见 DSML 分隔符漏写形态(如 `<|DSML tool_calls>`)、`DSML` 与工具标签名黏连的常见 typo(如 `<DSMLtool_calls>`)、控制分隔符漂移(如 `<DSML␂tool_calls>` / 原始 STX `\x02`)、CJK 尖括号、全角感叹号、顿号、PascalCase 本地名、弯引号属性值与属性尾部分隔符漂移(如 `<DSM|parameter name="command"|>...〈/DSM|parameter〉` / `<!DSML!invoke name=“Bash”>` / `<、DSML、tool_calls>` / `<DSmartToolCalls>` / `<DSMLtool_calls※>`)、任意协议前缀壳(如 `<proto💥tool_calls>`),以及旧式 canonical XML `<tool_calls>` → `<invoke name="...">` → `<parameter name="...">`。实现上采用结构扫描:只要固定本地标签名是 `tool_calls` / `invoke` / `parameter`,标签名前或标签名后的非结构性分隔符会在解析入口归一化;CDATA 开头也会容错 `<![CDATA[` / `<、[CDATA[` 这类分隔符漂移;只有 `tool_calls` wrapper 或可修复的缺失 opening wrapper 会进入工具路径,裸 `<invoke>` 不计为已支持语法;流式场景继续执行防泄漏筛分。若参数体本身是合法 JSON 字面量(如 `123`、`true`、`null`、数组或对象),会按结构化值输出,不再一律当作字符串;显式空字符串和纯空白参数会结构化保留为空字符串,是否拒绝缺参由工具执行侧决定;完整但 malformed 的 wrapper 会作为普通文本释放,不会吞掉或伪造成工具调用;若 CDATA 偶发漏闭合,则会在最终 parse / flush 恢复阶段做窄修复,尽量保住已完整包裹的外层工具调用。
- `Admin API` 将配置与运行时策略分开:`/admin/config*` 管静态配置,`/admin/settings*` 管运行时行为。
- 当上游返回 thinking-only 响应(模型输出了推理链但无可见文本)时,Go 主路径与 Vercel Node 流式路径都会先自动重试一次:以多轮对话 follow-up 方式追加 prompt 后缀 `"Please provide a non-empty final answer or tool call."` 并设置 `parent_message_id` 在同一 DeepSeek session 内让模型重新输出;同账号重试最大 1 次。若同账号重试后仍即将返回 `429 upstream_empty_output`,托管账号模式会在返回 429 前自动切换到下一个可用账号,新建 session,用原始 payload 再 fresh retry 一次。
- 引用标记处理边界:流式输出默认隐藏 `[citation:N]` / `[reference:N]` 这类上游内部占位符;非流式输出默认把 DeepSeek 搜索引用标记转换为 Markdown 引用链接。

---

## 配置最佳实践

推荐把 `config.json` 作为唯一配置源:

```bash
cp config.example.json config.json
# 编辑 config.json(keys/accounts)
```

按部署方式使用:

- 本地运行:直接读取 `config.json`
- Docker / Vercel:从 `config.json` 生成 Base64,填入 `DS2API_CONFIG_JSON`,也可以直接填原始 JSON

```bash
DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')"
```

Vercel 一键部署可先只填 `DS2API_ADMIN_KEY`,部署后在 `/admin` 导入配置,再通过 “Vercel 同步” 写回环境变量。

---

## 鉴权规则

### 业务接口(`/v1/*`、`/anthropic/*`、`/v1beta/models/*`)

支持两种传参方式:

| 方式 | 示例 |
| --- | --- |
| Bearer Token | `Authorization: Bearer <token>` |
| API Key Header | `x-api-key: <token>`(无 `Bearer` 前缀) |
| Gemini 兼容 | `x-goog-api-key: <token>` 或 `?key=<token>` / `?api_key=<token>` |

**鉴权行为**:

- token 在 `config.keys` 中 → **托管账号模式**,自动轮询选择账号
- token 不在 `config.keys` 中 → **直通 token 模式**,直接作为 DeepSeek token 使用

**可选请求头**:`X-Ds2-Target-Account: <email_or_mobile>` — 指定使用某个托管账号;如果目标账号不存在,或管理账号队列已耗尽,相关业务请求会返回 `429`,当前不会附带 `Retry-After` 头。若账号存在但登录/刷新失败,则返回对应的 `401` 或上游错误。未指定目标账号时,托管账号模式的 completion 空输出 429 会先尝试切到另一个可用账号 fresh retry 一次;指定目标账号或无其他可用账号时不会切号。
Gemini 兼容客户端还可以使用 `x-goog-api-key`、`?key=` 或 `?api_key=` 作为凭据来源。

### Admin 接口(`/admin/*`)

| 端点 | 鉴权 |
| --- | --- |
| `POST /admin/login` | 无需鉴权 |
| `GET /admin/verify` | `Authorization: Bearer <jwt>`(仅 JWT) |
| 其他 `/admin/*` | `Authorization: Bearer <jwt>` 或 `Authorization: Bearer <admin_key>`(直传管理密钥) |

---

## 路由总览

| 方法 | 路径 | 鉴权 | 说明 |
| --- | --- | --- | --- |
| GET | `/healthz` | 无 | 存活探针 |
| HEAD | `/healthz` | 无 | 存活探针(无响应体) |
| GET | `/readyz` | 无 | 就绪探针 |
| HEAD | `/readyz` | 无 | 就绪探针(无响应体) |
| GET | `/v1/models` | 无 | OpenAI 模型列表 |
| GET | `/v1/models/{id}` | 无 | OpenAI 单模型查询(支持 alias 入参) |
| POST | `/v1/chat/completions` | 业务 | OpenAI 对话补全 |
| POST | `/v1/responses` | 业务 | OpenAI Responses 接口(流式/非流式) |
| GET | `/v1/responses/{response_id}` | 业务 | 查询已生成 response(内存 TTL) |
| POST | `/v1/embeddings` | 业务 | OpenAI Embeddings 接口 |
| POST | `/v1/files` | 业务 | OpenAI Files 上传(multipart/form-data) |
| GET | `/v1/files/{file_id}` | 业务 | 查询已上传文件状态 |
| GET | `/anthropic/v1/models` | 无 | Claude 模型列表 |
| POST | `/anthropic/v1/messages` | 业务 | Claude 消息接口 |
| POST | `/anthropic/v1/messages/count_tokens` | 业务 | Claude token 计数 |
| POST | `/v1/messages` | 业务 | Claude 消息快捷路径 |
| POST | `/messages` | 业务 | Claude 消息快捷路径 |
| POST | `/v1/messages/count_tokens` | 业务 | Claude token 计数快捷路径 |
| POST | `/messages/count_tokens` | 业务 | Claude token 计数快捷路径 |
| POST | `/v1beta/models/{model}:generateContent` | 业务 | Gemini 非流式 |
| POST | `/v1beta/models/{model}:streamGenerateContent` | 业务 | Gemini 流式 |
| POST | `/v1/models/{model}:generateContent` | 业务 | Gemini 非流式兼容路径 |
| POST | `/v1/models/{model}:streamGenerateContent` | 业务 | Gemini 流式兼容路径 |
| GET | `/api/version` | 无 | Ollama 版本接口 |
| GET | `/api/tags` | 无 | Ollama 模型列表 |
| POST | `/api/show` | 无 | Ollama 单模型能力查询(返回 `id` 与 `capabilities`) |
| POST | `/admin/login` | 无 | 管理登录 |
| GET | `/admin/verify` | JWT | 校验管理 JWT |
| GET | `/admin/vercel/config` | Admin | 读取 Vercel 预配置 |
| GET | `/admin/config` | Admin | 读取配置(脱敏) |
| POST | `/admin/config` | Admin | 更新配置 |
| GET | `/admin/settings` | Admin | 读取运行时设置 |
| PUT | `/admin/settings` | Admin | 更新运行时设置(热更新) |
| POST | `/admin/settings/password` | Admin | 更新 Admin 密码并使旧 JWT 失效 |
| GET | `/admin/skills` | Admin | 读取技能库配置(全局开关、截断长度、条目列表) |
| PUT | `/admin/skills` | Admin | 整体替换技能库配置(校验失败返回 400) |
| POST | `/admin/config/import` | Admin | 导入配置(merge/replace) |
| GET | `/admin/config/export` | Admin | 导出完整配置(含 `config`/`json`/`base64`) |
| POST | `/admin/keys` | Admin | 添加 API key(可附 name/remark) |
| PUT | `/admin/keys/{key}` | Admin | 更新 API key 备注信息 |
| DELETE | `/admin/keys/{key}` | Admin | 删除 API key |
| GET | `/admin/proxies` | Admin | 代理列表 |
| POST | `/admin/proxies` | Admin | 添加代理 |
| PUT | `/admin/proxies/{proxyID}` | Admin | 更新代理(留空 password 表示保留原密码) |
| DELETE | `/admin/proxies/{proxyID}` | Admin | 删除代理(自动解绑引用该代理的账号) |
| POST | `/admin/proxies/test` | Admin | 测试代理连通性 |
| GET | `/admin/accounts` | Admin | 分页账号列表 |
| POST | `/admin/accounts` | Admin | 添加账号 |
| PUT | `/admin/accounts/{identifier}` | Admin | 更新账号 name/remark |
| DELETE | `/admin/accounts/{identifier}` | Admin | 删除账号 |
| PUT | `/admin/accounts/{identifier}/proxy` | Admin | 为账号绑定/解绑代理 |
| GET | `/admin/queue/status` | Admin | 账号队列状态 |
| POST | `/admin/accounts/test` | Admin | 测试单个账号 |
| POST | `/admin/accounts/test-all` | Admin | 测试全部账号 |
| POST | `/admin/accounts/sessions/delete-all` | Admin | 删除某账号的全部会话 |
| POST | `/admin/import` | Admin | 批量导入 keys/accounts |
| POST | `/admin/test` | Admin | 测试当前 API 可用性 |
| POST | `/admin/dev/raw-samples/capture` | Admin | 直接发起一次请求并保存为 raw sample |
| GET | `/admin/dev/raw-samples/query` | Admin | 按问题关键词查询当前内存抓包链 |
| POST | `/admin/dev/raw-samples/save` | Admin | 把命中的内存抓包链保存为 raw sample |
| POST | `/admin/vercel/sync` | Admin | 同步配置到 Vercel |
| GET | `/admin/vercel/status` | Admin | Vercel 同步状态 |
| POST | `/admin/vercel/status` | Admin | Vercel 同步状态 / 草稿对比 |
| GET | `/admin/export` | Admin | 导出配置 JSON/Base64 |
| GET | `/admin/dev/captures` | Admin | 查看本地抓包记录 |
| DELETE | `/admin/dev/captures` | Admin | 清空本地抓包记录 |
| GET | `/admin/chat-history` | Admin | 查看服务器端对话记录(支持按 caller/status/surface/关键词筛选与分页) |
| DELETE | `/admin/chat-history` | Admin | 清空服务器端对话记录 |
| GET | `/admin/chat-history/{id}` | Admin | 查看单条服务器端对话记录 |
| DELETE | `/admin/chat-history/{id}` | Admin | 删除单条服务器端对话记录 |
| PUT | `/admin/chat-history/settings` | Admin | 更新对话记录保留条数(`0`..`500`) |
| GET | `/admin/chat-history/export` | Admin | 按相同筛选条件导出对话记录(`json` / `markdown`) |
| GET | `/admin/version` | Admin | 查询当前版本与最新 Release |

OpenAI `/v1/*` 仍是规范路径。对于只配置 DS2API 根地址的客户端,同一套 OpenAI handler 也通过根路径快捷路由暴露:`/models`、`/models/{id}`、`/chat/completions`、`/responses`、`/responses/{response_id}`、`/embeddings`、`/files`、`/files/{file_id}`。

服务器端记录本质上是 DeepSeek 上游响应归档:OpenAI Chat、OpenAI Responses、Claude Messages、Gemini GenerateContent 等直连 DeepSeek 的生成接口,在收到上游响应后会于各协议回译/裁剪前写入记录;列表按请求创建时间倒序展示,流式请求会在生成过程中持续刷新状态与详情。WebUI「API 测试」发出的请求也会进入该记录。每条记录带 `caller_id`(API Key 调用方)与 `caller_label`(Key 的名称/备注/脱敏值),以及 `surface`(来源接口)、`status`、`status_code` 等可筛选字段;列表、详情、导出共用同一份数据,详见 [对话记录](#get-adminchat-history)。

---

## 健康检查

### `GET /healthz`

```json
{"status": "ok"}
```

### `GET /readyz`

```json
{"status": "ready"}
```

---

## OpenAI 兼容接口

### `GET /v1/models`

无需鉴权。返回当前支持的 DeepSeek 原生模型列表。

**响应示例**:

```json
{
  "object": "list",
  "data": [
    {"id": "deepseek-v4-flash", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []},
    {"id": "deepseek-v4-flash-nothinking", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []},
    {"id": "deepseek-v4-flash-search", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []},
    {"id": "deepseek-v4-flash-search-nothinking", "object": "model", "created": 1677610602, "owned_by": "deepseek", "permission": []}
  ]
}
```

> 说明:`/v1/models` 返回的是规范化后的 DeepSeek 原生模型 ID;常见 alias 仅用于请求入参解析,不会在该接口中单独展开返回。带 `-nothinking` 后缀的模型表示无论请求里是否显式开启 thinking / reasoning,都会强制关闭思考输出。
>
> 模型系列已收敛到上表 4 个 flash 变体:`deepseek-v4-pro` / `deepseek-v4-pro-search` / `deepseek-v4-vision` 及其 `-nothinking` 形式已下线,不再出现在本接口、Ollama `/api/tags` 与 `/anthropic/v1/models` 中,但旧名字仍可被请求,解析时静默映射到 flash 对应项(见下一节)。

### 模型 alias 解析策略

对 `chat` / `responses` / `embeddings` 的 `model` 字段采用“宽进严出”:

1. 先匹配 DeepSeek 原生模型。
2. 再匹配 `model_aliases` 精确映射。
3. 如果请求名以 `-nothinking` 结尾,则在最终解析出的规范模型上追加对应的无思考变体。
4. 仍未命中则返回 `invalid_request_error`。当前不会按未知模型家族做启发式兜底;需要新增兼容名时请通过 `model_aliases` 明确配置。

当前内置默认 alias 来自 `internal/config/models.go`,`config.model_aliases` 会在运行时覆盖或补充同名映射。节选(全部指向 flash 系列):

- OpenAI / Codex:`gpt-4o`、`gpt-4.1`、`gpt-5`、`gpt-5.5`、`gpt-5-codex`、`gpt-5.3-codex`、`codex-mini-latest` → `deepseek-v4-flash`
- OpenAI reasoning:`o1`、`o3`、`o4-mini` → `deepseek-v4-flash`;`o3-deep-research`、`o4-mini-deep-research` → `deepseek-v4-flash-search`
- Claude:`claude-opus-4-6`、`claude-sonnet-4-6`、`claude-haiku-4-5`、`claude-3-5-sonnet-latest` → `deepseek-v4-flash`
- Gemini:`gemini-2.5-pro`、`gemini-2.5-flash`、`gemini-3.1-pro`、`gemini-3-pro`、`gemini-3-flash`、`gemini-3.1-flash-lite`、`gemini-pro-vision` → `deepseek-v4-flash`
- 其他内置精确 alias:`llama-3.1-70b-instruct`、`qwen-max` → `deepseek-v4-flash`

上述 alias 若在请求名后追加 `-nothinking` 后缀,也会映射到对应的强制关闭 thinking 版本。

已下线的 DeepSeek 模型名不会让老客户端失效,解析时静默归一:

| 请求模型名 | 实际解析结果 |
| --- | --- |
| `deepseek-v4-pro` / `deepseek-v4-vision` | `deepseek-v4-flash` |
| `deepseek-v4-pro-search` | `deepseek-v4-flash-search` |
| `deepseek-v4-pro-nothinking` / `deepseek-v4-vision-nothinking` | `deepseek-v4-flash-nothinking` |
| `deepseek-v4-pro-search-nothinking` | `deepseek-v4-flash-search-nothinking` |

上游 `model_type` 现在只剩 `default`(历史配置里出现的 `expert` / `vision` 按 `default` 处理),因此不存在独立的视觉或 expert 通道。

退役历史模型(如 `claude-1.*`、`claude-2.*`、`claude-instant-*`、`gpt-3.5*`)会被显式拒绝。

### 思考与联网搜索解析策略

同一套规则适用于 `/v1/chat/completions`、`/v1/responses`、`/anthropic/v1/messages` 与 Gemini `generateContent`,实现在 `internal/util/thinking.go`、`internal/util/search.go`,不在各协议适配器里。

**思考(thinking)**,优先级从低到高:

| 顺序 | 来源 | 说明 |
| --- | --- | --- |
| 1 | 模型默认值 | `-nothinking` 变体默认关;其余 flash 模型默认开 |
| 2 | 客户端显式覆盖 | 命中任一字段即生效 |
| 3 | `-nothinking` 模型 | 强制关闭,优先级最高,客户端无法打开 |

客户端覆盖字段(`extra_body` 里的同名键同样会被读取):

| 字段 | 接受写法 |
| --- | --- |
| `thinking` | bool;`"enabled"` / `"disabled"`(含 `on` / `off` / `true` / `false` / `none`);`{"type": ...}` |
| `reasoning` | `{"effort": ...}`、`{"type": ...}`、`{"enabled": ...}`;字符串按 effort 解析 |
| `reasoning_effort` | `minimal` / `low` / `medium` / `high` / `xhigh` / `max` = 开启;`none` / `disabled` / `disable` / `off` / `false` = 关闭 |

Kelivo 这类客户端的「思维强度」界面就是靠 `reasoning_effort` 传开关:不发该字段表示跟随默认(开启思考),传 `none` 表示关闭。本项目只区分开/关,不区分强度档位。

**联网搜索(search)**,优先级为「模型默认 → 客户端显式信号优先」:

| 信号 | 说明 |
| --- | --- |
| `enable_search` | bool,或 `"true"` / `"false"` / `"on"` / `"off"` / `"enabled"` / `"disabled"` 等字符串 |
| `web_search_options` | 字段出现即视为开启 |
| `search_options` | 字段出现即视为开启 |
| `tools` 原生搜索类型 | `type` 以 `web_search` 开头、`type=google_search`,或工具对象含 `googleSearch` / `google_search` / `web_search` 字段 |

`-search` 变体默认开启联网;显式 `enable_search=false` 可在该模型上临时关掉。普通 function 工具(例如 Kelivo 客户端自己执行的 `search_web`)不算联网信号,所以默认仍不开启上游联网——要联网请选 `-search` 模型;一旦客户端开始发送上述原生信号,服务端会自动跟随,无需改配置。

### `POST /v1/chat/completions`

> 路径说明:除规范路径 `/v1/chat/completions` 外,也支持根路径快捷别名 `/chat/completions`。在 Vercel Runtime 上,`vercel.json` 仅把规范路径 `/v1/chat/completions` 重写到 Node 流式桥接;根路径快捷别名仍走 Go 主链路。因此 Vercel 上需要实时流式时请使用 `/v1/chat/completions`。

**请求头**:

```http
Authorization: Bearer your-api-key
Content-Type: application/json
```

**请求体**:

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `model` | string | ✅ | 支持 DeepSeek 原生模型 + 常见 alias(如 `gpt-5.5`、`gpt-5.4-mini`、`gpt-5.3-codex`、`o3`、`claude-opus-4-6`、`claude-sonnet-4-6`、`gemini-2.5-pro`、`gemini-3.1-pro`、`gemini-3-flash` 等);若模型名带 `-nothinking` 后缀,则强制关闭 thinking / reasoning |
| `messages` | array | ✅ | OpenAI 风格消息数组 |
| `stream` | boolean | ❌ | 默认 `false` |
| `tools` | array | ❌ | Function Calling 定义 |
| `thinking` | bool / string / object | ❌ | 显式思考开关,覆盖模型默认;`-nothinking` 模型会忽略 |
| `reasoning` / `reasoning_effort` | object / string | ❌ | 与 `thinking` 等价的另一种写法;`reasoning_effort=none` 关闭思考,`minimal`~`max` 开启 |
| `enable_search` | bool / string | ❌ | 显式联网开关,覆盖模型默认(用于在 `-search` 模型上临时关闭) |
| `temperature` 等 | any | ❌ | 兼容透传字段(最终效果由上游决定) |

> 思考与联网的完整解析优先级见 [思考与联网搜索解析策略](#思考与联网搜索解析策略)。

#### 非流式响应

```json
{
  "id": "<chat_session_id>",
  "object": "chat.completion",
  "created": 1738400000,
  "model": "deepseek-v4-flash",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "最终回复",
        "reasoning_content": "思考内容(开启 thinking 时)"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 10,
    "completion_tokens": 20,
    "total_tokens": 30,
    "completion_tokens_details": {
      "reasoning_tokens": 5
    }
  }
}
```

#### 流式响应(`stream=true`)

SSE 格式:每段为 `data: <json>\n\n`,结束为 `data: [DONE]`。

```text
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}

data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"reasoning_content":"..."},"index":0}]}

data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"..."},"index":0}]}

data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":{...}}

data: [DONE]
```

**字段说明**:

- 首个 delta 包含 `role: assistant`
- 开启 thinking 时会输出 `delta.reasoning_content`
- 普通文本输出 `delta.content`
- 最后一段包含 `finish_reason` 和 `usage`
- token 计数优先透传上游 DeepSeek SSE(如 `accumulated_token_usage` / `token_usage`);仅在上游缺失时回退本地估算。失败/中断型结束(例如 `response.failed`)可能不会携带 `usage`

#### Tool Calls

> 若 `tools.enabled=false`(默认),请求里的 `tools` 会被忽略,响应不会包含 `tool_calls`。下面描述的行为只在开启工具调用后生效,开启风险见 [docs/prompt-compatibility.md](docs/prompt-compatibility.md)。

当请求中含 `tools` 时,DS2API 做防泄漏处理:

**非流式**:识别到工具调用时,返回 `message.tool_calls`,设置 `finish_reason=tool_calls`,`message.content=null`。

```json
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_xxx",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"beijing\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}
```

**流式**:命中高置信特征后立即输出 `delta.tool_calls`(不等待完整工具参数闭合),并持续发送 arguments 增量;已确认的工具调用片段不会回流到 `delta.content`。

补充说明:

- **非代码块上下文**下,工具负载即使与普通文本混合,也会按特征识别并产出可执行 tool call(前后普通文本仍可透传)。
- 解析器当前把推荐半角管道符 DSML 外壳(`<|DSML|tool_calls>` / `<|DSML|invoke name="...">` / `<|DSML|parameter name="...">`)、DSML wrapper 别名(`<dsml|tool_calls>`、`<|tool_calls>`)、常见 DSML 分隔符漏写形态(如 `<|DSML tool_calls>` / `<|DSML invoke>` / `<|DSML parameter>`)、`DSML` 与工具标签名黏连的常见 typo(如 `<DSMLtool_calls>` / `<DSMLinvoke>` / `<DSMLparameter>`)、控制分隔符漂移(如 `<DSML␂tool_calls>` / 原始 STX `\x02`)、CJK 尖括号、全角感叹号、顿号、PascalCase 本地名、弯引号属性值与属性尾部分隔符漂移(如 `<DSM|parameter name="command"|>...〈/DSM|parameter〉` / `<!DSML!invoke name=“Bash”>` / `<、DSML、tool_calls>` / `<DSmartToolCalls>` / `<DSMLtool_calls※>`)、任意协议前缀壳(如 `<proto💥tool_calls>`)和旧式 canonical XML 工具块(`<tool_calls>` / `<invoke name="...">` / `<parameter name="...">`)作为可执行调用解析;这些非结构性分隔符壳会先归一化回 XML,内部仍以 XML 解析语义为准,CDATA 开头也会容错 `<![CDATA[` / `<、[CDATA[`。旧式 `<tools>`、`<tool_call>`、`<tool_name>`、`<param>`、`<function_call>`、`tool_use`、antml 风格与纯 JSON `tool_calls` 片段默认都会按普通文本处理;完整但 malformed 的 wrapper 同样会作为普通文本释放。
- 解析层不会因为参数值为空而丢弃工具调用;显式空字符串或纯空白参数会按空字符串进入结构化 `tool_calls`。Prompt 会要求模型不要主动输出空参数,缺参/空命令的拒绝应由工具执行侧或客户端 schema 校验负责。
- 当最终可见正文为空但思维链里包含可执行工具调用时,Chat / Responses 会在收尾阶段补发标准 OpenAI `tool_calls` / `function_call` 输出;如果客户端未开启 thinking / reasoning,该思维链只用于检测,不会作为可见正文或 `reasoning_content` 暴露。
- Markdown fenced code block(例如 ```json ... ```)和行内 code span(例如 `` `<tool_calls>...</tool_calls>` ``)中的 `tool_calls` 仅视为示例文本,不会被执行。

---

### `GET /v1/models/{id}`

无需鉴权。入参支持 alias(例如 `gpt-4o`),返回的是映射后的 DeepSeek 模型对象。

### `POST /v1/responses`

OpenAI Responses 风格接口,兼容 `input` 或 `messages`。

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `model` | string | ✅ | 支持原生模型 + alias 自动映射 |
| `input` | string/array/object | ❌ | 与 `messages` 二选一 |
| `messages` | array | ❌ | 与 `input` 二选一 |
| `instructions` | string | ❌ | 自动前置为 system 消息 |
| `stream` | boolean | ❌ | 默认 `false` |
| `tools` | array | ❌ | 与 chat 同样的工具识别与转译策略(含代码块示例豁免) |
| `tool_choice` | string/object | ❌ | 支持 `auto`/`none`/`required` 与强制函数(`{"type":"function","name":"..."}`) |

**非流式响应**:返回标准 `response` 对象,`id` 形如 `resp_xxx`,并写入内存 TTL 存储。
当 `tool_choice=required` 且未产出有效工具调用时,返回 HTTP `422`(`error.code=tool_choice_violation`)。

**流式响应(SSE)**:最小事件序列如下。

```text
event: response.created
data: {"type":"response.created","id":"resp_xxx","status":"in_progress",...}

event: response.output_item.added
data: {"type":"response.output_item.added","response_id":"resp_xxx","item":{"type":"message|function_call",...},...}

event: response.content_part.added
data: {"type":"response.content_part.added","response_id":"resp_xxx","part":{"type":"output_text",...},...}

event: response.output_text.delta
data: {"type":"response.output_text.delta","response_id":"resp_xxx","item_id":"msg_xxx","output_index":0,"content_index":0,"delta":"..."}

event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","response_id":"resp_xxx","call_id":"call_xxx","delta":"..."}

event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","response_id":"resp_xxx","call_id":"call_xxx","name":"tool","arguments":"{...}"}

event: response.content_part.done
data: {"type":"response.content_part.done","response_id":"resp_xxx",...}

event: response.output_item.done
data: {"type":"response.output_item.done","response_id":"resp_xxx","item":{"type":"message|function_call",...},...}

event: response.completed
data: {"type":"response.completed","response":{...}}

data: [DONE]
```

流式场景下若 `tool_choice=required` 违规,会返回 `response.failed` 后结束(不再发送 `response.completed`)。

> 当前版本说明:解析层默认“尽量提取结构化 tool call”,未启用基于 `tools` allow-list 的硬拒绝;是否执行仍应由你的工具执行器做白名单校验。

### `GET /v1/responses/{response_id}`

需要业务鉴权。查询 `POST /v1/responses` 生成并缓存的 response 对象(按调用方鉴权隔离,仅同一 key/token 可读取)。

> 当前为内存 TTL 存储,默认过期时间 `900s`(可用 `responses.store_ttl_seconds` 调整)。

### `POST /v1/embeddings`

需要业务鉴权。返回 OpenAI Embeddings 兼容结构。

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `model` | string | ✅ | 支持原生模型 + alias 自动映射 |
| `input` | string/array | ✅ | 支持字符串、字符串数组、token 数组 |

> 需配置 `embeddings.provider`。当前支持:`mock` / `deterministic` / `builtin`(三者都走同一套本地确定性实现)。未配置或不支持时返回标准错误结构(HTTP 501)。

### `POST /v1/files`

需要业务鉴权。兼容 OpenAI Files 上传接口,当前仅支持 `multipart/form-data`。

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `file` | file | ✅ | 上传文件二进制 |
| `purpose` | string | ❌ | 透传到上游用途字段 |

约束与行为:

- 请求必须为 `multipart/form-data`,否则返回 `400`。
- 请求体总大小上限 **100 MiB**(超限返回 `413`)。
- 成功返回 OpenAI `file` 对象(`id/object/bytes/filename/purpose/status` 等字段),并附带 `account_id` 便于定位来源账号。

### `GET /v1/files/{file_id}`

需要业务鉴权。查询 DeepSeek 上传文件的当前状态,并返回 OpenAI `file` 对象;未找到匹配文件时返回 `404`。

---

## Claude 兼容接口

除标准路径 `/anthropic/v1/*` 外,还支持快捷路径 `/v1/messages`、`/messages`、`/v1/messages/count_tokens`、`/messages/count_tokens`。
实现上统一走 OpenAI Chat Completions 解析与回译链路,避免多套解析逻辑分叉维护。

### `GET /anthropic/v1/models`

无需鉴权。

**响应示例**:

```json
{
  "object": "list",
  "data": [
    {"id": "claude-sonnet-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
    {"id": "claude-sonnet-4-6-nothinking", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
    {"id": "claude-haiku-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
    {"id": "claude-haiku-4-5-nothinking", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
    {"id": "claude-opus-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
    {"id": "claude-opus-4-6-nothinking", "object": "model", "created": 1715635200, "owned_by": "anthropic"}
  ],
  "first_id": "claude-opus-4-6",
  "last_id": "claude-3-haiku-20240307-nothinking",
  "has_more": false
}
```

> 说明:示例仅展示部分模型;实际返回除当前主别名外,还包含 Claude 4.x snapshots、3.x 历史模型 ID 与常见别名,并为这些可映射模型额外提供 `-nothinking` 变体。

### `POST /anthropic/v1/messages`

**请求头**:

```http
x-api-key: your-api-key
Content-Type: application/json
anthropic-version: 2023-06-01
```

> `anthropic-version` 可省略,服务端会自动补为 `2023-06-01`。

**请求体**:

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `model` | string | ✅ | 例如 `claude-sonnet-4-6` / `claude-opus-4-6` / `claude-haiku-4-5`(兼容 `claude-sonnet-4-5`、`claude-3-5-haiku-latest`),并支持历史 Claude 模型 ID;若模型名带 `-nothinking` 后缀,则强制关闭 thinking / reasoning |
| `messages` | array | ✅ | Claude 风格消息数组 |
| `max_tokens` | number | ❌ | 缺省自动补 `8192`;当前实现不会硬性截断上游输出 |
| `stream` | boolean | ❌ | 默认 `false` |
| `system` | string | ❌ | 可选系统提示 |
| `tools` | array | ❌ | Claude tool 定义 |
| `thinking` | object | ❌ | Anthropic thinking 配置;会转译为下游 reasoning 控制,`-nothinking` 模型会忽略 |
| `temperature` | number | ❌ | 透传到下游;若同时提供 `top_p`,以 `temperature` 为准 |
| `top_p` | number | ❌ | 当未提供 `temperature` 时透传到下游 |
| `stop_sequences` | array | ❌ | 透传到下游停用序列 |
| `tool_choice` | string/object | ❌ | 支持 `auto` / `none` / `required` / `{"type":"function","name":"..."}`,并会转译为下游工具选择 |

> 说明:上述 `thinking`、`temperature`、`top_p`、`stop_sequences`、`tool_choice` 都会走兼容层转译;最终是否生效仍取决于当前模型和上游能力。`temperature` 与 `top_p` 同时存在时,`temperature` 优先。思考与联网的解析优先级与 OpenAI 接口完全一致,见 [思考与联网搜索解析策略](#思考与联网搜索解析策略)。若 `tools.enabled=false`(默认),请求里的 `tools` 会被忽略,响应不会包含 `tool_calls`(Claude 侧即 `tool_use` 块)。

#### 非流式响应

```json
{
  "id": "msg_1738400000000000000",
  "type": "message",
  "role": "assistant",
  "model": "claude-sonnet-4-6",
  "content": [
    {"type": "text", "text": "回复内容"}
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 12,
    "output_tokens": 34
  }
}
```

若识别到工具调用,`stop_reason=tool_use`,`content` 中返回 `tool_use` block。

#### 流式响应(`stream=true`)

SSE 使用 `event:` + `data:` 双行格式,JSON 中保留 `type` 字段。

```text
event: message_start
data: {"type":"message_start","message":{...}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hello"}}

event: ping
data: {"type":"ping"}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":12}}

event: message_stop
data: {"type":"message_stop"}
```

**说明**:

- 默认支持 thinking 的模型会输出 `thinking` block / `thinking_delta`;请求显式关闭 thinking 或使用 `-nothinking` 模型时不会输出
- 带 `-nothinking` 后缀的模型会强制关闭 thinking,即使请求显式传了 `thinking` / `reasoning` / `reasoning_effort` 也不会输出 `thinking_delta`
- 不会输出 `signature_delta`(上游 DeepSeek 未提供可验证签名)
- `tools` 场景优先避免泄露原始工具 JSON,不强制发送 `input_json_delta`

### `POST /anthropic/v1/messages/count_tokens`

**请求**:

```json
{
  "model": "claude-sonnet-4-6",
  "messages": [
    {"role": "user", "content": "你好"}
  ]
}
```

**响应**:

```json
{
  "input_tokens": 5
}
```

---

## Gemini 兼容接口

支持路径:

- `/v1beta/models/{model}:generateContent`
- `/v1beta/models/{model}:streamGenerateContent`
- `/v1/models/{model}:generateContent`(兼容路径)
- `/v1/models/{model}:streamGenerateContent`(兼容路径)

鉴权方式同业务接口(`Authorization: Bearer <token>` 或 `x-api-key`)。
实现上统一走 OpenAI Chat Completions 解析与回译链路,避免多套解析逻辑分叉维护。

### `POST /v1beta/models/{model}:generateContent`

请求体兼容 Gemini `contents` / `tools` 字段,模型名可用 alias 自动映射到 DeepSeek 模型;若路径中的模型名带 `-nothinking` 后缀,则最终会映射到对应的无思考模型。思考与联网的解析优先级与 OpenAI 接口一致,见 [思考与联网搜索解析策略](#思考与联网搜索解析策略)。若 `tools.enabled=false`(默认),请求里的 `tools` 会被忽略,响应不会包含 `tool_calls`(Gemini 侧即 `functionCall`)。

响应为 Gemini 兼容结构,核心字段包括:

- `candidates[].content.parts[].text`
- `candidates[].content.parts[].thought=true`(thinking 输出)
- `candidates[].content.parts[].functionCall`(工具调用时)
- `usageMetadata`(`promptTokenCount` / `candidatesTokenCount` / `totalTokenCount`)

### `POST /v1beta/models/{model}:streamGenerateContent`

返回 SSE(`text/event-stream`),每个 chunk 为一条 `data: <json>`:

- 常规文本:持续返回增量文本 chunk
- thinking:持续返回 `parts[].thought=true` 的增量 chunk
- `tools` 场景:会缓冲并在结束时输出 `functionCall` 结构
- 结束 chunk:包含 `finishReason: "STOP"` 与 `usageMetadata`
- token 计数优先透传上游 DeepSeek SSE(如 `accumulated_token_usage` / `token_usage`);仅在上游缺失时回退本地估算

---

## Ollama 兼容接口

- `POST /api/show` 请求体:`{"model":"<model-id>"}`。
- `GET /api/tags` 只列出 flash 系列的 4 个模型 ID(`deepseek-v4-flash`、`deepseek-v4-flash-search` 及各自 `-nothinking` 变体),与 `/v1/models` 保持一致;已下线的 pro / vision 名字不再出现,但仍可作为 `model` 入参被请求。
- 响应字段使用小写 `id`(不是 `ID`),并返回 `capabilities` 数组,便于与 Ollama 风格客户端/严格 schema 对齐。

示例响应:

```json
{
  "id": "deepseek-v4-flash",
  "capabilities": ["tools", "thinking"]
}
```

## Admin 接口

### `POST /admin/login`

无需鉴权。

**请求**:

```json
{
  "admin_key": "admin",
  "expire_hours": 24
}
```

`expire_hours` 可省略,默认 `24`。

**响应**:

```json
{
  "success": true,
  "token": "<jwt>",
  "expires_in": 86400
}
```

### `GET /admin/verify`

需要 JWT:`Authorization: Bearer <jwt>`

**响应**:

```json
{
  "valid": true,
  "expires_at": 1738400000,
  "remaining_seconds": 72000
}
```

### `GET /admin/vercel/config`

返回 Vercel 预配置状态。优先读取环境变量,其次回退到已保存的 `vercel` 配置块。

```json
{
  "has_token": true,
  "token_preview": "vc****en",
  "token_source": "config",
  "project_id": "prj_xxx",
  "team_id": null
}
```

### `GET /admin/config`

返回脱敏后的配置,包含 `keys` 与 `api_keys`。

```json
{
  "keys": ["k1", "k2"],
  "api_keys": [
    {"key": "k1", "name": "主 Key", "remark": "生产流量"},
    {"key": "k2", "name": "备用 Key", "remark": "压测"}
  ],
  "env_backed": false,
  "env_source_present": true,
  "env_writeback_enabled": true,
  "config_path": "/data/config.json",
  "vercel": {
    "has_token": true,
    "token_preview": "vc****en",
    "project_id": "prj_xxx",
    "team_id": ""
  },
  "accounts": [
    {
      "identifier": "user@example.com",
      "email": "user@example.com",
      "mobile": "",
      "has_password": true,
      "has_token": true,
      "token_preview": "abcde..."
    }
  ],
  "model_aliases": {
    "claude-sonnet-4-6": "deepseek-v4-flash",
    "claude-opus-4-6": "deepseek-v4-flash"
  }
}
```

### `POST /admin/config`

只更新 `keys`、`api_keys`、`accounts`、`model_aliases`。
如果同时发送 `api_keys` 与 `keys`,优先保留 `api_keys` 中的结构化 `name` / `remark`;`keys` 仅作为旧格式兼容回退。

**请求**:

```json
{
  "keys": ["k1", "k2"],
  "api_keys": [
    {"key": "k1", "name": "主 Key", "remark": "生产流量"},
    {"key": "k2", "name": "备用 Key", "remark": "压测"}
  ],
  "accounts": [
    {"email": "user@example.com", "password": "pwd", "token": ""}
  ],
  "model_aliases": {
    "claude-sonnet-4-6": "deepseek-v4-flash",
    "claude-opus-4-6": "deepseek-v4-flash"
  }
}
```

### `api_keys[].thinking_injection`(单 Key 覆盖思考格式注入)

`api_keys` 的每一项都可以带 `thinking_injection`,用于给单个 API Key 定制注入提示词;`keys` 旧格式不支持该字段。

```json
{
  "api_keys": [
    {
      "key": "k1",
      "name": "主 Key",
      "remark": "生产流量",
      "thinking_injection": {
        "custom": true,
        "enabled": true,
        "prompt": "回答前先列清楚已知条件,再给结论。"
      }
    },
    {"key": "k2", "name": "备用 Key"}
  ]
}
```

| 字段 | 说明 |
| --- | --- |
| `custom` | 默认 `false`。为 `false`(或整段省略)时完全跟随全局 `thinking_injection` |
| `enabled` | 仅在 `custom=true` 时生效;省略表示继续跟随全局开关 |
| `prompt` | 仅在 `custom=true` 时生效;为空或全空白表示继续跟随全局提示词 |

全局开关与默认提示词见 [`GET /admin/settings`](#get-adminsettings) 的 `thinking_injection`;注入位置与默认提示词内容见 [docs/prompt-compatibility.md](docs/prompt-compatibility.md)。

### `api_keys[].prompt_profile`(单 Key 覆盖提示词特化层)

`api_keys` 的每一项都可以带 `prompt_profile`,用于给单个 API Key 指定特化层预设或正文,与 `thinking_injection`、`skills` 并列;`keys` 旧格式不支持该字段。该字段目前只在配置文件/导入导出里生效,后台面板只提供全局段(见下)。

```json
{
  "api_keys": [
    {
      "key": "k1",
      "name": "写作 Key",
      "prompt_profile": {"custom": true, "enabled": true, "name": "writing"}
    },
    {"key": "k2", "name": "默认 Key"}
  ]
}
```

| 字段 | 说明 |
| --- | --- |
| `custom` | 默认 `false`;为 `false` 或整段省略时完全跟随全局 `prompt_profile` |
| `enabled` | 仅在 `custom=true` 时生效;为 `false` 表示该 Key 不注入特化层 |
| `name` | 仅在 `custom=true` 时生效;`default` / `writing`,非空时覆盖全局预设 |
| `prompt` | 仅在 `custom=true` 时生效;非空时覆盖预设正文(上限 20000 字符) |

全局 `prompt_profile.enabled=false` 时上表全部不生效,任何 Key 都不注入。

### `prompt_style`(内置提示词风格,全局)

顶层字符串字段,只影响内置文案,不影响你自己配置的提示词:

```json
{"prompt_style": "legacy"}
```

| 值 | 行为 |
| --- | --- |
| `v2`(缺省,也接受空串) | 历史拆分后的衔接指令带「执行要求」(上文要求逐条满足)并注入内置执行契约(思考注入) |
| `legacy` | 衔接指令换回 ce5a019 时代的短文案,且**不注入内置执行契约**;显式配置过 `thinking_injection.prompt` 时仍然注入自定义内容 |

未知值不会让配置加载失败:按 `v2` 处理并打一条告警。后台 `PUT /admin/settings` 传未知值直接返回 400。

### `model_profiles`(自定义模型列表,全局)

给同一个 flash 基模挂出多个对外模型名,每个名字带自己的提示词。客户端请求里写哪个模型名,就用哪一份提示词,互不影响。

```json
{
  "model_profiles": [
    {
      "id": "deepseek-writer",
      "base": "deepseek-v4-flash",
      "name": "写作",
      "remark": "长文与改稿",
      "prompt": "## 写作要求\n…"
    }
  ]
}
```

| 字段 | 说明 |
| --- | --- |
| `id` | 必填,对外模型名。字符集 `[A-Za-z0-9._-]`,长度 1-64,不可与内置模型名(如 `deepseek-v4-flash`)冲突,条目间不可重复(大小写不敏感) |
| `base` | 必填,基模,只能是 `deepseek-v4-flash` / `-search` / `-nothinking` / `-search-nothinking` |
| `name` / `remark` | 可选,仅用于后台与导出可读性 |
| `prompt` | 可选,该模型专属提示词,上限 20000 字符;为空表示不额外注入 |

解析与注入语义:

- 模型名解析优先于内置别名映射:`id` 命中自定义模型时按它的 `base` 调用上游(`/v1/models`、`/v1/models/{id}`、`/v1/chat/completions`、Claude / Gemini 入口共用同一套解析)。
- `/v1/models` 会把自定义模型一起列出来,`owned_by` 为 `ds2api`。
- 提示词注入点与 `prompt_profile` 相同(历史拆分之后、技能层之前),两者**互斥**:该模型配了非空 `prompt` 就用它,否则回落到全局 `prompt_profile`。
- 数量上限 50 条。接口见 [`GET` / `PUT /admin/model_profiles`](#get--put-adminmodel_profiles)。

### `api_keys[].skills`(单 Key 覆盖技能注入)

`api_keys` 的每一项都可以带 `skills`,用来指定这个 API Key 注入哪些技能,与 `thinking_injection` 并列;`keys` 旧格式不支持该字段。

```json
{
  "api_keys": [
    {"key": "k1", "name": "主 Key", "skills": {"custom": true, "enabled": true, "ids": ["sql-report"]}},
    {"key": "k2", "name": "备用 Key"}
  ]
}
```

| 字段 | 说明 |
| --- | --- |
| `custom` | 默认 `false`;为 `false` 或整段省略时使用全部 `skills.items` |
| `enabled` | 仅在 `custom=true` 时生效;为 `false` 表示该 Key 不注入 |
| `ids` | 仅在 `custom=true` 时生效;非空时只用命中的技能,为空时该 Key 不注入 |

| `custom` | `enabled` | `ids` | 结果 |
| --- | --- | --- | --- |
| 省略或 `false` | 任意 | 任意 | 使用全部 `items` |
| `true` | `false` | 任意 | 不注入 |
| `true` | 省略或 `true` | 非空 | 只用命中的 `items` |
| `true` | 省略或 `true` | 空 | 不注入 |

全局 `skills.enabled=false` 时上表全部不生效,任何 Key 都不注入。技能正文与管理接口见 [`GET` / `PUT /admin/skills`](#get--put-adminskills)。

### `GET /admin/settings`

读取运行时设置与状态,返回:

- `success`
- `admin`(`has_password_hash`、`jwt_expire_hours`、`jwt_valid_after_unix`、`default_password_warning`)
- `runtime`(`account_max_inflight`、`account_max_queue`、`global_max_inflight`、`token_refresh_interval_hours`)
- `runtime.token_refresh_interval_hours` 的语义是「会话体检间隔」:到期只做一次无密码复核(`GET /api/v0/users/current`),只有服务端明确拒绝会话时才回退到密码登录
- `config_snapshot`:原始配置快照(`admin` / `runtime` / `responses` / `embeddings` / `auto_delete` / `current_input_file` / `thinking_injection` / `prompt_profile` / `prompt_style` / `model_profiles` / `model_aliases`),用于区分环境变量覆盖前的配置
- `responses` / `embeddings`
- `auto_delete`(`mode`:`none` / `single` / `all`;旧配置 `sessions=true` 仍按 `all` 处理)
- `current_input_file`(`enabled` 默认返回 `true`、`min_chars`)
- `thinking_injection`(`enabled` 缺省为 `true`、`prompt`、`default_prompt`);`prompt` 为空时使用内置默认提示词,单 Key 可用 `api_keys[].thinking_injection` 覆盖
- `prompt_profile`(`enabled` 缺省 `false`、`enabled_default` 固定 `false`、`name`、`prompt`、`presets`);`presets` 是 `[{name, body}]` 的内置预设清单,`body` 为该预设正文,供面板在输入框里显示「留空时使用的默认正文」
- `prompt_style`(`mode`:生效风格 `v2` / `legacy`;`modes`:可选值;`default`:`v2`;`value_raw`:配置里原样存的值)
- `model_profiles`(`count`、`max_count`、`base_models`、`max_id_chars`、`max_prompt_chars`);条目本身用 `/admin/model_profiles` 读写
- `model_aliases`
- `tools`(`enabled`:工具调用总开关,缺省 `false`;`default_enabled`:固定 `false`,供面板显示默认值)
- `env_backed`、`needs_vercel_sync`
- `toolcall` 策略已固定为 `feature_match + high`,不再通过 settings 返回或修改

### `PUT /admin/settings`

热更新运行时设置。支持更新:

- `admin.jwt_expire_hours`
- `runtime.account_max_inflight` / `runtime.account_max_queue` / `runtime.global_max_inflight` / `runtime.token_refresh_interval_hours`
- `responses.store_ttl_seconds`
- `embeddings.provider`
- `auto_delete.mode`
- `current_input_file.enabled` / `current_input_file.min_chars`
- `thinking_injection.enabled` / `thinking_injection.prompt`
- `prompt_profile.enabled` / `prompt_profile.name` / `prompt_profile.prompt`
- `prompt_style`
- `tools.enabled`
- `model_aliases`
- `toolcall` 策略已固定,不再作为可写入字段

`prompt_profile` 是本次新增的「提示词特化层」,缺省关闭;`name` 只接受 `default` / `writing`(其他值返回 400),`prompt` 上限 20000 字符(超限返回 400),只传部分字段时其余字段保持不变:

```json
{"prompt_profile": {"enabled": true, "name": "writing"}}
```

注入位置、拼接顺序与预设正文见 [docs/prompt-compatibility.md](docs/prompt-compatibility.md) 与 [docs/prompt-design.md](docs/prompt-design.md)。

`prompt_style` 是顶层字符串(不是对象),只接受 `v2` / `legacy` / 空串(空串等于回到 v2):

```json
{"prompt_style": "legacy"}
```

自定义模型列表不走 settings,用下面的 `/admin/model_profiles` 整体替换。

`tools.enabled` 只改请求体里传了的字段,其它设置的现有语义不变:

```json
{"tools": {"enabled": true}}
```

关闭(默认)时带 `tools` 的请求按纯对话处理;DSML 工具调用可能触发上游风控导致账号封禁,是否开启请自行评估,风险说明见 [docs/prompt-compatibility.md](docs/prompt-compatibility.md)。

### `POST /admin/settings/password`

更新管理密码并使旧 JWT 失效。

请求示例:

```json
{"new_password":"your-new-password"}
```

也兼容 `{"password":"your-new-password"}`。

### `GET` / `PUT /admin/skills`

纯文本技能库,实验性功能,缺省关闭(`skills.enabled=false`)。技能正文由服务端拼进 prompt,模型不需要工具调用就能按固定套路工作。

- 全局配置:`skills.enabled`(缺省 `false`)、`skills.max_chars`(缺省 `8000`)、`skills.items`
- 单 Key 覆盖:`api_keys[].skills`,语义见上文「api_keys[].skills(单 Key 覆盖技能注入)」
- 注入形态、注入顺序与「为什么不用工具调用」:[docs/prompt-compatibility.md](docs/prompt-compatibility.md) 的「技能注入(skills)」

**GET 响应**:

```json
{
  "enabled": false,
  "max_chars": 8000,
  "items": [
    {
      "id": "sql-report",
      "name": "SQL 报表",
      "description": "按固定口径写 SQL",
      "body": "# 技能正文 markdown\n..."
    }
  ]
}
```

**PUT 请求**:与 GET 同形状,整体替换,不做增量合并。

```json
{
  "enabled": true,
  "max_chars": 8000,
  "items": [
    {
      "id": "sql-report",
      "name": "SQL 报表",
      "description": "按固定口径写 SQL",
      "body": "# 报表口径\n先按天聚合,再按渠道拆分。"
    }
  ]
}
```

**PUT 成功响应**:

```json
{"success": true, "count": 1}
```

**校验规则**(任一不满足返回 `400`,响应体为 `{"detail": "..."}`):

| 字段 | 规则 |
| --- | --- |
| `items` | 最多 50 条 |
| `id` | 必填、唯一,字符集 `[a-z0-9_-]` |
| `body` | 必填;单条 ≤ 100000 字符,全部条目合计 ≤ 500000 字符 |
| `max_chars` | 缺省 `8000`;按字符(rune)截断,截断时追加「(技能内容已截断)」;`0` 表示不限制 |

校验在写入时执行,与 `enabled` 无关:先关着开关把内容配好、改好,再打开即可。超限、重复 `id`、非法 `id` 都不会被静默修正。

### `GET` / `PUT /admin/model_profiles`

自定义模型列表(模型名 + 基模 + 该模型专属提示词)。与技能库同样是独立接口、整体替换语义:PUT 请求里的 `items` 就是最终列表,传空数组表示清空。

**GET 响应**:

```json
{
  "items": [
    {"id": "deepseek-writer", "base": "deepseek-v4-flash", "name": "写作", "remark": "长文", "prompt": "## 写作要求\n…"}
  ],
  "base_models": ["deepseek-v4-flash", "deepseek-v4-flash-search", "deepseek-v4-flash-nothinking", "deepseek-v4-flash-search-nothinking"],
  "max_count": 50,
  "max_id_chars": 64,
  "max_prompt_chars": 20000
}
```

**PUT 请求**:

```json
{"items": [{"id": "deepseek-writer", "base": "deepseek-v4-flash", "prompt": "…"}]}
```

**校验规则**(任一条不满足返回 `400`,响应体为 `{"detail": "..."}`):

| 字段 | 规则 |
| --- | --- |
| `items` | 最多 50 条 |
| `id` | 必填、唯一(大小写不敏感),字符集 `[A-Za-z0-9._-]`,长度 1-64,不可与内置模型名冲突 |
| `base` | 必填,只能是 `deepseek-v4-flash` 系列的四个名字之一 |
| `prompt` | 可空;≤ 20000 字符 |

保存后立即生效:`/v1/models` 列表与模型名解析都会包含新条目,无需重启。

### `POST /admin/config/import`

导入完整配置,支持:

- `mode=merge`(默认)
- `mode=replace`

请求可直接传配置对象,或使用 `{"config": {...}, "mode":"merge"}` 包裹格式。
也支持在查询参数里传 `?mode=merge` / `?mode=replace`。
`replace` 模式会按完整配置结构替换(保留 Vercel 同步元信息);`merge` 模式会合并 `keys`、`api_keys`、`accounts`、`model_aliases`,并覆盖 `admin`、`runtime`、`responses`、`embeddings` 中的非空字段。`auto_delete`、`current_input_file` 建议通过 `/admin/settings` 或配置文件管理;`compat` 与 `toolcall` 相关字段会被忽略。

> 注意:`merge` 模式不会更新 `auto_delete`、`current_input_file`。

### `GET /admin/config/export`

导出完整配置,返回 `config`、`json`、`base64` 三种格式。

响应示例:


> 注:`_vercel_sync_hash` 和 `_vercel_sync_time` 为内部同步元数据字段,用于 Vercel 配置漂移检测。

### `POST /admin/keys`

```json
{"key": "new-api-key", "name": "主 Key", "remark": "生产流量"}
```

**响应**:`{"success": true, "total_keys": 3}`

### `PUT /admin/keys/{key}`

更新指定 API key 的 `name` / `remark`,路径参数中的 `key` 为只读标识,不可修改。

```json
{"name": "备用 Key", "remark": "压测"}
```

**响应**:`{"success": true, "total_keys": 3}`

### `DELETE /admin/keys/{key}`

**响应**:`{"success": true, "total_keys": 2}`

### `GET /admin/proxies`

列出代理配置(密码不回传,仅返回 `has_password` 标记)。

### `POST /admin/proxies`

新增代理。请求体支持 `id`(可选,未传则自动生成)、`name`、`type`(`http` / `socks5`)、`host`、`port`、`username`、`password`。

### `PUT /admin/proxies/{proxyID}`

更新指定代理。若请求中 `password` 为空字符串,则保留原密码。

### `DELETE /admin/proxies/{proxyID}`

删除代理,并自动清空所有引用该代理账号的 `proxy_id`。

### `POST /admin/proxies/test`

测试代理连通性:传 `proxy_id` 时测试已保存代理;不传时按请求体代理字段做临时连通性测试。

### `GET /admin/accounts`

**查询参数**:

| 参数 | 默认 | 范围 |
| --- | --- | --- |
| `page` | `1` | ≥ 1 |
| `page_size` | `10` | 1–5000 |
| `q` | 空 | 按 identifier / email / mobile 过滤 |

**响应**:

```json
{
  "items": [
    {
      "identifier": "user@example.com",
      "email": "user@example.com",
      "mobile": "",
      "has_password": true,
      "has_token": true,
      "token_preview": "abc...",
      "test_status": "ok"
    }
  ],
  "total": 25,
  "page": 1,
  "page_size": 10,
  "total_pages": 3
}
```

### `POST /admin/accounts`

```json
{"email": "user@example.com", "password": "pwd"}
```

**响应**:`{"success": true, "total_accounts": 6}`

### `PUT /admin/accounts/{identifier}`

更新指定账号的 `name` / `remark`。路径参数中的 `identifier` 可以是 email 或 mobile,且不可修改。

```json
{"name": "主账号", "remark": "团队共享"}
```

**响应**:`{"success": true, "total_accounts": 6}`

### `DELETE /admin/accounts/{identifier}`

`identifier` 可为 email、mobile,或 token-only 账号的合成标识(`token:<hash>`)。

**响应**:`{"success": true, "total_accounts": 5}`

### `PUT /admin/accounts/{identifier}/proxy`

更新指定账号绑定代理。

- 请求体:`{"proxy_id":"..."}`;
- `proxy_id` 传空字符串时表示解绑代理;
- `identifier` 支持 email / mobile / token-only 合成标识。

### `GET /admin/queue/status`

```json
{
  "available": 3,
  "in_use": 1,
  "total": 4,
  "available_accounts": ["a@example.com"],
  "in_use_accounts": ["b@example.com"],
  "max_inflight_per_account": 2,
  "global_max_inflight": 8,
  "recommended_concurrency": 8,
  "waiting": 0,
  "max_queue_size": 8
}
```

| 字段 | 说明 |
| --- | --- |
| `available` | 仍有剩余并发槽位的账号数 |
| `in_use` | 当前已占用的 in-flight 槽位数 |
| `total` | 总账号数 |
| `available_accounts` | 仍有剩余并发槽位的账号 ID 列表 |
| `in_use_accounts` | 当前处于使用中的账号 ID 列表 |
| `max_inflight_per_account` | 每账号并发上限 |
| `global_max_inflight` | 全局并发上限 |
| `recommended_concurrency` | 建议并发值(`total × max_inflight_per_account`) |
| `waiting` | 当前等待中的请求数 |
| `max_queue_size` | 等待队列上限 |

### `POST /admin/accounts/test`

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `identifier` | ✅ | email / mobile / token-only 合成标识 |
| `model` | ❌ | 默认 `deepseek-v4-flash` |
| `message` | ❌ | 空字符串时仅测试会话创建 |

**响应**:

```json
{
  "account": "user@example.com",
  "success": true,
  "response_time": 1240,
  "message": "API 测试成功(仅会话创建)",
  "model": "deepseek-v4-flash",
  "session_count": 0,
  "config_writable": true,
  "config_warning": ""
}
```

如果传入 `message`,还会附带 `thinking`(当上游返回思考内容时)。

当部署环境配置文件路径不可写(例如容器内默认 `/app/config.json` 只读)时,登录与会话测试仍可继续;此时会返回 `config_warning` 提示 token 仅保存在内存、重启后丢失。

### `POST /admin/accounts/test-all`

可选请求字段:`model`

```json
{
  "total": 5,
  "success": 4,
  "failed": 1,
  "results": [...]
}
```

内部并发上限当前固定为 5。

### `POST /admin/accounts/sessions/delete-all`

清空指定账号的所有 DeepSeek 会话。请求体示例:

```json
{"identifier":"user@example.com"}
```

响应:

```json
{"success": true, "message": "删除成功"}
```

如果账号不存在或删除失败,`success` 会是 `false`,`message` 会返回错误原因。

### `POST /admin/import`

批量导入 keys 与 accounts。

**请求**:

```json
{
  "keys": ["k1", "k2"],
  "accounts": [
    {"email": "user@example.com", "password": "pwd", "token": ""}
  ]
}
```

**响应**:

```json
{
  "success": true,
  "imported_keys": 2,
  "imported_accounts": 1
}
```

### `POST /admin/test`

测试当前 API 可用性(通过自身接口调用)。

| 字段 | 必填 | 默认值 |
| --- | --- | --- |
| `model` | ❌ | `deepseek-v4-flash` |
| `message` | ❌ | `你好` |
| `api_key` | ❌ | 配置中第一个 key |

**响应**:

```json
{
  "success": true,
  "status_code": 200,
  "response": {"id": "..."}
}
```

### `POST /admin/dev/raw-samples/capture`

直接通过服务自身发起一次 `/v1/chat/completions` 请求,并把请求元信息和上游原始 SSE 保存到 `tests/raw_stream_samples/<sample-id>/`。

常用请求字段:

| 字段 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `message` | 否 | `你好` | 便捷单轮用户消息 |
| `messages` | 否 | 自动由 `message` 生成 | OpenAI 风格消息数组 |
| `model` | 否 | `deepseek-v4-flash` | 目标模型 |
| `stream` | 否 | `true` | 建议保留流式,以记录原始 SSE |
| `api_key` | 否 | 配置中第一个 key | 调用业务接口使用的 key |
| `sample_id` | 否 | 自动生成 | 样本目录名 |

成功时会在响应头里附带:

- `X-Ds2-Sample-Id`
- `X-Ds2-Sample-Dir`
- `X-Ds2-Sample-Meta`
- `X-Ds2-Sample-Upstream`

如果请求本身成功,但当前进程没有记录到新的上游抓包,会返回:

```json
{"detail":"no upstream capture was recorded"}
```

### `GET /admin/dev/raw-samples/query`

按关键词查询当前进程内存里的抓包记录,并按 `chat_session_id` 归并 `completion + continue` 链。

**查询参数**:

| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `q` | 空 | 按请求体/响应体关键词模糊匹配 |
| `limit` | `20` | 返回链条数上限 |

**响应字段**包含:

- `items[].chain_key`
- `items[].capture_ids`
- `items[].round_count`
- `items[].initial_label`
- `items[].request_preview`
- `items[].response_preview`

### `POST /admin/dev/raw-samples/save`

把当前内存中的某条抓包链落盘为 `tests/raw_stream_samples/<sample-id>/`。

支持以下任一种选中方式:

```json
{"chain_key":"session:xxxx","sample_id":"tmp-from-memory"}
```

```json
{"capture_id":"cap_xxx","sample_id":"tmp-from-memory"}
```

```json
{"query":"广州天气","sample_id":"tmp-from-memory"}
```

成功响应会返回 `sample_id`、`dir`、`meta_path`、`upstream_path`。

### `POST /admin/vercel/sync`

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `vercel_token` | ❌ | 空或 `__USE_PRECONFIG__` 则读环境变量,再回退到已保存配置 |
| `project_id` | ❌ | 空则读 `VERCEL_PROJECT_ID`,再回退到已保存配置 |
| `team_id` | ❌ | 空则读 `VERCEL_TEAM_ID`,再回退到已保存配置 |
| `auto_validate` | ❌ | 默认 `true` |
| `save_credentials` | ❌ | 默认 `true`;保存本次显式填写的 Vercel 凭据,供下次同步复用 |
| `config_override` | ❌ | 传入完整配置对象,覆盖当前内存配置用于同步 |

**成功响应**:

```json
{
  "success": true,
  "validated_accounts": 3,
  "message": "配置已同步,正在重新部署...",
  "deployment_url": "https://..."
}
```

或需要手动部署:

```json
{
  "success": true,
  "validated_accounts": 3,
  "message": "配置已同步到 Vercel,请手动触发重新部署",
  "manual_deploy_required": true
}
```

失败校验的账号会通过 `failed_accounts` 返回;成功保存到 Vercel 的凭据会通过 `saved_credentials` 返回。

### `GET /admin/vercel/status`

也支持 `POST /admin/vercel/status`,可提交 `config_override` 用于比对草稿是否与已同步配置一致。

```json
{
  "synced": true,
  "last_sync_time": 1738400000,
  "has_synced_before": true,
  "env_backed": false,
  "config_hash": "....",
  "last_synced_hash": "....",
  "draft_hash": "....",
  "draft_differs": false
}
```

`POST /admin/vercel/status` 还可以携带 `config_override`,用于对比“草稿配置”和当前已同步配置。

### `GET /admin/export`

```json
{
  "json": "{...}",
  "base64": "ey4uLn0="
}
```

该接口与 `GET /admin/config/export` 返回相同内容,只是路径更短。

### `GET /admin/version`

查询当前构建版本与 GitHub 最新 Release:

```json
{
  "success": true,
  "current_version": "3.0.0",
  "current_tag": "v3.0.0",
  "source": "file:VERSION",
  "checked_at": "2026-03-29T00:00:00Z",
  "latest_tag": "v3.0.0",
  "latest_version": "3.0.0",
  "release_url": "https://github.com/CJackHwang/ds2api/releases/tag/v3.0.0",
  "published_at": "2026-03-28T12:00:00Z",
  "has_update": false
}
```

如果 GitHub API 不可用,响应里会额外包含 `check_error`,但 HTTP 状态仍为 200。

### `GET /admin/dev/captures`

查看本地抓包状态与最近记录(需 Admin 鉴权):

- `enabled`
- `limit`
- `max_body_bytes`
- `items`

### `DELETE /admin/dev/captures`

清空抓包记录,返回:

```json
{"success":true,"detail":"capture logs cleared"}
```

### `GET /admin/chat-history`

查询服务器端对话记录,支持筛选与分页。

**查询参数**:

| 参数 | 默认 | 匹配方式 |
| --- | --- | --- |
| `caller` | 空 | 与 `caller_id` 全等(取值用响应里的 `callers[].caller_id`,不是 Key 名称) |
| `status` | 空 | 与记录状态不区分大小写全等:`streaming` / `success` / `error` / `stopped` |
| `surface` | 空 | 与来源接口不区分大小写全等,如 `openai.chat_completions`、`openai.responses`、`claude.messages`、`gemini.generate_content` |
| `q` | 空 | 不区分大小写包含匹配,命中 `user_input` / `preview` / `model` / `caller_label` / `error` |
| `offset` | `0` | 分页偏移;`offset` 与 `limit` 都为 `0` 表示不分页 |
| `limit` | `0` | 单页条数;`0` 表示不限制条数(不按 settings 的保留条数分页) |

非法或负数的 `offset` / `limit` 按 `0` 处理。

**响应**:

```json
{
  "version": 2,
  "limit": 20,
  "revision": 128,
  "total": 137,
  "callers": [
    {"caller_id": "caller:9f2c1a2b3c4d5e6f", "label": "主 Key", "configured": true, "count": 96},
    {"caller_id": "caller:41ab0c9d8e7f6a5b", "label": "备用 Key", "configured": true, "count": 0}
  ],
  "items": [
    {
      "id": "chat_20260418_093012_ab12",
      "status": "success",
      "caller_id": "caller:9f2c1a2b3c4d5e6f",
      "caller_label": "主 Key",
      "account_id": "user@example.com",
      "surface": "openai.chat_completions",
      "model": "deepseek-v4-flash",
      "stream": true,
      "user_input": "今天的新闻",
      "preview": "……",
      "status_code": 200,
      "elapsed_ms": 4210,
      "finish_reason": "stop",
      "created_at": 1777000000000,
      "updated_at": 1777000004000,
      "detail_revision": 3
    }
  ],
  "path": "/data/chat_history.json"
}
```

- `total` 是筛选后的总条数,`items` 是分页后的当前页;不带任何筛选/分页参数时 `items` 仍是全部摘要。
- `caller_label` 是发起方 API Key 的可读标签(名称 / 备注 / 脱敏 Key),没有备注时退回脱敏值,避免面板里只剩一串哈希。
- `callers` 汇总记录里出现过的 `caller_id`,并补上配置里已登记但还没有记录的 Key(`count` 为 `0`,`configured=true`),按 `count` 倒序、`label` 升序排列。
- 时间戳字段(`created_at` / `updated_at` / `completed_at`)单位是毫秒。
- `If-None-Match` + `ETag` 的 `304` 协商只在**不带任何筛选/分页参数**时生效,因为带参数的响应内容本身随查询条件变化。

### `GET /admin/chat-history/{id}`

查看单条记录全文,响应为 `{"item": {...}}`。除摘要字段外还包含 `messages`、`history_text`(历史拆分上传的对话记录全文)、`final_prompt`、`reasoning_content`、`content`、`error`、`usage`、`file_infos`(上传文件 ID、审核结果、来源)等。同样支持 `ETag` / `304`。

### `DELETE /admin/chat-history`

清空全部服务器端记录,返回 `{"success": true}`。

### `DELETE /admin/chat-history/{id}`

删除单条记录,返回 `{"success": true}`;记录不存在返回 `404`。

### `PUT /admin/chat-history/settings`

更新保留条数。`limit` 接受 `0`..`500` 的任意整数,不再是固定档位。

```json
{"limit": 120}
```

| 取值 | 行为 |
| --- | --- |
| `0` | 关闭记录:不再写新记录 |
| `1`..`500` | 保留最近该数量的记录,超出部分按创建时间淘汰 |
| 其他 | 返回 `400`,`detail` 为 `unsupported chat history limit: <n>` |

响应带 `success`、`limit`、`revision` 与裁剪后的 `items`。

### `GET /admin/chat-history/export`

按与列表接口相同的筛选与分页参数(`caller` / `status` / `surface` / `q` / `offset` / `limit`)导出记录,响应是文件下载。

| 参数 | 说明 |
| --- | --- |
| `format=json`(默认) | JSON 附件,结构为 `{exported_at, count, filters, items}`;`exported_at` 为 RFC3339,`filters` 回显本次筛选条件,`items` 是完整记录(含 `messages` / `final_prompt` / `history_text` 等大字段) |
| `format=markdown` | Markdown 附件,标题 `# ds2api 响应记录导出`,逐条记录 `## N. <记录 ID>` 分节,含时间、接口、模型、账号、调用方、状态、耗时、结束原因,以及 `UserInput` / `FinalPrompt` / `ReasoningContent` / `Content` / `Error` 代码块 |
| 其他值 | 返回 `400`,`detail` 为 `unsupported export format: <format>` |

响应头带 `Content-Disposition: attachment`(文件名为 `ds2api-logs-<YYYYMMDD-HHMMSS>.json|.md`)与 `Cache-Control: no-store`。导出过程中若某条记录已被删除,该条会被跳过,`count` 反映实际导出的条数。

---

## 错误响应格式

兼容路由(`/v1/*`、`/anthropic/*`)统一使用以下结构:

```json
{
  "error": {
    "message": "...",
    "type": "invalid_request_error",
    "code": "invalid_request",
    "param": null
  }
}
```

Admin 接口保持 `{"detail":"..."}`。

Gemini 路由使用 Google 风格错误结构:

```json
{
  "error": {
    "code": 400,
    "message": "invalid json",
    "status": "INVALID_ARGUMENT"
  }
}
```

建议客户端处理逻辑:检查 HTTP 状态码 + 解析 `error` 或 `detail` 字段。

**常见状态码**:

| 状态码 | 说明 |
| --- | --- |
| `401` | 鉴权失败(key/token 无效,或 Admin JWT 过期) |
| `429` | 请求过多(超出并发上限 + 等待队列,或上游账号 thinking-only 后仍无可见输出;托管账号模式会先尝试一次切号 fresh retry;当前不附带 `Retry-After` 头) |
| `503` | 模型不可用或上游服务异常 |

---

## cURL 示例

### OpenAI 非流式

```bash
curl http://localhost:5001/v1/chat/completions \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [{"role": "user", "content": "你好"}],
    "stream": false
  }'
```

### OpenAI 流式

```bash
curl http://localhost:5001/v1/chat/completions \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [{"role": "user", "content": "解释一下量子纠缠"}],
    "stream": true
  }'
```

### OpenAI Responses(流式)

```bash
curl http://localhost:5001/v1/responses \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.3-codex",
    "input": "写一个 golang 的 hello world",
    "stream": true
  }'
```

### OpenAI Embeddings

```bash
curl http://localhost:5001/v1/embeddings \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "input": ["第一段文本", "第二段文本"]
  }'
```

### OpenAI 带搜索

```bash
curl http://localhost:5001/v1/chat/completions \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash-search",
    "messages": [{"role": "user", "content": "今天的新闻"}],
    "stream": true
  }'
```

### OpenAI Tool Calling

```bash
curl http://localhost:5001/v1/chat/completions \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [{"role": "user", "content": "北京今天天气怎么样?"}],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "description": "获取指定城市的天气",
          "parameters": {
            "type": "object",
            "properties": {
              "city": {"type": "string", "description": "城市名"}
            },
            "required": ["city"]
          }
        }
      }
    ]
  }'
```

### Gemini 非流式

```bash
curl "http://localhost:5001/v1beta/models/gemini-2.5-pro:generateContent" \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [
      {
        "role": "user",
        "parts": [{"text": "用三句话介绍 Go 语言"}]
      }
    ]
  }'
```

### Gemini 流式

```bash
curl "http://localhost:5001/v1beta/models/gemini-2.5-flash:streamGenerateContent" \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [
      {
        "role": "user",
        "parts": [{"text": "写一个简短摘要"}]
      }
    ]
  }'
```

### Claude 非流式

```bash
curl http://localhost:5001/anthropic/v1/messages \
  -H "x-api-key: your-api-key" \
  -H "Content-Type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-sonnet-4-6",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "你好"}]
  }'
```

### Claude 流式

```bash
curl http://localhost:5001/anthropic/v1/messages \
  -H "x-api-key: your-api-key" \
  -H "Content-Type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-opus-4-6",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "解释相对论"}],
    "stream": true
  }'
```

### Admin 登录

```bash
curl http://localhost:5001/admin/login \
  -H "Content-Type: application/json" \
  -d '{"admin_key": "admin"}'
```

### 指定账号请求

```bash
curl http://localhost:5001/v1/chat/completions \
  -H "Authorization: Bearer your-api-key" \
  -H "X-Ds2-Target-Account: user@example.com" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [{"role": "user", "content": "你好"}]
  }'
```