liuw15 commited on
Commit
a802bc5
·
1 Parent(s): 37a00c9

初步添加claude接口

Browse files
Files changed (2) hide show
  1. src/server/index.js +300 -3
  2. src/utils/utils.js +296 -1
src/server/index.js CHANGED
@@ -4,7 +4,7 @@ import path from 'path';
4
  import fs from 'fs';
5
  import { fileURLToPath } from 'url';
6
  import { generateAssistantResponse, generateAssistantResponseNoStream, getAvailableModels, generateImageForSD, closeRequester } from '../api/client.js';
7
- import { generateRequestBody, generateGeminiRequestBody, prepareImageRequest } from '../utils/utils.js';
8
  import logger from '../utils/logger.js';
9
  import config from '../config/config.js';
10
  import tokenManager from '../auth/token_manager.js';
@@ -301,10 +301,10 @@ app.use((req, res, next) => {
301
  if (req.path.startsWith('/v1/')) {
302
  const apiKey = config.security?.apiKey;
303
  if (apiKey) {
304
- const authHeader = req.headers.authorization;
305
  const providedKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
306
  if (providedKey !== apiKey) {
307
- logger.warn(`API Key 验证失败: ${req.method} ${req.path}`);
308
  return res.status(401).json({ error: 'Invalid API Key' });
309
  }
310
  }
@@ -611,6 +611,303 @@ app.post('/v1beta/models/:model\\:generateContent', (req, res) => {
611
  handleGeminiRequest(req, res, modelName, isStream);
612
  });
613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  const server = app.listen(config.server.port, config.server.host, () => {
615
  logger.info(`服务器已启动: ${config.server.host}:${config.server.port}`);
616
  });
 
4
  import fs from 'fs';
5
  import { fileURLToPath } from 'url';
6
  import { generateAssistantResponse, generateAssistantResponseNoStream, getAvailableModels, generateImageForSD, closeRequester } from '../api/client.js';
7
+ import { generateRequestBody, generateGeminiRequestBody, generateClaudeRequestBody, prepareImageRequest } from '../utils/utils.js';
8
  import logger from '../utils/logger.js';
9
  import config from '../config/config.js';
10
  import tokenManager from '../auth/token_manager.js';
 
301
  if (req.path.startsWith('/v1/')) {
302
  const apiKey = config.security?.apiKey;
303
  if (apiKey) {
304
+ const authHeader = req.headers.authorization || req.headers['x-api-key'];
305
  const providedKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
306
  if (providedKey !== apiKey) {
307
+ logger.warn(`API Key 验证失败: ${req.method} ${req.path} (提供的Key: ${providedKey ? providedKey.substring(0, 10) + '...' : '无'})`);
308
  return res.status(401).json({ error: 'Invalid API Key' });
309
  }
310
  }
 
611
  handleGeminiRequest(req, res, modelName, isStream);
612
  });
613
 
614
+ // ==================== Claude API ====================
615
+
616
+ // Claude 错误响应构造
617
+ const buildClaudeErrorPayload = (error, statusCode) => {
618
+ let message = error.message || 'Internal server error';
619
+ if (error.isUpstreamApiError && error.rawBody) {
620
+ try {
621
+ const raw = typeof error.rawBody === 'string' ? JSON.parse(error.rawBody) : error.rawBody;
622
+ message = raw.error?.message || raw.message || message;
623
+ } catch {}
624
+ }
625
+
626
+ return {
627
+ type: "error",
628
+ error: {
629
+ type: statusCode === 401 ? "authentication_error" :
630
+ statusCode === 429 ? "rate_limit_error" :
631
+ statusCode === 400 ? "invalid_request_error" :
632
+ "api_error",
633
+ message: message
634
+ }
635
+ };
636
+ };
637
+
638
+ // Claude 流式响应工具
639
+ const createClaudeStreamEvent = (eventType, data) => {
640
+ return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
641
+ };
642
+
643
+ // Claude 非流式响应构建
644
+ const createClaudeResponse = (id, model, content, reasoning, toolCalls, stopReason, usage) => {
645
+ const contentBlocks = [];
646
+
647
+ // 思维链内容(如果有)- Claude 格式用 thinking 类型
648
+ if (reasoning) {
649
+ contentBlocks.push({
650
+ type: "thinking",
651
+ thinking: reasoning
652
+ });
653
+ }
654
+
655
+ // 文本内容
656
+ if (content) {
657
+ contentBlocks.push({
658
+ type: "text",
659
+ text: content
660
+ });
661
+ }
662
+
663
+ // 工具调用
664
+ if (toolCalls && toolCalls.length > 0) {
665
+ for (const tc of toolCalls) {
666
+ try {
667
+ contentBlocks.push({
668
+ type: "tool_use",
669
+ id: tc.id,
670
+ name: tc.function.name,
671
+ input: JSON.parse(tc.function.arguments)
672
+ });
673
+ } catch (e) {
674
+ // 解析失败时传入空对象
675
+ contentBlocks.push({
676
+ type: "tool_use",
677
+ id: tc.id,
678
+ name: tc.function.name,
679
+ input: {}
680
+ });
681
+ }
682
+ }
683
+ }
684
+
685
+ return {
686
+ id: id,
687
+ type: "message",
688
+ role: "assistant",
689
+ content: contentBlocks,
690
+ model: model,
691
+ stop_reason: stopReason,
692
+ stop_sequence: null,
693
+ usage: usage ? {
694
+ input_tokens: usage.prompt_tokens || 0,
695
+ output_tokens: usage.completion_tokens || 0
696
+ } : { input_tokens: 0, output_tokens: 0 }
697
+ };
698
+ };
699
+
700
+ // Claude API 处理函数
701
+ const handleClaudeRequest = async (req, res, isStream) => {
702
+ const { messages, model, system, tools, max_tokens, temperature, top_p, top_k, ...otherParams } = req.body;
703
+
704
+ try {
705
+ if (!messages) {
706
+ return res.status(400).json(buildClaudeErrorPayload({ message: 'messages is required' }, 400));
707
+ }
708
+
709
+ const token = await tokenManager.getToken();
710
+ if (!token) {
711
+ throw new Error('没有可用的token,请运行 npm run login 获取token');
712
+ }
713
+
714
+ // 构建参数
715
+ const parameters = {
716
+ max_tokens: max_tokens || config.defaults.max_tokens,
717
+ temperature: temperature ?? config.defaults.temperature,
718
+ top_p: top_p ?? config.defaults.top_p,
719
+ top_k: top_k ?? config.defaults.top_k,
720
+ ...otherParams
721
+ };
722
+
723
+ const requestBody = generateClaudeRequestBody(messages, model, parameters, tools, system, token);
724
+
725
+ const msgId = `msg_${Date.now()}`;
726
+ const maxRetries = Number(config.retryTimes || 0);
727
+ const safeRetries = maxRetries > 0 ? Math.floor(maxRetries) : 0;
728
+
729
+ if (isStream) {
730
+ setStreamHeaders(res);
731
+ const heartbeatTimer = createHeartbeat(res);
732
+
733
+ try {
734
+ let contentIndex = 0;
735
+ let usageData = null;
736
+ let hasToolCall = false;
737
+ let currentBlockType = null;
738
+ let reasoningSent = false;
739
+
740
+ // 发送 message_start
741
+ res.write(createClaudeStreamEvent('message_start', {
742
+ type: "message_start",
743
+ message: {
744
+ id: msgId,
745
+ type: "message",
746
+ role: "assistant",
747
+ content: [],
748
+ model: model,
749
+ stop_reason: null,
750
+ stop_sequence: null,
751
+ usage: { input_tokens: 0, output_tokens: 0 }
752
+ }
753
+ }));
754
+
755
+ await with429Retry(
756
+ () => generateAssistantResponse(requestBody, token, (data) => {
757
+ if (data.type === 'usage') {
758
+ usageData = data.usage;
759
+ } else if (data.type === 'reasoning') {
760
+ // 思维链内容 - 使用 thinking 类型
761
+ if (!reasoningSent) {
762
+ // 开始思维块
763
+ res.write(createClaudeStreamEvent('content_block_start', {
764
+ type: "content_block_start",
765
+ index: contentIndex,
766
+ content_block: { type: "thinking", thinking: "" }
767
+ }));
768
+ currentBlockType = 'thinking';
769
+ reasoningSent = true;
770
+ }
771
+ // 发送思维增量
772
+ res.write(createClaudeStreamEvent('content_block_delta', {
773
+ type: "content_block_delta",
774
+ index: contentIndex,
775
+ delta: { type: "thinking_delta", thinking: data.reasoning_content || '' }
776
+ }));
777
+ } else if (data.type === 'tool_calls') {
778
+ hasToolCall = true;
779
+ // 结束之前的块(如果有)
780
+ if (currentBlockType) {
781
+ res.write(createClaudeStreamEvent('content_block_stop', {
782
+ type: "content_block_stop",
783
+ index: contentIndex
784
+ }));
785
+ contentIndex++;
786
+ }
787
+ // 工具调用
788
+ for (const tc of data.tool_calls) {
789
+ try {
790
+ const inputObj = JSON.parse(tc.function.arguments);
791
+ res.write(createClaudeStreamEvent('content_block_start', {
792
+ type: "content_block_start",
793
+ index: contentIndex,
794
+ content_block: { type: "tool_use", id: tc.id, name: tc.function.name, input: {} }
795
+ }));
796
+ // 发送 input 增量
797
+ res.write(createClaudeStreamEvent('content_block_delta', {
798
+ type: "content_block_delta",
799
+ index: contentIndex,
800
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(inputObj) }
801
+ }));
802
+ res.write(createClaudeStreamEvent('content_block_stop', {
803
+ type: "content_block_stop",
804
+ index: contentIndex
805
+ }));
806
+ contentIndex++;
807
+ } catch (e) {
808
+ // 解析失败,跳过
809
+ }
810
+ }
811
+ currentBlockType = null;
812
+ } else {
813
+ // 普通文本内容
814
+ if (currentBlockType === 'thinking') {
815
+ // 结束思维块
816
+ res.write(createClaudeStreamEvent('content_block_stop', {
817
+ type: "content_block_stop",
818
+ index: contentIndex
819
+ }));
820
+ contentIndex++;
821
+ currentBlockType = null;
822
+ }
823
+ if (currentBlockType !== 'text') {
824
+ // 开始文本块
825
+ res.write(createClaudeStreamEvent('content_block_start', {
826
+ type: "content_block_start",
827
+ index: contentIndex,
828
+ content_block: { type: "text", text: "" }
829
+ }));
830
+ currentBlockType = 'text';
831
+ }
832
+ // 发送文本增量
833
+ res.write(createClaudeStreamEvent('content_block_delta', {
834
+ type: "content_block_delta",
835
+ index: contentIndex,
836
+ delta: { type: "text_delta", text: data.content || '' }
837
+ }));
838
+ }
839
+ }),
840
+ safeRetries,
841
+ 'claude.stream '
842
+ );
843
+
844
+ // 结束最后一个内容块
845
+ if (currentBlockType) {
846
+ res.write(createClaudeStreamEvent('content_block_stop', {
847
+ type: "content_block_stop",
848
+ index: contentIndex
849
+ }));
850
+ }
851
+
852
+ // 发送 message_delta
853
+ const stopReason = hasToolCall ? 'tool_use' : 'end_turn';
854
+ res.write(createClaudeStreamEvent('message_delta', {
855
+ type: "message_delta",
856
+ delta: { stop_reason: stopReason, stop_sequence: null },
857
+ usage: usageData ? { output_tokens: usageData.completion_tokens || 0 } : { output_tokens: 0 }
858
+ }));
859
+
860
+ // 发送 message_stop
861
+ res.write(createClaudeStreamEvent('message_stop', {
862
+ type: "message_stop"
863
+ }));
864
+
865
+ clearInterval(heartbeatTimer);
866
+ res.end();
867
+ } catch (error) {
868
+ clearInterval(heartbeatTimer);
869
+ throw error;
870
+ }
871
+ } else {
872
+ // 非流式请求
873
+ req.setTimeout(0);
874
+ res.setTimeout(0);
875
+
876
+ const { content, reasoningContent, toolCalls, usage } = await with429Retry(
877
+ () => generateAssistantResponseNoStream(requestBody, token),
878
+ safeRetries,
879
+ 'claude.no_stream '
880
+ );
881
+
882
+ const stopReason = toolCalls.length > 0 ? 'tool_use' : 'end_turn';
883
+ const response = createClaudeResponse(
884
+ msgId,
885
+ model,
886
+ content,
887
+ reasoningContent,
888
+ toolCalls,
889
+ stopReason,
890
+ usage
891
+ );
892
+
893
+ res.json(response);
894
+ }
895
+ } catch (error) {
896
+ logger.error('Claude 请求失败:', error.message);
897
+ if (res.headersSent) return;
898
+
899
+ const statusCode = Number(error.status) || 500;
900
+ const errorPayload = buildClaudeErrorPayload(error, statusCode);
901
+ res.status(statusCode).json(errorPayload);
902
+ }
903
+ };
904
+
905
+ // Claude Messages API 端点
906
+ app.post('/v1/messages', (req, res) => {
907
+ const isStream = req.body.stream === true;
908
+ handleClaudeRequest(req, res, isStream);
909
+ });
910
+
911
  const server = app.listen(config.server.port, config.server.host, () => {
912
  logger.info(`服务器已启动: ${config.server.host}:${config.server.port}`);
913
  });
src/utils/utils.js CHANGED
@@ -5,7 +5,7 @@ import os from 'os';
5
  import { getReasoningSignature, getToolSignature } from './thoughtSignatureCache.js';
6
  import { setToolNameMapping } from './toolNameCache.js';
7
 
8
- // 思维链签名常量
9
  // Claude 模型签名
10
  const CLAUDE_THOUGHT_SIGNATURE = 'RXFRRENrZ0lDaEFDR0FJcVFKV1Bvcy9GV20wSmtMV2FmWkFEbGF1ZTZzQTdRcFlTc1NvbklmemtSNFo4c1dqeitIRHBOYW9hS2NYTE1TeTF3bjh2T1RHdE1KVjVuYUNQclZ5cm9DMFNETHk4M0hOSWsrTG1aRUhNZ3hvTTl0ZEpXUDl6UUMzOExxc2ZJakI0UkkxWE1mdWJ1VDQrZnY0Znp0VEoyTlhtMjZKL2daYi9HL1gwcmR4b2x0VE54empLemtLcEp0ZXRia2plb3NBcWlRSWlXUHloMGhVVTk1dHNha1dyNDVWNUo3MTJjZDNxdHQ5Z0dkbjdFaFk4dUllUC9CcThVY2VZZC9YbFpYbDc2bHpEbmdzL2lDZXlNY3NuZXdQMjZBTDRaQzJReXdibVQzbXlSZmpld3ZSaUxxOWR1TVNidHIxYXRtYTJ0U1JIRjI0Z0JwUnpadE1RTmoyMjR4bTZVNUdRNXlOSWVzUXNFNmJzRGNSV0RTMGFVOEZERExybmhVQWZQT2JYMG5lTGR1QnU1VGZOWW9NZGlRbTgyUHVqVE1xaTlmN0t2QmJEUUdCeXdyVXR2eUNnTEFHNHNqeWluZDRCOEg3N2ZJamt5blI3Q3ZpQzlIOTVxSENVTCt3K3JzMmsvV0sxNlVsbGlTK0pET3UxWXpPMWRPOUp3V3hEMHd5ZVU0a0Y5MjIxaUE5Z2lUd2djZXhSU2c4TWJVMm1NSjJlaGdlY3g0YjJ3QloxR0FFPQ==';
11
  // Gemini 思维链签名
@@ -564,10 +564,305 @@ function generateGeminiRequestBody(geminiBody, modelName, token){
564
  return requestBody;
565
  }
566
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  export{
568
  generateRequestId,
569
  generateRequestBody,
570
  generateGeminiRequestBody,
 
571
  prepareImageRequest,
572
  getDefaultIp
573
  }
 
5
  import { getReasoningSignature, getToolSignature } from './thoughtSignatureCache.js';
6
  import { setToolNameMapping } from './toolNameCache.js';
7
 
8
+ // 思维链签名常量 - 暂时123占位
9
  // Claude 模型签名
10
  const CLAUDE_THOUGHT_SIGNATURE = 'RXFRRENrZ0lDaEFDR0FJcVFKV1Bvcy9GV20wSmtMV2FmWkFEbGF1ZTZzQTdRcFlTc1NvbklmemtSNFo4c1dqeitIRHBOYW9hS2NYTE1TeTF3bjh2T1RHdE1KVjVuYUNQclZ5cm9DMFNETHk4M0hOSWsrTG1aRUhNZ3hvTTl0ZEpXUDl6UUMzOExxc2ZJakI0UkkxWE1mdWJ1VDQrZnY0Znp0VEoyTlhtMjZKL2daYi9HL1gwcmR4b2x0VE54empLemtLcEp0ZXRia2plb3NBcWlRSWlXUHloMGhVVTk1dHNha1dyNDVWNUo3MTJjZDNxdHQ5Z0dkbjdFaFk4dUllUC9CcThVY2VZZC9YbFpYbDc2bHpEbmdzL2lDZXlNY3NuZXdQMjZBTDRaQzJReXdibVQzbXlSZmpld3ZSaUxxOWR1TVNidHIxYXRtYTJ0U1JIRjI0Z0JwUnpadE1RTmoyMjR4bTZVNUdRNXlOSWVzUXNFNmJzRGNSV0RTMGFVOEZERExybmhVQWZQT2JYMG5lTGR1QnU1VGZOWW9NZGlRbTgyUHVqVE1xaTlmN0t2QmJEUUdCeXdyVXR2eUNnTEFHNHNqeWluZDRCOEg3N2ZJamt5blI3Q3ZpQzlIOTVxSENVTCt3K3JzMmsvV0sxNlVsbGlTK0pET3UxWXpPMWRPOUp3V3hEMHd5ZVU0a0Y5MjIxaUE5Z2lUd2djZXhSU2c4TWJVMm1NSjJlaGdlY3g0YjJ3QloxR0FFPQ==';
11
  // Gemini 思维链签名
 
564
  return requestBody;
565
  }
