Selaa lähdekoodia

browser compatibility

- add dynamic import for node specific features
Bruce MacDonald 1 vuosi sitten
vanhempi
commit
f537562f98
4 muutettua tiedostoa jossa 146 lisäystä ja 117 poistoa
  1. 19 116
      src/index.ts
  2. 120 0
      src/node.js
  3. 6 0
      src/utils.ts
  4. 1 1
      tsconfig.json

+ 19 - 116
src/index.ts

@@ -1,9 +1,5 @@
 import * as utils from './utils.js'
 import 'whatwg-fetch'
-import fs, { promises, createReadStream } from 'fs'
-import { join, resolve, dirname } from 'path'
-import { createHash } from 'crypto'
-import { homedir } from 'os'
 
 import type {
   Fetch,
@@ -104,111 +100,19 @@ export class Ollama {
       const result = Buffer.from(image).toString('base64')
       return result
     }
-    try {
-      if (fs.existsSync(image)) {
-        // this is a filepath, read the file and convert it to base64
-        const fileBuffer = await promises.readFile(resolve(image))
-        return Buffer.from(fileBuffer).toString('base64')
+    if (utils.isNode()) {
+      const { readImage } = await import('../src/node.js');
+      try {
+        // if this succeeds the image exists locally at this filepath and has been read
+        return await readImage(image)
+      } catch {
+        // couldn't read an image at the filepath, continue
       }
-    } catch {
-      // continue
     }
-    // the string may be base64 encoded
+    // the string should be base64 encoded already
     return image
   }
 
-  private async parseModelfile(
-    modelfile: string,
-    mfDir: string = process.cwd(),
-  ): Promise<string> {
-    const out: string[] = []
-    const lines = modelfile.split('\n')
-    for (const line of lines) {
-      const [command, args] = line.split(' ', 2)
-      if (['FROM', 'ADAPTER'].includes(command.toUpperCase())) {
-        const path = this.resolvePath(args.trim(), mfDir)
-        if (await this.fileExists(path)) {
-          out.push(`${command} @${await this.createBlob(path)}`)
-        } else {
-          out.push(`${command} ${args}`)
-        }
-      } else {
-        out.push(line)
-      }
-    }
-    return out.join('\n')
-  }
-
-  private resolvePath(inputPath, mfDir) {
-    if (inputPath.startsWith('~')) {
-      return join(homedir(), inputPath.slice(1))
-    }
-    return resolve(mfDir, inputPath)
-  }
-
-  private async fileExists(path: string): Promise<boolean> {
-    try {
-      await promises.access(path)
-      return true
-    } catch {
-      return false
-    }
-  }
-
-  private async createBlob(path: string): Promise<string> {
-    if (typeof ReadableStream === 'undefined') {
-      // Not all fetch implementations support streaming
-      // TODO: support non-streaming uploads
-      throw new Error('Streaming uploads are not supported in this environment.')
-    }
-
-    // Create a stream for reading the file
-    const fileStream = createReadStream(path)
-
-    // Compute the SHA256 digest
-    const sha256sum = await new Promise<string>((resolve, reject) => {
-      const hash = createHash('sha256')
-      fileStream.on('data', (data) => hash.update(data))
-      fileStream.on('end', () => resolve(hash.digest('hex')))
-      fileStream.on('error', reject)
-    })
-
-    const digest = `sha256:${sha256sum}`
-
-    try {
-      await utils.head(this.fetch, `${this.config.host}/api/blobs/${digest}`)
-    } catch (e) {
-      if (e instanceof Error && e.message.includes('404')) {
-        // Create a new readable stream for the fetch request
-        const readableStream = new ReadableStream({
-          start(controller) {
-            fileStream.on('data', (chunk) => {
-              controller.enqueue(chunk) // Enqueue the chunk directly
-            })
-
-            fileStream.on('end', () => {
-              controller.close() // Close the stream when the file ends
-            })
-
-            fileStream.on('error', (err) => {
-              controller.error(err) // Propagate errors to the stream
-            })
-          },
-        })
-
-        await utils.post(
-          this.fetch,
-          `${this.config.host}/api/blobs/${digest}`,
-          readableStream,
-        )
-      } else {
-        throw e
-      }
-    }
-
-    return digest
-  }
-
   generate(
     request: GenerateRequest & { stream: true },
   ): Promise<AsyncGenerator<GenerateResponse>>
@@ -273,23 +177,22 @@ export class Ollama {
   async create(
     request: CreateRequest,
   ): Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> {
-    let modelfileContent = ''
-    if (request.path) {
-      modelfileContent = await promises.readFile(request.path, { encoding: 'utf8' })
-      modelfileContent = await this.parseModelfile(
-        modelfileContent,
-        dirname(request.path),
-      )
-    } else if (request.modelfile) {
-      modelfileContent = await this.parseModelfile(request.modelfile)
-    } else {
-      throw new Error('Must provide either path or modelfile to create a model')
+
+    if (utils.isNode()) {
+      const { readModelfile } = await import('../src/node.js');
+      let modelfileContent = await readModelfile(this, request);
+      request.modelfile = modelfileContent
+    }
+
+    if (request.modelfile == "") {
+      // request.path will resolve to a modelfile in node environments, otherwise is it required
+      throw new Error("modelfile is requrired")
     }
 
     return this.processStreamableRequest<ProgressResponse>('create', {
       name: request.model,
       stream: request.stream,
-      modelfile: modelfileContent,
+      modelfile: request.modelfile,
     })
   }
 

+ 120 - 0
src/node.js

@@ -0,0 +1,120 @@
+const fs = require('fs');
+const path = require('path');
+const utils = require('./utils');
+const { createHash } = require('crypto');
+const { homedir } = require('os');
+
+async function parseModelfile(ollama, modelfile, mfDir = process.cwd()) {
+  const out = [];
+  const lines = modelfile.split('\n');
+  for (const line of lines) {
+    const [command, args] = line.split(' ', 2);
+    if (['FROM', 'ADAPTER'].includes(command.toUpperCase())) {
+      const resolvedPath = resolvePath(args.trim(), mfDir);
+      if (await fileExists(resolvedPath)) {
+        out.push(`${command} @${await createBlob(ollama, resolvedPath)}`);
+      } else {
+        out.push(`${command} ${args}`);
+      }
+    } else {
+      out.push(line);
+    }
+  }
+  return out.join('\n');
+}
+
+function resolvePath(inputPath, mfDir) {
+  if (inputPath.startsWith('~')) {
+    return path.join(homedir(), inputPath.slice(1));
+  }
+  return path.resolve(mfDir, inputPath);
+}
+
+async function fileExists(filePath) {
+  try {
+    await fs.promises.access(filePath);
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+async function createBlob(ollama, filePath) {
+  if (typeof ReadableStream === 'undefined') {
+    throw new Error('Streaming uploads are not supported in this environment.');
+  }
+
+  const fileStream = fs.createReadStream(filePath);
+  const sha256sum = await new Promise((resolve, reject) => {
+    const hash = createHash('sha256');
+    fileStream.on('data', (data) => hash.update(data));
+    fileStream.on('end', () => resolve(hash.digest('hex')));
+    fileStream.on('error', reject);
+  });
+
+  const digest = `sha256:${sha256sum}`;
+
+  try {
+    await utils.head(
+      ollama.fetch,
+      `${ollama.config.host}/api/blobs/${digest}`,
+      { signal: ollama.abortController.signal },
+    );
+  } catch (e) {
+    if (e instanceof Error && e.message.includes('404')) {
+      const readableStream = new ReadableStream({
+        start(controller) {
+          fileStream.on('data', (chunk) => {
+            controller.enqueue(chunk);
+          });
+
+          fileStream.on('end', () => {
+            controller.close();
+          });
+
+          fileStream.on('error', (err) => {
+            controller.error(err);
+          });
+        },
+      });
+
+      await utils.post(
+        ollama.fetch,
+        `${ollama.config.host}/api/blobs/${digest}`,
+        readableStream,
+        { signal: ollama.abortController.signal },
+      );
+    } else {
+      throw e;
+    }
+  }
+
+  return digest;
+}
+
+export async function readModelfile(ollama, request) {
+    let modelfileContent = ''
+    if (request.path) {
+      modelfileContent = await fs.promises.readFile(request.path, { encoding: 'utf8' })
+      modelfileContent = await parseModelfile(
+        ollama,
+        modelfileContent,
+        path.dirname(request.path),
+      )
+    } else if (request.modelfile) {
+      modelfileContent = await parseModelfile(ollama, request.modelfile)
+    } else {
+      throw new Error('Must provide either path or modelfile to create a model')
+    }
+
+    return modelfileContent;
+}
+
+export async function readImage(imgPath) {
+  if (fs.existsSync(imgPath)) {
+    // this is a filepath, read the file and convert it to base64
+    const fileBuffer = await fs.promises.readFile(path.resolve(imgPath))
+    return Buffer.from(fileBuffer).toString('base64')
+  }
+  throw new Error(`Image path ${imgPath} does not exist`)
+}

+ 6 - 0
src/utils.ts

@@ -50,6 +50,12 @@ function getPlatform() {
   return '' // unknown
 }
 
+export const isNode = () => {
+  return typeof process !== "undefined" &&
+      process.versions != null &&
+      process.versions.node != null;
+};
+
 const fetchWithHeaders = async (
   fetch: Fetch,
   url: string,

+ 1 - 1
tsconfig.json

@@ -20,7 +20,7 @@
     "esm": true,
   },
 
-  "include": ["./src/**/*.ts"],
+  "include": ["./src/**/*.ts", "src/node.js"],
 
   "exclude": ["node_modules"],
 }