stream.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package ollama
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "one-api/common"
  9. "one-api/dto"
  10. "one-api/logger"
  11. relaycommon "one-api/relay/common"
  12. "one-api/relay/helper"
  13. "one-api/service"
  14. "one-api/types"
  15. "strings"
  16. "time"
  17. "github.com/gin-gonic/gin"
  18. )
  19. type ollamaChatStreamChunk struct {
  20. Model string `json:"model"`
  21. CreatedAt string `json:"created_at"`
  22. // chat
  23. Message *struct {
  24. Role string `json:"role"`
  25. Content string `json:"content"`
  26. Thinking json.RawMessage `json:"thinking"`
  27. ToolCalls []struct {
  28. Function struct {
  29. Name string `json:"name"`
  30. Arguments interface{} `json:"arguments"`
  31. } `json:"function"`
  32. } `json:"tool_calls"`
  33. } `json:"message"`
  34. // generate
  35. Response string `json:"response"`
  36. Done bool `json:"done"`
  37. DoneReason string `json:"done_reason"`
  38. TotalDuration int64 `json:"total_duration"`
  39. LoadDuration int64 `json:"load_duration"`
  40. PromptEvalCount int `json:"prompt_eval_count"`
  41. EvalCount int `json:"eval_count"`
  42. PromptEvalDuration int64 `json:"prompt_eval_duration"`
  43. EvalDuration int64 `json:"eval_duration"`
  44. }
  45. func toUnix(ts string) int64 {
  46. if ts == "" {
  47. return time.Now().Unix()
  48. }
  49. // try time.RFC3339 or with nanoseconds
  50. t, err := time.Parse(time.RFC3339Nano, ts)
  51. if err != nil {
  52. t2, err2 := time.Parse(time.RFC3339, ts)
  53. if err2 == nil {
  54. return t2.Unix()
  55. }
  56. return time.Now().Unix()
  57. }
  58. return t.Unix()
  59. }
  60. func ollamaStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
  61. if resp == nil || resp.Body == nil {
  62. return nil, types.NewOpenAIError(fmt.Errorf("empty response"), types.ErrorCodeBadResponse, http.StatusBadRequest)
  63. }
  64. defer service.CloseResponseBodyGracefully(resp)
  65. helper.SetEventStreamHeaders(c)
  66. scanner := bufio.NewScanner(resp.Body)
  67. usage := &dto.Usage{}
  68. var model = info.UpstreamModelName
  69. var responseId = common.GetUUID()
  70. var created = time.Now().Unix()
  71. var toolCallIndex int
  72. start := helper.GenerateStartEmptyResponse(responseId, created, model, nil)
  73. if data, err := common.Marshal(start); err == nil {
  74. _ = helper.StringData(c, string(data))
  75. }
  76. for scanner.Scan() {
  77. line := scanner.Text()
  78. line = strings.TrimSpace(line)
  79. if line == "" {
  80. continue
  81. }
  82. var chunk ollamaChatStreamChunk
  83. if err := json.Unmarshal([]byte(line), &chunk); err != nil {
  84. logger.LogError(c, "ollama stream json decode error: "+err.Error()+" line="+line)
  85. return usage, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
  86. }
  87. if chunk.Model != "" {
  88. model = chunk.Model
  89. }
  90. created = toUnix(chunk.CreatedAt)
  91. if !chunk.Done {
  92. // delta content
  93. var content string
  94. if chunk.Message != nil {
  95. content = chunk.Message.Content
  96. } else {
  97. content = chunk.Response
  98. }
  99. delta := dto.ChatCompletionsStreamResponse{
  100. Id: responseId,
  101. Object: "chat.completion.chunk",
  102. Created: created,
  103. Model: model,
  104. Choices: []dto.ChatCompletionsStreamResponseChoice{{
  105. Index: 0,
  106. Delta: dto.ChatCompletionsStreamResponseChoiceDelta{Role: "assistant"},
  107. }},
  108. }
  109. if content != "" {
  110. delta.Choices[0].Delta.SetContentString(content)
  111. }
  112. if chunk.Message != nil && len(chunk.Message.Thinking) > 0 {
  113. raw := strings.TrimSpace(string(chunk.Message.Thinking))
  114. if raw != "" && raw != "null" {
  115. delta.Choices[0].Delta.SetReasoningContent(raw)
  116. }
  117. }
  118. // tool calls
  119. if chunk.Message != nil && len(chunk.Message.ToolCalls) > 0 {
  120. delta.Choices[0].Delta.ToolCalls = make([]dto.ToolCallResponse, 0, len(chunk.Message.ToolCalls))
  121. for _, tc := range chunk.Message.ToolCalls {
  122. // arguments -> string
  123. argBytes, _ := json.Marshal(tc.Function.Arguments)
  124. toolId := fmt.Sprintf("call_%d", toolCallIndex)
  125. tr := dto.ToolCallResponse{ID: toolId, Type: "function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: string(argBytes)}}
  126. tr.SetIndex(toolCallIndex)
  127. toolCallIndex++
  128. delta.Choices[0].Delta.ToolCalls = append(delta.Choices[0].Delta.ToolCalls, tr)
  129. }
  130. }
  131. if data, err := common.Marshal(delta); err == nil {
  132. _ = helper.StringData(c, string(data))
  133. }
  134. continue
  135. }
  136. // done frame
  137. // finalize once and break loop
  138. usage.PromptTokens = chunk.PromptEvalCount
  139. usage.CompletionTokens = chunk.EvalCount
  140. usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
  141. finishReason := chunk.DoneReason
  142. if finishReason == "" {
  143. finishReason = "stop"
  144. }
  145. // emit stop delta
  146. if stop := helper.GenerateStopResponse(responseId, created, model, finishReason); stop != nil {
  147. if data, err := common.Marshal(stop); err == nil {
  148. _ = helper.StringData(c, string(data))
  149. }
  150. }
  151. // emit usage frame
  152. if final := helper.GenerateFinalUsageResponse(responseId, created, model, *usage); final != nil {
  153. if data, err := common.Marshal(final); err == nil {
  154. _ = helper.StringData(c, string(data))
  155. }
  156. }
  157. // send [DONE]
  158. helper.Done(c)
  159. break
  160. }
  161. if err := scanner.Err(); err != nil && err != io.EOF {
  162. logger.LogError(c, "ollama stream scan error: "+err.Error())
  163. }
  164. return usage, nil
  165. }
  166. // non-stream handler for chat/generate
  167. func ollamaChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
  168. body, err := io.ReadAll(resp.Body)
  169. if err != nil {
  170. return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError)
  171. }
  172. service.CloseResponseBodyGracefully(resp)
  173. raw := string(body)
  174. if common.DebugEnabled {
  175. println("ollama non-stream raw resp:", raw)
  176. }
  177. lines := strings.Split(raw, "\n")
  178. var (
  179. aggContent strings.Builder
  180. reasoningBuilder strings.Builder
  181. lastChunk ollamaChatStreamChunk
  182. parsedAny bool
  183. )
  184. for _, ln := range lines {
  185. ln = strings.TrimSpace(ln)
  186. if ln == "" {
  187. continue
  188. }
  189. var ck ollamaChatStreamChunk
  190. if err := json.Unmarshal([]byte(ln), &ck); err != nil {
  191. if len(lines) == 1 {
  192. return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
  193. }
  194. continue
  195. }
  196. parsedAny = true
  197. lastChunk = ck
  198. if ck.Message != nil && len(ck.Message.Thinking) > 0 {
  199. raw := strings.TrimSpace(string(ck.Message.Thinking))
  200. if raw != "" && raw != "null" {
  201. reasoningBuilder.WriteString(raw)
  202. }
  203. }
  204. if ck.Message != nil && ck.Message.Content != "" {
  205. aggContent.WriteString(ck.Message.Content)
  206. } else if ck.Response != "" {
  207. aggContent.WriteString(ck.Response)
  208. }
  209. }
  210. if !parsedAny {
  211. var single ollamaChatStreamChunk
  212. if err := json.Unmarshal(body, &single); err != nil {
  213. return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
  214. }
  215. lastChunk = single
  216. if single.Message != nil {
  217. if len(single.Message.Thinking) > 0 {
  218. raw := strings.TrimSpace(string(single.Message.Thinking))
  219. if raw != "" && raw != "null" {
  220. reasoningBuilder.WriteString(raw)
  221. }
  222. }
  223. aggContent.WriteString(single.Message.Content)
  224. } else {
  225. aggContent.WriteString(single.Response)
  226. }
  227. }
  228. model := lastChunk.Model
  229. if model == "" {
  230. model = info.UpstreamModelName
  231. }
  232. created := toUnix(lastChunk.CreatedAt)
  233. usage := &dto.Usage{PromptTokens: lastChunk.PromptEvalCount, CompletionTokens: lastChunk.EvalCount, TotalTokens: lastChunk.PromptEvalCount + lastChunk.EvalCount}
  234. content := aggContent.String()
  235. finishReason := lastChunk.DoneReason
  236. if finishReason == "" {
  237. finishReason = "stop"
  238. }
  239. msg := dto.Message{Role: "assistant", Content: contentPtr(content)}
  240. if rc := reasoningBuilder.String(); rc != "" {
  241. msg.ReasoningContent = rc
  242. }
  243. full := dto.OpenAITextResponse{
  244. Id: common.GetUUID(),
  245. Model: model,
  246. Object: "chat.completion",
  247. Created: created,
  248. Choices: []dto.OpenAITextResponseChoice{{
  249. Index: 0,
  250. Message: msg,
  251. FinishReason: finishReason,
  252. }},
  253. Usage: *usage,
  254. }
  255. out, _ := common.Marshal(full)
  256. service.IOCopyBytesGracefully(c, resp, out)
  257. return usage, nil
  258. }
  259. func contentPtr(s string) *string {
  260. if s == "" {
  261. return nil
  262. }
  263. return &s
  264. }