566
 
567
+ // ==================== Claude API 转换函数 ====================
568
+
569
+ /**
570
+ * 从 Claude 消息内容中提取图片
571
+ * Claude 格式: { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } }
572
+ */
573
+ function extractImagesFromClaudeContent(content) {
574
+ const result = { text: '', images: [] };
575
+
576
+ if (typeof content === 'string') {
577
+ result.text = content;
578
+ return result;
579
+ }
580
+
581
+ if (Array.isArray(content)) {
582
+ for (const item of content) {
583
+ if (item.type === 'text') {
584
+ result.text += item.text || '';
585
+ } else if (item.type === 'image') {
586
+ // Claude 格式的图片
587
+ const source = item.source;
588
+ if (source && source.type === 'base64' && source.data) {
589
+ result.images.push({
590
+ inlineData: {
591
+ mimeType: source.media_type || 'image/png',
592
+ data: source.data
593
+ }
594
+ });
595
+ }
596
+ }
597
+ }
598
+ }
599
+
600
+ return result;
601
+ }
602
+
603
+ /**
604
+ * 处理 Claude 用户消息
605
+ */
606
+ function handleClaudeUserMessage(extracted, antigravityMessages) {
607
+ antigravityMessages.push({
608
+ role: "user",
609
+ parts: [
610
+ { text: extracted.text },
611
+ ...extracted.images
612
+ ]
613
+ });
614
+ }
615
+
616
+ /**
617
+ * 处理 Claude 助手消息(包含 tool_use)
618
+ */
619
+ function handleClaudeAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId) {
620
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
621
+ const content = message.content;
622
+
623
+ // 解析 content 数组
624
+ let textContent = '';
625
+ const toolCalls = [];
626
+
627
+ if (typeof content === 'string') {
628
+ textContent = content;
629
+ } else if (Array.isArray(content)) {
630
+ for (const item of content) {
631
+ if (item.type === 'text') {
632
+ textContent += item.text || '';
633
+ } else if (item.type === 'tool_use') {
634
+ // Claude 的 tool_use 格式
635
+ const originalName = item.name;
636
+ const safeName = sanitizeToolName(originalName);
637
+
638
+ const part = {
639
+ functionCall: {
640
+ id: item.id,
641
+ name: safeName,
642
+ args: {
643
+ query: JSON.stringify(item.input || {})
644
+ }
645
+ }
646
+ };
647
+
648
+ // 记录工具名映射
649
+ if (sessionId && actualModelName && safeName !== originalName) {
650
+ setToolNameMapping(sessionId, actualModelName, safeName, originalName);
651
+ }
652
+
653
+ toolCalls.push(part);
654
+ }
655
+ }
656
+ }
657
+
658
+ const hasToolCalls = toolCalls.length > 0;
659
+ const hasContent = textContent && textContent.trim() !== '';
660
+
661
+ if (lastMessage?.role === "model" && hasToolCalls && !hasContent) {
662
+ lastMessage.parts.push(...toolCalls);
663
+ } else {
664
+ const parts = [];
665
+
666
+ // 思维链处理(与 OpenAI 相同)
667
+ if (enableThinking) {
668
+ const cachedSig = getReasoningSignature(sessionId, actualModelName);
669
+ const thoughtSignature = cachedSig || getThoughtSignatureForModel(actualModelName);
670
+ parts.push({ text: ' ', thought: true });
671
+ parts.push({ text: ' ', thoughtSignature });
672
+ }
673
+
674
+ if (hasContent) parts.push({ text: textContent.trimEnd() });
675
+ parts.push(...toolCalls);
676
+
677
+ antigravityMessages.push({
678
+ role: "model",
679
+ parts
680
+ });
681
+ }
682
+ }
683
+
684
+ /**
685
+ * 处理 Claude tool_result 消息
686
+ */
687
+ function handleClaudeToolResult(message, antigravityMessages) {
688
+ const content = message.content;
689
+
690
+ if (!Array.isArray(content)) return;
691
+
692
+ for (const item of content) {
693
+ if (item.type !== 'tool_result') continue;
694
+
695
+ const toolUseId = item.tool_use_id;
696
+
697
+ // 从之前的 model 消息中找到对应的 functionCall name
698
+ let functionName = '';
699
+ for (let i = antigravityMessages.length - 1; i >= 0; i--) {
700
+ if (antigravityMessages[i].role === 'model') {
701
+ const parts = antigravityMessages[i].parts;
702
+ for (const part of parts) {
703
+ if (part.functionCall && part.functionCall.id === toolUseId) {
704
+ functionName = part.functionCall.name;
705
+ break;
706
+ }
707
+ }
708
+ if (functionName) break;
709
+ }
710
+ }
711
+
712
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
713
+
714
+ // 提取工具结果内容
715
+ let resultContent = '';
716
+ if (typeof item.content === 'string') {
717
+ resultContent = item.content;
718
+ } else if (Array.isArray(item.content)) {
719
+ resultContent = item.content
720
+ .filter(c => c.type === 'text')
721
+ .map(c => c.text)
722
+ .join('');
723
+ }
724
+
725
+ const functionResponse = {
726
+ functionResponse: {
727
+ id: toolUseId,
728
+ name: functionName,
729
+ response: {
730
+ output: resultContent
731
+ }
732
+ }
733
+ };
734
+
735
+ // 如果上一条消息是 user 且包含 functionResponse,则合并
736
+ if (lastMessage?.role === "user" && lastMessage.parts.some(p => p.functionResponse)) {
737
+ lastMessage.parts.push(functionResponse);
738
+ } else {
739
+ antigravityMessages.push({
740
+ role: "user",
741
+ parts: [functionResponse]
742
+ });
743
+ }
744
+ }
745
+ }
746
+
747
+ /**
748
+ * 将 Claude 消息转换为 Antigravity 格式
749
+ */
750
+ function claudeMessageToAntigravity(claudeMessages, enableThinking, actualModelName, sessionId) {
751
+ const antigravityMessages = [];
752
+
753
+ for (const message of claudeMessages) {
754
+ if (message.role === "user") {
755
+ // 检查是否包含 tool_result
756
+ const content = message.content;
757
+ if (Array.isArray(content) && content.some(item => item.type === 'tool_result')) {
758
+ handleClaudeToolResult(message, antigravityMessages);
759
+ } else {
760
+ const extracted = extractImagesFromClaudeContent(content);
761
+ handleClaudeUserMessage(extracted, antigravityMessages);
762
+ }
763
+ } else if (message.role === "assistant") {
764
+ handleClaudeAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId);
765
+ }
766
+ }
767
+
768
+ return antigravityMessages;
769
+ }
770
+
771
+ /**
772
+ * 将 Claude 工具格式转换为 Antigravity 格式
773
+ * Claude: { name, description, input_schema: {...} }
774
+ * Antigravity/Gemini: { functionDeclarations: [{ name, description, parameters: {...} }] }
775
+ */
776
+ function convertClaudeToolsToAntigravity(claudeTools, sessionId, actualModelName) {
777
+ if (!claudeTools || claudeTools.length === 0) return [];
778
+
779
+ return claudeTools.map((tool) => {
780
+ // 清洗参数
781
+ const rawParams = tool.input_schema || {};
782
+ const cleanedParams = cleanParameters(rawParams) || {};
783
+
784
+ // 确保顶层是合法的 JSON Schema 对象
785
+ if (cleanedParams.type === undefined) {
786
+ cleanedParams.type = 'object';
787
+ }
788
+ if (cleanedParams.type === 'object' && cleanedParams.properties === undefined) {
789
+ cleanedParams.properties = {};
790
+ }
791
+
792
+ const originalName = tool.name;
793
+ const safeName = sanitizeToolName(originalName);
794
+
795
+ // 缓存映射
796
+ if (sessionId && actualModelName && safeName !== originalName) {
797
+ setToolNameMapping(sessionId, actualModelName, safeName, originalName);
798
+ }
799
+
800
+ return {
801
+ functionDeclarations: [
802
+ {
803
+ name: safeName,
804
+ description: tool.description || '',
805
+ parameters: cleanedParams
806
+ }
807
+ ]
808
+ };
809
+ });
810
+ }
811
+
812
+ /**
813
+ * 生成 Claude 请求体并转换为 Antigravity 格式
814
+ */
815
+ function generateClaudeRequestBody(claudeMessages, modelName, parameters, claudeTools, systemPrompt, token) {
816
+ const enableThinking = isEnableThinking(modelName);
817
+ const actualModelName = modelMapping(modelName);
818
+
819
+ // 合并 system 指令
820
+ const baseSystem = config.systemInstruction || '';
821
+ let mergedSystem = '';
822
+
823
+ if (config.useContextSystemPrompt && systemPrompt) {
824
+ const parts = [];
825
+ if (baseSystem.trim()) parts.push(baseSystem.trim());
826
+ if (systemPrompt.trim()) parts.push(systemPrompt.trim());
827
+ mergedSystem = parts.join('\n\n');
828
+ } else {
829
+ mergedSystem = baseSystem;
830
+ }
831
+
832
+ const requestBody = {
833
+ project: token.projectId,
834
+ requestId: generateRequestId(),
835
+ request: {
836
+ contents: claudeMessageToAntigravity(claudeMessages, enableThinking, actualModelName, token.sessionId),
837
+ tools: convertClaudeToolsToAntigravity(claudeTools, token.sessionId, actualModelName),
838
+ toolConfig: {
839
+ functionCallingConfig: {
840
+ mode: "VALIDATED"
841
+ }
842
+ },
843
+ generationConfig: generateGenerationConfig(parameters, enableThinking, actualModelName),
844
+ sessionId: token.sessionId
845
+ },
846
+ model: actualModelName,
847
+ userAgent: "antigravity"
848
+ };
849
+
850
+ // 只有当有 system 指令时才添加
851
+ if (mergedSystem) {
852
+ requestBody.request.systemInstruction = {
853
+ role: "user",
854
+ parts: [{ text: mergedSystem }]
855
+ };
856
+ }
857
+
858
+ return requestBody;
859
+ }
860
+
861
  export{
862
  generateRequestId,
863
  generateRequestBody,
864
  generateGeminiRequestBody,
865
+ generateClaudeRequestBody,
866
  prepareImageRequest,
867
  getDefaultIp
868
  }