Bruce MacDonald преди 1 година
родител
ревизия
9ff3b3137e
променени са 10 файла, в които са добавени 596 реда и са изтрити 556 реда
  1. 23 37
      .eslintrc.cjs
  2. 8 0
      .prettierrc.json
  3. 13 12
      README.md
  4. 18 18
      jest.config.cjs
  5. 2 0
      package.json
  6. 275 237
      src/index.ts
  7. 123 123
      src/interfaces.ts
  8. 110 101
      src/utils.ts
  9. 3 3
      test/index.spec.ts
  10. 21 25
      tsconfig.json

+ 23 - 37
.eslintrc.cjs

@@ -1,38 +1,24 @@
 module.exports = {
-	env: {
-		commonjs: true,
-		es2021: true,
-		node: true,
-		jest: true
-	},
-	parserOptions: {
-		ecmaVersion: "latest"
-	},
-	parser: "@typescript-eslint/parser",
-	extends: [
-		"eslint:recommended"
-	],
-	rules: {
-		curly: [1, "all"],
-		// disallow single quotes
-		quotes: [1, "double", { allowTemplateLiterals: true }],
-		// force semi-colons
-		semi: 1,
-		// allow tabs
-		"no-tabs": [0],
-		// use tab indentation
-		indent: [1, "tab", {
-			SwitchCase: 1
-		}],
-		// prevent commar dangles
-		"comma-dangle": [1, "never"],
-		// allow paren-less arrow functions
-		"arrow-parens": 0,
-		// allow async-await
-		"generator-star-spacing": 0,
-		"no-unused-vars": [0, { args: "after-used", vars: "local" }],
-		"no-constant-condition": 0,
-		// allow debugger during development
-		"no-debugger": process.env.NODE_ENV === "production" ? 2 : 0
-	}
-};
+  env: {
+    commonjs: true,
+    es2021: true,
+    node: true,
+    jest: true,
+  },
+  parserOptions: {
+    ecmaVersion: 'latest',
+  },
+  parser: '@typescript-eslint/parser',
+  extends: ['eslint:recommended'],
+  rules: {
+    curly: [1, 'all'],
+    // allow paren-less arrow functions
+    'arrow-parens': 0,
+    // allow async-await
+    'generator-star-spacing': 0,
+    'no-unused-vars': [0, { args: 'after-used', vars: 'local' }],
+    'no-constant-condition': 0,
+    // allow debugger during development
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+  },
+}

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "http://json.schemastore.org/prettierrc",
+  "semi": false,
+  "printWidth": 90,
+  "trailingComma": "all",
+  "singleQuote": true,
+  "endOfLine": "lf"
+}

+ 13 - 12
README.md

@@ -1,4 +1,5 @@
 # ollama
+
 Interface with an ollama instance over HTTP.
 
 ## Table of Contents
@@ -26,12 +27,12 @@ npm i ollama
 ## Usage
 
 ```javascript
-import { Ollama } from "ollama";
+import { Ollama } from 'ollama'
 
-const ollama = new Ollama();
+const ollama = new Ollama()
 
-for await (const token of ollama.generate("llama2", "What is a llama?")) {
-	process.stdout.write(token);
+for await (const token of ollama.generate('llama2', 'What is a llama?')) {
+  process.stdout.write(token)
 }
 ```
 
