convert.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  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 StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) []*dto.ClaudeResponse {
  210. if info.ClaudeConvertInfo.Done {
  211. return nil
  212. }
  213. var claudeResponses []*dto.ClaudeResponse
  214. // stopOpenBlocks emits the required content_block_stop event(s) for the currently open block(s)
  215. // according to Anthropic's SSE streaming state machine:
  216. // content_block_start -> content_block_delta* -> content_block_stop (per index).
  217. //
  218. // For text/thinking, there is at most one open block at info.ClaudeConvertInfo.Index.
  219. // For tools, OpenAI tool_calls can stream multiple parallel tool_use blocks (indexed from 0),
  220. // so we may have multiple open blocks and must stop each one explicitly.
  221. stopOpenBlocks := func() {
  222. switch info.ClaudeConvertInfo.LastMessagesType {
  223. case relaycommon.LastMessageTypeText, relaycommon.LastMessageTypeThinking:
  224. claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
  225. case relaycommon.LastMessageTypeTools:
  226. base := info.ClaudeConvertInfo.ToolCallBaseIndex
  227. for offset := 0; offset <= info.ClaudeConvertInfo.ToolCallMaxIndexOffset; offset++ {
  228. claudeResponses = append(claudeResponses, generateStopBlock(base+offset))
  229. }
  230. }
  231. }
  232. // stopOpenBlocksAndAdvance closes the currently open block(s) and advances the content block index
  233. // to the next available slot for subsequent content_block_start events.
  234. //
  235. // This prevents invalid streams where a content_block_delta (e.g. thinking_delta) is emitted for an
  236. // index whose active content_block type is different (the typical cause of "Mismatched content block type").
  237. stopOpenBlocksAndAdvance := func() {
  238. if info.ClaudeConvertInfo.LastMessagesType == relaycommon.LastMessageTypeNone {
  239. return
  240. }
  241. stopOpenBlocks()
  242. switch info.ClaudeConvertInfo.LastMessagesType {
  243. case relaycommon.LastMessageTypeTools:
  244. info.ClaudeConvertInfo.Index = info.ClaudeConvertInfo.ToolCallBaseIndex + info.ClaudeConvertInfo.ToolCallMaxIndexOffset + 1
  245. info.ClaudeConvertInfo.ToolCallBaseIndex = 0
  246. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
  247. default:
  248. info.ClaudeConvertInfo.Index++
  249. }
  250. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeNone
  251. }
  252. if info.SendResponseCount == 1 {
  253. msg := &dto.ClaudeMediaMessage{
  254. Id: openAIResponse.Id,
  255. Model: openAIResponse.Model,
  256. Type: "message",
  257. Role: "assistant",
  258. Usage: &dto.ClaudeUsage{
  259. InputTokens: info.GetEstimatePromptTokens(),
  260. OutputTokens: 0,
  261. },
  262. }
  263. msg.SetContent(make([]any, 0))
  264. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  265. Type: "message_start",
  266. Message: msg,
  267. })
  268. //claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  269. // Type: "ping",
  270. //})
  271. if openAIResponse.IsToolCall() {
  272. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
  273. info.ClaudeConvertInfo.ToolCallBaseIndex = 0
  274. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
  275. var toolCall dto.ToolCallResponse
  276. if len(openAIResponse.Choices) > 0 && len(openAIResponse.Choices[0].Delta.ToolCalls) > 0 {
  277. toolCall = openAIResponse.Choices[0].Delta.ToolCalls[0]
  278. } else {
  279. first := openAIResponse.GetFirstToolCall()
  280. if first != nil {
  281. toolCall = *first
  282. } else {
  283. toolCall = dto.ToolCallResponse{}
  284. }
  285. }
  286. resp := &dto.ClaudeResponse{
  287. Type: "content_block_start",
  288. ContentBlock: &dto.ClaudeMediaMessage{
  289. Id: toolCall.ID,
  290. Type: "tool_use",
  291. Name: toolCall.Function.Name,
  292. Input: map[string]interface{}{},
  293. },
  294. }
  295. resp.SetIndex(0)
  296. claudeResponses = append(claudeResponses, resp)
  297. // 首块包含工具 delta,则追加 input_json_delta
  298. if toolCall.Function.Arguments != "" {
  299. idx := 0
  300. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  301. Index: &idx,
  302. Type: "content_block_delta",
  303. Delta: &dto.ClaudeMediaMessage{
  304. Type: "input_json_delta",
  305. PartialJson: &toolCall.Function.Arguments,
  306. },
  307. })
  308. }
  309. } else {
  310. }
  311. // 判断首个响应是否存在内容(非标准的 OpenAI 响应)
  312. if len(openAIResponse.Choices) > 0 {
  313. reasoning := openAIResponse.Choices[0].Delta.GetReasoningContent()
  314. content := openAIResponse.Choices[0].Delta.GetContentString()
  315. if reasoning != "" {
  316. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
  317. stopOpenBlocksAndAdvance()
  318. }
  319. idx := info.ClaudeConvertInfo.Index
  320. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  321. Index: &idx,
  322. Type: "content_block_start",
  323. ContentBlock: &dto.ClaudeMediaMessage{
  324. Type: "thinking",
  325. Thinking: common.GetPointer[string](""),
  326. },
  327. })
  328. idx2 := idx
  329. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  330. Index: &idx2,
  331. Type: "content_block_delta",
  332. Delta: &dto.ClaudeMediaMessage{
  333. Type: "thinking_delta",
  334. Thinking: &reasoning,
  335. },
  336. })
  337. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
  338. } else if content != "" {
  339. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
  340. stopOpenBlocksAndAdvance()
  341. }
  342. idx := info.ClaudeConvertInfo.Index
  343. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  344. Index: &idx,
  345. Type: "content_block_start",
  346. ContentBlock: &dto.ClaudeMediaMessage{
  347. Type: "text",
  348. Text: common.GetPointer[string](""),
  349. },
  350. })
  351. idx2 := idx
  352. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  353. Index: &idx2,
  354. Type: "content_block_delta",
  355. Delta: &dto.ClaudeMediaMessage{
  356. Type: "text_delta",
  357. Text: common.GetPointer[string](content),
  358. },
  359. })
  360. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText
  361. }
  362. }
  363. // 如果首块就带 finish_reason,需要立即发送停止块
  364. if len(openAIResponse.Choices) > 0 && openAIResponse.Choices[0].FinishReason != nil && *openAIResponse.Choices[0].FinishReason != "" {
  365. info.FinishReason = *openAIResponse.Choices[0].FinishReason
  366. stopOpenBlocks()
  367. oaiUsage := openAIResponse.Usage
  368. if oaiUsage == nil {
  369. oaiUsage = info.ClaudeConvertInfo.Usage
  370. }
  371. if oaiUsage != nil {
  372. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  373. Type: "message_delta",
  374. Usage: &dto.ClaudeUsage{
  375. InputTokens: oaiUsage.PromptTokens,
  376. OutputTokens: oaiUsage.CompletionTokens,
  377. CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
  378. CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
  379. },
  380. Delta: &dto.ClaudeMediaMessage{
  381. StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
  382. },
  383. })
  384. }
  385. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  386. Type: "message_stop",
  387. })
  388. info.ClaudeConvertInfo.Done = true
  389. }
  390. return claudeResponses
  391. }
  392. if len(openAIResponse.Choices) == 0 {
  393. // no choices
  394. // 可能为非标准的 OpenAI 响应,判断是否已经完成
  395. if info.ClaudeConvertInfo.Done {
  396. stopOpenBlocks()
  397. oaiUsage := info.ClaudeConvertInfo.Usage
  398. if oaiUsage != nil {
  399. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  400. Type: "message_delta",
  401. Usage: &dto.ClaudeUsage{
  402. InputTokens: oaiUsage.PromptTokens,
  403. OutputTokens: oaiUsage.CompletionTokens,
  404. CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
  405. CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
  406. },
  407. Delta: &dto.ClaudeMediaMessage{
  408. StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
  409. },
  410. })
  411. }
  412. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  413. Type: "message_stop",
  414. })
  415. }
  416. return claudeResponses
  417. } else {
  418. chosenChoice := openAIResponse.Choices[0]
  419. doneChunk := chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != ""
  420. if doneChunk {
  421. info.FinishReason = *chosenChoice.FinishReason
  422. }
  423. var claudeResponse dto.ClaudeResponse
  424. var isEmpty bool
  425. claudeResponse.Type = "content_block_delta"
  426. if len(chosenChoice.Delta.ToolCalls) > 0 {
  427. toolCalls := chosenChoice.Delta.ToolCalls
  428. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeTools {
  429. stopOpenBlocksAndAdvance()
  430. info.ClaudeConvertInfo.ToolCallBaseIndex = info.ClaudeConvertInfo.Index
  431. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
  432. }
  433. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
  434. base := info.ClaudeConvertInfo.ToolCallBaseIndex
  435. maxOffset := info.ClaudeConvertInfo.ToolCallMaxIndexOffset
  436. for i, toolCall := range toolCalls {
  437. offset := 0
  438. if toolCall.Index != nil {
  439. offset = *toolCall.Index
  440. } else {
  441. offset = i
  442. }
  443. if offset > maxOffset {
  444. maxOffset = offset
  445. }
  446. blockIndex := base + offset
  447. idx := blockIndex
  448. if toolCall.Function.Name != "" {
  449. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  450. Index: &idx,
  451. Type: "content_block_start",
  452. ContentBlock: &dto.ClaudeMediaMessage{
  453. Id: toolCall.ID,
  454. Type: "tool_use",
  455. Name: toolCall.Function.Name,
  456. Input: map[string]interface{}{},
  457. },
  458. })
  459. }
  460. if len(toolCall.Function.Arguments) > 0 {
  461. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  462. Index: &idx,
  463. Type: "content_block_delta",
  464. Delta: &dto.ClaudeMediaMessage{
  465. Type: "input_json_delta",
  466. PartialJson: &toolCall.Function.Arguments,
  467. },
  468. })
  469. }
  470. }
  471. info.ClaudeConvertInfo.ToolCallMaxIndexOffset = maxOffset
  472. info.ClaudeConvertInfo.Index = base + maxOffset
  473. } else {
  474. reasoning := chosenChoice.Delta.GetReasoningContent()
  475. textContent := chosenChoice.Delta.GetContentString()
  476. if reasoning != "" || textContent != "" {
  477. if reasoning != "" {
  478. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
  479. stopOpenBlocksAndAdvance()
  480. idx := info.ClaudeConvertInfo.Index
  481. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  482. Index: &idx,
  483. Type: "content_block_start",
  484. ContentBlock: &dto.ClaudeMediaMessage{
  485. Type: "thinking",
  486. Thinking: common.GetPointer[string](""),
  487. },
  488. })
  489. }
  490. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
  491. claudeResponse.Delta = &dto.ClaudeMediaMessage{
  492. Type: "thinking_delta",
  493. Thinking: &reasoning,
  494. }
  495. } else {
  496. if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
  497. stopOpenBlocksAndAdvance()
  498. idx := info.ClaudeConvertInfo.Index
  499. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  500. Index: &idx,
  501. Type: "content_block_start",
  502. ContentBlock: &dto.ClaudeMediaMessage{
  503. Type: "text",
  504. Text: common.GetPointer[string](""),
  505. },
  506. })
  507. }
  508. info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText
  509. claudeResponse.Delta = &dto.ClaudeMediaMessage{
  510. Type: "text_delta",
  511. Text: common.GetPointer[string](textContent),
  512. }
  513. }
  514. } else {
  515. isEmpty = true
  516. }
  517. }
  518. claudeResponse.Index = common.GetPointer[int](info.ClaudeConvertInfo.Index)
  519. if !isEmpty && claudeResponse.Delta != nil {
  520. claudeResponses = append(claudeResponses, &claudeResponse)
  521. }
  522. if doneChunk || info.ClaudeConvertInfo.Done {
  523. stopOpenBlocks()
  524. oaiUsage := openAIResponse.Usage
  525. if oaiUsage == nil {
  526. oaiUsage = info.ClaudeConvertInfo.Usage
  527. }
  528. if oaiUsage != nil {
  529. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  530. Type: "message_delta",
  531. Usage: &dto.ClaudeUsage{
  532. InputTokens: oaiUsage.PromptTokens,
  533. OutputTokens: oaiUsage.CompletionTokens,
  534. CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
  535. CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
  536. },
  537. Delta: &dto.ClaudeMediaMessage{
  538. StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
  539. },
  540. })
  541. }
  542. claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
  543. Type: "message_stop",
  544. })
  545. info.ClaudeConvertInfo.Done = true
  546. return claudeResponses
  547. }
  548. }
  549. return claudeResponses
  550. }
  551. func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.ClaudeResponse {
  552. var stopReason string
  553. contents := make([]dto.ClaudeMediaMessage, 0)
  554. claudeResponse := &dto.ClaudeResponse{
  555. Id: openAIResponse.Id,
  556. Type: "message",
  557. Role: "assistant",
  558. Model: openAIResponse.Model,
  559. }
  560. for _, choice := range openAIResponse.Choices {
  561. stopReason = stopReasonOpenAI2Claude(choice.FinishReason)
  562. if choice.FinishReason == "tool_calls" {
  563. for _, toolUse := range choice.Message.ParseToolCalls() {
  564. claudeContent := dto.ClaudeMediaMessage{}
  565. claudeContent.Type = "tool_use"
  566. claudeContent.Id = toolUse.ID
  567. claudeContent.Name = toolUse.Function.Name
  568. var mapParams map[string]interface{}
  569. if err := common.Unmarshal([]byte(toolUse.Function.Arguments), &mapParams); err == nil {
  570. claudeContent.Input = mapParams
  571. } else {
  572. claudeContent.Input = toolUse.Function.Arguments
  573. }
  574. contents = append(contents, claudeContent)
  575. }
  576. } else {
  577. claudeContent := dto.ClaudeMediaMessage{}
  578. claudeContent.Type = "text"
  579. claudeContent.SetText(choice.Message.StringContent())
  580. contents = append(contents, claudeContent)
  581. }
  582. }
  583. claudeResponse.Content = contents
  584. claudeResponse.StopReason = stopReason
  585. claudeResponse.Usage = &dto.ClaudeUsage{
  586. InputTokens: openAIResponse.PromptTokens,
  587. OutputTokens: openAIResponse.CompletionTokens,
  588. }
  589. return claudeResponse
  590. }
  591. func stopReasonOpenAI2Claude(reason string) string {
  592. return reasonmap.OpenAIFinishReasonToClaudeStopReason(reason)
  593. }
  594. func toJSONString(v interface{}) string {
  595. b, err := json.Marshal(v)
  596. if err != nil {
  597. return "{}"
  598. }
  599. return string(b)
  600. }
  601. func GeminiToOpenAIRequest(geminiRequest *dto.GeminiChatRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) {
  602. openaiRequest := &dto.GeneralOpenAIRequest{
  603. Model: info.UpstreamModelName,
  604. Stream: lo.ToPtr(info.IsStream),
  605. }
  606. // 转换 messages
  607. var messages []dto.Message
  608. for _, content := range geminiRequest.Contents {
  609. message := dto.Message{
  610. Role: convertGeminiRoleToOpenAI(content.Role),
  611. }
  612. // 处理 parts
  613. var mediaContents []dto.MediaContent
  614. var toolCalls []dto.ToolCallRequest
  615. for _, part := range content.Parts {
  616. if part.Text != "" {
  617. mediaContent := dto.MediaContent{
  618. Type: "text",
  619. Text: part.Text,
  620. }
  621. mediaContents = append(mediaContents, mediaContent)
  622. } else if part.InlineData != nil {
  623. mediaContent := dto.MediaContent{
  624. Type: "image_url",
  625. ImageUrl: &dto.MessageImageUrl{
  626. Url: fmt.Sprintf("data:%s;base64,%s", part.InlineData.MimeType, part.InlineData.Data),
  627. Detail: "auto",
  628. MimeType: part.InlineData.MimeType,
  629. },
  630. }
  631. mediaContents = append(mediaContents, mediaContent)
  632. } else if part.FileData != nil {
  633. mediaContent := dto.MediaContent{
  634. Type: "image_url",
  635. ImageUrl: &dto.MessageImageUrl{
  636. Url: part.FileData.FileUri,
  637. Detail: "auto",
  638. MimeType: part.FileData.MimeType,
  639. },
  640. }
  641. mediaContents = append(mediaContents, mediaContent)
  642. } else if part.FunctionCall != nil {
  643. // 处理 Gemini 的工具调用
  644. toolCall := dto.ToolCallRequest{
  645. ID: fmt.Sprintf("call_%d", len(toolCalls)+1), // 生成唯一ID
  646. Type: "function",
  647. Function: dto.FunctionRequest{
  648. Name: part.FunctionCall.FunctionName,
  649. Arguments: toJSONString(part.FunctionCall.Arguments),
  650. },
  651. }
  652. toolCalls = append(toolCalls, toolCall)
  653. } else if part.FunctionResponse != nil {
  654. // 处理 Gemini 的工具响应,创建单独的 tool 消息
  655. toolMessage := dto.Message{
  656. Role: "tool",
  657. ToolCallId: fmt.Sprintf("call_%d", len(toolCalls)), // 使用对应的调用ID
  658. }
  659. toolMessage.SetStringContent(toJSONString(part.FunctionResponse.Response))
  660. messages = append(messages, toolMessage)
  661. }
  662. }
  663. // 设置消息内容
  664. if len(toolCalls) > 0 {
  665. // 如果有工具调用,设置工具调用
  666. message.SetToolCalls(toolCalls)
  667. } else if len(mediaContents) == 1 && mediaContents[0].Type == "text" {
  668. // 如果只有一个文本内容,直接设置字符串
  669. message.Content = mediaContents[0].Text
  670. } else if len(mediaContents) > 0 {
  671. // 如果有多个内容或包含媒体,设置为数组
  672. message.SetMediaContent(mediaContents)
  673. }
  674. // 只有当消息有内容或工具调用时才添加
  675. if len(message.ParseContent()) > 0 || len(message.ToolCalls) > 0 {
  676. messages = append(messages, message)
  677. }
  678. }
  679. openaiRequest.Messages = messages
  680. if geminiRequest.GenerationConfig.Temperature != nil {
  681. openaiRequest.Temperature = geminiRequest.GenerationConfig.Temperature
  682. }
  683. if geminiRequest.GenerationConfig.TopP != nil && *geminiRequest.GenerationConfig.TopP > 0 {
  684. openaiRequest.TopP = lo.ToPtr(*geminiRequest.GenerationConfig.TopP)
  685. }
  686. if geminiRequest.GenerationConfig.TopK != nil && *geminiRequest.GenerationConfig.TopK > 0 {
  687. openaiRequest.TopK = lo.ToPtr(int(*geminiRequest.GenerationConfig.TopK))
  688. }
  689. if geminiRequest.GenerationConfig.MaxOutputTokens != nil && *geminiRequest.GenerationConfig.MaxOutputTokens > 0 {
  690. openaiRequest.MaxTokens = lo.ToPtr(*geminiRequest.GenerationConfig.MaxOutputTokens)
  691. }
  692. // gemini stop sequences 最多 5 个,openai stop 最多 4 个
  693. if len(geminiRequest.GenerationConfig.StopSequences) > 0 {
  694. openaiRequest.Stop = geminiRequest.GenerationConfig.StopSequences[:4]
  695. }
  696. if geminiRequest.GenerationConfig.CandidateCount != nil && *geminiRequest.GenerationConfig.CandidateCount > 0 {
  697. openaiRequest.N = lo.ToPtr(*geminiRequest.GenerationConfig.CandidateCount)
  698. }
  699. // 转换工具调用
  700. if len(geminiRequest.GetTools()) > 0 {
  701. var tools []dto.ToolCallRequest
  702. for _, tool := range geminiRequest.GetTools() {
  703. if tool.FunctionDeclarations != nil {
  704. functionDeclarations, err := common.Any2Type[[]dto.FunctionRequest](tool.FunctionDeclarations)
  705. if err != nil {
  706. common.SysError(fmt.Sprintf("failed to parse gemini function declarations: %v (type=%T)", err, tool.FunctionDeclarations))
  707. continue
  708. }
  709. for _, function := range functionDeclarations {
  710. openAITool := dto.ToolCallRequest{
  711. Type: "function",
  712. Function: dto.FunctionRequest{
  713. Name: function.Name,
  714. Description: function.Description,
  715. Parameters: function.Parameters,
  716. },
  717. }
  718. tools = append(tools, openAITool)
  719. }
  720. }
  721. }
  722. if len(tools) > 0 {
  723. openaiRequest.Tools = tools
  724. }
  725. }
  726. // gemini system instructions
  727. if geminiRequest.SystemInstructions != nil {
  728. // 将系统指令作为第一条消息插入
  729. systemMessage := dto.Message{
  730. Role: "system",
  731. Content: extractTextFromGeminiParts(geminiRequest.SystemInstructions.Parts),
  732. }
  733. openaiRequest.Messages = append([]dto.Message{systemMessage}, openaiRequest.Messages...)
  734. }
  735. return openaiRequest, nil
  736. }
  737. func convertGeminiRoleToOpenAI(geminiRole string) string {
  738. switch geminiRole {
  739. case "user":
  740. return "user"
  741. case "model":
  742. return "assistant"
  743. case "function":
  744. return "function"
  745. default:
  746. return "user"
  747. }
  748. }
  749. func extractTextFromGeminiParts(parts []dto.GeminiPart) string {
  750. var texts []string
  751. for _, part := range parts {
  752. if part.Text != "" {
  753. texts = append(texts, part.Text)
  754. }
  755. }
  756. return strings.Join(texts, "\n")
  757. }
  758. // ResponseOpenAI2Gemini 将 OpenAI 响应转换为 Gemini 格式
  759. func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse {
  760. geminiResponse := &dto.GeminiChatResponse{
  761. Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)),
  762. UsageMetadata: dto.GeminiUsageMetadata{
  763. PromptTokenCount: openAIResponse.PromptTokens,
  764. CandidatesTokenCount: openAIResponse.CompletionTokens,
  765. TotalTokenCount: openAIResponse.PromptTokens + openAIResponse.CompletionTokens,
  766. },
  767. }
  768. for _, choice := range openAIResponse.Choices {
  769. candidate := dto.GeminiChatCandidate{
  770. Index: int64(choice.Index),
  771. SafetyRatings: []dto.GeminiChatSafetyRating{},
  772. }
  773. // 设置结束原因
  774. var finishReason string
  775. switch choice.FinishReason {
  776. case "stop":
  777. finishReason = "STOP"
  778. case "length":
  779. finishReason = "MAX_TOKENS"
  780. case "content_filter":
  781. finishReason = "SAFETY"
  782. case "tool_calls":
  783. finishReason = "STOP"
  784. default:
  785. finishReason = "STOP"
  786. }
  787. candidate.FinishReason = &finishReason
  788. // 转换消息内容
  789. content := dto.GeminiChatContent{
  790. Role: "model",
  791. Parts: make([]dto.GeminiPart, 0),
  792. }
  793. // 处理工具调用
  794. toolCalls := choice.Message.ParseToolCalls()
  795. if len(toolCalls) > 0 {
  796. for _, toolCall := range toolCalls {
  797. // 解析参数
  798. var args map[string]interface{}
  799. if toolCall.Function.Arguments != "" {
  800. if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
  801. args = map[string]interface{}{"arguments": toolCall.Function.Arguments}
  802. }
  803. } else {
  804. args = make(map[string]interface{})
  805. }
  806. part := dto.GeminiPart{
  807. FunctionCall: &dto.FunctionCall{
  808. FunctionName: toolCall.Function.Name,
  809. Arguments: args,
  810. },
  811. }
  812. content.Parts = append(content.Parts, part)
  813. }
  814. } else {
  815. // 处理文本内容
  816. textContent := choice.Message.StringContent()
  817. if textContent != "" {
  818. part := dto.GeminiPart{
  819. Text: textContent,
  820. }
  821. content.Parts = append(content.Parts, part)
  822. }
  823. }
  824. candidate.Content = content
  825. geminiResponse.Candidates = append(geminiResponse.Candidates, candidate)
  826. }
  827. return geminiResponse
  828. }
  829. // StreamResponseOpenAI2Gemini 将 OpenAI 流式响应转换为 Gemini 格式
  830. func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse {
  831. // 检查是否有实际内容或结束标志
  832. hasContent := false
  833. hasFinishReason := false
  834. for _, choice := range openAIResponse.Choices {
  835. if len(choice.Delta.GetContentString()) > 0 || (choice.Delta.ToolCalls != nil && len(choice.Delta.ToolCalls) > 0) {
  836. hasContent = true
  837. }
  838. if choice.FinishReason != nil {
  839. hasFinishReason = true
  840. }
  841. }
  842. // 如果没有实际内容且没有结束标志,跳过。主要针对 openai 流响应开头的空数据
  843. if !hasContent && !hasFinishReason {
  844. return nil
  845. }
  846. geminiResponse := &dto.GeminiChatResponse{
  847. Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)),
  848. UsageMetadata: dto.GeminiUsageMetadata{
  849. PromptTokenCount: info.GetEstimatePromptTokens(),
  850. CandidatesTokenCount: 0, // 流式响应中可能没有完整的 usage 信息
  851. TotalTokenCount: info.GetEstimatePromptTokens(),
  852. },
  853. }
  854. if openAIResponse.Usage != nil {
  855. geminiResponse.UsageMetadata.PromptTokenCount = openAIResponse.Usage.PromptTokens
  856. geminiResponse.UsageMetadata.CandidatesTokenCount = openAIResponse.Usage.CompletionTokens
  857. geminiResponse.UsageMetadata.TotalTokenCount = openAIResponse.Usage.TotalTokens
  858. }
  859. for _, choice := range openAIResponse.Choices {
  860. candidate := dto.GeminiChatCandidate{
  861. Index: int64(choice.Index),
  862. SafetyRatings: []dto.GeminiChatSafetyRating{},
  863. }
  864. // 设置结束原因
  865. if choice.FinishReason != nil {
  866. var finishReason string
  867. switch *choice.FinishReason {
  868. case "stop":
  869. finishReason = "STOP"
  870. case "length":
  871. finishReason = "MAX_TOKENS"
  872. case "content_filter":
  873. finishReason = "SAFETY"
  874. case "tool_calls":
  875. finishReason = "STOP"
  876. default:
  877. finishReason = "STOP"
  878. }
  879. candidate.FinishReason = &finishReason
  880. }
  881. // 转换消息内容
  882. content := dto.GeminiChatContent{
  883. Role: "model",
  884. Parts: make([]dto.GeminiPart, 0),
  885. }
  886. // 处理工具调用
  887. if choice.Delta.ToolCalls != nil {
  888. for _, toolCall := range choice.Delta.ToolCalls {
  889. // 解析参数
  890. var args map[string]interface{}
  891. if toolCall.Function.Arguments != "" {
  892. if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
  893. args = map[string]interface{}{"arguments": toolCall.Function.Arguments}
  894. }
  895. } else {
  896. args = make(map[string]interface{})
  897. }
  898. part := dto.GeminiPart{
  899. FunctionCall: &dto.FunctionCall{
  900. FunctionName: toolCall.Function.Name,
  901. Arguments: args,
  902. },
  903. }
  904. content.Parts = append(content.Parts, part)
  905. }
  906. } else {
  907. // 处理文本内容
  908. textContent := choice.Delta.GetContentString()
  909. if textContent != "" {
  910. part := dto.GeminiPart{
  911. Text: textContent,
  912. }
  913. content.Parts = append(content.Parts, part)
  914. }
  915. }
  916. candidate.Content = content
  917. geminiResponse.Candidates = append(geminiResponse.Candidates, candidate)
  918. }
  919. return geminiResponse
  920. }