convert.go 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "github.com/QuantumNous/new-api/common"
  7. "github.com/QuantumNous/new-api/constant"
  8. "github.com/QuantumNous/new-api/dto"
  9. "github.com/QuantumNous/new-api/relay/channel/openrouter"
  10. relaycommon "github.com/QuantumNous/new-api/relay/common"
  11. "github.com/QuantumNous/new-api/relay/reasonmap"
  12. "github.com/samber/lo"
  13. )
  14. func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) {
  15. openAIRequest := dto.GeneralOpenAIRequest{
  16. Model: claudeRequest.Model,
  17. Temperature: claudeRequest.Temperature,
  18. }
  19. if claudeRequest.MaxTokens != nil {
  20. openAIRequest.MaxTokens = lo.ToPtr(lo.FromPtr(claudeRequest.MaxTokens))
  21. }
  22. if claudeRequest.TopP != nil {
  23. openAIRequest.TopP = lo.ToPtr(lo.FromPtr(claudeRequest.TopP))
  24. }
  25. if claudeRequest.TopK != nil {
  26. openAIRequest.TopK = lo.ToPtr(lo.FromPtr(claudeRequest.TopK))
  27. }
  28. if claudeRequest.Stream != nil {
  29. openAIRequest.Stream = lo.ToPtr(lo.FromPtr(claudeRequest.Stream))
  30. }
  31. isOpenRouter := info.ChannelType == constant.ChannelTypeOpenRouter
  32. if isOpenRouter {
  33. if effort := claudeRequest.GetEfforts(); effort != "" {
  34. effortBytes, _ := json.Marshal(effort)
  35. openAIRequest.Verbosity = effortBytes
  36. }
  37. if claudeRequest.Thinking != nil {
  38. var reasoning openrouter.RequestReasoning
  39. if claudeRequest.Thinking.Type == "enabled" {
  40. reasoning = openrouter.RequestReasoning{
  41. Enabled: true,
  42. MaxTokens: claudeRequest.Thinking.GetBudgetTokens(),
  43. }
  44. } else if claudeRequest.Thinking.Type == "adaptive" {
  45. reasoning = openrouter.RequestReasoning{
  46. Enabled: true,
  47. }
  48. }
  49. reasoningJSON, err := json.Marshal(reasoning)
  50. if err != nil {
  51. return nil, fmt.Errorf("failed to marshal reasoning: %w", err)
  52. }
  53. openAIRequest.Reasoning = reasoningJSON
  54. }
  55. } else {
  56. thinkingSuffix := "-thinking"
  57. if strings.HasSuffix(info.OriginModelName, thinkingSuffix) &&
  58. !strings.HasSuffix(openAIRequest.Model, thinkingSuffix) {
  59. openAIRequest.Model = openAIRequest.Model + thinkingSuffix
  60. }
  61. }
  62. // Convert stop sequences
  63. if len(claudeRequest.StopSequences) == 1 {
  64. openAIRequest.Stop = claudeRequest.StopSequences[0]
  65. } else if len(claudeRequest.StopSequences) > 1 {
  66. openAIRequest.Stop = claudeRequest.StopSequences
  67. }
  68. // Convert tools
  69. tools, _ := common.Any2Type[[]dto.Tool](claudeRequest.Tools)
  70. openAITools := make([]dto.ToolCallRequest, 0)
  71. for _, claudeTool := range tools {
  72. openAITool := dto.ToolCallRequest{
  73. Type: "function",
  74. Function: dto.FunctionRequest{
  75. Name: claudeTool.Name,
  76. Description: claudeTool.Description,
  77. Parameters: claudeTool.InputSchema,
  78. },
  79. }
  80. openAITools = append(openAITools, openAITool)
  81. }
  82. openAIRequest.Tools = openAITools
  83. // Convert messages
  84. openAIMessages := make([]dto.Message, 0)
  85. // Add system message if present
  86. if claudeRequest.System != nil {
  87. if claudeRequest.IsStringSystem() && claudeRequest.GetStringSystem() != "" {
  88. openAIMessage := dto.Message{
  89. Role: "system",
  90. }
  91. openAIMessage.SetStringContent(claudeRequest.GetStringSystem())
  92. openAIMessages = append(openAIMessages, openAIMessage)
  93. } else {
  94. systems := claudeRequest.ParseSystem()
  95. if len(systems) > 0 {
  96. openAIMessage := dto.Message{
  97. Role: "system",
  98. }
  99. isOpenRouterClaude := isOpenRouter && strings.HasPrefix(info.UpstreamModelName, "anthropic/claude")
  100. if isOpenRouterClaude {
  101. systemMediaMessages := make([]dto.MediaContent, 0, len(systems))
  102. for _, system := range systems {
  103. message := dto.MediaContent{
  104. Type: "text",
  105. Text: system.GetText(),
  106. CacheControl: system.CacheControl,
  107. }
  108. systemMediaMessages = append(systemMediaMessages, message)
  109. }
  110. openAIMessage.SetMediaContent(systemMediaMessages)
  111. } else {
  112. systemStr := ""
  113. for _, system := range systems {
  114. if system.Text != nil {
  115. systemStr += *system.Text
  116. }
  117. }
  118. openAIMessage.SetStringContent(systemStr)
  119. }
  120. openAIMessages = append(openAIMessages, openAIMessage)
  121. }
  122. }
  123. }
  124. for _, claudeMessage := range claudeRequest.Messages {
  125. openAIMessage := dto.Message{
  126. Role: claudeMessage.Role,
  127. }
  128. //log.Printf("claudeMessage.Content: %v", claudeMessage.Content)
  129. if claudeMessage.IsStringContent() {
  130. openAIMessage.SetStringContent(claudeMessage.GetStringContent())
  131. } else {
  132. content, err := claudeMessage.ParseContent()
  133. if err != nil {
  134. return nil, err
  135. }
  136. contents := content
  137. var toolCalls []dto.ToolCallRequest
  138. mediaMessages := make([]dto.MediaContent, 0, len(contents))
  139. for _, mediaMsg := range contents {
  140. switch mediaMsg.Type {
  141. case "text", "input_text":
  142. message := dto.MediaContent{
  143. Type: "text",
  144. Text: mediaMsg.GetText(),
  145. CacheControl: mediaMsg.CacheControl,
  146. }
  147. mediaMessages = append(mediaMessages, message)
  148. case "image":
  149. // Handle image conversion (base64 to URL or keep as is)
  150. imageData := fmt.Sprintf("data:%s;base64,%s", mediaMsg.Source.MediaType, mediaMsg.Source.Data)
  151. //textContent += fmt.Sprintf("[Image: %s]", imageData)
  152. mediaMessage := dto.MediaContent{
  153. Type: "image_url",
  154. ImageUrl: &dto.MessageImageUrl{Url: imageData},
  155. }
  156. mediaMessages = append(mediaMessages, mediaMessage)
  157. case "tool_use":
  158. toolCall := dto.ToolCallRequest{
  159. ID: mediaMsg.Id,
  160. Type: "function",
  161. Function: dto.FunctionRequest{
  162. Name: mediaMsg.Name,
  163. Arguments: toJSONString(mediaMsg.Input),
  164. },
  165. }
  166. toolCalls = append(toolCalls, toolCall)
  167. case "tool_result":
  168. // Add tool result as a separate message
  169. toolName := mediaMsg.Name
  170. if toolName == "" {
  171. toolName = claudeRequest.SearchToolNameByToolCallId(mediaMsg.ToolUseId)
  172. }
  173. oaiToolMessage := dto.Message{
  174. Role: "tool",
  175. Name: &toolName,
  176. ToolCallId: mediaMsg.ToolUseId,
  177. }
  178. //oaiToolMessage.SetStringContent(*mediaMsg.GetMediaContent().Text)
  179. if mediaMsg.IsStringContent() {
  180. oaiToolMessage.SetStringContent(mediaMsg.GetStringContent())
  181. } else {
  182. mediaContents := mediaMsg.ParseMediaContent()
  183. encodeJson, _ := common.Marshal(mediaContents)
  184. oaiToolMessage.SetStringContent(string(encodeJson))
  185. }
  186. openAIMessages = append(openAIMessages, oaiToolMessage)
  187. }
  188. }
  189. if len(toolCalls) > 0 {
  190. openAIMessage.SetToolCalls(toolCalls)
  191. }
  192. if len(mediaMessages) > 0 && len(toolCalls) == 0 {
  193. openAIMessage.SetMediaContent(mediaMessages)
  194. }
  195. }
  196. if len(openAIMessage.ParseContent()) > 0 || len(openAIMessage.ToolCalls) > 0 {
  197. openAIMessages = append(openAIMessages, openAIMessage)
  198. }
  199. }
  200. openAIRequest.Messages = openAIMessages
  201. return &openAIRequest, nil
  202. }
  203. func generateStopBlock(index int) *dto.ClaudeResponse {
  204. return &dto.ClaudeResponse{
  205. Type: "content_block_stop",
  206. Index: common.GetPointer[int](index),
  207. }
  208. }
  209. func buildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
  210. if oaiUsage == nil {
  211. return nil
  212. }
  213. cacheCreation5m, cacheCreation1h := NormalizeCacheCreationSplit(
  214. oaiUsage.PromptTokensDetails.CachedCreationTokens,
  215. oaiUsage.ClaudeCacheCreation5mTokens,
  216. oaiUsage.ClaudeCacheCreation1hTokens,
  217. )
  218. usage := &dto.ClaudeUsage{
  219. InputTokens: oaiUsage.PromptTokens,
  220. OutputTokens: oaiUsage.CompletionTokens,
  221. CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
  222. CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
  223. }
  224. if cacheCreation5m > 0 || cacheCreation1h > 0 {
  225. usage.CacheCreation = &dto.ClaudeCacheCreationUsage{
  226. Ephemeral5mInputTokens: cacheCreation5m,
  227. Ephemeral1hInputTokens: cacheCreation1h,
  228. }
  229. }
  230. return usage
  231. }
  232. func NormalizeCacheCreationSplit(totalTokens int, tokens5m int, tokens1h int) (int, int) {
  233. remainder := lo.Max([]int{totalTokens - tokens5m - tokens1h, 0})
  234. return tokens5m + remainder, tokens1h
  235. }
  236. func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) []*dto.ClaudeResponse {
  237. if info.ClaudeConvertInfo.Done {
  238. return nil
  239. }
  240. var claudeResponses []*dto.ClaudeResponse
  241. // stopOpenBlocks emits the required content_block_stop event(s) for the currently open block(s)
  242. // according to Anthropic's SSE streaming state machine:
  243. // content_block_start -> content_block_delta* -> content_block_stop (per index).
  244. //
  245. // For text/thinking, there is at most one open block at info.ClaudeConvertInfo.Index.
  246. // For tools, OpenAI tool_calls can stream multiple parallel tool_use blocks (indexed from 0),
  247. // so we may have multiple open blocks and must stop each one explicitly.
  248. stopOpenBlocks := func() {
  249. switch info.ClaudeConvertInfo.LastMessagesType {
  250. case relaycommon.LastMessageTypeText, relaycommon.LastMessageTypeThinking:
  251. claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
  252. case relaycommon.LastMessageTypeTools:
  253. base := info.ClaudeConvertInfo.ToolCallBaseIndex
  254. for offset := 0; offset <= info.ClaudeConvertInfo.ToolCallMaxIndexOffset; offset++ {
  255. claudeResponses = append(claudeResponses, generateStopBlock(base+offset))
  256. }
  257. }
  258. }
  259. // stopOpenBlocksAndAdvance closes the currently open block(s) and advances the content block index
  260. // to the next available slot for subsequent content_block_start events.
  261. //
  262. // This prevents invalid streams where a content_block_delta (e.g. thinking_delta) is emitted for an
  263. // index whose active content_block type is different (the typical cause of "Mismatched content block type").
  264. stopOpenBlocksAndAdvance := func() {
  265. if info.ClaudeConvertInfo.LastMessagesType == relaycommon.LastMessageTypeNone {
  266. return
  267. }
  268. stopOpenBlocks()
  269. switch info.ClaudeConvertInfo.LastMessagesType {
  270. case relaycommon.LastMessageTypeTools:
  271. info.ClaudeConvertInfo.Index = info.ClaudeConvertInfo.ToolCallBaseIndex + info.ClaudeConvertInfo.ToolCallMaxIndexOffset + 1
  272. info.ClaudeConvertInfo.ToolCallBaseIndex = 0
  273. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
  274. default:
  275. info.ClaudeConvertInfo.Index++
  276. }
  277. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeNone
  278. }
  279. if info.SendResponseCount == 1 {
  280. msg := &dto.ClaudeMediaMessage{
  281. Id: openAIResponse.Id,
  282. Model: openAIResponse.Model,
  283. Type: "message",
  284. Role: "assistant",
  285. Usage: &dto.ClaudeUsage{
  286. InputTokens: info.GetEstimatePromptTokens(),
  287. OutputTokens: 0,
  288. },
  289. }
  290. msg.SetContent(make([]any, 0))
  291. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  292. Type: "message_start",
  293. Message: msg,
  294. })
  295. //claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  296. // Type: "ping",
  297. //})
  298. if openAIResponse.IsToolCall() {
  299. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
  300. info.ClaudeConvertInfo.ToolCallBaseIndex = 0
  301. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
  302. var toolCall dto.ToolCallResponse
  303. if len(openAIResponse.Choices) > 0 && len(openAIResponse.Choices[0].Delta.ToolCalls) > 0 {
  304. toolCall = openAIResponse.Choices[0].Delta.ToolCalls[0]
  305. } else {
  306. first := openAIResponse.GetFirstToolCall()
  307. if first != nil {
  308. toolCall = *first
  309. } else {
  310. toolCall = dto.ToolCallResponse{}
  311. }
  312. }
  313. resp := &dto.ClaudeResponse{
  314. Type: "content_block_start",
  315. ContentBlock: &dto.ClaudeMediaMessage{
  316. Id: toolCall.ID,
  317. Type: "tool_use",
  318. Name: toolCall.Function.Name,
  319. Input: map[string]interface{}{},
  320. },
  321. }
  322. resp.SetIndex(0)
  323. claudeResponses = append(claudeResponses, resp)
  324. // 首块包含工具 delta,则追加 input_json_delta
  325. if toolCall.Function.Arguments != "" {
  326. idx := 0
  327. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  328. Index: &idx,
  329. Type: "content_block_delta",
  330. Delta: &dto.ClaudeMediaMessage{
  331. Type: "input_json_delta",
  332. PartialJson: &toolCall.Function.Arguments,
  333. },
  334. })
  335. }
  336. } else {
  337. }
  338. // 判断首个响应是否存在内容(非标准的 OpenAI 响应)
  339. if len(openAIResponse.Choices) > 0 {
  340. reasoning := openAIResponse.Choices[0].Delta.GetReasoningContent()
  341. content := openAIResponse.Choices[0].Delta.GetContentString()
  342. if reasoning != "" {
  343. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
  344. stopOpenBlocksAndAdvance()
  345. }
  346. idx := info.ClaudeConvertInfo.Index
  347. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  348. Index: &idx,
  349. Type: "content_block_start",
  350. ContentBlock: &dto.ClaudeMediaMessage{
  351. Type: "thinking",
  352. Thinking: common.GetPointer[string](""),
  353. },
  354. })
  355. idx2 := idx
  356. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  357. Index: &idx2,
  358. Type: "content_block_delta",
  359. Delta: &dto.ClaudeMediaMessage{
  360. Type: "thinking_delta",
  361. Thinking: &reasoning,
  362. },
  363. })
  364. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
  365. } else if content != "" {
  366. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
  367. stopOpenBlocksAndAdvance()
  368. }
  369. idx := info.ClaudeConvertInfo.Index
  370. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  371. Index: &idx,
  372. Type: "content_block_start",
  373. ContentBlock: &dto.ClaudeMediaMessage{
  374. Type: "text",
  375. Text: common.GetPointer[string](""),
  376. },
  377. })
  378. idx2 := idx
  379. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  380. Index: &idx2,
  381. Type: "content_block_delta",
  382. Delta: &dto.ClaudeMediaMessage{
  383. Type: "text_delta",
  384. Text: common.GetPointer[string](content),
  385. },
  386. })
  387. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText
  388. }
  389. }
  390. // 如果首块就带 finish_reason,需要立即发送停止块
  391. if len(openAIResponse.Choices) > 0 && openAIResponse.Choices[0].FinishReason != nil && *openAIResponse.Choices[0].FinishReason != "" {
  392. info.FinishReason = *openAIResponse.Choices[0].FinishReason
  393. stopOpenBlocks()
  394. oaiUsage := openAIResponse.Usage
  395. if oaiUsage == nil {
  396. oaiUsage = info.ClaudeConvertInfo.Usage
  397. }
  398. if oaiUsage != nil {
  399. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  400. Type: "message_delta",
  401. Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
  402. Delta: &dto.ClaudeMediaMessage{
  403. StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
  404. },
  405. })
  406. }
  407. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  408. Type: "message_stop",
  409. })
  410. info.ClaudeConvertInfo.Done = true
  411. }
  412. return claudeResponses
  413. }
  414. if len(openAIResponse.Choices) == 0 {
  415. // Some OpenAI-compatible upstreams end with a usage-only SSE chunk.
  416. oaiUsage := openAIResponse.Usage
  417. if oaiUsage == nil {
  418. oaiUsage = info.ClaudeConvertInfo.Usage
  419. }
  420. if oaiUsage != nil {
  421. stopOpenBlocks()
  422. stopReason := stopReasonOpenAI2Claude(info.FinishReason)
  423. if stopReason == "" {
  424. stopReason = "end_turn"
  425. }
  426. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  427. Type: "message_delta",
  428. Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
  429. Delta: &dto.ClaudeMediaMessage{
  430. StopReason: common.GetPointer[string](stopReason),
  431. },
  432. })
  433. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  434. Type: "message_stop",
  435. })
  436. info.ClaudeConvertInfo.Done = true
  437. }
  438. return claudeResponses
  439. } else {
  440. chosenChoice := openAIResponse.Choices[0]
  441. doneChunk := chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != ""
  442. if doneChunk {
  443. info.FinishReason = *chosenChoice.FinishReason
  444. oaiUsage := openAIResponse.Usage
  445. if oaiUsage == nil {
  446. oaiUsage = info.ClaudeConvertInfo.Usage
  447. // Some upstreams emit finish_reason first, then send a final usage-only chunk.
  448. // Defer closing until usage is available so the final message_delta carries it.
  449. return claudeResponses
  450. }
  451. }
  452. var claudeResponse dto.ClaudeResponse
  453. var isEmpty bool
  454. claudeResponse.Type = "content_block_delta"
  455. if len(chosenChoice.Delta.ToolCalls) > 0 {
  456. toolCalls := chosenChoice.Delta.ToolCalls
  457. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeTools {
  458. stopOpenBlocksAndAdvance()
  459. info.ClaudeConvertInfo.ToolCallBaseIndex = info.ClaudeConvertInfo.Index
  460. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
  461. }
  462. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
  463. base := info.ClaudeConvertInfo.ToolCallBaseIndex
  464. maxOffset := info.ClaudeConvertInfo.ToolCallMaxIndexOffset
  465. for i, toolCall := range toolCalls {
  466. offset := 0
  467. if toolCall.Index != nil {
  468. offset = *toolCall.Index
  469. } else {
  470. offset = i
  471. }
  472. if offset > maxOffset {
  473. maxOffset = offset
  474. }
  475. blockIndex := base + offset
  476. idx := blockIndex
  477. if toolCall.Function.Name != "" {
  478. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  479. Index: &idx,
  480. Type: "content_block_start",
  481. ContentBlock: &dto.ClaudeMediaMessage{
  482. Id: toolCall.ID,
  483. Type: "tool_use",
  484. Name: toolCall.Function.Name,
  485. Input: map[string]interface{}{},
  486. },
  487. })
  488. }
  489. if len(toolCall.Function.Arguments) > 0 {
  490. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  491. Index: &idx,
  492. Type: "content_block_delta",
  493. Delta: &dto.ClaudeMediaMessage{
  494. Type: "input_json_delta",
  495. PartialJson: &toolCall.Function.Arguments,
  496. },
  497. })
  498. }
  499. }
  500. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = maxOffset
  501. info.ClaudeConvertInfo.Index = base + maxOffset
  502. } else {
  503. reasoning := chosenChoice.Delta.GetReasoningContent()
  504. textContent := chosenChoice.Delta.GetContentString()
  505. if reasoning != "" || textContent != "" {
  506. if reasoning != "" {
  507. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
  508. stopOpenBlocksAndAdvance()
  509. idx := info.ClaudeConvertInfo.Index
  510. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  511. Index: &idx,
  512. Type: "content_block_start",
  513. ContentBlock: &dto.ClaudeMediaMessage{
  514. Type: "thinking",
  515. Thinking: common.GetPointer[string](""),
  516. },
  517. })
  518. }
  519. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
  520. claudeResponse.Delta = &dto.ClaudeMediaMessage{
  521. Type: "thinking_delta",
  522. Thinking: &reasoning,
  523. }
  524. } else {
  525. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
  526. stopOpenBlocksAndAdvance()
  527. idx := info.ClaudeConvertInfo.Index
  528. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  529. Index: &idx,
  530. Type: "content_block_start",
  531. ContentBlock: &dto.ClaudeMediaMessage{
  532. Type: "text",
  533. Text: common.GetPointer[string](""),
  534. },
  535. })
  536. }
  537. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText
  538. claudeResponse.Delta = &dto.ClaudeMediaMessage{
  539. Type: "text_delta",
  540. Text: common.GetPointer[string](textContent),
  541. }
  542. }
  543. } else {
  544. isEmpty = true
  545. }
  546. }
  547. claudeResponse.Index = common.GetPointer[int](info.ClaudeConvertInfo.Index)
  548. if !isEmpty && claudeResponse.Delta != nil {
  549. claudeResponses = append(claudeResponses, &claudeResponse)
  550. }
  551. if doneChunk || info.ClaudeConvertInfo.Done {
  552. stopOpenBlocks()
  553. oaiUsage := openAIResponse.Usage
  554. if oaiUsage == nil {
  555. oaiUsage = info.ClaudeConvertInfo.Usage
  556. }
  557. if oaiUsage != nil {
  558. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  559. Type: "message_delta",
  560. Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
  561. Delta: &dto.ClaudeMediaMessage{
  562. StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
  563. },
  564. })
  565. }
  566. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  567. Type: "message_stop",
  568. })
  569. info.ClaudeConvertInfo.Done = true
  570. return claudeResponses
  571. }
  572. }
  573. return claudeResponses
  574. }
  575. func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.ClaudeResponse {
  576. var stopReason string
  577. contents := make([]dto.ClaudeMediaMessage, 0)
  578. claudeResponse := &dto.ClaudeResponse{
  579. Id: openAIResponse.Id,
  580. Type: "message",
  581. Role: "assistant",
  582. Model: openAIResponse.Model,
  583. }
  584. for _, choice := range openAIResponse.Choices {
  585. stopReason = stopReasonOpenAI2Claude(choice.FinishReason)
  586. if choice.FinishReason == "tool_calls" {
  587. for _, toolUse := range choice.Message.ParseToolCalls() {
  588. claudeContent := dto.ClaudeMediaMessage{}
  589. claudeContent.Type = "tool_use"
  590. claudeContent.Id = toolUse.ID
  591. claudeContent.Name = toolUse.Function.Name
  592. var mapParams map[string]interface{}
  593. if err := common.Unmarshal([]byte(toolUse.Function.Arguments), &mapParams); err == nil {
  594. claudeContent.Input = mapParams
  595. } else {
  596. claudeContent.Input = toolUse.Function.Arguments
  597. }
  598. contents = append(contents, claudeContent)
  599. }
  600. } else {
  601. claudeContent := dto.ClaudeMediaMessage{}
  602. claudeContent.Type = "text"
  603. claudeContent.SetText(choice.Message.StringContent())
  604. contents = append(contents, claudeContent)
  605. }
  606. }
  607. claudeResponse.Content = contents
  608. claudeResponse.StopReason = stopReason
  609. claudeResponse.Usage = buildClaudeUsageFromOpenAIUsage(&openAIResponse.Usage)
  610. return claudeResponse
  611. }
  612. func stopReasonOpenAI2Claude(reason string) string {
  613. return reasonmap.OpenAIFinishReasonToClaudeStopReason(reason)
  614. }
  615. func toJSONString(v interface{}) string {
  616. b, err := json.Marshal(v)
  617. if err != nil {
  618. return "{}"
  619. }
  620. return string(b)
  621. }
  622. func GeminiToOpenAIRequest(geminiRequest *dto.GeminiChatRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) {
  623. openaiRequest := &dto.GeneralOpenAIRequest{
  624. Model: info.UpstreamModelName,
  625. Stream: lo.ToPtr(info.IsStream),
  626. }
  627. // 转换 messages
  628. var messages []dto.Message
  629. for _, content := range geminiRequest.Contents {
  630. message := dto.Message{
  631. Role: convertGeminiRoleToOpenAI(content.Role),
  632. }
  633. // 处理 parts
  634. var mediaContents []dto.MediaContent
  635. var toolCalls []dto.ToolCallRequest
  636. for _, part := range content.Parts {
  637. if part.Text != "" {
  638. mediaContent := dto.MediaContent{
  639. Type: "text",
  640. Text: part.Text,
  641. }
  642. mediaContents = append(mediaContents, mediaContent)
  643. } else if part.InlineData != nil {
  644. mediaContent := dto.MediaContent{
  645. Type: "image_url",
  646. ImageUrl: &dto.MessageImageUrl{
  647. Url: fmt.Sprintf("data:%s;base64,%s", part.InlineData.MimeType, part.InlineData.Data),
  648. Detail: "auto",
  649. MimeType: part.InlineData.MimeType,
  650. },
  651. }
  652. mediaContents = append(mediaContents, mediaContent)
  653. } else if part.FileData != nil {
  654. mediaContent := dto.MediaContent{
  655. Type: "image_url",
  656. ImageUrl: &dto.MessageImageUrl{
  657. Url: part.FileData.FileUri,
  658. Detail: "auto",
  659. MimeType: part.FileData.MimeType,
  660. },
  661. }
  662. mediaContents = append(mediaContents, mediaContent)
  663. } else if part.FunctionCall != nil {
  664. // 处理 Gemini 的工具调用
  665. toolCall := dto.ToolCallRequest{
  666. ID: fmt.Sprintf("call_%d", len(toolCalls)+1), // 生成唯一ID
  667. Type: "function",
  668. Function: dto.FunctionRequest{
  669. Name: part.FunctionCall.FunctionName,
  670. Arguments: toJSONString(part.FunctionCall.Arguments),
  671. },
  672. }
  673. toolCalls = append(toolCalls, toolCall)
  674. } else if part.FunctionResponse != nil {
  675. // 处理 Gemini 的工具响应,创建单独的 tool 消息
  676. toolMessage := dto.Message{
  677. Role: "tool",
  678. ToolCallId: fmt.Sprintf("call_%d", len(toolCalls)), // 使用对应的调用ID
  679. }
  680. toolMessage.SetStringContent(toJSONString(part.FunctionResponse.Response))
  681. messages = append(messages, toolMessage)
  682. }
  683. }
  684. // 设置消息内容
  685. if len(toolCalls) > 0 {
  686. // 如果有工具调用,设置工具调用
  687. message.SetToolCalls(toolCalls)
  688. } else if len(mediaContents) == 1 && mediaContents[0].Type == "text" {
  689. // 如果只有一个文本内容,直接设置字符串
  690. message.Content = mediaContents[0].Text
  691. } else if len(mediaContents) > 0 {
  692. // 如果有多个内容或包含媒体,设置为数组
  693. message.SetMediaContent(mediaContents)
  694. }
  695. // 只有当消息有内容或工具调用时才添加
  696. if len(message.ParseContent()) > 0 || len(message.ToolCalls) > 0 {
  697. messages = append(messages, message)
  698. }
  699. }
  700. openaiRequest.Messages = messages
  701. if geminiRequest.GenerationConfig.Temperature != nil {
  702. openaiRequest.Temperature = geminiRequest.GenerationConfig.Temperature
  703. }
  704. if geminiRequest.GenerationConfig.TopP != nil && *geminiRequest.GenerationConfig.TopP > 0 {
  705. openaiRequest.TopP = lo.ToPtr(*geminiRequest.GenerationConfig.TopP)
  706. }
  707. if geminiRequest.GenerationConfig.TopK != nil && *geminiRequest.GenerationConfig.TopK > 0 {
  708. openaiRequest.TopK = lo.ToPtr(int(*geminiRequest.GenerationConfig.TopK))
  709. }
  710. if geminiRequest.GenerationConfig.MaxOutputTokens != nil && *geminiRequest.GenerationConfig.MaxOutputTokens > 0 {
  711. openaiRequest.MaxTokens = lo.ToPtr(*geminiRequest.GenerationConfig.MaxOutputTokens)
  712. }
  713. // gemini stop sequences 最多 5 个,openai stop 最多 4 个
  714. if len(geminiRequest.GenerationConfig.StopSequences) > 0 {
  715. openaiRequest.Stop = geminiRequest.GenerationConfig.StopSequences[:4]
  716. }
  717. if geminiRequest.GenerationConfig.CandidateCount != nil && *geminiRequest.GenerationConfig.CandidateCount > 0 {
  718. openaiRequest.N = lo.ToPtr(*geminiRequest.GenerationConfig.CandidateCount)
  719. }
  720. // 转换工具调用
  721. if len(geminiRequest.GetTools()) > 0 {
  722. var tools []dto.ToolCallRequest
  723. for _, tool := range geminiRequest.GetTools() {
  724. if tool.FunctionDeclarations != nil {
  725. functionDeclarations, err := common.Any2Type[[]dto.FunctionRequest](tool.FunctionDeclarations)
  726. if err != nil {
  727. common.SysError(fmt.Sprintf("failed to parse gemini function declarations: %v (type=%T)", err, tool.FunctionDeclarations))
  728. continue
  729. }
  730. for _, function := range functionDeclarations {
  731. openAITool := dto.ToolCallRequest{
  732. Type: "function",
  733. Function: dto.FunctionRequest{
  734. Name: function.Name,
  735. Description: function.Description,
  736. Parameters: function.Parameters,
  737. },
  738. }
  739. tools = append(tools, openAITool)
  740. }
  741. }
  742. }
  743. if len(tools) > 0 {
  744. openaiRequest.Tools = tools
  745. }
  746. }
  747. // gemini system instructions
  748. if geminiRequest.SystemInstructions != nil {
  749. // 将系统指令作为第一条消息插入
  750. systemMessage := dto.Message{
  751. Role: "system",
  752. Content: extractTextFromGeminiParts(geminiRequest.SystemInstructions.Parts),
  753. }
  754. openaiRequest.Messages = append([]dto.Message{systemMessage}, openaiRequest.Messages...)
  755. }
  756. return openaiRequest, nil
  757. }
  758. func convertGeminiRoleToOpenAI(geminiRole string) string {
  759. switch geminiRole {
  760. case "user":
  761. return "user"
  762. case "model":
  763. return "assistant"
  764. case "function":
  765. return "function"
  766. default:
  767. return "user"
  768. }
  769. }
  770. func extractTextFromGeminiParts(parts []dto.GeminiPart) string {
  771. var texts []string
  772. for _, part := range parts {
  773. if part.Text != "" {
  774. texts = append(texts, part.Text)
  775. }
  776. }
  777. return strings.Join(texts, "\n")
  778. }
  779. // ResponseOpenAI2Gemini 将 OpenAI 响应转换为 Gemini 格式
  780. func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse {
  781. geminiResponse := &dto.GeminiChatResponse{
  782. Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)),
  783. UsageMetadata: dto.GeminiUsageMetadata{
  784. PromptTokenCount: openAIResponse.PromptTokens,
  785. CandidatesTokenCount: openAIResponse.CompletionTokens,
  786. TotalTokenCount: openAIResponse.PromptTokens + openAIResponse.CompletionTokens,
  787. },
  788. }
  789. for _, choice := range openAIResponse.Choices {
  790. candidate := dto.GeminiChatCandidate{
  791. Index: int64(choice.Index),
  792. SafetyRatings: []dto.GeminiChatSafetyRating{},
  793. }
  794. // 设置结束原因
  795. var finishReason string
  796. switch choice.FinishReason {
  797. case "stop":
  798. finishReason = "STOP"
  799. case "length":
  800. finishReason = "MAX_TOKENS"
  801. case "content_filter":
  802. finishReason = "SAFETY"
  803. case "tool_calls":
  804. finishReason = "STOP"
  805. default:
  806. finishReason = "STOP"
  807. }
  808. candidate.FinishReason = &finishReason
  809. // 转换消息内容
  810. content := dto.GeminiChatContent{
  811. Role: "model",
  812. Parts: make([]dto.GeminiPart, 0),
  813. }
  814. // 处理工具调用
  815. toolCalls := choice.Message.ParseToolCalls()
  816. if len(toolCalls) > 0 {
  817. for _, toolCall := range toolCalls {
  818. // 解析参数
  819. var args map[string]interface{}
  820. if toolCall.Function.Arguments != "" {
  821. if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
  822. args = map[string]interface{}{"arguments": toolCall.Function.Arguments}
  823. }
  824. } else {
  825. args = make(map[string]interface{})
  826. }
  827. part := dto.GeminiPart{
  828. FunctionCall: &dto.FunctionCall{
  829. FunctionName: toolCall.Function.Name,
  830. Arguments: args,
  831. },
  832. }
  833. content.Parts = append(content.Parts, part)
  834. }
  835. } else {
  836. // 处理文本内容
  837. textContent := choice.Message.StringContent()
  838. if textContent != "" {
  839. part := dto.GeminiPart{
  840. Text: textContent,
  841. }
  842. content.Parts = append(content.Parts, part)
  843. }
  844. }
  845. candidate.Content = content
  846. geminiResponse.Candidates = append(geminiResponse.Candidates, candidate)
  847. }
  848. return geminiResponse
  849. }
  850. // StreamResponseOpenAI2Gemini 将 OpenAI 流式响应转换为 Gemini 格式
  851. func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse {
  852. // 检查是否有实际内容或结束标志
  853. hasContent := false
  854. hasFinishReason := false
  855. for _, choice := range openAIResponse.Choices {
  856. if len(choice.Delta.GetContentString()) > 0 || (choice.Delta.ToolCalls != nil && len(choice.Delta.ToolCalls) > 0) {
  857. hasContent = true
  858. }
  859. if choice.FinishReason != nil {
  860. hasFinishReason = true
  861. }
  862. }
  863. // 如果没有实际内容且没有结束标志,跳过。主要针对 openai 流响应开头的空数据
  864. if !hasContent && !hasFinishReason {
  865. return nil
  866. }
  867. geminiResponse := &dto.GeminiChatResponse{
  868. Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)),
  869. UsageMetadata: dto.GeminiUsageMetadata{
  870. PromptTokenCount: info.GetEstimatePromptTokens(),
  871. CandidatesTokenCount: 0, // 流式响应中可能没有完整的 usage 信息
  872. TotalTokenCount: info.GetEstimatePromptTokens(),
  873. },
  874. }
  875. if openAIResponse.Usage != nil {
  876. geminiResponse.UsageMetadata.PromptTokenCount = openAIResponse.Usage.PromptTokens
  877. geminiResponse.UsageMetadata.CandidatesTokenCount = openAIResponse.Usage.CompletionTokens
  878. geminiResponse.UsageMetadata.TotalTokenCount = openAIResponse.Usage.TotalTokens
  879. }
  880. for _, choice := range openAIResponse.Choices {
  881. candidate := dto.GeminiChatCandidate{
  882. Index: int64(choice.Index),
  883. SafetyRatings: []dto.GeminiChatSafetyRating{},
  884. }
  885. // 设置结束原因
  886. if choice.FinishReason != nil {
  887. var finishReason string
  888. switch *choice.FinishReason {
  889. case "stop":
  890. finishReason = "STOP"
  891. case "length":
  892. finishReason = "MAX_TOKENS"
  893. case "content_filter":
  894. finishReason = "SAFETY"
  895. case "tool_calls":
  896. finishReason = "STOP"
  897. default:
  898. finishReason = "STOP"
  899. }
  900. candidate.FinishReason = &finishReason
  901. }
  902. // 转换消息内容
  903. content := dto.GeminiChatContent{
  904. Role: "model",
  905. Parts: make([]dto.GeminiPart, 0),
  906. }
  907. // 处理工具调用
  908. if choice.Delta.ToolCalls != nil {
  909. for _, toolCall := range choice.Delta.ToolCalls {
  910. // 解析参数
  911. var args map[string]interface{}
  912. if toolCall.Function.Arguments != "" {
  913. if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
  914. args = map[string]interface{}{"arguments": toolCall.Function.Arguments}
  915. }
  916. } else {
  917. args = make(map[string]interface{})
  918. }
  919. part := dto.GeminiPart{
  920. FunctionCall: &dto.FunctionCall{
  921. FunctionName: toolCall.Function.Name,
  922. Arguments: args,
  923. },
  924. }
  925. content.Parts = append(content.Parts, part)
  926. }
  927. } else {
  928. // 处理文本内容
  929. textContent := choice.Delta.GetContentString()
  930. if textContent != "" {
  931. part := dto.GeminiPart{
  932. Text: textContent,
  933. }
  934. content.Parts = append(content.Parts, part)
  935. }
  936. }
  937. candidate.Content = content
  938. geminiResponse.Candidates = append(geminiResponse.Candidates, candidate)
  939. }
  940. return geminiResponse
  941. }