mirror of
https://github.com/ollama/ollama-js.git
synced 2026-07-01 11:16:25 -04:00
add prettier config
This commit is contained in:
+23
-37
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"printWidth": 90,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
|
||||
describe("Empty test", () => {
|
||||
it("runs", () => {});
|
||||
});
|
||||
describe('Empty test', () => {
|
||||
it('runs', () => {})
|
||||
})
|
||||
|
||||
+21
-25
@@ -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"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user