Bruce MacDonald hace 6 meses
padre
commit
639f2e6067
Se han modificado 4 ficheros con 112 adiciones y 163 borrados
  1. 18 5
      package-lock.json
  2. 3 2
      package.json
  3. 90 151
      src/index.ts
  4. 1 5
      src/interfaces.ts

+ 18 - 5
package-lock.json

@@ -13,6 +13,7 @@
       },
       "devDependencies": {
         "@swc/core": "^1.3.14",
+        "@types/glob": "^8.1.0",
         "@types/whatwg-fetch": "^0.0.33",
         "@typescript-eslint/eslint-plugin": "^5.42.1",
         "@typescript-eslint/parser": "^5.42.1",
@@ -1520,17 +1521,31 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/glob": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+      "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+      "dev": true,
+      "dependencies": {
+        "@types/minimatch": "^5.1.2",
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/json-schema": {
       "version": "7.0.15",
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/minimatch": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+      "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+      "dev": true
+    },
     "node_modules/@types/node": {
       "version": "20.11.0",
       "dev": true,
       "license": "MIT",
-      "optional": true,
-      "peer": true,
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -4634,9 +4649,7 @@
     "node_modules/undici-types": {
       "version": "5.26.5",
       "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/universalify": {
       "version": "2.0.1",

+ 3 - 2
package.json

@@ -34,14 +34,15 @@
   "license": "MIT",
   "devDependencies": {
     "@swc/core": "^1.3.14",
+    "@types/glob": "^8.1.0",
     "@types/whatwg-fetch": "^0.0.33",
     "@typescript-eslint/eslint-plugin": "^5.42.1",
     "@typescript-eslint/parser": "^5.42.1",
     "eslint": "^8.29.0",
-    "vitest": "^2.1.6",
     "prettier": "^3.2.4",
     "typescript": "^5.3.2",
-    "unbuild": "^2.0.0"
+    "unbuild": "^2.0.0",
+    "vitest": "^2.1.6"
   },
   "dependencies": {
     "whatwg-fetch": "^3.6.20"

+ 90 - 151
src/index.ts

@@ -2,13 +2,13 @@ import * as utils from './utils.js'
 import { AbortableAsyncIterator } from './utils.js'
 
 import fs, { createReadStream, promises } from 'fs'
-import { join, resolve } from 'path'
+import { join, resolve, basename } from 'path'
 import { createHash } from 'crypto'
 import { homedir } from 'os'
+import glob from 'glob'
 import { Ollama as OllamaBrowser } from './browser.js'
 
 import type { CreateRequest, ProgressResponse } from './interfaces.js'
-import { a } from 'vitest/dist/chunks/suite.B2jumIFP.js'
 
 export class Ollama extends OllamaBrowser {
   async encodeImage(image: Uint8Array | Buffer | string): Promise<string> {
@@ -29,125 +29,6 @@ export class Ollama extends OllamaBrowser {
     return image
   }
 
-  /**
-   * Parse the modelfile and replace the FROM and ADAPTER commands with the corresponding blob hashes.
-   * @param modelfile {string} - The modelfile content
-   * @param mfDir {string} - The directory of the modelfile
-   * @private @internal
-   */
-  private async parseModelfile(
-    model: string,
-    modelfile: string,
-    baseDir: string = process.cwd(),
-  ): Promise<CreateRequest> {
-    const lines = modelfile.split('\n');
-    const request: CreateRequest = {
-      model,
-      files: {},
-      adapters: {},
-      parameters: {},
-    };
-  
-    let multilineBuffer = '';
-    let currentCommand = '';
-  
-    for (const line of lines) {
-      const [command, ...rest] = line.split(' ');
-      let lineArgs = rest.join(' ').trim();
-  
-      // Handle multiline arguments
-      if (lineArgs.startsWith('"""')) {
-        if (lineArgs.endsWith('"""') && lineArgs.length > 6) {
-          // Single-line block
-          multilineBuffer = lineArgs.slice(3, -3);
-        } else {
-          // Start multiline block
-          multilineBuffer = lineArgs.slice(3);
-          currentCommand = command.toUpperCase();
-          continue;
-        }
-      } else if (multilineBuffer) {
-        // Accumulate multiline content
-        if (lineArgs.endsWith('"""')) {
-          multilineBuffer += '\n' + lineArgs.slice(0, -3);
-          lineArgs = multilineBuffer;
-          multilineBuffer = '';
-        } else {
-          multilineBuffer += '\n' + lineArgs;
-          continue;
-        }
-      }
-  
-      const args = multilineBuffer || lineArgs.replace(/^"(.*)"$/, '$1');
-  
-      // Handle commands
-      switch ((currentCommand || command).toUpperCase()) {
-        case 'FROM': {
-          const path = this.resolvePath(args, baseDir);
-          if (await this.fileExists(path)) {
-            request.files = {
-              ...request.files,
-              [args]: await this.createBlob(path),
-            };
-          } else {
-            request.from = args;
-          }
-          break;
-        }
-        case 'ADAPTER': {
-          const path = this.resolvePath(args, baseDir);
-          if (await this.fileExists(path)) {
-            request.adapters = {
-              ...request.adapters,
-              [args]: await this.createBlob(path),
-            };
-          }
-          break;
-        }
-        case 'TEMPLATE':
-          request.template = args;
-          break;
-        case 'SYSTEM':
-          request.system = args;
-          break;
-        case 'MESSAGE': {
-          const [role, content] = args.split(': ', 2);
-          request.messages = request.messages || [];
-          request.messages.push({ role, content });
-          break;
-        }
-        case 'LICENSE':
-          request.license = request.license || [];
-          request.license.push(args);
-          break;
-          default: {
-            if (!request.parameters) {
-              request.parameters = {}
-            }
-              request.parameters[command.toLowerCase()] = args
-            }
-          }
-  
-      currentCommand = '';
-      multilineBuffer = '';
-    }
-  
-    return request;
-  }  
-
-  /**
-   * Resolve the path to an absolute path.
-   * @param inputPath {string} - The input path
-   * @param mfDir {string} - The directory of the modelfile
-   * @private @internal
-   */
-  private resolvePath(inputPath, mfDir) {
-    if (inputPath.startsWith('~')) {
-      return join(homedir(), inputPath.slice(1))
-    }
-    return resolve(mfDir, inputPath)
-  }
-
   /**
    * checks if a file exists
    * @param path {string} - The path to the file
@@ -170,23 +51,19 @@ export class Ollama extends OllamaBrowser {
       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}`
+    const hash = createHash('sha256')
+    const stream = createReadStream(path)
+    for await (const chunk of stream) {
+      hash.update(chunk)
+    }
+    const digest = `sha256:${hash.digest('hex')}`
 
     try {
       await utils.head(this.fetch, `${this.config.host}/api/blobs/${digest}`)
     } catch (e) {
       if (e instanceof Error && e.message.includes('404')) {
+        const fileStream = createReadStream(path)
         // Create a new readable stream for the fetch request
         const readableStream = new ReadableStream({
           start(controller) {
@@ -216,31 +93,93 @@ export class Ollama extends OllamaBrowser {
 
     return digest
   }
+  
+  async findModelFiles(path: string): Promise<string[]> {
+    const files: string[] = []
+    const modelPath = resolve(path)
+  
+    // Check for various model file patterns
+    const patterns = [
+      'model*.safetensors',
+      'adapters.safetensors', 
+      'adapter_model.safetensors',
+      'pytorch_model*.bin',
+      'consolidated*.pth',
+      '*.gguf',
+      '*.bin'
+    ]
+  
+    // Look for model files
+    for (const pattern of patterns) {
+      const matches = glob.sync(join(modelPath, pattern))
+      if (matches.length > 0) {
+        files.push(...matches)
+        break
+      }
+    }
+  
+    if (files.length === 0) {
+      throw new Error('No model files found')
+    }
+  
+    // Add config and tokenizer files
+    try {
+      const configFiles = glob.sync(join(modelPath, '*.json'))
+      files.push(...configFiles)
+  
+      const nestedConfigFiles = glob.sync(join(modelPath, '**/*.json'))
+      files.push(...nestedConfigFiles)
+  
+      const tokenizerFiles = glob.sync(join(modelPath, 'tokenizer.model'))
+      if (tokenizerFiles.length > 0) {
+        files.push(...tokenizerFiles)
+      } else {
+        const nestedTokenizerFiles = glob.sync(join(modelPath, '**/tokenizer.model'))
+        files.push(...nestedTokenizerFiles)
+      }
+    } catch (e) {
+      // Continue if config/tokenizer files not found
+    }
+  
+    return files
+  }
+
+  async files(from: string): Promise<Record<string, string>> {
+      // Check if from is a local file/directory
+      const exists = await this.fileExists(from)
+      if (!exists) {
+        // If not a local path, assume it's a model name
+        return {}
+      }
+  
+      const fileMap: Record<string, string> = {}
+      const stats = await promises.stat(from)
+      let files: string[]
+      if (stats.isDirectory()) {
+        files = await this.findModelFiles(from)
+      } else {
+        files = [from]
+      }
+  
+      for (const file of files) {
+        const digest = await this.createBlob(file)
+        fileMap[basename(file)] = digest
+      }
+  
+    return fileMap
+  }
 
   create(
     request: CreateRequest & { stream: true },
   ): Promise<AbortableAsyncIterator<ProgressResponse>>
   create(request: CreateRequest & { stream?: false }): Promise<ProgressResponse>
 
-  async create(
-    request: CreateRequest,
-  ): Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>> {
-    // let modelfileContent = ''
-    // if (request.path) {
-    //   modelfileContent = await promises.readFile(request.path, { encoding: 'utf8' })
-    //   modelfileContent = await this.parseModelfile(
-    //     request.model,
-    //     modelfileContent,
-    //     dirname(request.path),
-    //   )
-    // } else if (request.modelfile) {
-    //   modelfileContent = await this.parseModelfile(request.model, request.modelfile)
-    // } else {
-    //   throw new Error('Must provide either path or modelfile to create a model')
-    // }
-    // request.modelfile = modelfileContent
-
-    // check stream here so that typescript knows which overload to use
+  async create(request: CreateRequest): Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>> {
+    if (request.from && !request.files) {
+      request.files = await this.files(request.from)
+    }
+  
+    // Handle stream flag
     if (request.stream) {
       return super.create(request as CreateRequest & { stream: true })
     } else {

+ 1 - 5
src/interfaces.ts

@@ -126,17 +126,13 @@ export interface CreateRequest {
   files?: Record<string, string>
   adapters?: Record<string, string>
   template?: string
-  license?: string[] // TODO: double check this
+  license?: string | string[]
   system?: string
   parameters?: Record<string, unknown>
   messages?: Message[]
 
   /** @deprecated Use model instead */
   name?: string
-  /** @deprecated Set with direct request options instead */
-  path?: string
-  /** @deprecated Set with other request options instead */
-  modelfile?: string
   /** @deprecated Use quantize instead */
   quantization?: string
 }