convert.go 31 KB

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