@@ -42,7 +43,7 @@ The API aims to mirror the [HTTP API for Ollama](https://github.com/jmorganca/ol
 ### Ollama
 
 ```javascript
-new Ollama(config);
+new Ollama(config)
 ```
 
 - `config` `<Object>` The configuration object for Ollama.
@@ -53,7 +54,7 @@ Create a new API handler for ollama.
 ### generate
 
 ```javascript
-ollama.generate(model, prompt, [options]);
+ollama.generate(model, prompt, [options])
 ```
 
 - `model` `<string>` The name of the model to use for the prompt.
@@ -70,7 +71,7 @@ Generate a response for a given prompt with a provided model. The final response
 ### create
 
 ```javascript
-ollama.create(name, path);
+ollama.create(name, path)
 ```
 
 - `name` `<string>` The name of the model.
@@ -82,7 +83,7 @@ Create a model from a Modelfile.
 ### tags
 
 ```javascript
-ollama.tags();
+ollama.tags()
 ```
 
 - Returns: `Promise<Tag[]>` A list of tags.
@@ -92,7 +93,7 @@ List models that are available locally.
 ### copy
 
 ```javascript
-ollama.copy(source, destination);
+ollama.copy(source, destination)
 ```
 
 - `source` `<string>` The name of the model to copy.
@@ -104,7 +105,7 @@ Copy a model. Creates a model with another name from an existing model.
 ### delete
 
 ```javascript
-ollama.delete(model);
+ollama.delete(model)
 ```
 
 - `model` `<string>` The name of the model to delete.
@@ -115,7 +116,7 @@ Delete a model and its data.
 ### pull
 
 ```javascript
-ollama.pull(name);
+ollama.pull(name)
 ```
 
 - `name` `<string>` The name of the model to download.
@@ -126,7 +127,7 @@ Download a model from a the model registry. Cancelled pulls are resumed from whe
 ### embeddings
 
 ```javascript
-ollama.embeddings(model, prompt, [parameters]);
+ollama.embeddings(model, prompt, [parameters])
 ```
 
 - `model` `<string>` The name of the model to generate embeddings for.

+ 18 - 18
jest.config.cjs

@@ -1,20 +1,20 @@
 /** @type {import('ts-jest').JestConfigWithTsJest} */
 module.exports = {
-	preset: "ts-jest",
-	testEnvironment: "node",
-	maxWorkers: 1,
-	extensionsToTreatAsEsm: [".ts"],
-	moduleNameMapper: {
-		"^(\\.{1,2}/.*)\\.js$": "$1"
-	},
-	transform: {
-		// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
-		// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
-		"^.+\\.tsx?$": [
-			"ts-jest",
-			{
-				useESM: true
-			}
-		]
-	}
-};
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+  maxWorkers: 1,
+  extensionsToTreatAsEsm: ['.ts'],
+  moduleNameMapper: {
+    '^(\\.{1,2}/.*)\\.js$': '$1',
+  },
+  transform: {
+    // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
+    // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
+    '^.+\\.tsx?$': [
+      'ts-jest',
+      {
+        useESM: true,
+      },
+    ],
+  },
+}

+ 2 - 0
package.json

@@ -6,6 +6,7 @@
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
   "scripts": {
+    "format": "prettier --write .",
     "test": "jest --config=jest.config.cjs ./test/*",
     "build": "mkdir -p dist && touch dist/cleanup && rm dist/* && tsc -b",
     "lint": "eslint ./src/* ./test/*",
@@ -27,6 +28,7 @@
     "eslint": "^8.29.0",
     "eslint-plugin-jest": "^27.1.4",
     "jest": "^29.3.0",
+    "prettier": "^3.2.4",
     "ts-jest": "^29.0.3",
     "typescript": "^4.8.4"
   },

+ 275 - 237
src/index.ts

