add prettier config

This commit is contained in:
Bruce MacDonald
2024-01-19 11:38:33 -05:00
parent 6ff57b326e
commit 9ff3b3137e
10 changed files with 582 additions and 542 deletions
+23 -37
View File
@@ -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
View File
@@ -0,0 +1,8 @@
{
"$schema": "http://json.schemastore.org/prettierrc",
"semi": false,
"printWidth": 90,
"trailingComma": "all",
"singleQuote": true,
"endOfLine": "lf"
}
+13 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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"
},
+278 -240
View File
@@ -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");
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
}
}
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.");
})();
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 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 {
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;
out.push(`${command} ${args}`)
}
} else {
out.push(line)
}
}
return out.join('\n')
}
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
}
}
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.')
}
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');
}
// Create a stream for reading the file
const fileStream = createReadStream(path)
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');
// 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
}
}
private resolvePath(inputPath, mfDir) {
if (inputPath.startsWith('~')) {
return join(homedir(), inputPath.slice(1));
return digest
}
generate(
request: GenerateRequest & { stream: true },
): Promise<AsyncGenerator<GenerateResponse>>
generate(request: GenerateRequest & { stream?: false }): Promise<GenerateResponse>
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 resolve(mfDir, inputPath);
}
}
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')
}
private async fileExists(path: string): Promise<boolean> {
try {
await promises.access(path);
return true;
} catch {
return false;
}
}
return this.processStreamableRequest<ProgressResponse>('create', {
name: request.model,
stream: request.stream,
modelfile: modelfileContent,
})
}
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 delete(request: DeleteRequest): Promise<StatusResponse> {
await utils.del(this.fetch, `${this.config.address}/api/delete`, {
name: request.model,
})
return { status: 'success' }
}
// Create a stream for reading the file
const fileStream = createReadStream(path);
async copy(request: CopyRequest): Promise<StatusResponse> {
await utils.post(this.fetch, `${this.config.address}/api/copy`, { ...request })
return { status: 'success' }
}
// 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);
});
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
}
const digest = `sha256:${sha256sum}`;
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
}
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;
}
}
return digest;
}
generate(request: GenerateRequest & { stream: true }): Promise<AsyncGenerator<GenerateResponse>>;
generate(request: GenerateRequest & { stream?: false }): Promise<GenerateResponse>;
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<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');
}
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;
}
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();
+122 -122
View File
@@ -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;
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[];
// 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
}
+94 -85
View File
@@ -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.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.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"
});
const response = await fetch(formatAddress(address), {
method: 'HEAD',
})
await checkOk(response);
await checkOk(response)
return 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);
};
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 formattedData = isRecord(data) ? JSON.stringify(data) : data
const response = await fetch(formatAddress(address), {
method: "POST",
body: formattedData
});
const response = await fetch(formatAddress(address), {
method: 'POST',
body: formattedData,
})
await checkOk(response);
await checkOk(response)
return 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),
})
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)
await checkOk(response);
return response
}
return response;
};
export const parseJSON = async function* <T = unknown>(
itr: ReadableStream<Uint8Array>,
): AsyncGenerator<T> {
const decoder = new TextDecoder('utf-8')
let buffer = ''
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)
// 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')
const parts = buffer.split("\n");
buffer = parts.pop() ?? ''
buffer = parts.pop() ?? "";
for (const part of parts) {
try {
yield JSON.parse(part)
} catch (error) {
console.warn('invalid json: ', part)
}
}
}
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
View File
@@ -1,3 +1,3 @@
describe("Empty test", () => {
it("runs", () => {});
});
describe('Empty test', () => {
it('runs', () => {})
})
+21 -25
View File
@@ -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"],
}