File size: 80,230 Bytes
8a03d2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""请求和响应转换工具模块

负责:
1. 将 Gemini 格式的 Payload 转换为 Vcore AI 内部格式
2. 将流式响应聚合成完整的非流式响应
"""

import json
import re
import time
from typing import Any, cast

from src.core.errors import (
    VcoreError,
    InternalError,
    parse_error_response,
)
from src.api.model_config import ModelConfigBuilder
from src.utils.logger import get_logger
from src.utils.string_utils import snake_to_camel, camel_to_snake

logger = get_logger(__name__)

_GEMINI_FUNCTION_NAME_RE = re.compile(r"[^A-Za-z0-9_.-]+")

class RequestTransformer:
    """请求参数转换器"""
    
    def __init__(self, model_builder: ModelConfigBuilder):
        self.model_builder = model_builder

    def build_vcore_payload(
        self,
        model: str,
        gemini_payload: dict[str, Any],
        original_body: dict[str, Any],
        kwargs: dict[str, Any]
    ) -> dict[str, Any]:
        """
        构建 Vcore AI 请求 Payload
        
        Returns:
            new_body
        """
        original_vars: Any = original_body.get('variables', {})
        new_variables: dict[str, Any]
        if hasattr(original_vars, 'model_dump'):
             new_variables = cast(dict[str, Any], original_vars.model_dump())
        elif isinstance(original_vars, dict):
            new_variables = {str(k): v for k, v in cast(dict[Any, Any], original_vars).items()}
        else:
             new_variables = {}

        gemini_payload = self._normalize_gemini_payload(gemini_payload)
        gemini_payload = self._normalize_thought_signature_aliases(gemini_payload)

        target_model = self.model_builder.parse_model_name(model)
        new_variables['model'] = target_model

        # 支持的字段列表(统一使用 camelCase 格式)。尽量覆盖 Gemini generateContent
        # 可透传到 Vcore AI Studio 匿名 GraphQL variables 的字段。
        supported_fields = self._supported_variable_fields()
        canonical_payload = self._canonicalize_supported_fields(gemini_payload, supported_fields)
        
        try:
            from src.core.types import GeminiPayload
            gemini_payload_obj = GeminiPayload.model_validate(canonical_payload)
            dumped_payload = gemini_payload_obj.model_dump(by_alias=True, exclude_none=True)
            
            for field in supported_fields:
                if field in dumped_payload:
                     new_variables[field] = dumped_payload[field]
        except Exception as e:
            logger.debug(f"Pydantic 验证失败,使用基础转换: {e}")
            # 尝试直接从 gemini_payload 透传字段,支持 snake_case 和 camelCase
            for field in supported_fields:
                # 优先使用 camelCase 版本
                if field in canonical_payload:
                    new_variables[field] = canonical_payload[field]
                else:
                    # 尝试 snake_case 版本
                    snake_field = camel_to_snake(field)
                    if snake_field in canonical_payload:
                        new_variables[field] = canonical_payload[snake_field]

        # tools/toolConfig 内部存在大量 snake_case、实验字段和空对象默认值,
        # 使用原始规范化 payload 再走后续专用转换,避免 Pydantic dump 过早丢失未知/None 字段。
        if 'tools' in canonical_payload:
            new_variables['tools'] = canonical_payload['tools']
        if 'toolConfig' in canonical_payload:
            new_variables['toolConfig'] = canonical_payload['toolConfig']

        # 处理 systemInstruction:如果没有 user content,则转换为 user message
        self._handle_system_instruction(new_variables)

        # 特殊处理:contents 格式转换
        if 'contents' in new_variables:
            converted_contents = self._normalize_contents(new_variables['contents'])
            converted_contents = self._handle_inline_data_case(converted_contents)
            converted_contents = self._normalize_contents(converted_contents)
            converted_contents = self._handle_base64_in_contents(converted_contents)
            # 过滤掉空的 parts(Vcore AI 要求每个 content 至少有一个 part)
            converted_contents = self._filter_empty_contents(converted_contents)
            converted_contents = self._ensure_function_call_thought_signatures(converted_contents)
            # 处理 thoughtSignature 字段的 base64 编码
            converted_contents = self._handle_thought_signature(converted_contents)
            new_variables['contents'] = converted_contents
        
        # 特殊处理:tools 格式转换
        if 'tools' in new_variables:
            normalized_tools = self._normalize_tools_format(new_variables['tools'])
            if normalized_tools:
                new_variables['tools'] = normalized_tools
            else:
                # 如果转换结果为空列表,确保移除 tools 字段,同时移除 toolConfig 避免 API 报错
                del new_variables['tools']
                if 'toolConfig' in new_variables:
                    del new_variables['toolConfig']
        
        # 特殊处理:toolConfig 格式转换
        if 'toolConfig' in new_variables:
            normalized_tool_config = self._normalize_tool_config(new_variables['toolConfig'])
            if normalized_tool_config:
                new_variables['toolConfig'] = normalized_tool_config
            else:
                del new_variables['toolConfig']

        # 特殊处理 generationConfig (使用 ModelConfigBuilder 进行格式转换)
        gen_config = self.model_builder.build_generation_config(
            gen_config={},
            gemini_payload=gemini_payload,
            **kwargs
        )
        if gen_config:
            new_variables['generationConfig'] = gen_config
            
        # 特殊处理 safetySettings (如果未提供,则使用默认的宽松设置)
        if 'safetySettings' not in new_variables and 'safety_settings' not in gemini_payload:
            new_variables['safetySettings'] = self.model_builder.build_safety_settings()

        new_body: dict[str, Any] = {
            "querySignature": original_body.get('querySignature'),
            "operationName": original_body.get('operationName'),
            "variables": new_variables
        }
        self._sanitize_vcore_variables_in_place(new_variables)
        
        return new_body

    def _supported_variable_fields(self) -> list[str]:
        """Gemini 下游请求可透传到上游 variables 的字段。"""
        return [
            'contents', 'tools', 'toolConfig', 'systemInstruction',
            'safetySettings', 'generationConfig', 'cachedContent', 'labels',
            # Gemini/Vcore 常见高级字段;上游不支持时会由上游返回明确错误,
            # 这里不主动丢弃,以最大化暴露上游能力。
            'modelArmorConfig', 'cachedContentName', 'requestOptions',
            'session', 'context', 'examples', 'instances', 'parameters',
        ]

    def _canonicalize_supported_fields(self, payload: dict[str, Any], supported_fields: list[str]) -> dict[str, Any]:
        """把下游 Gemini REST/SDK 常见 snake_case 顶层字段并入 camelCase 标准字段。"""
        canonical = payload.copy()
        for field in supported_fields:
            snake_field = camel_to_snake(field)
            if field not in canonical and snake_field in canonical:
                canonical[field] = canonical[snake_field]
        return canonical

    def _normalize_gemini_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
        """兼容 REST、SDK 和部分 OpenAI-like 客户端传来的 Gemini 请求形态。"""
        normalized = payload.copy()
        if 'contents' in normalized:
            normalized['contents'] = self._normalize_contents(normalized['contents'])
        elif 'prompt' in normalized:
            normalized['contents'] = [{"role": "user", "parts": [{"text": str(normalized['prompt'])}]}]
        return normalized

    def _normalize_thought_signature_aliases(self, data: Any) -> Any:
        """递归兼容 thought_signature / thoughtSignature 两种字段名。"""
        if isinstance(data, list):
            return [self._normalize_thought_signature_aliases(item) for item in cast(list[Any], data)]
        if isinstance(data, dict):
            data_dict = cast(dict[str, Any], data)
            normalized: dict[str, Any] = {}
            for key, value in data_dict.items():
                normalized_value = self._normalize_thought_signature_aliases(value) if isinstance(value, (dict, list)) else value
                if key == 'thought_signature':
                    normalized['thoughtSignature'] = normalized_value
                elif key == 'thoughtSignature':
                    normalized['thoughtSignature'] = normalized_value
                else:
                    normalized[key] = normalized_value
            return normalized
        return data

    def _normalize_contents(self, contents: Any) -> Any:
        if contents is None:
            return []
        if isinstance(contents, str):
            return [{"role": "user", "parts": [{"text": contents}]}]
        if isinstance(contents, dict):
            return [self._normalize_content(contents)]
        if isinstance(contents, list):
            normalized: list[Any] = []
            pending_text_parts: list[dict[str, Any]] = []
            for item in cast(list[Any], contents):
                if isinstance(item, str):
                    pending_text_parts.append({"text": item})
                elif isinstance(item, dict):
                    if pending_text_parts:
                        normalized.append({"role": "user", "parts": pending_text_parts})
                        pending_text_parts = []
                    normalized.append(self._normalize_content(cast(dict[str, Any], item)))
            if pending_text_parts:
                normalized.append({"role": "user", "parts": pending_text_parts})
            return normalized
        return contents

    def _normalize_content(self, content: dict[str, Any]) -> dict[str, Any]:
        normalized = content.copy()
        source_role = normalized.get('role')
        if source_role in {'tool', 'function'} and 'parts' not in normalized and ('content' in normalized or 'response' in normalized):
            raw_response = normalized.get('response', normalized.get('content', {}))
            normalized['parts'] = [{
                "functionResponse": {
                    "name": str(normalized.get('name') or normalized.get('function_name') or ""),
                    "response": self._coerce_function_response(raw_response),
                }
            }]
            normalized.pop('content', None)
            normalized.pop('response', None)
        elif 'content' in normalized and 'parts' not in normalized:
            normalized['parts'] = self._normalize_parts(normalized.get('content'))
            normalized.pop('content', None)
        elif 'parts' in normalized:
            normalized['parts'] = self._normalize_parts(normalized.get('parts'))
        elif 'text' in normalized:
            normalized['parts'] = [{"text": str(normalized.pop('text'))}]
        else:
            normalized.setdefault('parts', [])

        if source_role in {'assistant', 'model'}:
            extra_parts = self._openai_tool_history_parts(normalized)
            if extra_parts:
                normalized['parts'] = list(cast(list[Any], normalized.get('parts') or [])) + extra_parts
                normalized.pop('tool_calls', None)
                normalized.pop('function_call', None)
                normalized.pop('functionCall', None)

        role = normalized.get('role')
        if role == 'assistant':
            normalized['role'] = 'model'
        elif role == 'tool':
            normalized['role'] = 'function'
        elif not role:
            normalized['role'] = 'user'
        return normalized

    def _normalize_parts(self, parts: Any) -> list[dict[str, Any]]:
        if parts is None:
            return []
        if isinstance(parts, str):
            return [{"text": parts}]
        if isinstance(parts, dict):
            return [self._normalize_part(parts)]
        if isinstance(parts, list):
            normalized: list[dict[str, Any]] = []
            for part in cast(list[Any], parts):
                if isinstance(part, str):
                    normalized.append({"text": part})
                elif isinstance(part, dict):
                    normalized_part = self._normalize_part(cast(dict[str, Any], part))
                    if normalized_part:
                        normalized.append(normalized_part)
            return normalized
        return [{"text": str(parts)}]

    def _openai_tool_history_parts(self, message: dict[str, Any]) -> list[dict[str, Any]]:
        """兼容下游把 OpenAI tool_calls/function_call 混入 Gemini endpoint 的历史。"""
        parts: list[dict[str, Any]] = []
        tool_calls = message.get('tool_calls') or message.get('toolCalls')
        if isinstance(tool_calls, list):
            for tool_call in cast(list[Any], tool_calls):
                part = self._openai_tool_call_to_part(tool_call)
                if part:
                    parts.append(part)

        function_call = message.get('function_call') or message.get('functionCall')
        if isinstance(function_call, dict):
            part = self._openai_tool_call_to_part({"function": function_call})
            if part:
                parts.append(part)
        return parts

    def _openai_tool_call_to_part(self, tool_call: Any) -> dict[str, Any] | None:
        if not isinstance(tool_call, dict):
            return None
        tool_call_dict = cast(dict[str, Any], tool_call)
        func = tool_call_dict.get('function')
        if isinstance(func, dict):
            func_dict = cast(dict[str, Any], func)
            name = func_dict.get('name') or tool_call_dict.get('name') or tool_call_dict.get('functionName')
            args = func_dict.get('arguments', tool_call_dict.get('arguments', tool_call_dict.get('args', {})))
            thought_signature = (
                tool_call_dict.get('thoughtSignature')
                or tool_call_dict.get('thought_signature')
                or func_dict.get('thoughtSignature')
                or func_dict.get('thought_signature')
            )
        else:
            name = tool_call_dict.get('name') or tool_call_dict.get('function_name') or tool_call_dict.get('functionName')
            args = tool_call_dict.get('arguments', tool_call_dict.get('args', {}))
            thought_signature = tool_call_dict.get('thoughtSignature') or tool_call_dict.get('thought_signature')

        if not name:
            return None
        part: dict[str, Any] = {
            "functionCall": {
                "name": self._sanitize_function_name(str(name)),
                "args": self._coerce_function_args(args),
            }
        }
        if thought_signature:
            part["thoughtSignature"] = thought_signature
        return part

    def _coerce_function_args(self, args: Any) -> dict[str, Any]:
        if isinstance(args, dict):
            return cast(dict[str, Any], args)
        if isinstance(args, str):
            try:
                parsed = json.loads(args)
                return parsed if isinstance(parsed, dict) else {"value": parsed}
            except json.JSONDecodeError:
                return {"raw": args}
        if args is None:
            return {}
        return {"value": args}

    def _coerce_function_response(self, response: Any) -> dict[str, Any]:
        if isinstance(response, dict):
            return cast(dict[str, Any], response)
        if isinstance(response, str):
            try:
                parsed = json.loads(response)
                return parsed if isinstance(parsed, dict) else {"result": parsed}
            except json.JSONDecodeError:
                return {"result": response}
        if response is None:
            return {}
        return {"result": response}

    def _normalize_part(self, part: dict[str, Any]) -> dict[str, Any]:
        part_type = part.get('type')
        if part_type in {'text', 'input_text'}:
            return {"text": str(part.get('text', ''))}
        if part_type in {'image_url', 'input_image'}:
            url_obj = part.get('image_url') or part.get('input_image') or {}
            url = url_obj.get('url') if isinstance(url_obj, dict) else url_obj
            if isinstance(url, str) and url.startswith('data:'):
                mime, data = self._parse_data_uri(url)
                if mime and data:
                    return {"inlineData": {"mimeType": mime, "data": data}}
            if isinstance(url, str) and url.startswith(('http://', 'https://', 'gs://')):
                return {"fileData": {"mimeType": self._guess_mime_from_uri(url), "fileUri": url}}
        if part_type in {'media', 'file', 'file_data'}:
            file_uri = part.get('fileUri') or part.get('file_uri') or part.get('uri') or part.get('url')
            mime_type = part.get('mimeType') or part.get('mime_type') or self._guess_mime_from_uri(str(file_uri or ''))
            if file_uri:
                return {"fileData": {"mimeType": str(mime_type), "fileUri": str(file_uri)}}
        if part_type in {'inline_data', 'inlineData'}:
            inline = part.get('inlineData') or part.get('inline_data') or part
            if isinstance(inline, dict):
                data = inline.get('data')
                mime_type = inline.get('mimeType') or inline.get('mime_type') or part.get('mimeType') or part.get('mime_type')
                if data and mime_type:
                    return {"inlineData": {"mimeType": str(mime_type), "data": str(data)}}

        normalized: dict[str, Any] = {}
        for k, v in part.items():
            if k == 'type':
                continue
            normalized[snake_to_camel(k)] = v
        return normalized

    def _parse_data_uri(self, uri: str) -> tuple[str, str]:
        try:
            header, data = uri.split(',', 1)
            mime = header.split(':', 1)[1].split(';', 1)[0]
            return mime, data
        except (ValueError, IndexError):
            return "", ""

    def _guess_mime_from_uri(self, uri: str) -> str:
        lower = uri.lower().split('?', 1)[0].split('#', 1)[0]
        if lower.endswith(('.jpg', '.jpeg')):
            return 'image/jpeg'
        if lower.endswith('.webp'):
            return 'image/webp'
        if lower.endswith('.gif'):
            return 'image/gif'
        if lower.endswith('.png'):
            return 'image/png'
        if lower.endswith('.mp4'):
            return 'video/mp4'
        if lower.endswith('.mov'):
            return 'video/quicktime'
        if lower.endswith('.webm'):
            return 'video/webm'
        if lower.endswith('.mp3'):
            return 'audio/mpeg'
        if lower.endswith('.wav'):
            return 'audio/wav'
        if lower.endswith('.ogg'):
            return 'audio/ogg'
        if lower.endswith('.pdf'):
            return 'application/pdf'
        if lower.endswith('.txt'):
            return 'text/plain'
        return 'image/png'

    def _convert_tools_format(self, data: Any) -> Any:
        """专门处理工具格式转换,统一转换为 camelCase"""
        if isinstance(data, dict):
            new_dict: dict[str, Any] = {}
            data_dict: dict[str, Any] = cast(dict[str, Any], data)
            for k, v in data_dict.items():
                camel_k = snake_to_camel(k) if '_' in k else k
                # 转换 function_declarations 为 functionDeclarations
                if k in ['function_declarations', 'functionDeclarations']:
                    new_dict['functionDeclarations'] = self._convert_tools_format(v)
                elif k in ['google_search', 'googleSearch']:
                    new_dict['googleSearch'] = self._convert_tools_format(v) if isinstance(v, (dict, list)) else v
                elif k in ['google_search_retrieval', 'googleSearchRetrieval']:
                    new_dict['googleSearchRetrieval'] = self._convert_tools_format(v) if isinstance(v, (dict, list)) else v
                elif k in ['code_execution', 'codeExecution']:
                    new_dict['codeExecution'] = self._convert_tools_format(v) if isinstance(v, (dict, list)) else v
                elif k in ['url_context', 'urlContext']:
                    new_dict['urlContext'] = self._convert_tools_format(v) if isinstance(v, (dict, list)) else v
                elif k in ['function_calling_config', 'functionCallingConfig']:
                    new_dict['functionCallingConfig'] = self._convert_tools_format(v)
                elif k in ['allowed_function_names', 'allowedFunctionNames']:
                    new_dict['allowedFunctionNames'] = self._convert_tools_format(v) if isinstance(v, (dict, list)) else v
                elif camel_k == "parametersJsonSchema" and isinstance(v, dict):
                    # parametersJsonSchema 需要特殊处理,确保 properties 和 required 字段一致
                    new_dict['parametersJsonSchema'] = self._convert_parameters_schema(cast(dict[str, Any], v))
                elif k == "parameters" and isinstance(v, dict):
                    # Schema 对象需要特殊处理
                    converted_v = v.copy() if isinstance(v, dict) else v
                    new_dict[k] = self._ensure_function_parameters_schema(self._to_native_schema(converted_v))
                elif k == "input_schema" and isinstance(v, dict):
                    new_dict['parameters'] = self._ensure_function_parameters_schema(self._to_native_schema(cast(dict[str, Any], v)))
                elif k == "inputSchema" and isinstance(v, dict):
                    new_dict['parameters'] = self._ensure_function_parameters_schema(self._to_native_schema(cast(dict[str, Any], v)))
                elif k == "name" and not v:  # Vcore AI Function name cannot be empty
                    continue
                elif k == "name" and v:
                    new_dict[k] = self._sanitize_function_name(str(v))
                else:
                    # 对于其他字段,转换为 camelCase(除了特殊字段)
                    new_dict[camel_k] = self._convert_tools_format(v) if isinstance(v, (dict, list)) else v
            return new_dict
        elif isinstance(data, list):
            return [self._convert_tools_format(item) for item in cast(list[Any], data)]
        else:
            return data

    def _convert_parameters_schema(self, schema: dict[str, Any]) -> dict[str, Any]:
        """
        转换 parametersJsonSchema,确保 properties 和 required 字段中的参数名一致
        统一使用 snake_case 格式,避免 camelCase 和 snake_case 混用导致的不匹配
        """
        new_schema: dict[str, Any] = schema.copy()
        unsupported_keys = {
            '$schema', '$id', '$defs', 'definitions', 'additionalProperties',
            'patternProperties', 'unevaluatedProperties', 'dependentSchemas',
            'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'not',
            'examples', 'default', 'nullable'
        }
        for key in unsupported_keys:
            new_schema.pop(key, None)

        if isinstance(new_schema.get('type'), list):
            non_null_types = [item for item in cast(list[Any], new_schema['type']) if item != 'null']
            new_schema['type'] = non_null_types[0] if non_null_types else 'string'
        
        # 处理 properties 字段:将 camelCase 转换为 snake_case
        if 'properties' in new_schema and isinstance(new_schema['properties'], dict):
            old_properties: dict[str, Any] = cast(dict[str, Any], new_schema['properties'])
            new_properties: dict[str, Any] = {}
            
            for prop_name, prop_def in old_properties.items():
                # 将 camelCase 转换为 snake_case
                snake_name = camel_to_snake(str(prop_name))
                new_properties[snake_name] = prop_def
                
                # 递归处理嵌套的 schema
                if isinstance(prop_def, dict):
                    new_properties[snake_name] = self._convert_parameters_schema(cast(dict[str, Any], prop_def))
            
            new_schema['properties'] = new_properties
        
        # 处理 required 字段:确保使用 snake_case(通常已经是正确的)
        if 'required' in new_schema and isinstance(new_schema['required'], list):
            # required 字段通常已经是 snake_case,但为了保险起见,也进行转换
            new_required: list[Any] = []
            for req_name in cast(list[Any], new_schema['required']):
                if isinstance(req_name, str):
                    snake_name = camel_to_snake(req_name)
                    new_required.append(snake_name)
                else:
                    new_required.append(req_name)
            new_schema['required'] = new_required
        
        # 处理其他可能包含 schema 的字段
        for key, value in list(new_schema.items()):
            if key not in ['properties', 'required'] and isinstance(value, dict):
                new_schema[key] = self._convert_parameters_schema(cast(dict[str, Any], value))
        
        return new_schema

    def _sanitize_function_name(self, name: str) -> str:
        cleaned = _GEMINI_FUNCTION_NAME_RE.sub("_", name.strip())[:64].strip("._-")
        if cleaned and not (cleaned[0].isalpha() or cleaned[0] == "_"):
            cleaned = f"tool_{cleaned}"[:64]
        return cleaned or "tool"

    def _ensure_function_parameters_schema(self, schema: Any) -> dict[str, Any]:
        if not isinstance(schema, dict):
            return {"type": "OBJECT", "properties": []}
        ensured = cast(dict[str, Any], schema).copy()
        schema_type = ensured.get('type')
        if not schema_type:
            ensured['type'] = 'OBJECT'
        elif isinstance(schema_type, str):
            ensured['type'] = schema_type.upper()
        if ensured.get('type') == 'OBJECT' and 'properties' not in ensured:
            ensured['properties'] = []
        return ensured

    def _to_native_schema(self, standard_schema: dict[str, Any]) -> dict[str, Any]:
        """
        将标准 JSON Schema 转换为 Vcore AI 原生 Map-style Schema
        
        Args:
            standard_schema: 标准 JSON Schema 对象
            
        Returns:
            Vcore AI 原生 Schema
        """
        
        native_schema = standard_schema.copy()
        unsupported_keys = {
            '$schema', '$id', '$defs', 'definitions', 'additionalProperties',
            'patternProperties', 'unevaluatedProperties', 'dependentSchemas',
            'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'not',
            'examples', 'default', 'nullable'
        }
        for key in unsupported_keys:
            native_schema.pop(key, None)
        
        # Vcore AI 要求类型必须是大写 (例如: STRING, OBJECT, INTEGER)
        if 'type' in native_schema and isinstance(native_schema['type'], list):
            non_null_types = [item for item in cast(list[Any], native_schema['type']) if item != 'null']
            native_schema['type'] = non_null_types[0] if non_null_types else 'string'

        if 'type' in native_schema and isinstance(native_schema['type'], str):
            native_schema['type'] = native_schema['type'].upper()
            
        if 'properties' in native_schema and isinstance(native_schema['properties'], dict):
            native_props: list[dict[str, str | dict[str, Any]]] = []
            props_dict = cast(dict[str, Any], native_schema['properties'])
            for key, value in props_dict.items():
                # 递归处理嵌套对象
                if isinstance(value, dict):
                    converted_value = self._to_native_schema(cast(dict[str, Any], value))
                else:
                    converted_value = {}
                
                native_props.append({
                    "key": str(key),
                    "value": converted_value
                })
            native_schema['properties'] = native_props
        
        # 处理数组项
        if 'items' in native_schema and isinstance(native_schema['items'], dict):
            items_dict = cast(dict[str, Any], native_schema['items'])
            native_schema['items'] = self._to_native_schema(items_dict)
            
        return native_schema
    
    def _handle_system_instruction(self, new_variables: dict[str, Any]) -> None:
        """处理 systemInstruction:如果没有 user content,则转换为 user message"""
        system_instruction_content = new_variables.get('systemInstruction')
        if not system_instruction_content:
            return
            
        contents = new_variables.get('contents', [])
        
        # 检查是否已有 user 角色
        contents_list: list[Any] = cast(list[Any], contents) if isinstance(contents, list) else []
        has_user_role = any(
            isinstance(content, dict) and cast(dict[str, Any], content).get('role') == 'user'
            for content in contents_list
        )
        
        if has_user_role:
            return
            
        # 提取文本内容
        text_from_system = self._extract_text_from_instruction(system_instruction_content)
        if not text_from_system:
            return
            
        # 转换为 user message
        user_message = {
            'role': 'user',
            'parts': [{'text': text_from_system}]
        }
        
        # 显式转换 contents 为 list[Any] 以修复 pylance 报错
        new_contents: list[Any] = list(contents_list)
        new_contents.insert(0, user_message)
        new_variables['contents'] = new_contents
        del new_variables['systemInstruction']
    
    def _extract_text_from_instruction(self, instruction: Any) -> str:
        """从 system instruction 中提取文本内容"""
        if isinstance(instruction, str):
            return instruction
        elif isinstance(instruction, dict):
            instruction_dict = cast(dict[str, Any], instruction)
            parts = instruction_dict.get('parts', [])
            if isinstance(parts, list):
                text_parts = []
                for part in parts:
                    if isinstance(part, dict) and 'text' in part:
                        text_parts.append(str(part['text']))
                return "".join(text_parts)
        return ""
    
    def _normalize_tools_format(self, tools: Any) -> list[dict[str, Any]]:
        """标准化 tools 格式为 Vcore AI 期望的格式 (List[Tool])"""
        converted_tools: Any = self._convert_tools_format(tools)
        tool_keys = {
            'functionDeclarations', 'googleSearch', 'googleSearchRetrieval',
            'codeExecution', 'retrieval', 'urlContext'
        }
        
        if isinstance(converted_tools, dict):
            # 如果是字典,且包含 functionDeclarations,将其包裹在列表中
            if any(key in converted_tools for key in tool_keys):
                normalized_tool = self._normalize_single_tool(cast(dict[str, Any], converted_tools), tool_keys)
                return [normalized_tool] if normalized_tool else []
            # 如果是单个 FunctionDeclaration,包裹成 Tool 再包裹在列表中
            if 'name' in converted_tools:
                decl = self._normalize_function_declaration(cast(dict[str, Any], converted_tools))
                return [{"functionDeclarations": [decl]}] if decl else []
            return []
            
        if not isinstance(converted_tools, list) or len(cast(list[Any], converted_tools)) == 0:
            return []
            
        converted_tools_list: list[Any] = cast(list[Any], converted_tools)
        normalized_tools: list[dict[str, Any]] = []
        function_decls: list[dict[str, Any]] = []
        for item in converted_tools_list:
            if not isinstance(item, dict):
                continue
            item_dict = cast(dict[str, Any], item)
            if any(key in item_dict for key in tool_keys):
                normalized_tool = self._normalize_single_tool(item_dict, tool_keys)
                if normalized_tool:
                    normalized_tools.append(normalized_tool)
            elif item_dict.get('name'):
                decl = self._normalize_function_declaration(item_dict)
                if decl:
                    function_decls.append(decl)

        if function_decls:
            normalized_tools.insert(0, {"functionDeclarations": function_decls})

        return normalized_tools

    def _normalize_single_tool(self, tool: dict[str, Any], tool_keys: set[str]) -> dict[str, Any] | None:
        normalized = {k: v for k, v in tool.items() if k in tool_keys or k.startswith('x')}

        func_decls = tool.get('functionDeclarations')
        if isinstance(func_decls, list):
            declarations: list[dict[str, Any]] = []
            for decl in cast(list[Any], func_decls):
                if isinstance(decl, dict):
                    normalized_decl = self._normalize_function_declaration(cast(dict[str, Any], decl))
                    if normalized_decl:
                        declarations.append(normalized_decl)
            if declarations:
                normalized['functionDeclarations'] = declarations
            else:
                normalized.pop('functionDeclarations', None)

        for native_key in ('googleSearch', 'googleSearchRetrieval', 'codeExecution', 'retrieval', 'urlContext'):
            if native_key in tool:
                native_value = tool.get(native_key)
                normalized[native_key] = native_value if isinstance(native_value, dict) else {}

        return normalized if any(key in normalized for key in tool_keys) else None

    def _normalize_function_declaration(self, declaration: dict[str, Any]) -> dict[str, Any] | None:
        raw_name = declaration.get('name') or declaration.get('function') or declaration.get('functionName')
        if not raw_name:
            return None
        normalized = declaration.copy()
        normalized['name'] = self._sanitize_function_name(str(raw_name))

        if 'parameters' in normalized:
            normalized['parameters'] = self._ensure_function_parameters_schema(normalized['parameters'])
        elif 'parametersJsonSchema' in normalized and isinstance(normalized['parametersJsonSchema'], dict):
            normalized['parametersJsonSchema'] = self._convert_parameters_schema(cast(dict[str, Any], normalized['parametersJsonSchema']))
        else:
            normalized['parameters'] = {"type": "OBJECT", "properties": []}

        return normalized

    def _normalize_tool_config(self, tool_config: Any) -> Any:
        converted = self._convert_tools_format(tool_config)
        if not isinstance(converted, dict):
            return converted
        config = cast(dict[str, Any], converted)
        fcc = config.get('functionCallingConfig')
        if isinstance(fcc, dict):
            fcc_dict = cast(dict[str, Any], fcc).copy()
            mode = fcc_dict.get('mode')
            if isinstance(mode, str):
                mode_upper = mode.upper()
                if mode_upper in {'AUTO', 'ANY', 'NONE', 'MODE_UNSPECIFIED'}:
                    fcc_dict['mode'] = mode_upper
            allowed = fcc_dict.get('allowedFunctionNames')
            if isinstance(allowed, list):
                fcc_dict['allowedFunctionNames'] = [
                    self._sanitize_function_name(str(name))
                    for name in cast(list[Any], allowed)
                    if name is not None and str(name).strip()
                ]
            if not fcc_dict.get('allowedFunctionNames'):
                fcc_dict.pop('allowedFunctionNames', None)
            config['functionCallingConfig'] = fcc_dict
        if not config.get('functionCallingConfig'):
            config.pop('functionCallingConfig', None)
        return config

    def _handle_inline_data_case(self, contents: Any) -> Any:
        """
        递归处理 contents,确保 inlineData/mimeType 字段名正确 (适配各种客户端传参)
        """
        if isinstance(contents, list):
            return [self._handle_inline_data_case(item) for item in cast(list[Any], contents)]
        if isinstance(contents, dict):
            new_dict: dict[str, Any] = {}
            for k, v in cast(dict[str, Any], contents).items():
                camel_k = snake_to_camel(k)
                if k == 'thought_signature':
                    new_dict['thoughtSignature'] = self._handle_inline_data_case(v)
                    continue
                if camel_k == 'inlineData' and isinstance(v, dict):
                    v_dict = cast(dict[str, Any], v)
                    new_inline_data = {}
                    for ik, iv in v_dict.items():
                        camel_ik = snake_to_camel(ik)
                        new_inline_data[camel_ik] = iv
                    new_dict['inlineData'] = new_inline_data
                elif camel_k == 'fileData' and isinstance(v, dict):
                    v_dict = cast(dict[str, Any], v)
                    new_file_data = {}
                    for ik, iv in v_dict.items():
                        camel_ik = snake_to_camel(ik)
                        new_file_data[camel_ik] = iv
                    new_dict['fileData'] = new_file_data
                elif camel_k == 'functionCall' and isinstance(v, dict):
                    func_call = self._convert_tools_format(v)
                    if isinstance(func_call, dict):
                        new_dict['functionCall'] = func_call
                elif camel_k == 'functionResponse' and isinstance(v, dict):
                    func_response = self._convert_tools_format(v)
                    if isinstance(func_response, dict):
                        new_dict['functionResponse'] = func_response
                else:
                    new_dict[camel_k] = self._handle_inline_data_case(v)
            return new_dict
        return contents

    def _handle_base64_in_contents(self, contents: Any) -> Any:
        """
        递归处理 contents 中的 base64 数据。
        将 URL-safe Base64 转换为标准 Base64 并补全 padding。
        """
        try:
            if isinstance(contents, list):
                res_list: list[Any] = [self._handle_base64_in_contents(item) for item in cast(list[Any], contents)]
                return cast(Any, res_list)
            if isinstance(contents, dict):
                new_dict: dict[str, Any] = {}
                for k, v in cast(dict[str, Any], contents).items():
                    if k == 'inlineData' and isinstance(v, dict):
                        v_dict = cast(dict[str, Any], v)
                        if 'data' in v_dict and isinstance(v_dict['data'], str):
                            try:
                                b64_data: str = v_dict['data']
                                b64_data = b64_data.replace('-', '+').replace('_', '/')
                                padding = len(b64_data) % 4
                                if padding:
                                    b64_data += '=' * (4 - padding)
                                
                                new_inline_data = v_dict.copy()
                                new_inline_data['data'] = b64_data
                                new_dict[k] = new_inline_data
                                continue
                            except Exception:
                                pass
                    new_dict[k] = self._handle_base64_in_contents(v)
                return cast(Any, new_dict)
            return contents
        except Exception as e:
            logger.warning(f"Base64 内容处理失败: {e}")
            return cast(Any, contents)

    def _filter_empty_contents(self, contents: Any) -> Any:
        """
        过滤掉空的 contents(parts 为空数组的消息)
        Vcore AI 要求每个 content 至少包含一个 part
        """
        if not isinstance(contents, list):
            return contents
        
        filtered_contents: list[Any] = []
        contents_list: list[Any] = cast(list[Any], contents)
        
        # 收集所有 functionCall 的名称,用于修复 functionResponse
        function_call_names: list[str] = []
        for content in contents_list:
            if isinstance(content, dict):
                content_dict = cast(dict[str, Any], content)
                parts = content_dict.get('parts', [])
                if isinstance(parts, list):
                    for part in cast(list[Any], parts):
                        if isinstance(part, dict):
                            part_dict = cast(dict[str, Any], part)
                            func_call = part_dict.get('functionCall')
                            if not isinstance(func_call, dict):
                                func_call = part_dict.get('function_call')
                            if isinstance(func_call, dict):
                                func_call_dict = cast(dict[str, Any], func_call)
                                name = func_call_dict.get('name')
                                if name and isinstance(name, str):
                                    function_call_names.append(self._sanitize_function_name(name))
        
        for content in contents_list:
            if isinstance(content, dict):
                content_dict: dict[str, Any] = cast(dict[str, Any], content)
                parts = content_dict.get('parts', [])
                # 只保留有 parts 且 parts 不为空的 content
                if isinstance(parts, list) and len(cast(list[Any], parts)) > 0:
                    parts_list: list[Any] = cast(list[Any], parts)
                    # 过滤并验证 parts 中的有效内容
                    valid_parts: list[Any] = []
                    for part in parts_list:
                        if isinstance(part, dict):
                            part_dict = cast(dict[str, Any], part)
                            
                            # 清理并修复 part
                            cleaned_part = self._clean_part_metadata(part_dict, function_call_names)
                            if cleaned_part:
                                valid_parts.append(cleaned_part)
                    
                    if valid_parts:
                        # 更新 content 的 parts
                        filtered_content = content_dict.copy()
                        filtered_content['parts'] = valid_parts
                        filtered_contents.append(filtered_content)
                    else:
                        logger.debug(f"过滤掉空的 content: role={content_dict.get('role', 'unknown')}")
                else:
                    logger.debug(f"过滤掉空的 content: role={content_dict.get('role', 'unknown')}")
            else:
                # 非 Dict 类型的 content,保留
                filtered_contents.append(content)
        
        return filtered_contents

    def _clean_part_metadata(self, part_dict: dict[str, Any], function_call_names: list[str]) -> dict[str, Any] | None:
        """
        清理 part 中的空元数据字段,修复无效的 functionResponse
        
        Args:
            part_dict: 原始 part 字典
            function_call_names: 可用的函数调用名称列表
            
        Returns:
            清理后的 part 字典,如果 part 无效则返回 None
        """
        cleaned_part: dict[str, Any] = {}
        has_valid_content = False
        
        # 处理文本内容
        if 'text' in part_dict:
            text_value = part_dict['text']
            if text_value is not None and str(text_value) != "":
                cleaned_part['text'] = text_value
                has_valid_content = True
        
        # 处理思考标记
        if 'thought' in part_dict:
            cleaned_part['thought'] = part_dict['thought']
        
        # 处理思考签名 (thoughtSignature)
        if 'thoughtSignature' in part_dict:
            cleaned_part['thoughtSignature'] = part_dict['thoughtSignature']
        elif 'thought_signature' in part_dict:
            cleaned_part['thoughtSignature'] = part_dict['thought_signature']
        
        # 处理函数调用
        if 'functionCall' in part_dict or 'function_call' in part_dict:
            func_call = part_dict.get('functionCall') or part_dict.get('function_call')
            if isinstance(func_call, dict):
                func_call_dict = cast(dict[str, Any], func_call)
                if func_call_dict.get('name'):  # 只保留有名称的函数调用
                    fixed_func_call = func_call_dict.copy()
                    fixed_func_call['name'] = self._sanitize_function_name(str(fixed_func_call['name']))
                    if 'args' not in fixed_func_call or fixed_func_call.get('args') is None:
                        fixed_func_call['args'] = {}
                    if isinstance(fixed_func_call.get('args'), str):
                        try:
                            fixed_func_call['args'] = json.loads(cast(str, fixed_func_call['args']))
                        except json.JSONDecodeError:
                            fixed_func_call['args'] = {"raw": fixed_func_call['args']}
                    if not isinstance(fixed_func_call.get('args'), dict):
                        fixed_func_call['args'] = {"value": fixed_func_call.get('args')}
                    cleaned_part['functionCall'] = fixed_func_call
                    has_valid_content = True
        
        # 处理函数响应
        if 'functionResponse' in part_dict or 'function_response' in part_dict:
            func_response = part_dict.get('functionResponse') or part_dict.get('function_response')
            if isinstance(func_response, dict):
                func_response_dict = cast(dict[str, Any], func_response)
                current_name = (
                    func_response_dict.get('name')
                    or func_response_dict.get('functionName')
                    or func_response_dict.get('function_name')
                )
                
                # 如果 name 为空,尝试修复
                if not current_name and function_call_names:
                    inferred_name = function_call_names[-1]  # 使用最后一个 functionCall 的名称
                    logger.warning(f"修复空的 functionResponse.name,推断为: {inferred_name}")
                    
                    fixed_func_response = func_response_dict.copy()
                    fixed_func_response['name'] = self._sanitize_function_name(inferred_name)
                    fixed_func_response['response'] = self._coerce_function_response(fixed_func_response.get('response', {}))
                    cleaned_part['functionResponse'] = fixed_func_response
                    has_valid_content = True
                elif current_name:
                    # name 不为空,直接保留
                    fixed_func_response = func_response_dict.copy()
                    fixed_func_response['name'] = self._sanitize_function_name(str(current_name))
                    fixed_func_response['response'] = self._coerce_function_response(fixed_func_response.get('response', {}))
                    cleaned_part['functionResponse'] = fixed_func_response
                    has_valid_content = True
                # 如果 name 为空且无法推断,则丢弃这个 functionResponse
        
        # 处理内联数据
        if 'inlineData' in part_dict:
            inline_data = part_dict['inlineData']
            if isinstance(inline_data, dict):
                inline_data_dict = cast(dict[str, Any], inline_data)
                # 只保留有实际数据的 inlineData
                if (inline_data_dict.get('data') and
                    str(inline_data_dict['data']).strip() and
                    inline_data_dict.get('mimeType') and
                    str(inline_data_dict['mimeType']).strip()):
                    cleaned_part['inlineData'] = inline_data
                    has_valid_content = True
        
        # 处理文件数据
        if 'fileData' in part_dict:
            file_data = part_dict['fileData']
            if isinstance(file_data, dict):
                file_data_dict = cast(dict[str, Any], file_data)
                # 只保留有实际数据的 fileData
                if (file_data_dict.get('fileUri') and
                    str(file_data_dict['fileUri']).strip() and
                    file_data_dict.get('mimeType') and
                    str(file_data_dict['mimeType']).strip()):
                    cleaned_part['fileData'] = file_data
                    has_valid_content = True

        # 处理代码执行相关 part
        for code_key in ('executableCode', 'codeExecutionResult'):
            if code_key in part_dict and part_dict[code_key]:
                cleaned_part[code_key] = part_dict[code_key]
                has_valid_content = True

        # 透传 Gemini 新增/实验 part;只在字段非空时保留。
        # 核心 part 字段已在上方严格清洗,不能兜底透传;否则会把
        # functionResponse.name=null、空 inlineData/fileData/functionCall 等非法字段重新加入上游 payload。
        passthrough_part_keys = (
            'videoMetadata', 'mediaResolution', 'thought', 'thoughtSignature', 'thought_signature',
        )
        for key in passthrough_part_keys:
            if key in part_dict and key not in cleaned_part and part_dict[key]:
                cleaned_key = 'thoughtSignature' if key == 'thought_signature' else key
                cleaned_part[cleaned_key] = part_dict[key]
                if cleaned_key not in {'thought', 'thoughtSignature', 'videoMetadata', 'mediaResolution'}:
                    has_valid_content = True

        # 保留与有效媒体 part 绑定的元数据
        for metadata_key in ('videoMetadata', 'mediaResolution'):
            if metadata_key in part_dict and part_dict[metadata_key]:
                cleaned_part[metadata_key] = part_dict[metadata_key]
        
        # 只返回有有效内容的 part
        if has_valid_content:
            return cleaned_part
        else:
            logger.debug("过滤掉没有有效内容的 part")
            return None

    def _sanitize_vcore_variables_in_place(self, variables: dict[str, Any]) -> None:
        """最终出口兜底,确保发往匿名 Vcore 的 contents 不含非法核心 part。"""
        contents = variables.get('contents')
        if not isinstance(contents, list):
            return

        function_call_names: list[str] = []
        for content in cast(list[Any], contents):
            if not isinstance(content, dict):
                continue
            parts = cast(dict[str, Any], content).get('parts')
            if not isinstance(parts, list):
                continue
            for part in cast(list[Any], parts):
                if not isinstance(part, dict):
                    continue
                func_call = cast(dict[str, Any], part).get('functionCall') or cast(dict[str, Any], part).get('function_call')
                if isinstance(func_call, dict):
                    name = cast(dict[str, Any], func_call).get('name')
                    if name and str(name).strip():
                        function_call_names.append(self._sanitize_function_name(str(name)))

        sanitized_contents: list[dict[str, Any]] = []
        dropped_function_responses = 0
        for content in cast(list[Any], contents):
            if not isinstance(content, dict):
                continue
            content_dict = cast(dict[str, Any], content).copy()
            parts = content_dict.get('parts')
            if not isinstance(parts, list):
                continue

            sanitized_parts: list[dict[str, Any]] = []
            for part in cast(list[Any], parts):
                if not isinstance(part, dict):
                    continue
                part_dict = cast(dict[str, Any], part)
                cleaned = self._sanitize_vcore_part_final(part_dict, function_call_names)
                if cleaned:
                    if ('functionResponse' in part_dict or 'function_response' in part_dict) and 'functionResponse' not in cleaned:
                        dropped_function_responses += 1
                    sanitized_parts.append(cleaned)

            if sanitized_parts:
                content_dict['parts'] = sanitized_parts
                sanitized_contents.append(content_dict)

        variables['contents'] = sanitized_contents
        if dropped_function_responses:
            logger.warning(f"最终出口丢弃非法 functionResponse part: count={dropped_function_responses}")

    def _sanitize_vcore_part_final(self, part: dict[str, Any], function_call_names: list[str]) -> dict[str, Any] | None:
        cleaned: dict[str, Any] = {}
        has_content = False

        text = part.get('text')
        if text is not None and str(text) != "":
            cleaned['text'] = text
            has_content = True

        if 'thought' in part:
            cleaned['thought'] = part['thought']
        signature = part.get('thoughtSignature') or part.get('thought_signature')
        if signature:
            cleaned['thoughtSignature'] = signature

        func_call = part.get('functionCall') or part.get('function_call')
        if isinstance(func_call, dict):
            call = cast(dict[str, Any], func_call).copy()
            name = call.get('name')
            if name and str(name).strip():
                call['name'] = self._sanitize_function_name(str(name))
                call['args'] = self._coerce_function_args(call.get('args', {}))
                cleaned['functionCall'] = call
                has_content = True

        func_response = part.get('functionResponse') or part.get('function_response')
        if isinstance(func_response, dict):
            response = cast(dict[str, Any], func_response).copy()
            name = response.get('name') or response.get('functionName') or response.get('function_name')
            if not name and function_call_names:
                name = function_call_names[-1]
            if name and str(name).strip():
                response['name'] = self._sanitize_function_name(str(name))
                response['response'] = self._coerce_function_response(response.get('response', {}))
                cleaned['functionResponse'] = response
                has_content = True

        inline_data = part.get('inlineData') or part.get('inline_data')
        if isinstance(inline_data, dict):
            inline_dict = cast(dict[str, Any], inline_data)
            data = inline_dict.get('data')
            mime_type = inline_dict.get('mimeType') or inline_dict.get('mime_type')
            if data and str(data).strip() and mime_type and str(mime_type).strip():
                cleaned['inlineData'] = {**inline_dict, 'mimeType': str(mime_type)}
                cleaned['inlineData'].pop('mime_type', None)
                has_content = True

        file_data = part.get('fileData') or part.get('file_data')
        if isinstance(file_data, dict):
            file_dict = cast(dict[str, Any], file_data)
            file_uri = file_dict.get('fileUri') or file_dict.get('file_uri')
            mime_type = file_dict.get('mimeType') or file_dict.get('mime_type')
            if file_uri and str(file_uri).strip() and mime_type and str(mime_type).strip():
                cleaned['fileData'] = {**file_dict, 'fileUri': str(file_uri), 'mimeType': str(mime_type)}
                cleaned['fileData'].pop('file_uri', None)
                cleaned['fileData'].pop('mime_type', None)
                has_content = True

        for code_key in ('executableCode', 'codeExecutionResult'):
            if part.get(code_key):
                cleaned[code_key] = part[code_key]
                has_content = True

        for metadata_key in ('videoMetadata', 'mediaResolution'):
            if part.get(metadata_key):
                cleaned[metadata_key] = part[metadata_key]

        return cleaned if has_content else None

    def _ensure_function_call_thought_signatures(self, contents: Any) -> Any:
        """
        Gemini 3 工具调用要求历史中的 functionCall part 带 thoughtSignature。
        如果客户端没有回传签名,使用官方保底哨兵值跳过校验,避免 400 错误。
        """
        if not isinstance(contents, list):
            return contents

        patched_count = 0
        patched_names: set[str] = set()
        patched_contents: list[Any] = []
        for content in cast(list[Any], contents):
            if not isinstance(content, dict):
                patched_contents.append(content)
                continue

            content_dict = cast(dict[str, Any], content)
            parts = content_dict.get('parts')
            if not isinstance(parts, list):
                patched_contents.append(content)
                continue

            patched_parts: list[Any] = []
            for part in cast(list[Any], parts):
                if isinstance(part, dict):
                    part_dict = cast(dict[str, Any], part)
                    function_call = part_dict.get('functionCall')
                    if not isinstance(function_call, dict):
                        function_call = part_dict.get('function_call')
                    if isinstance(function_call, dict):
                        patched_part = part_dict.copy()
                        if 'function_call' in patched_part and 'functionCall' not in patched_part:
                            patched_part['functionCall'] = patched_part.pop('function_call')
                        if 'thought_signature' in patched_part and 'thoughtSignature' not in patched_part:
                            patched_part['thoughtSignature'] = patched_part.pop('thought_signature')
                        if not patched_part.get('thoughtSignature'):
                            func_call = cast(dict[str, Any], patched_part.get('functionCall') or {})
                            patched_count += 1
                            patched_names.add(str(func_call.get('name') or 'unknown'))
                            patched_part['thoughtSignature'] = 'skip_thought_signature_validator'
                        patched_parts.append(patched_part)
                    else:
                        patched_parts.append(part)
                else:
                    patched_parts.append(part)

            patched_content = content_dict.copy()
            patched_content['parts'] = patched_parts
            patched_contents.append(patched_content)

        if patched_count:
            logger.debug(
                "已为 %d 个缺少 thoughtSignature 的 functionCall 使用兼容兜底,函数: %s",
                patched_count,
                ", ".join(sorted(patched_names))
            )

        return patched_contents

    def _handle_thought_signature(self, contents: Any) -> Any:
        """
        处理 thoughtSignature 字段的 base64 编码
        确保 thoughtSignature 字段正确编码为 base64 字符串
        """
        import base64
        
        if isinstance(contents, list):
            return [self._handle_thought_signature(item) for item in cast(list[Any], contents)]
        if isinstance(contents, dict):
            new_dict: dict[str, Any] = {}
            contents_dict: dict[str, Any] = cast(dict[str, Any], contents)
            for k, v in contents_dict.items():
                if k == 'parts' and isinstance(v, list):
                    v_list: list[Any] = cast(list[Any], v)
                    # 处理 parts 数组中的每个 part
                    new_parts: list[Any] = []
                    for part in v_list:
                        if isinstance(part, dict):
                            new_part: dict[str, Any] = cast(dict[str, Any], part).copy()
                            # 检查是否有 thoughtSignature 字段
                            if 'thoughtSignature' in new_part:
                                signature_value = new_part['thoughtSignature']
                                if isinstance(signature_value, str):
                                    # 如果是特定的字符串,进行 base64 编码
                                    if signature_value == "skip_thought_signature_validator":
                                        encoded_bytes = base64.b64encode(signature_value.encode('utf-8'))
                                        new_part['thoughtSignature'] = encoded_bytes.decode('utf-8')
                                    # 如果已经是 base64 编码的字符串,保持不变
                                    # 其他情况也保持不变
                            new_parts.append(new_part)
                        else:
                            new_parts.append(part)
                    new_dict[k] = new_parts
                else:
                    new_dict[k] = self._handle_thought_signature(v) if isinstance(v, (dict, list)) else v
            return new_dict
        return contents

    @staticmethod
    def prepare_headers(creds: dict[str, Any]) -> dict[str, str]:
        """准备请求头"""
        # 提取原始头信息
        headers = RequestTransformer._extract_headers_from_creds(creds)
        
        # 设置必要的头信息
        headers['content-type'] = 'application/json'
        
        # 移除可能导致问题的头
        problematic_headers = [
            'content-length', 'Content-Length', 'host', 'Host',
            'connection', 'Connection', 'accept-encoding'
        ]
        for header in problematic_headers:
            headers.pop(header, None)
            
        return headers
    
    @staticmethod
    def _extract_headers_from_creds(creds: dict[str, Any]) -> dict[str, str]:
        """从凭证中提取头信息"""
        if hasattr(creds, 'model_dump') and hasattr(creds, 'headers'):
            headers_attr = getattr(creds, 'headers')
            if isinstance(headers_attr, dict):
                return cast(dict[str, str], headers_attr).copy()
        
        raw_headers = creds.get('headers')
        if isinstance(raw_headers, dict):
            return cast(dict[str, str], raw_headers).copy()
            
        return {}


class ResponseAggregator:
    """响应聚合器"""

    @staticmethod
    def log_stream_forward_complete(prefix: str, chunks: int, bytes_total: int, elapsed: float) -> None:
        logger.info(
            f"{prefix} 流式转发完成: chunks={chunks}, "
            f"bytes={bytes_total}, 耗时={elapsed:.1f}s"
        )

    @staticmethod
    async def aggregate_stream(
        stream_generator: Any,
        _raw_image_response: bool = False,
        progress_context: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        """
        聚合流式响应为非流式对象
        """
        all_parts: list[dict[str, Any]] = []
        candidate_parts_by_index: dict[int, list[dict[str, Any]]] = {}
        finish_reason: str | None = None
        finish_message: str | None = None
        safety_ratings: list[dict[str, Any]] = []
        citation_metadata: dict[str, Any] = {}
        grounding_metadata: dict[str, Any] = {}
        token_count: int | None = None
        avg_logprobs: float | None = None
        logprobs_result: dict[str, Any] | None = None
        candidate_index = 0
        usage_metadata: dict[str, Any] = {}
        
        create_time: str | None = None
        model_version: str | None = None
        prompt_feedback: dict[str, Any] = {}
        response_id: str | None = None
        model_status: dict[str, Any] | None = None
        buffered_chunk_count = 0
        buffered_bytes_total = 0
        aggregate_started_at = time.monotonic()
        aggregate_error: Exception | None = None
        
        try:
            async for stream_item in stream_generator:
                try:
                    if isinstance(stream_item, dict):
                        chunk = stream_item
                        chunk_bytes = 0
                    else:
                        actual_json_str = str(stream_item).strip()
                        if actual_json_str.startswith("data: "):
                            actual_json_str = actual_json_str[6:]

                        if not actual_json_str:
                            continue
                        chunk = json.loads(actual_json_str)
                        chunk_bytes = len(actual_json_str.encode('utf-8'))

                    buffered_chunk_count += 1
                    buffered_bytes_total += chunk_bytes

                    # 检查 chunk 是否包含错误 (使用统一解析逻辑)
                    parsed_error = parse_error_response(chunk)
                    if parsed_error:
                        raise parsed_error

                    # 提取顶层元数据
                    create_time = chunk.get('createTime') or create_time
                    model_version = chunk.get('modelVersion') or model_version
                    prompt_feedback = chunk.get('promptFeedback', {}) or prompt_feedback
                    response_id = chunk.get('responseId') or response_id
                    usage_metadata = chunk.get('usageMetadata', {}) or usage_metadata
                    model_status = chunk.get('modelStatus') or model_status
                    
                    candidates = chunk.get('candidates', [])
                    if candidates:
                        candidates_to_read = candidates if _raw_image_response else candidates[:1]
                        for candidate_offset, candidate in enumerate(candidates_to_read):
                            if not isinstance(candidate, dict):
                                continue
                            candidate_result_index = candidate_offset
                            if candidate.get('index') is not None:
                                try:
                                    candidate_result_index = int(candidate['index'])
                                except (TypeError, ValueError):
                                    candidate_result_index = candidate_offset

                            # 提取 parts。raw 图片响应会读取所有 candidates,支持一次上游响应返回多张图;
                            # 普通 Gemini 非流式响应仍只聚合第一个 candidate,避免改变文本接口语义。
                            content_obj = candidate.get('content', {})
                            raw_parts = content_obj.get('parts', []) if isinstance(content_obj, dict) else []
                            parts = ResponseAggregator._normalize_response_parts(raw_parts)
                            if parts:
                                all_parts.extend(parts)
                                candidate_parts_by_index.setdefault(candidate_result_index, []).extend(
                                    [cast(dict[str, Any], part) for part in cast(list[Any], parts) if isinstance(part, dict)]
                                )

                            # 提取 candidate 元数据;多图 raw 响应不依赖这些字段,只保留首个 candidate 的元数据。
                            if candidate_offset == 0:
                                finish_reason = candidate.get('finishReason') or finish_reason
                                finish_message = candidate.get('finishMessage') or finish_message
                                safety_ratings = candidate.get('safetyRatings') or safety_ratings
                                citation_metadata = candidate.get('citationMetadata') or citation_metadata
                                grounding_metadata = candidate.get('groundingMetadata') or grounding_metadata
                                if candidate.get('tokenCount') is not None:
                                    token_count = candidate['tokenCount']
                                if candidate.get('avgLogprobs') is not None:
                                    avg_logprobs = candidate['avgLogprobs']
                                if candidate.get('logprobsResult') is not None:
                                    logprobs_result = candidate['logprobsResult']
                                if candidate.get('index') is not None:
                                    candidate_index = candidate['index']
                            
                except json.JSONDecodeError as e:
                    logger.debug(f"JSON 解析失败,跳过此块: {e}")
                    continue
                    
        except VcoreError as e:
            aggregate_error = e
            raise
        except Exception as e:
            aggregate_error = e
            raise InternalError(message=f"Non-streaming request error: {e}")
        finally:
            if buffered_chunk_count > 0:
                progress_prefix = progress_context.get("prefix") if isinstance(progress_context, dict) else ""
                prefix = f"{progress_prefix} " if progress_prefix else ""
                state = "异常" if aggregate_error is not None else "完成"
                log_fields = [
                    f"chunks={buffered_chunk_count}",
                    f"bytes={buffered_bytes_total}",
                    f"状态={state}",
                ]
                log_fields.append(f"耗时={time.monotonic() - aggregate_started_at:.1f}s")
                logger.info(
                    f"{prefix}非流式接收缓存结束: {', '.join(log_fields)}"
                )

        # 流式上游通常把一段连续文本拆成多个 text part。非流式响应若原样返回
        # 多个相邻 text part,部分 SDK/客户端会在 part 边界自行插入换行或段落,
        # 导致最终文本出现类似“当\n\n掉”“</\n\nrelationships>”的伪换行。
        # 因此在构造非流式结果前合并相邻、同类型的文本 part,保持文本内容本身不变。
        all_parts = ResponseAggregator._merge_adjacent_text_parts(all_parts)
        if candidate_parts_by_index:
            candidate_parts_by_index = {
                idx: ResponseAggregator._merge_adjacent_text_parts(parts)
                for idx, parts in candidate_parts_by_index.items()
            }
        
        # 处理图片响应特例
        image_outputs = ResponseAggregator._extract_image_outputs(all_parts)
        is_image_response = bool(image_outputs)
        if _raw_image_response and image_outputs:
            return ResponseAggregator._raw_image_response_from_images(image_outputs)

        if is_image_response and candidate_parts_by_index:
            result_candidates: list[dict[str, Any]] = []
            for idx, parts in sorted(candidate_parts_by_index.items(), key=lambda item: item[0]):
                item: dict[str, Any] = {
                    "index": idx,
                    "content": {
                        "parts": parts,
                        "role": "model",
                    },
                }
                if finish_reason:
                    item["finishReason"] = finish_reason.upper()
                result_candidates.append(item)

            result: dict[str, Any] = {"candidates": result_candidates}
            optional_result_fields: dict[str, Any] = {
                "createTime": create_time,
                "modelVersion": model_version,
                "promptFeedback": prompt_feedback,
                "responseId": response_id,
                "usageMetadata": usage_metadata,
                "modelStatus": model_status,
            }
            for key, value in optional_result_fields.items():
                if value is not None and value != {} and value != "":
                    result[key] = value
            return result

        # 处理图片响应特例
        image_outputs = ResponseAggregator._extract_image_outputs(all_parts)
        if _raw_image_response and image_outputs:
            return ResponseAggregator._raw_image_response_from_images(image_outputs)
        
        # 构建最终响应
        if not all_parts:
            all_parts = [{"text": " "}]
        
        result_candidate: dict[str, Any] = {
            "index": candidate_index
        }
        if finish_reason:
            result_candidate["finishReason"] = finish_reason.upper()
            
        result_candidate["content"] = {
            "parts": all_parts,
            "role": "model"
        }
        
        # 构建候选结果,只添加非空字段
        optional_candidate_fields: dict[str, Any] = {
            "finishMessage": finish_message,
            "safetyRatings": safety_ratings,
            "citationMetadata": citation_metadata,
            "groundingMetadata": grounding_metadata,
            "tokenCount": token_count,
            "avgLogprobs": avg_logprobs,
            "logprobsResult": logprobs_result
        }
        
        for key, value in optional_candidate_fields.items():
            if value is not None and value != [] and value != {}:
                result_candidate[key] = value
        
        # 构建最终结果,只添加非空字段
        result: dict[str, Any] = {"candidates": [result_candidate]}
        
        optional_result_fields: dict[str, Any] = {
            "createTime": create_time,
            "modelVersion": model_version,
            "promptFeedback": prompt_feedback,
            "responseId": response_id,
            "usageMetadata": usage_metadata,
            "modelStatus": model_status
        }
        
        for key, value in optional_result_fields.items():
            if value is not None and value != {} and value != "":
                result[key] = value
        
        return result

    @staticmethod
    def _extract_image_outputs(parts: list[dict[str, Any]]) -> list[dict[str, str]]:
        images: list[dict[str, str]] = []
        for part in parts:
            inline_data = part.get('inlineData') or part.get('inline_data')
            if isinstance(inline_data, dict):
                mime_type = inline_data.get('mimeType') or inline_data.get('mime_type') or 'image/png'
                data = inline_data.get('data')
                if isinstance(data, str) and data.strip():
                    images.append({"b64_json": data, "mime_type": str(mime_type)})

            file_data = part.get('fileData') or part.get('file_data')
            if isinstance(file_data, dict):
                mime_type = file_data.get('mimeType') or file_data.get('mime_type') or 'image/png'
                file_uri = file_data.get('fileUri') or file_data.get('file_uri') or file_data.get('uri') or file_data.get('url')
                if isinstance(file_uri, str) and file_uri.strip():
                    images.append({"url": file_uri.strip(), "mime_type": str(mime_type)})
        return images

    @staticmethod
    def _merge_adjacent_text_parts(parts: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """合并相邻文本 part,避免非流式客户端在 part 边界额外插入换行。"""
        merged: list[dict[str, Any]] = []

        def mergeable_text_signature(part: dict[str, Any]) -> tuple[Any, Any] | None:
            if "text" not in part:
                return None
            # 只要是纯文本 part 就允许合并。Vcore/Gemini 可能在 text part 上附带
            # thought/thoughtSignature 以外的无害元数据;这些元数据如果阻止合并,
            # Gemini 非流式客户端仍会在碎片边界插入额外换行。
            blocking_keys = {
                "inlineData", "inline_data", "fileData", "file_data",
                "functionCall", "function_call", "functionResponse", "function_response",
                "executableCode", "executable_code", "codeExecutionResult", "code_execution_result",
            }
            if any(key in part and ResponseAggregator._has_meaningful_part_value(part.get(key)) for key in blocking_keys):
                return None
            # thoughtSignature 标记了独立上下文,保守起见不跨 signature 合并。
            signature = part.get("thoughtSignature") or part.get("thought_signature")
            return (bool(part.get("thought")), signature)

        for raw_part in parts:
            part = dict(raw_part)
            sig = mergeable_text_signature(part)
            if sig is None or not merged:
                merged.append(part)
                continue

            previous = merged[-1]
            previous_sig = mergeable_text_signature(previous)
            if previous_sig == sig:
                previous["text"] = str(previous.get("text", "")) + str(part.get("text", ""))
            else:
                merged.append(part)

        return merged

    @staticmethod
    def _normalize_response_parts(raw_parts: Any) -> list[dict[str, Any]]:
        """清洗上游 part 中的空占位字段,避免 Gemini 非流式返回碎片化 parts。"""
        if not isinstance(raw_parts, list):
            return []

        normalized: list[dict[str, Any]] = []
        placeholder_keys = {
            "inlineData", "inline_data", "fileData", "file_data",
            "functionCall", "function_call", "functionResponse", "function_response",
            "executableCode", "executable_code", "codeExecutionResult", "code_execution_result",
        }

        for item in raw_parts:
            if not isinstance(item, dict):
                continue
            part = dict(cast(dict[str, Any], item))
            if 'inline_data' in part and 'inlineData' not in part:
                inline_data = part['inline_data']
                if isinstance(inline_data, dict):
                    part['inlineData'] = {snake_to_camel(str(k)): v for k, v in cast(dict[Any, Any], inline_data).items()}
                else:
                    part['inlineData'] = inline_data
            if 'file_data' in part and 'fileData' not in part:
                file_data = part['file_data']
                if isinstance(file_data, dict):
                    part['fileData'] = {snake_to_camel(str(k)): v for k, v in cast(dict[Any, Any], file_data).items()}
                else:
                    part['fileData'] = file_data
            for key in list(part.keys()):
                if key in placeholder_keys and not ResponseAggregator._has_meaningful_part_value(part.get(key)):
                    part.pop(key, None)
            # 部分上游/客户端会把 part 类型放在 data/type 上;Gemini 响应无需保留
            # data='text' 这类非标准占位,否则下游可能把 text part 当成多模态 part。
            if part.get("data") == "text":
                part.pop("data", None)
            if part.get("type") == "text":
                part.pop("type", None)
            normalized.append(part)

        return normalized

    @staticmethod
    def _has_meaningful_part_value(value: Any) -> bool:
        """判断 part 子字段是否真实有内容;空壳对象不应阻止文本合并。"""
        if value is None or value is False:
            return False
        if isinstance(value, str):
            return value != ""
        if isinstance(value, dict):
            return any(ResponseAggregator._has_meaningful_part_value(v) for v in value.values())
        if isinstance(value, (list, tuple, set)):
            return any(ResponseAggregator._has_meaningful_part_value(v) for v in value)
        return True

    @staticmethod
    def _raw_image_response_from_images(images: list[dict[str, str]]) -> dict[str, Any]:
        return {
            "created": int(time.time()),
            "data": [
                dict(image)
                for image in images
            ],
        }