@@ -6,31 +6,30 @@ import { createHash } from 'crypto';
 import { homedir } from 'os';
 
 import type {
-	Fetch,
-	Config,
-    GenerateRequest,
-    PullRequest,
-    PushRequest,
-    CreateRequest,
-    EmbeddingsRequest,
-	GenerateResponse,
-	EmbeddingsResponse,
-    ListResponse,
-    ProgressResponse,
-    ErrorResponse,
-    StatusResponse,
-    DeleteRequest,
-    CopyRequest,
-    ShowResponse,
-    ShowRequest,
-    ChatRequest,
-    ChatResponse,
-} from "./interfaces.js";
-
+  Fetch,
+  Config,
+  GenerateRequest,
+  PullRequest,
+  PushRequest,
+  CreateRequest,
+  EmbeddingsRequest,
+  GenerateResponse,
+  EmbeddingsResponse,
+  ListResponse,
+  ProgressResponse,
+  ErrorResponse,
+  StatusResponse,
+  DeleteRequest,
+  CopyRequest,
+  ShowResponse,
+  ShowRequest,
+  ChatRequest,
+  ChatResponse,
+} from './interfaces.js'
 
 export class Ollama {
-	private readonly config: Config;
-	private readonly fetch: Fetch;
+  private readonly config: Config
+  private readonly fetch: Fetch
 
     constructor (config?: Partial<Config>) {
         this.config = {
@@ -43,239 +42,278 @@ export class Ollama {
         }
     }
 
-    private async processStreamableRequest<T extends object>(endpoint: string, request: { stream?: boolean } & Record<string, any>): Promise<T | AsyncGenerator<T>> {
-        request.stream = request.stream ?? false;
-        const response = await utils.post(this.fetch, `${this.config.address}/api/${endpoint}`, { ...request });
-    
-        if (!response.body) {
-            throw new Error("Missing body");
-        }
-    
-        const itr = utils.parseJSON<T | ErrorResponse>(response.body);
-    
-        if (request.stream) {
-            return (async function* () {
-                for await (const message of itr) {
-                    if ('error' in message) {
-                        throw new Error(message.error);
-                    }
-                    yield message;
-                    // message will be done in the case of chat and generate
-                    // message will be success in the case of a progress response (pull, push, create)
-                    if ((message as any).done || (message as any).status === "success") {
-                        return;
-                    }
-                }
-                throw new Error("Did not receive done or success response in stream.");
-            })();
-        } else {
-            const message = await itr.next();
-            if (!message.value.done && (message.value as any).status !== "success") {
-                throw new Error("Expected a completed response.");
-            }
-            return message.value;
-        }
+  private async processStreamableRequest<T extends object>(
+    endpoint: string,
+    request: { stream?: boolean } & Record<string, any>,
+  ): Promise<T | AsyncGenerator<T>> {
+    request.stream = request.stream ?? false
+    const response = await utils.post(
+      this.fetch,
+      `${this.config.address}/api/${endpoint}`,
+      { ...request },
+    )
+
+    if (!response.body) {
+      throw new Error('Missing body')
     }
 
-	private async encodeImage(image: Uint8Array | Buffer | string): Promise<string> {
-		if (typeof image !== 'string') {
-			// image is Uint8Array or Buffer, convert it to base64
-			const result = Buffer.from(image).toString('base64');
-			return result;
-		}
-		const base64Pattern = /^[A-Za-z0-9+/]+={1,2}$/; // detect by checking for equals signs at the end
-		if (base64Pattern.test(image)) {
-			// the string is already base64 encoded
-			return 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');
-	}	
-
-    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);
-            }
+    const itr = utils.parseJSON<T | ErrorResponse>(response.body)
+
+    if (request.stream) {
+      return (async function* () {
+        for await (const message of itr) {
+          if ('error' in message) {
+            throw new Error(message.error)
+          }
+          yield message
+          // message will be done in the case of chat and generate
+          // message will be success in the case of a progress response (pull, push, create)
+          if ((message as any).done || (message as any).status === 'success') {
+            return
+          }
         }
-        return out.join('\n');
+        throw new Error('Did not receive done or success response in stream.')
+      })()
+    } else {
+      const message = await itr.next()
+      if (!message.value.done && (message.value as any).status !== 'success') {
+        throw new Error('Expected a completed response.')
+      }
+      return message.value
     }
+  }
 
-    private resolvePath(inputPath, mfDir) {
-        if (inputPath.startsWith('~')) {
-            return join(homedir(), inputPath.slice(1));
-        }
-        return resolve(mfDir, inputPath);
+  private async encodeImage(image: Uint8Array | Buffer | string): Promise<string> {
+    if (typeof image !== 'string') {
+      // image is Uint8Array or Buffer, convert it to base64
+      const result = Buffer.from(image).toString('base64')
+      return result
     }
-
-    private async fileExists(path: string): Promise<boolean> {
-        try {
-            await promises.access(path);
-            return true;
-        } catch {
-            return false;
-        }
+    const base64Pattern = /^[A-Za-z0-9+/]+={1,2}$/ // detect by checking for equals signs at the end
+    if (base64Pattern.test(image)) {
+      // the string is already base64 encoded
+      return image
     }
-
-    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.address}/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.address}/api/blobs/${digest}`, readableStream);
-            } else {
-                throw e;
-            }
+    // 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')
+  }
+
+  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')
+  }
 
-        return digest;
+  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
     }
+  }
 
-    generate(request: GenerateRequest & { stream: true }): Promise<AsyncGenerator<GenerateResponse>>;
-    generate(request: GenerateRequest & { stream?: false }): Promise<GenerateResponse>;
+  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.')
+    }
 
-    async generate(request: GenerateRequest): Promise<GenerateResponse | AsyncGenerator<GenerateResponse>> {
-        if (request.images) {
-            request.images = await Promise.all(request.images.map(this.encodeImage.bind(this)));
-        }
-        return this.processStreamableRequest<GenerateResponse>('generate', request);
+    // 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.address}/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.address}/api/blobs/${digest}`,
+          readableStream,
+        )
+      } else {
+        throw e
+      }
     }
 
