utils_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. //go:build integration
  2. package integration
  3. import (
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "log/slog"
  11. "math/rand"
  12. "net"
  13. "net/http"
  14. "net/url"
  15. "os"
  16. "path/filepath"
  17. "runtime"
  18. "strconv"
  19. "strings"
  20. "sync"
  21. "testing"
  22. "time"
  23. "github.com/ollama/ollama/api"
  24. "github.com/ollama/ollama/app/lifecycle"
  25. "github.com/stretchr/testify/assert"
  26. "github.com/stretchr/testify/require"
  27. )
  28. func Init() {
  29. lifecycle.InitLogging()
  30. }
  31. func FindPort() string {
  32. port := 0
  33. if a, err := net.ResolveTCPAddr("tcp", "localhost:0"); err == nil {
  34. var l *net.TCPListener
  35. if l, err = net.ListenTCP("tcp", a); err == nil {
  36. port = l.Addr().(*net.TCPAddr).Port
  37. l.Close()
  38. }
  39. }
  40. if port == 0 {
  41. port = rand.Intn(65535-49152) + 49152 // get a random port in the ephemeral range
  42. }
  43. return strconv.Itoa(port)
  44. }
  45. func GetTestEndpoint() (*api.Client, string) {
  46. defaultPort := "11434"
  47. ollamaHost := os.Getenv("OLLAMA_HOST")
  48. scheme, hostport, ok := strings.Cut(ollamaHost, "://")
  49. if !ok {
  50. scheme, hostport = "http", ollamaHost
  51. }
  52. // trim trailing slashes
  53. hostport = strings.TrimRight(hostport, "/")
  54. host, port, err := net.SplitHostPort(hostport)
  55. if err != nil {
  56. host, port = "127.0.0.1", defaultPort
  57. if ip := net.ParseIP(strings.Trim(hostport, "[]")); ip != nil {
  58. host = ip.String()
  59. } else if hostport != "" {
  60. host = hostport
  61. }
  62. }
  63. if os.Getenv("OLLAMA_TEST_EXISTING") == "" && port == defaultPort {
  64. port = FindPort()
  65. }
  66. slog.Info("server connection", "host", host, "port", port)
  67. return api.NewClient(
  68. &url.URL{
  69. Scheme: scheme,
  70. Host: net.JoinHostPort(host, port),
  71. },
  72. http.DefaultClient), fmt.Sprintf("%s:%s", host, port)
  73. }
  74. var serverMutex sync.Mutex
  75. var serverReady bool
  76. func startServer(ctx context.Context, ollamaHost string) error {
  77. // Make sure the server has been built
  78. CLIName, err := filepath.Abs("../ollama")
  79. if err != nil {
  80. return err
  81. }
  82. if runtime.GOOS == "windows" {
  83. CLIName += ".exe"
  84. }
  85. _, err = os.Stat(CLIName)
  86. if err != nil {
  87. return fmt.Errorf("CLI missing, did you forget to build first? %w", err)
  88. }
  89. serverMutex.Lock()
  90. defer serverMutex.Unlock()
  91. if serverReady {
  92. return nil
  93. }
  94. if tmp := os.Getenv("OLLAMA_HOST"); tmp != ollamaHost {
  95. slog.Info("setting env", "OLLAMA_HOST", ollamaHost)
  96. os.Setenv("OLLAMA_HOST", ollamaHost)
  97. }
  98. slog.Info("starting server", "url", ollamaHost)
  99. done, err := lifecycle.SpawnServer(ctx, "../ollama")
  100. if err != nil {
  101. return fmt.Errorf("failed to start server: %w", err)
  102. }
  103. go func() {
  104. <-ctx.Done()
  105. serverMutex.Lock()
  106. defer serverMutex.Unlock()
  107. exitCode := <-done
  108. if exitCode > 0 {
  109. slog.Warn("server failure", "exit", exitCode)
  110. }
  111. serverReady = false
  112. }()
  113. // TODO wait only long enough for the server to be responsive...
  114. time.Sleep(500 * time.Millisecond)
  115. serverReady = true
  116. return nil
  117. }
  118. func PullIfMissing(ctx context.Context, client *api.Client, modelName string) error {
  119. slog.Info("checking status of model", "model", modelName)
  120. showReq := &api.ShowRequest{Name: modelName}
  121. showCtx, cancel := context.WithDeadlineCause(
  122. ctx,
  123. time.Now().Add(5*time.Second),
  124. fmt.Errorf("show for existing model %s took too long", modelName),
  125. )
  126. defer cancel()
  127. _, err := client.Show(showCtx, showReq)
  128. var statusError api.StatusError
  129. switch {
  130. case errors.As(err, &statusError) && statusError.StatusCode == http.StatusNotFound:
  131. break
  132. case err != nil:
  133. return err
  134. default:
  135. slog.Info("model already present", "model", modelName)
  136. return nil
  137. }
  138. slog.Info("model missing", "model", modelName)
  139. stallDuration := 30 * time.Second // This includes checksum verification, which can take a while on larger models
  140. stallTimer := time.NewTimer(stallDuration)
  141. fn := func(resp api.ProgressResponse) error {
  142. // fmt.Print(".")
  143. if !stallTimer.Reset(stallDuration) {
  144. return fmt.Errorf("stall was detected, aborting status reporting")
  145. }
  146. return nil
  147. }
  148. stream := true
  149. pullReq := &api.PullRequest{Name: modelName, Stream: &stream}
  150. var pullError error
  151. done := make(chan int)
  152. go func() {
  153. pullError = client.Pull(ctx, pullReq, fn)
  154. done <- 0
  155. }()
  156. select {
  157. case <-stallTimer.C:
  158. return fmt.Errorf("download stalled")
  159. case <-done:
  160. return pullError
  161. }
  162. }
  163. var serverProcMutex sync.Mutex
  164. // Returns an Client, the testEndpoint, and a cleanup function, fails the test on errors
  165. // Starts the server if needed
  166. func InitServerConnection(ctx context.Context, t *testing.T) (*api.Client, string, func()) {
  167. client, testEndpoint := GetTestEndpoint()
  168. if os.Getenv("OLLAMA_TEST_EXISTING") == "" {
  169. serverProcMutex.Lock()
  170. fp, err := os.CreateTemp("", "ollama-server-*.log")
  171. if err != nil {
  172. t.Fatalf("failed to generate log file: %s", err)
  173. }
  174. lifecycle.ServerLogFile = fp.Name()
  175. fp.Close()
  176. require.NoError(t, startServer(ctx, testEndpoint))
  177. }
  178. return client, testEndpoint, func() {
  179. if os.Getenv("OLLAMA_TEST_EXISTING") == "" {
  180. defer serverProcMutex.Unlock()
  181. if t.Failed() {
  182. fp, err := os.Open(lifecycle.ServerLogFile)
  183. if err != nil {
  184. slog.Error("failed to open server log", "logfile", lifecycle.ServerLogFile, "error", err)
  185. return
  186. }
  187. data, err := io.ReadAll(fp)
  188. if err != nil {
  189. slog.Error("failed to read server log", "logfile", lifecycle.ServerLogFile, "error", err)
  190. return
  191. }
  192. slog.Warn("SERVER LOG FOLLOWS")
  193. os.Stderr.Write(data)
  194. slog.Warn("END OF SERVER")
  195. }
  196. err := os.Remove(lifecycle.ServerLogFile)
  197. if err != nil && !os.IsNotExist(err) {
  198. slog.Warn("failed to cleanup", "logfile", lifecycle.ServerLogFile, "error", err)
  199. }
  200. }
  201. }
  202. }
  203. func GenerateTestHelper(ctx context.Context, t *testing.T, genReq api.GenerateRequest, anyResp []string) {
  204. client, _, cleanup := InitServerConnection(ctx, t)
  205. defer cleanup()
  206. require.NoError(t, PullIfMissing(ctx, client, genReq.Model))
  207. DoGenerate(ctx, t, client, genReq, anyResp, 30*time.Second, 10*time.Second)
  208. }
  209. func DoGenerate(ctx context.Context, t *testing.T, client *api.Client, genReq api.GenerateRequest, anyResp []string, initialTimeout, streamTimeout time.Duration) {
  210. stallTimer := time.NewTimer(initialTimeout)
  211. var buf bytes.Buffer
  212. fn := func(response api.GenerateResponse) error {
  213. // fmt.Print(".")
  214. buf.Write([]byte(response.Response))
  215. if !stallTimer.Reset(streamTimeout) {
  216. return fmt.Errorf("stall was detected while streaming response, aborting")
  217. }
  218. return nil
  219. }
  220. stream := true
  221. genReq.Stream = &stream
  222. done := make(chan int)
  223. var genErr error
  224. go func() {
  225. genErr = client.Generate(ctx, &genReq, fn)
  226. done <- 0
  227. }()
  228. select {
  229. case <-stallTimer.C:
  230. if buf.Len() == 0 {
  231. t.Errorf("generate never started. Timed out after :%s", initialTimeout.String())
  232. } else {
  233. t.Errorf("generate stalled. Response so far:%s", buf.String())
  234. }
  235. case <-done:
  236. require.NoError(t, genErr, "failed with %s request prompt %s ", genReq.Model, genReq.Prompt)
  237. // Verify the response contains the expected data
  238. response := buf.String()
  239. atLeastOne := false
  240. for _, resp := range anyResp {
  241. if strings.Contains(strings.ToLower(response), resp) {
  242. atLeastOne = true
  243. break
  244. }
  245. }
  246. require.True(t, atLeastOne, "none of %v found in %s", anyResp, response)
  247. slog.Info("test pass", "model", genReq.Model, "prompt", genReq.Prompt, "contains", anyResp, "response", response)
  248. case <-ctx.Done():
  249. t.Error("outer test context done while waiting for generate")
  250. }
  251. }
  252. // Generate a set of requests
  253. // By default each request uses orca-mini as the model
  254. func GenerateRequests() ([]api.GenerateRequest, [][]string) {
  255. stream := false
  256. return []api.GenerateRequest{
  257. {
  258. Model: "orca-mini",
  259. Prompt: "why is the ocean blue?",
  260. Stream: &stream,
  261. Options: map[string]interface{}{
  262. "seed": 42,
  263. "temperature": 0.0,
  264. },
  265. }, {
  266. Model: "orca-mini",
  267. Prompt: "why is the color of dirt brown?",
  268. Stream: &stream,
  269. Options: map[string]interface{}{
  270. "seed": 42,
  271. "temperature": 0.0,
  272. },
  273. }, {
  274. Model: "orca-mini",
  275. Prompt: "what is the origin of the us thanksgiving holiday?",
  276. Stream: &stream,
  277. Options: map[string]interface{}{
  278. "seed": 42,
  279. "temperature": 0.0,
  280. },
  281. }, {
  282. Model: "orca-mini",
  283. Prompt: "what is the origin of independence day?",
  284. Stream: &stream,
  285. Options: map[string]interface{}{
  286. "seed": 42,
  287. "temperature": 0.0,
  288. },
  289. }, {
  290. Model: "orca-mini",
  291. Prompt: "what is the composition of air?",
  292. Stream: &stream,
  293. Options: map[string]interface{}{
  294. "seed": 42,
  295. "temperature": 0.0,
  296. },
  297. },
  298. },
  299. [][]string{
  300. []string{"sunlight"},
  301. []string{"soil", "organic", "earth", "black", "tan"},
  302. []string{"england", "english", "massachusetts", "pilgrims"},
  303. []string{"fourth", "july", "declaration", "independence"},
  304. []string{"nitrogen", "oxygen", "carbon", "dioxide"},
  305. }
  306. }
  307. func EmbeddingTestHelper(ctx context.Context, t *testing.T, client *http.Client, req api.EmbeddingRequest) api.EmbeddingResponse {
  308. // TODO maybe stuff in an init routine?
  309. lifecycle.InitLogging()
  310. requestJSON, err := json.Marshal(req)
  311. if err != nil {
  312. t.Fatalf("Error serializing request: %v", err)
  313. }
  314. defer func() {
  315. if os.Getenv("OLLAMA_TEST_EXISTING") == "" {
  316. defer serverProcMutex.Unlock()
  317. if t.Failed() {
  318. fp, err := os.Open(lifecycle.ServerLogFile)
  319. if err != nil {
  320. slog.Error("failed to open server log", "logfile", lifecycle.ServerLogFile, "error", err)
  321. return
  322. }
  323. data, err := io.ReadAll(fp)
  324. if err != nil {
  325. slog.Error("failed to read server log", "logfile", lifecycle.ServerLogFile, "error", err)
  326. return
  327. }
  328. slog.Warn("SERVER LOG FOLLOWS")
  329. os.Stderr.Write(data)
  330. slog.Warn("END OF SERVER")
  331. }
  332. err = os.Remove(lifecycle.ServerLogFile)
  333. if err != nil && !os.IsNotExist(err) {
  334. slog.Warn("failed to cleanup", "logfile", lifecycle.ServerLogFile, "error", err)
  335. }
  336. }
  337. }()
  338. scheme, testEndpoint := GetTestEndpoint()
  339. if os.Getenv("OLLAMA_TEST_EXISTING") == "" {
  340. serverProcMutex.Lock()
  341. fp, err := os.CreateTemp("", "ollama-server-*.log")
  342. if err != nil {
  343. t.Fatalf("failed to generate log file: %s", err)
  344. }
  345. lifecycle.ServerLogFile = fp.Name()
  346. fp.Close()
  347. assert.NoError(t, StartServer(ctx, testEndpoint))
  348. }
  349. err = PullIfMissing(ctx, client, scheme, testEndpoint, req.Model)
  350. if err != nil {
  351. t.Fatalf("Error pulling model: %v", err)
  352. }
  353. // Make the request and get the response
  354. httpReq, err := http.NewRequest("POST", scheme+"://"+testEndpoint+"/api/embeddings", bytes.NewReader(requestJSON))
  355. if err != nil {
  356. t.Fatalf("Error creating request: %v", err)
  357. }
  358. // Set the content type for the request
  359. httpReq.Header.Set("Content-Type", "application/json")
  360. // Make the request with the HTTP client
  361. response, err := client.Do(httpReq.WithContext(ctx))
  362. if err != nil {
  363. t.Fatalf("Error making request: %v", err)
  364. }
  365. defer response.Body.Close()
  366. body, err := io.ReadAll(response.Body)
  367. assert.NoError(t, err)
  368. assert.Equal(t, response.StatusCode, 200, string(body))
  369. // Verify the response is valid JSON
  370. var res api.EmbeddingResponse
  371. err = json.Unmarshal(body, &res)
  372. if err != nil {
  373. assert.NoError(t, err, body)
  374. }
  375. return res
  376. }