{"id":1492,"date":"2024-07-21T13:33:05","date_gmt":"2024-07-21T11:33:05","guid":{"rendered":"https:\/\/cln.io\/blog\/?p=1492"},"modified":"2024-10-12T19:38:59","modified_gmt":"2024-10-12T17:38:59","slug":"turn-a-cloudflare-ai-worker-into-ollama-rest-api-compatible-endpoint-migrate-from-ollama-to-cloudflare-ai","status":"publish","type":"post","link":"https:\/\/cln.io\/blog\/turn-a-cloudflare-ai-worker-into-ollama-rest-api-compatible-endpoint-migrate-from-ollama-to-cloudflare-ai\/","title":{"rendered":"Turn a Cloudflare AI worker into Ollama REST API-compatible endpoint (migrate from Ollama to Cloudflare AI)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">I&#8217;ve recently had to switch from a self-hosted ollama to a Cloudflare worker for development.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As I&#8217;m using API calls (with Tines) I wanted the transition to go as smooth as possible so I wanted my Ollama API calls to work on my cloudflare worker.<br>This is the result of the worker.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { Ai } from '.\/vendor\/@cloudflare\/ai.js';\n\nfunction decodeBase64(str) {\n  try {\n    return Buffer.from(str, 'base64').toString();\n  } catch (error) {\n    console.error('Error decoding Base64 string:', error);\n    return null;\n  }\n}\n\nfunction isValidAuth(header, expectedUsername, expectedPassword) {\n  if (!header) return false;\n\n  const [type, credentials] = header.split(' ');\n  if (type !== 'Basic') return false;\n\n  const decodedCredentials = decodeBase64(credentials);\n  if (!decodedCredentials) return false;\n\n  const [username, password] = decodedCredentials.split(':');\n  const usernameBuffer = Buffer.from(username);\n  const passwordBuffer = Buffer.from(password);\n  const expectedUsernameBuffer = Buffer.from(expectedUsername);\n  const expectedPasswordBuffer = Buffer.from(expectedPassword);\n\n  try {\n    const usernameEqual = crypto.timingSafeEqual(usernameBuffer, expectedUsernameBuffer);\n    const passwordEqual = crypto.timingSafeEqual(passwordBuffer, expectedPasswordBuffer);\n    return usernameEqual &amp;&amp; passwordEqual;\n  } catch (error) {\n    console.error('Error in secure comparison:', error);\n    return false;\n  }\n}\n\nexport default {\n  async fetch(request, env) {\n    if (request.method !== 'POST') {\n      return new Response('Method Not Allowed', {\n        status: 405,\n        headers: { 'Allow': 'POST' }\n      });\n    }\n\n    const tasks = []; \/\/ Initialize tasks array\n\n    try {\n      const ai = new Ai(env.AI);\n      const url = new URL(request.url);\n      const requestBody = await request.json();\n\n      const requiredFields = ['model'];\n      const isMissingField = requiredFields.some(field => !requestBody[field]);\n      if (isMissingField) {\n        return new Response('Missing required input', {\n          status: 400,\n          headers: { 'Content-Type': 'text\/plain' },\n        });\n      }\n\n      const modelRef = requestBody.model; \/\/ Use user-defined model\n      console.log(`Requested model is ${modelRef}`);\n      let response;\n\n      switch (url.pathname) {\n        case '\/api\/chat':\n          if (!requestBody.messages || !Array.isArray(requestBody.messages)) {\n            return new Response('Missing or invalid \"messages\" field', {\n              status: 400,\n              headers: { 'Content-Type': 'text\/plain' },\n            });\n          }\n\n          const chat = {\n            model: modelRef, \/\/ Use user-defined model\n            messages: requestBody.messages,\n            stream: requestBody.stream\n          };\n          console.log(`Chat initiated with model ${modelRef}`);\n          response = await ai.run(modelRef, chat); \/\/ Dynamically use the user-defined model\n          tasks.push({ inputs: chat, response }); \/\/ Push the task\n          break;\n\n        case '\/api\/generate':\n          if (!requestBody.prompt) {\n            return new Response('Missing \"prompt\" field', {\n              status: 400,\n              headers: { 'Content-Type': 'text\/plain' },\n            });\n          }\n\n          const simple = {\n            model: modelRef, \/\/ Use user-defined model\n            prompt: requestBody.prompt\n          };\n          console.log(`Generating response for model ${modelRef}`);\n          response = await ai.run(modelRef, simple); \/\/ Dynamically use the user-defined model\n          tasks.push({ inputs: simple, response }); \/\/ Push the task\n          break;\n\n        default:\n          return new Response('Unsupported endpoint', { status: 404 });\n      }\n\n      return new Response(JSON.stringify(tasks), { \/\/ Return the tasks array\n        headers: { 'Content-Type': 'application\/json' },\n      });\n    } catch (error) {\n      console.error('An unexpected error occurred:', error);\n      return new Response('An unexpected error occurred', {\n        status: 500,\n        headers: { 'Content-Type': 'text\/plain' },\n      });\n    }\n  }\n};\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;ve recently had to switch from a self-hosted ollama to a Cloudflare worker for development. As I&#8217;m using API calls (with Tines) I wanted the transition to go as smooth as possible so I wanted [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1684,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[57,26,58],"tags":[],"class_list":["post-1492","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-apis","category-it","category-miscellaneous"],"_links":{"self":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1492","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/comments?post=1492"}],"version-history":[{"count":1,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1492\/revisions"}],"predecessor-version":[{"id":1493,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1492\/revisions\/1493"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media\/1684"}],"wp:attachment":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media?parent=1492"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/categories?post=1492"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/tags?post=1492"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}