-    chat(request: ChatRequest & { stream: true }): Promise<AsyncGenerator<ChatResponse>>;
-    chat(request: ChatRequest & { stream?: false }): Promise<ChatResponse>;
+    return digest
+  }
 
-    async chat(request: ChatRequest): Promise<ChatResponse | AsyncGenerator<ChatResponse>> {
-        if (request.messages) {
-            for (const message of request.messages) {
-                if (message.images) {
-                    message.images = await Promise.all(message.images.map(this.encodeImage.bind(this)));
-                }
-            }
-        }
-        return this.processStreamableRequest<ChatResponse>('chat', request);
-    }
+  generate(
+    request: GenerateRequest & { stream: true },
+  ): Promise<AsyncGenerator<GenerateResponse>>
+  generate(request: GenerateRequest & { stream?: false }): Promise<GenerateResponse>
 
-    pull(request: PullRequest & { stream: true }): Promise<AsyncGenerator<ProgressResponse>>;
-    pull(request: PullRequest & { stream?: false }): Promise<ProgressResponse>;
-
-    async pull (request: PullRequest):  Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> {
-        return this.processStreamableRequest<ProgressResponse>('pull', {
-			name: request.model,
-			stream: request.stream,
-			insecure: request.insecure,
-			username: request.username,
-			password: request.password,
-		});
-	}
-
-    push(request: PushRequest & { stream: true }): Promise<AsyncGenerator<ProgressResponse>>;
-    push(request: PushRequest & { stream?: false }): Promise<ProgressResponse>;
-
-    async push (request: PushRequest):  Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> {
-        return this.processStreamableRequest<ProgressResponse>('push', {
-			name: request.model,
-			stream: request.stream,
-			insecure: request.insecure,
-			username: request.username,
-			password: request.password,
-		});
-	}
-
-    create(request: CreateRequest & { stream: true }): Promise<AsyncGenerator<ProgressResponse>>;
-    create(request: CreateRequest & { stream?: false }): Promise<ProgressResponse>;
-
-	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');
+  async generate(
+    request: GenerateRequest,
+  ): Promise<GenerateResponse | AsyncGenerator<GenerateResponse>> {
+    if (request.images) {
+      request.images = await Promise.all(request.images.map(this.encodeImage.bind(this)))
+    }
+    return this.processStreamableRequest<GenerateResponse>('generate', request)
+  }
+
+  chat(request: ChatRequest & { stream: true }): Promise<AsyncGenerator<ChatResponse>>
+  chat(request: ChatRequest & { stream?: false }): Promise<ChatResponse>
+
+  async chat(request: ChatRequest): Promise<ChatResponse | AsyncGenerator<ChatResponse>> {
+    if (request.messages) {
+      for (const message of request.messages) {
+        if (message.images) {
+          message.images = await Promise.all(
+            message.images.map(this.encodeImage.bind(this)),
+          )
         }
-
-        return this.processStreamableRequest<ProgressResponse>('create', {
-            name: request.model,
-            stream: request.stream,
-            modelfile: modelfileContent,
-        });
-	}
-
-    async delete (request: DeleteRequest): Promise<StatusResponse> {
-		await utils.del(this.fetch, `${this.config.address}/api/delete`, { name: request.model });
-        return { status: "success" };
-	}
-    
-    async copy (request: CopyRequest): Promise<StatusResponse> {
-		await utils.post(this.fetch, `${this.config.address}/api/copy`, { ...request });
-        return { status: "success" };
-	}
-
-    async list (): Promise<ListResponse> {
-		const response = await utils.get(this.fetch, `${this.config.address}/api/tags`);
-		const listResponse = await response.json() as ListResponse;
-		return listResponse;
-	}
-
-    async show (request: ShowRequest): Promise<ShowResponse> {
-        const response = await utils.post(this.fetch, `${this.config.address}/api/show`, { ...request });
-        const showResponse = await response.json() as ShowResponse;
-        return showResponse;
+      }
+    }
+    return this.processStreamableRequest<ChatResponse>('chat', request)
+  }
+
+  pull(request: PullRequest & { stream: true }): Promise<AsyncGenerator<ProgressResponse>>
+  pull(request: PullRequest & { stream?: false }): Promise<ProgressResponse>
+
+  async pull(
+    request: PullRequest,
+  ): Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> {
+    return this.processStreamableRequest<ProgressResponse>('pull', {
+      name: request.model,
+      stream: request.stream,
+      insecure: request.insecure,
+      username: request.username,
+      password: request.password,
+    })
+  }
+
+  push(request: PushRequest & { stream: true }): Promise<AsyncGenerator<ProgressResponse>>
+  push(request: PushRequest & { stream?: false }): Promise<ProgressResponse>
+
+  async push(
+    request: PushRequest,
+  ): Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> {
+    return this.processStreamableRequest<ProgressResponse>('push', {
+      name: request.model,
+      stream: request.stream,
+      insecure: request.insecure,
+      username: request.username,
+      password: request.password,
+    })
+  }
+
+  create(
+    request: CreateRequest & { stream: true },
+  ): Promise<AsyncGenerator<ProgressResponse>>
+  create(request: CreateRequest & { stream?: false }): Promise<ProgressResponse>
+
+  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')
     }
 
