diff --git a/.gitignore b/.gitignore index c6bba59..d305a15 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,9 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# IDEs +.idea + +# MacOS +.DS_Store \ No newline at end of file diff --git a/src/browser.ts b/src/browser.ts index d3721f9..81ce484 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -2,25 +2,25 @@ import * as utils from './utils.js' import 'whatwg-fetch' import type { - Fetch, + ChatRequest, + ChatResponse, Config, - GenerateRequest, - PullRequest, - PushRequest, + CopyRequest, + CreateRequest, + DeleteRequest, EmbeddingsRequest, - GenerateResponse, EmbeddingsResponse, + ErrorResponse, + Fetch, + GenerateRequest, + GenerateResponse, ListResponse, ProgressResponse, - ErrorResponse, - StatusResponse, - DeleteRequest, - CopyRequest, - ShowResponse, + PullRequest, + PushRequest, ShowRequest, - ChatRequest, - ChatResponse, - CreateRequest, + ShowResponse, + StatusResponse, } from './interfaces.js' export class Ollama { @@ -50,6 +50,17 @@ export class Ollama { this.abortController = new AbortController() } + /** + * Processes a request to the Ollama server. If the request is streamable, it will return an + * AsyncGenerator that yields the response messages. Otherwise, it will return the response + * object. + * @param endpoint {string} - The endpoint to send the request to. + * @param request {object} - The request object to send to the endpoint. + * @protected {T | AsyncGenerator} - The response object or an AsyncGenerator that yields + * response messages. + * @throws {Error} - If the response body is missing or if the response is an error. + * @returns {Promise>} - The response object or an AsyncGenerator that yields the streamed response. + */ protected async processStreamableRequest( endpoint: string, request: { stream?: boolean } & Record, @@ -94,13 +105,17 @@ export class Ollama { } } + /** + * Encodes an image to base64 if it is a Uint8Array. + * @param image {Uint8Array | string} - The image to encode. + * @returns {Promise} - The base64 encoded image. + */ async encodeImage(image: Uint8Array | string): Promise { if (typeof image !== 'string') { // image is Uint8Array convert it to base64 const uint8Array = new Uint8Array(image) const numberArray = Array.from(uint8Array) - const base64String = btoa(String.fromCharCode.apply(null, numberArray)) - return base64String + return btoa(String.fromCharCode.apply(null, numberArray)) } // the string may be base64 encoded return image @@ -110,7 +125,12 @@ export class Ollama { request: GenerateRequest & { stream: true }, ): Promise> generate(request: GenerateRequest & { stream?: false }): Promise - + /** + * Generates a response from a text prompt. + * @param request {GenerateRequest} - The request object. + * @returns {Promise>} - The response object or + * an AsyncGenerator that yields response messages. + */ async generate( request: GenerateRequest, ): Promise> { @@ -122,7 +142,14 @@ export class Ollama { chat(request: ChatRequest & { stream: true }): Promise> chat(request: ChatRequest & { stream?: false }): Promise - + /** + * Chats with the model. The request object can contain messages with images that are either + * Uint8Arrays or base64 encoded strings. The images will be base64 encoded before sending the + * request. + * @param request {ChatRequest} - The request object. + * @returns {Promise>} - The response object or an + * AsyncGenerator that yields response messages. + */ async chat(request: ChatRequest): Promise> { if (request.messages) { for (const message of request.messages) { @@ -140,7 +167,11 @@ export class Ollama { request: CreateRequest & { stream: true }, ): Promise> create(request: CreateRequest & { stream?: false }): Promise - + /** + * Creates a new model from a stream of data. + * @param request {CreateRequest} - The request object. + * @returns {Promise>} - The response object or a stream of progress responses. + */ async create( request: CreateRequest, ): Promise> { @@ -154,7 +185,13 @@ export class Ollama { pull(request: PullRequest & { stream: true }): Promise> pull(request: PullRequest & { stream?: false }): Promise - + /** + * Pulls a model from the Ollama registry. The request object can contain a stream flag to indicate if the + * response should be streamed. + * @param request {PullRequest} - The request object. + * @returns {Promise>} - The response object or + * an AsyncGenerator that yields response messages. + */ async pull( request: PullRequest, ): Promise> { @@ -167,7 +204,13 @@ export class Ollama { push(request: PushRequest & { stream: true }): Promise> push(request: PushRequest & { stream?: false }): Promise - + /** + * Pushes a model to the Ollama registry. The request object can contain a stream flag to indicate if the + * response should be streamed. + * @param request {PushRequest} - The request object. + * @returns {Promise>} - The response object or + * an AsyncGenerator that yields response messages. + */ async push( request: PushRequest, ): Promise> { @@ -178,6 +221,12 @@ export class Ollama { }) } + /** + * Deletes a model from the server. The request object should contain the name of the model to + * delete. + * @param request {DeleteRequest} - The request object. + * @returns {Promise} - The response object. + */ async delete(request: DeleteRequest): Promise { await utils.del(this.fetch, `${this.config.host}/api/delete`, { name: request.model, @@ -185,31 +234,49 @@ export class Ollama { return { status: 'success' } } + /** + * Copies a model from one name to another. The request object should contain the name of the + * model to copy and the new name. + * @param request {CopyRequest} - The request object. + * @returns {Promise} - The response object. + */ async copy(request: CopyRequest): Promise { await utils.post(this.fetch, `${this.config.host}/api/copy`, { ...request }) return { status: 'success' } } + /** + * Lists the models on the server. + * @returns {Promise} - The response object. + * @throws {Error} - If the response body is missing. + */ async list(): Promise { const response = await utils.get(this.fetch, `${this.config.host}/api/tags`) - const listResponse = (await response.json()) as ListResponse - return listResponse + return (await response.json()) as ListResponse } + /** + * Shows the metadata of a model. The request object should contain the name of the model. + * @param request {ShowRequest} - The request object. + * @returns {Promise} - The response object. + */ async show(request: ShowRequest): Promise { const response = await utils.post(this.fetch, `${this.config.host}/api/show`, { ...request, }) - const showResponse = (await response.json()) as ShowResponse - return showResponse + return (await response.json()) as ShowResponse } + /** + * Embeds a text prompt into a vector. + * @param request {EmbeddingsRequest} - The request object. + * @returns {Promise} - The response object. + */ async embeddings(request: EmbeddingsRequest): Promise { const response = await utils.post(this.fetch, `${this.config.host}/api/embeddings`, { ...request, }) - const embeddingsResponse = (await response.json()) as EmbeddingsResponse - return embeddingsResponse + return (await response.json()) as EmbeddingsResponse } } diff --git a/src/index.ts b/src/index.ts index f75ac60..b00882e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import * as utils from './utils.js' -import fs, { promises, createReadStream } from 'fs' -import { join, resolve, dirname } from 'path' +import fs, { createReadStream, promises } from 'fs' +import { dirname, join, resolve } from 'path' import { createHash } from 'crypto' import { homedir } from 'os' import { Ollama as OllamaBrowser } from './browser.js' @@ -11,8 +11,7 @@ export class Ollama extends OllamaBrowser { 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 + return Buffer.from(image).toString('base64') } try { if (fs.existsSync(image)) { @@ -27,6 +26,12 @@ 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( modelfile: string, mfDir: string = process.cwd(), @@ -49,6 +54,12 @@ export class Ollama extends OllamaBrowser { return out.join('\n') } + /** + * 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)) @@ -56,6 +67,12 @@ export class Ollama extends OllamaBrowser { return resolve(mfDir, inputPath) } + /** + * checks if a file exists + * @param path {string} - The path to the file + * @private @internal + * @returns {Promise} - Whether the file exists or not + */ private async fileExists(path: string): Promise { try { await promises.access(path) diff --git a/src/utils.ts b/src/utils.ts index 73543bc..b4235a5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,10 @@ import { version } from './version.js' import type { Fetch, ErrorResponse } from './interfaces.js' +/** + * An error class for response errors. + * @extends Error + */ class ResponseError extends Error { constructor( public error: string, @@ -15,33 +19,43 @@ class ResponseError extends Error { } } +/** + * Checks if the response is ok, if not throws an error. + * If the response is not ok, it will try to parse the response as JSON and use the error field as the error message. + * @param response {Response} - The response object to check + */ const checkOk = async (response: Response): Promise => { - if (!response.ok) { - let message = `Error ${response.status}: ${response.statusText}` - let errorData: ErrorResponse | null = null + if (response.ok) { + return + } + let message = `Error ${response.status}: ${response.statusText}` + let errorData: ErrorResponse | null = null - if (response.headers.get('content-type')?.includes('application/json')) { - try { - errorData = (await response.json()) as ErrorResponse - message = errorData.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') - } + if (response.headers.get('content-type')?.includes('application/json')) { + try { + errorData = (await response.json()) as ErrorResponse + message = errorData.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 ResponseError(message, response.status) } + + throw new ResponseError(message, response.status) } -function getPlatform() { +/** + * Returns the platform string based on the environment. + * @returns {string} - The platform string + */ +function getPlatform(): string { if (typeof window !== 'undefined' && window.navigator) { return `${window.navigator.platform.toLowerCase()} Browser/${navigator.userAgent};` } else if (typeof process !== 'undefined') { @@ -50,6 +64,13 @@ function getPlatform() { return '' // unknown } +/** + * A wrapper around fetch that adds default headers. + * @param fetch {Fetch} - The fetch function to use + * @param url {string} - The URL to fetch + * @param options {RequestInit} - The fetch options + * @returns {Promise} - The fetch response + */ const fetchWithHeaders = async ( fetch: Fetch, url: string, @@ -73,6 +94,12 @@ const fetchWithHeaders = async ( return fetch(url, options) } +/** + * A wrapper around the get method that adds default headers. + * @param fetch {Fetch} - The fetch function to use + * @param host {string} - The host to fetch + * @returns {Promise} - The fetch response + */ export const get = async (fetch: Fetch, host: string): Promise => { const response = await fetchWithHeaders(fetch, host) @@ -80,7 +107,12 @@ export const get = async (fetch: Fetch, host: string): Promise => { return response } - +/** + * A wrapper around the head method that adds default headers. + * @param fetch {Fetch} - The fetch function to use + * @param host {string} - The host to fetch + * @returns {Promise} - The fetch response + */ export const head = async (fetch: Fetch, host: string): Promise => { const response = await fetchWithHeaders(fetch, host, { method: 'HEAD', @@ -90,7 +122,14 @@ export const head = async (fetch: Fetch, host: string): Promise => { return response } - +/** + * A wrapper around the post method that adds default headers. + * @param fetch {Fetch} - The fetch function to use + * @param host {string} - The host to fetch + * @param data {Record | BodyInit} - The data to send + * @param options {{ signal: AbortSignal }} - The fetch options + * @returns {Promise} - The fetch response + */ export const post = async ( fetch: Fetch, host: string, @@ -113,7 +152,13 @@ export const post = async ( return response } - +/** + * A wrapper around the delete method that adds default headers. + * @param fetch {Fetch} - The fetch function to use + * @param host {string} - The host to fetch + * @param data {Record} - The data to send + * @returns {Promise} - The fetch response + */ export const del = async ( fetch: Fetch, host: string, @@ -128,7 +173,11 @@ export const del = async ( return response } - +/** + * Parses a ReadableStream of Uint8Array into JSON objects. + * @param itr {ReadableStream} - The stream to parse + * @returns {AsyncGenerator} - The parsed JSON objects + */ export const parseJSON = async function* ( itr: ReadableStream, ): AsyncGenerator { @@ -167,7 +216,11 @@ export const parseJSON = async function* ( } } } - +/** + * Formats the host string to include the protocol and port. + * @param host {string} - The host string to format + * @returns {string} - The formatted host string + */ export const formatHost = (host: string): string => { if (!host) { return 'http://127.0.0.1:11434'