Bruce MacDonald 6 mesi fa
parent
commit
2531ac4b57
5 ha cambiato i file con 168 aggiunte e 36 eliminazioni
  1. 3 5
      src/browser.ts
  2. 108 28
      src/index.ts
  3. 17 2
      src/interfaces.ts
  4. 39 0
      test/index.test.ts
  5. 1 1
      tsconfig.json

+ 3 - 5
src/browser.ts

@@ -176,13 +176,11 @@ async encodeImage(image: Uint8Array | string): Promise<string> {
    * @returns {Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>>} - The response object or a stream of progress responses.
    */
   async create(
-    request: CreateRequest,
+    request: CreateRequest
   ): Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>> {
     return this.processStreamableRequest<ProgressResponse>('create', {
-      name: request.model,
-      stream: request.stream,
-      modelfile: request.modelfile,
-      quantize: request.quantize,
+      ...request,
+      name: request.model
     })
   }
 

+ 108 - 28
src/index.ts

@@ -2,12 +2,13 @@ import * as utils from './utils.js'
 import { AbortableAsyncIterator } from './utils.js'
 
 import fs, { createReadStream, promises } from 'fs'
-import { dirname, join, resolve } from 'path'
+import { join, resolve } from 'path'
 import { createHash } from 'crypto'
 import { homedir } from 'os'
 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> {
@@ -35,26 +36,104 @@ export class Ollama extends OllamaBrowser {
    * @private @internal
    */
   private async parseModelfile(
+    model: string,
     modelfile: string,
-    mfDir: string = process.cwd(),
-  ): Promise<string> {
-    const out: string[] = []
-    const lines = modelfile.split('\n')
+    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, 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)}`)
+      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 {
-          out.push(`${command} ${args}`)
+          // 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;
         }
-      } else {
-        out.push(line)
       }
+  
+      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 out.join('\n')
-  }
+  
+    return request;
+  }  
 
   /**
    * Resolve the path to an absolute path.
@@ -146,19 +225,20 @@ export class Ollama extends OllamaBrowser {
   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(
-        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')
-    }
-    request.modelfile = modelfileContent
+    // 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
     if (request.stream) {

+ 17 - 2
src/interfaces.ts

@@ -120,10 +120,25 @@ export interface PushRequest {
 
 export interface CreateRequest {
   model: string
+  stream?: boolean
+  quantize?: string
+  from?: string
+  files?: Record<string, string>
+  adapters?: Record<string, string>
+  template?: string
+  license?: string[] // TODO: double check this
+  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
-  quantize?: string
-  stream?: boolean
+  /** @deprecated Use quantize instead */
+  quantization?: string
 }
 
 export interface DeleteRequest {

+ 39 - 0
test/index.test.ts

@@ -1,3 +1,4 @@
+import { Ollama } from '../src/index'
 import { describe, it, expect } from 'vitest'
 import { formatHost } from '../src/utils'
 
@@ -62,3 +63,41 @@ describe('formatHost Function Tests', () => {
     expect(formatHost(':56789/')).toBe('http://127.0.0.1:56789')
   })
 })
+
+describe('parseModelfile Function Tests', () => {
+  it('should correctly parse modelfile commands', async () => {
+    const ollama = new Ollama()
+    const modelfile = `FROM llama2
+ADAPTER ./path/to/adapter
+TEMPLATE "You are a helpful assistant."
+SYSTEM "Respond concisely"
+MESSAGE assistant: Hello
+MESSAGE user: Hi
+LICENSE """Apache License
+Version 2.0, January 2004"""
+parameter1 value1
+parameter2 value2
+parameter3 3`
+
+    const result = await ollama['parseModelfile']('mymodel', modelfile)
+
+    expect(result).toEqual({
+      model: 'mymodel',
+      from: 'llama2',
+      template: 'You are a helpful assistant.',
+      system: 'Respond concisely',
+      messages: [
+        { role: 'assistant', content: 'Hello' },
+        { role: 'user', content: 'Hi' }
+      ],
+      license: ['Apache License\n2.0, January 2004'],
+      parameters: {
+        parameter1: 'value1',
+        parameter2: 'value2',
+        parameter3: 3
+      },
+      files: {},
+      adapters: {}
+    })
+  })
+})

+ 1 - 1
tsconfig.json

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