-	async embeddings (request: EmbeddingsRequest): Promise<EmbeddingsResponse> {
-		const response = await utils.post(this.fetch, `${this.config.address}/api/embeddings`, { request });
-		const embeddingsResponse = await response.json() as EmbeddingsResponse;
-		return embeddingsResponse;
-	}
+    return this.processStreamableRequest<ProgressResponse>('create', {
+      name: request.model,
+      stream: request.stream,
+      modelfile: modelfileContent,
+    })
+  }
+
+  async delete(request: DeleteRequest): Promise<StatusResponse> {
+    await utils.del(this.fetch, `${this.config.address}/api/delete`, {
+      name: request.model,
+    })
+    return { status: 'success' }
+  }
+
+  async copy(request: CopyRequest): Promise<StatusResponse> {
+    await utils.post(this.fetch, `${this.config.address}/api/copy`, { ...request })
+    return { status: 'success' }
+  }
+
+  async list(): Promise<ListResponse> {
+    const response = await utils.get(this.fetch, `${this.config.address}/api/tags`)
+    const listResponse = (await response.json()) as ListResponse
+    return listResponse
+  }
+
+  async show(request: ShowRequest): Promise<ShowResponse> {
+    const response = await utils.post(this.fetch, `${this.config.address}/api/show`, {
+      ...request,
+    })
+    const showResponse = (await response.json()) as ShowResponse
+    return showResponse
+  }
+
+  async embeddings(request: EmbeddingsRequest): Promise<EmbeddingsResponse> {
+    const response = await utils.post(
+      this.fetch,
+      `${this.config.address}/api/embeddings`,
+      { request },
+    )
+    const embeddingsResponse = (await response.json()) as EmbeddingsResponse
+    return embeddingsResponse
+  }
 }
 
 export default new Ollama();

+ 123 - 123
src/interfaces.ts

