convert.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  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 = buildClaudeUsageFromOpenAIUsage(&openAIResponse.Usage)
  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. }