diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9d824e9..1464e32 100644 --- a/.eslintrc.cjs +++ b/.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, + }, +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..69821bb --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/prettierrc", + "semi": false, + "printWidth": 90, + "trailingComma": "all", + "singleQuote": true, + "endOfLine": "lf" +} diff --git a/README.md b/README.md index 686e6f9..6819db2 100644 --- a/README.md +++ b/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` `` 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` `` 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` `` The name of the model. @@ -82,7 +83,7 @@ Create a model from a Modelfile. ### tags ```javascript -ollama.tags(); +ollama.tags() ``` - Returns: `Promise` 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` `` 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` `` 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` `` 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` `` The name of the model to generate embeddings for. diff --git a/jest.config.cjs b/jest.config.cjs index 7cabb68..2543874 100644 --- a/jest.config.cjs +++ b/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, + }, + ], + }, +} diff --git a/package.json b/package.json index 81ae70b..faee296 100644 --- a/package.json +++ b/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" }, diff --git a/src/index.ts b/src/index.ts index b2ed32e..f7b4420 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,281 +1,319 @@ -import * as utils from "./utils.js"; -import 'whatwg-fetch'; -import { promises, createReadStream } from 'fs'; -import { join, resolve, dirname } from 'path'; -import { createHash } from 'crypto'; -import { homedir } from 'os'; +import * as utils from './utils.js' +import 'whatwg-fetch' +import { promises, createReadStream } from 'fs' +import { join, resolve, dirname } from 'path' +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; - - constructor (config?: Partial) { - this.config = { - address: config?.address ?? "http://127.0.0.1:11434" - }; - - this.fetch = fetch; - if (config?.fetch != null) { - this.fetch = config.fetch; - } - } + private readonly config: Config + private readonly fetch: Fetch - private async processStreamableRequest(endpoint: string, request: { stream?: boolean } & Record): Promise> { - 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(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; - } + constructor(config?: Partial) { + this.config = { + address: config?.address ?? 'http://127.0.0.1:11434', } - private async encodeImage(image: Uint8Array | Buffer | string): Promise { - 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 { - 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'); + this.fetch = fetch + if (config?.fetch != null) { + this.fetch = config.fetch } - - private resolvePath(inputPath, mfDir) { - if (inputPath.startsWith('~')) { - return join(homedir(), inputPath.slice(1)); - } - return resolve(mfDir, inputPath); + } + + private async processStreamableRequest( + endpoint: string, + request: { stream?: boolean } & Record, + ): Promise> { + 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 fileExists(path: string): Promise { - try { - await promises.access(path); - return true; - } catch { - return false; + const itr = utils.parseJSON(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 createBlob(path: string): Promise { - 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((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; - } + private async encodeImage(image: Uint8Array | Buffer | string): Promise { + 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 { + 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 { + try { + await promises.access(path) + return true + } catch { + return false + } + } - generate(request: GenerateRequest & { stream: true }): Promise>; - generate(request: GenerateRequest & { stream?: false }): Promise; + private async createBlob(path: string): Promise { + 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> { - if (request.images) { - request.images = await Promise.all(request.images.map(this.encodeImage.bind(this))); - } - return this.processStreamableRequest('generate', request); + // Create a stream for reading the file + const fileStream = createReadStream(path) + + // Compute the SHA256 digest + 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(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>; - chat(request: ChatRequest & { stream?: false }): Promise; + return digest + } - async chat(request: ChatRequest): Promise> { - 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('chat', request); - } + generate( + request: GenerateRequest & { stream: true }, + ): Promise> + generate(request: GenerateRequest & { stream?: false }): Promise - pull(request: PullRequest & { stream: true }): Promise>; - pull(request: PullRequest & { stream?: false }): Promise; - - async pull (request: PullRequest): Promise> { - return this.processStreamableRequest('pull', { - name: request.model, - stream: request.stream, - insecure: request.insecure, - username: request.username, - password: request.password, - }); - } - - push(request: PushRequest & { stream: true }): Promise>; - push(request: PushRequest & { stream?: false }): Promise; - - async push (request: PushRequest): Promise> { - return this.processStreamableRequest('push', { - name: request.model, - stream: request.stream, - insecure: request.insecure, - username: request.username, - password: request.password, - }); - } - - create(request: CreateRequest & { stream: true }): Promise>; - create(request: CreateRequest & { stream?: false }): Promise; - - async create (request: CreateRequest): Promise> { - 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> { + if (request.images) { + request.images = await Promise.all(request.images.map(this.encodeImage.bind(this))) + } + return this.processStreamableRequest('generate', request) + } + + chat(request: ChatRequest & { stream: true }): Promise> + chat(request: ChatRequest & { stream?: false }): Promise + + async chat(request: ChatRequest): Promise> { + 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('create', { - name: request.model, - stream: request.stream, - modelfile: modelfileContent, - }); - } - - async delete (request: DeleteRequest): Promise { - await utils.del(this.fetch, `${this.config.address}/api/delete`, { name: request.model }); - return { status: "success" }; - } - - async copy (request: CopyRequest): Promise { - await utils.post(this.fetch, `${this.config.address}/api/copy`, { ...request }); - return { status: "success" }; - } - - async list (): Promise { - 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 { - 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('chat', request) + } + + pull(request: PullRequest & { stream: true }): Promise> + pull(request: PullRequest & { stream?: false }): Promise + + async pull( + request: PullRequest, + ): Promise> { + return this.processStreamableRequest('pull', { + name: request.model, + stream: request.stream, + insecure: request.insecure, + username: request.username, + password: request.password, + }) + } + + push(request: PushRequest & { stream: true }): Promise> + push(request: PushRequest & { stream?: false }): Promise + + async push( + request: PushRequest, + ): Promise> { + return this.processStreamableRequest('push', { + name: request.model, + stream: request.stream, + insecure: request.insecure, + username: request.username, + password: request.password, + }) + } + + create( + request: CreateRequest & { stream: true }, + ): Promise> + create(request: CreateRequest & { stream?: false }): Promise + + async create( + request: CreateRequest, + ): Promise> { + 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 { - 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('create', { + name: request.model, + stream: request.stream, + modelfile: modelfileContent, + }) + } + + async delete(request: DeleteRequest): Promise { + await utils.del(this.fetch, `${this.config.address}/api/delete`, { + name: request.model, + }) + return { status: 'success' } + } + + async copy(request: CopyRequest): Promise { + await utils.post(this.fetch, `${this.config.address}/api/copy`, { ...request }) + return { status: 'success' } + } + + async list(): Promise { + 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 { + 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 { + 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(); +export default new Ollama() diff --git a/src/interfaces.ts b/src/interfaces.ts index d290802..1859842 100644 --- a/src/interfaces.ts +++ b/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?: Partial } 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?: Partial } 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 + model: string + system?: string + template?: string + options?: Partial } export interface EmbeddingsRequest { - model: string - prompt: string + model: string + prompt: string - options?: Partial + options?: Partial } // 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 } diff --git a/src/utils.ts b/src/utils.ts index da55510..0389f11 100644 --- a/src/utils.ts +++ b/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 => { - 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 => { - 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 => { - const response = await fetch(formatAddress(address), { - method: "HEAD" - }); - - await checkOk(response); - - return response; -}; - -export const post = async (fetch: Fetch, address: string, data?: Record | BodyInit): Promise => { - const isRecord = (input: any): input is Record => { - 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): Promise => { - const response = await fetch(formatAddress(address), { - method: "DELETE", - body: JSON.stringify(data) - }); - - await checkOk(response); - - return response; -}; - -export const parseJSON = async function * (itr: ReadableStream): AsyncGenerator { - 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) { - 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 | BodyInit, +): Promise => { + const isRecord = (input: any): input is Record => { + 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, +): Promise => { + const response = await fetch(formatAddress(address), { + method: 'DELETE', + body: JSON.stringify(data), + }) + + await checkOk(response) + + return response +} + +export const parseJSON = async function* ( + itr: ReadableStream, +): AsyncGenerator { + 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) { + 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) + } + } +} diff --git a/test/index.spec.ts b/test/index.spec.ts index 382133f..dc90741 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,3 +1,3 @@ -describe("Empty test", () => { - it("runs", () => {}); -}); +describe('Empty test', () => { + it('runs', () => {}) +}) diff --git a/tsconfig.json b/tsconfig.json index 7dda50a..102fe1f 100644 --- a/tsconfig.json +++ b/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"], }