@@ -1,194 +1,194 @@
 export type Fetch = typeof fetch
 
 export interface Config {
-	address: string,
-	fetch?: Fetch
+  address: string
+  fetch?: Fetch
 }
 
 // request types
 
 export interface Options {
-    numa: boolean;
-    num_ctx: number;
-    num_batch: number;
-    main_gpu: number;
-    low_vram: boolean;
-    f16_kv: boolean;
-    logits_all: boolean;
-    vocab_only: boolean;
-    use_mmap: boolean;
-    use_mlock: boolean;
-    embedding_only: boolean;
-    num_thread: number;
-
-    // Runtime options
-    num_keep: number;
-    seed: number;
-    num_predict: number;
-    top_k: number;
-    top_p: number;
-    tfs_z: number;
-    typical_p: number;
-    repeat_last_n: number;
-    temperature: number;
-    repeat_penalty: number;
-    presence_penalty: number;
-    frequency_penalty: number;
-    mirostat: number;
-    mirostat_tau: number;
-    mirostat_eta: number;
-    penalize_newline: boolean;
-    stop: string[];
+  numa: boolean
+  num_ctx: number
+  num_batch: number
+  main_gpu: number
+  low_vram: boolean
+  f16_kv: boolean
+  logits_all: boolean
+  vocab_only: boolean
+  use_mmap: boolean
+  use_mlock: boolean
+  embedding_only: boolean
+  num_thread: number
+
+  // Runtime options
+  num_keep: number
+  seed: number
+  num_predict: number
+  top_k: number
+  top_p: number
+  tfs_z: number
+  typical_p: number
+  repeat_last_n: number
+  temperature: number
+  repeat_penalty: number
+  presence_penalty: number
+  frequency_penalty: number
+  mirostat: number
+  mirostat_tau: number
+  mirostat_eta: number
+  penalize_newline: boolean
+  stop: string[]
 }
 
 export interface GenerateRequest {
-	model: string
-	prompt: string
-	system?: string
-	template?: string
-	context?: number[]
-	stream?: boolean
-	raw?: boolean
-	format?: string
-	images?: Uint8Array[] | string[]
+  model: string
+  prompt: string
+  system?: string
+  template?: string
+  context?: number[]
+  stream?: boolean
+  raw?: boolean
+  format?: string
+  images?: Uint8Array[] | string[]
 
-	options?: Partial<Options>
+  options?: Partial<Options>
 }
 
 export interface Message {
-	role: string
-	content: string
-	images?: Uint8Array[] | string[]
+  role: string
+  content: string
+  images?: Uint8Array[] | string[]
 }
 
 export interface ChatRequest {
-	model: string
-	messages?: Message[]
-	stream?: boolean
-	format?: string
+  model: string
+  messages?: Message[]
+  stream?: boolean
+  format?: string
 
-	options?: Partial<Options>
+  options?: Partial<Options>
 }
 
 export interface PullRequest {
-	model: string
-	insecure?: boolean
-	username?: string
-	password?: string
-	stream?: boolean
+  model: string
+  insecure?: boolean
+  username?: string
+  password?: string
+  stream?: boolean
 }
 
 export interface PushRequest {
-	model: string
-	insecure?: boolean
-	username?: string
-	password?: string
-	stream?: boolean
+  model: string
+  insecure?: boolean
+  username?: string
+  password?: string
+  stream?: boolean
 }
 
 export interface CreateRequest {
-	model: string
-	path?: string
-	modelfile?: string
-	stream?: boolean
+  model: string
+  path?: string
+  modelfile?: string
+  stream?: boolean
 }
 
 export interface DeleteRequest {
-	model: string
+  model: string
 }
 
 export interface CopyRequest {
-	source: string
-	destination: string
+  source: string
+  destination: string
 }
 
 export interface ShowRequest {
-	model: string
-	system?: string
-	template?: string
-	options?: Partial<Options>
+  model: string
+  system?: string
+  template?: string
+  options?: Partial<Options>
 }
 
 export interface EmbeddingsRequest {
-	model: string
-	prompt: string
+  model: string
+  prompt: string
 
-	options?: Partial<Options>
+  options?: Partial<Options>
 }
 
 // response types
 
 export interface GenerateResponse {
-	model: string
-	created_at: Date
-	response: string
-	done: boolean
-	context: number[]
-	total_duration: number
-	load_duration: number
-	prompt_eval_count: number
-	prompt_eval_duration: number
-	eval_count: number
-	eval_duration: number
+  model: string
+  created_at: Date
+  response: string
+  done: boolean
+  context: number[]
+  total_duration: number
+  load_duration: number
+  prompt_eval_count: number
+  prompt_eval_duration: number
+  eval_count: number
+  eval_duration: number
 }
 
 export interface ChatResponse {
-	model: string
-	created_at: Date
-	message: Message
-	done: boolean
-	total_duration: number
-	load_duration: number
-	prompt_eval_count: number
-	prompt_eval_duration: number
-	eval_count: number
-	eval_duration: number
+  model: string
+  created_at: Date
+  message: Message
+  done: boolean
+  total_duration: number
+  load_duration: number
+  prompt_eval_count: number
+  prompt_eval_duration: number
+  eval_count: number
+  eval_duration: number
 }
 
 export interface EmbeddingsResponse {
-	embedding: number[]
+  embedding: number[]
 }
 
 export interface ProgressResponse {
-	status: string
-	digest: string
-	total: number
-	completed: number
+  status: string
+  digest: string
+  total: number
+  completed: number
 }
 
 export interface ModelResponse {
-	name: string
-	modified_at: Date
-	size: number
-	digest: string
-	format: string
-	family: string
-	families: string[]
-	parameter_size: string
-	quatization_level: number
+  name: string
+  modified_at: Date
+  size: number
+  digest: string
+  format: string
+  family: string
+  families: string[]
+  parameter_size: string
+  quatization_level: number
 }
 
 export interface ShowResponse {
-	license: string
-	modelfile: string
-	parameters: string
-	template: string
-	system: string
-	format: string
-	family: string
-	families: string[]
-	parameter_size: string
-	quatization_level: number
+  license: string
+  modelfile: string
+  parameters: string
+  template: string
+  system: string
+  format: string
+  family: string
+  families: string[]
+  parameter_size: string
+  quatization_level: number
 }
 
 export interface ListResponse {
-	models: ModelResponse[]
+  models: ModelResponse[]
 }
 
 export interface ErrorResponse {
-	error: string
+  error: string
 }
 
 export interface StatusResponse {
-	status: string
+  status: string
 }

+ 110 - 101
src/utils.ts

@@ -1,115 +1,124 @@
-import type { Fetch, ErrorResponse } from "./interfaces.js";
+import type { Fetch, ErrorResponse } from './interfaces.js'
 
 export const formatAddress = (address: string): string => {
-	if (!address.startsWith("http://") && !address.startsWith("https://")) {
-		address = `http://${address}`;
-	}
+  if (!address.startsWith('http://') && !address.startsWith('https://')) {
+    address = `http://${address}`
+  }
 
-	while (address.endsWith("/")) {
-		address = address.substring(0, address.length - 1);
-	}
+  while (address.endsWith('/')) {
+    address = address.substring(0, address.length - 1)
+  }
 
-	return address;
-};
+  return address
+}
 
 const checkOk = async (response: Response): Promise<void> => {
-    if (!response.ok) {
-        let message = `Error ${response.status}: ${response.statusText}`;
-
-        if (response.headers.get('content-type')?.includes('application/json')) {
-            try {
-                const errorResponse = await response.json() as ErrorResponse;
-                message = errorResponse.error || message;
-            } catch(error) {
-                console.log("Failed to parse error response as JSON");
-            }
-        } else {
-            try {
-                console.log("Getting text from response");
-                const textResponse = await response.text();
-                message = textResponse || message;
-            } catch (error) {
-                console.log("Failed to get text from error response");
-            }
-        }
-
-        throw new Error(message);
+  if (!response.ok) {
+    let message = `Error ${response.status}: ${response.statusText}`
+
+    if (response.headers.get('content-type')?.includes('application/json')) {
+      try {
+        const errorResponse = (await response.json()) as ErrorResponse
+        message = errorResponse.error || message
+      } catch (error) {
+        console.log('Failed to parse error response as JSON')
+      }
+    } else {
+      try {
+        console.log('Getting text from response')
+        const textResponse = await response.text()
+        message = textResponse || message
+      } catch (error) {
+        console.log('Failed to get text from error response')
+      }
     }
-};
+
+    throw new Error(message)
+  }
+}
 
 export const get = async (fetch: Fetch, address: string): Promise<Response> => {
-	const response = await fetch(formatAddress(address));
+  const response = await fetch(formatAddress(address))
 
-	await checkOk(response);
+  await checkOk(response)
 
-	return response;
-};
+  return response
+}
 
 export const head = async (fetch: Fetch, address: string): Promise<Response> => {
-    const response = await fetch(formatAddress(address), {
-        method: "HEAD"
-    });
-
-    await checkOk(response);
-
-    return response;
-};
-
-export const post = async (fetch: Fetch, address: string, data?: Record<string, unknown> | BodyInit): Promise<Response> => {
-    const isRecord = (input: any): input is Record<string, unknown> => {
-        return input !== null && typeof input === 'object' && !Array.isArray(input);
-    };
-
-    const formattedData = isRecord(data) ? JSON.stringify(data) : data;
-
-    const response = await fetch(formatAddress(address), {
-        method: "POST",
-        body: formattedData
-    });
-
-    await checkOk(response);
-
-    return response;
-};
-
-
-export const del = async (fetch: Fetch, address: string, data?: Record<string, unknown>): Promise<Response> => {
-	const response = await fetch(formatAddress(address), {
-		method: "DELETE",
-		body: JSON.stringify(data)
-	});
-
-	await checkOk(response);
-
-	return response;
-};
-
-export const parseJSON = async function * <T = unknown>(itr: ReadableStream<Uint8Array>): AsyncGenerator<T> {
-	const decoder = new TextDecoder("utf-8");
-	let buffer = "";
-
-	// TS is a bit strange here, ReadableStreams are AsyncIterable but TS doesn't see it.
-	for await (const chunk of itr as unknown as AsyncIterable<Uint8Array>) {
-		buffer += decoder.decode(chunk);
-
-		const parts = buffer.split("\n");
-
-		buffer = parts.pop() ?? "";
-
-		for (const part of parts) {
-			try {
-				yield JSON.parse(part);
-			} catch (error) {
-				console.warn("invalid json: ", part);
-			}
-		}
-	}
+  const response = await fetch(formatAddress(address), {
+    method: 'HEAD',
+  })
+
+  await checkOk(response)
+
+  return response
+}
+
+export const post = async (
+  fetch: Fetch,
+  address: string,
+  data?: Record<string, unknown> | BodyInit,
+): Promise<Response> => {
+  const isRecord = (input: any): input is Record<string, unknown> => {
+    return input !== null && typeof input === 'object' && !Array.isArray(input)
+  }
+
+  const formattedData = isRecord(data) ? JSON.stringify(data) : data
+
+  const response = await fetch(formatAddress(address), {
+    method: 'POST',
+    body: formattedData,
+  })
+
+  await checkOk(response)
+
+  return response
+}
+
+export const del = async (
+  fetch: Fetch,
+  address: string,
+  data?: Record<string, unknown>,
+): Promise<Response> => {
+  const response = await fetch(formatAddress(address), {
+    method: 'DELETE',
+    body: JSON.stringify(data),
+  })
+
+  await checkOk(response)
+
+  return response
+}
+
+export const parseJSON = async function* <T = unknown>(
+  itr: ReadableStream<Uint8Array>,
+): AsyncGenerator<T> {
+  const decoder = new TextDecoder('utf-8')
+  let buffer = ''
+
+  // TS is a bit strange here, ReadableStreams are AsyncIterable but TS doesn't see it.
+  for await (const chunk of itr as unknown as AsyncIterable<Uint8Array>) {
+    buffer += decoder.decode(chunk)
+
+    const parts = buffer.split('\n')
+
+    buffer = parts.pop() ?? ''
+
+    for (const part of parts) {
+      try {
+        yield JSON.parse(part)
+      } catch (error) {
+        console.warn('invalid json: ', part)
+      }
+    }
+  }
 
-	for (const part of buffer.split("\n").filter(p => p !== "")) {
-		try {
-			yield JSON.parse(part);
-		} catch (error) {
-			console.warn("invalid json: ", part);
-		}
-	}
-};
+  for (const part of buffer.split('\n').filter((p) => p !== '')) {
+    try {
+      yield JSON.parse(part)
+    } catch (error) {
+      console.warn('invalid json: ', part)
+    }
+  }
+}

+ 3 - 3
test/index.spec.ts

@@ -1,3 +1,3 @@
-describe("Empty test", () => {
-	it("runs", () => {});
-});
+describe('Empty test', () => {
+  it('runs', () => {})
+})

+ 21 - 25
tsconfig.json

@@ -1,30 +1,26 @@
 {
-	"compilerOptions": {
-		"noImplicitAny": false,
-		"noImplicitThis": true,
-		"strictNullChecks": true,
-		"esModuleInterop": true,
-		"declaration": true,
-		"declarationMap": true,
-		"skipLibCheck": true,
-		"strict": true,
-		"forceConsistentCasingInFileNames": true,
-		"moduleResolution": "node",
-		"module": "ES2022",
-		"outDir": "./dist",
-		"target": "ES6"
-	},
+  "compilerOptions": {
+    "noImplicitAny": false,
+    "noImplicitThis": true,
+    "strictNullChecks": true,
+    "esModuleInterop": true,
+    "declaration": true,
+    "declarationMap": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "moduleResolution": "node",
+    "module": "ES2022",
+    "outDir": "./dist",
+    "target": "ES6",
+  },
 
-	"ts-node": {
-		"swc": true,
-		"esm": true
-	},
+  "ts-node": {
+    "swc": true,
+    "esm": true,
+  },
 
-	"include": [
-		"./src/**/*.ts"
-	],
+  "include": ["./src/**/*.ts"],
 
-	"exclude": [
-		"node_modules"
-	]
+  "exclude": ["node_modules"],
 }