|
@@ -2,13 +2,13 @@ import * as utils from './utils.js'
|
|
|
import { AbortableAsyncIterator } from './utils.js'
|
|
|
|
|
|
import fs, { createReadStream, promises } from 'fs'
|
|
|
-import { join, resolve } from 'path'
|
|
|
+import { join, resolve, basename } from 'path'
|
|
|
import { createHash } from 'crypto'
|
|
|
import { homedir } from 'os'
|
|
|
+import glob from 'glob'
|
|
|
import { Ollama as OllamaBrowser } from './browser.js'
|
|
|
|
|
|
import type { CreateRequest, ProgressResponse } from './interfaces.js'
|
|
|
-import { a } from 'vitest/dist/chunks/suite.B2jumIFP.js'
|
|
|
|
|
|
export class Ollama extends OllamaBrowser {
|
|
|
async encodeImage(image: Uint8Array | Buffer | string): Promise<string> {
|
|
@@ -29,125 +29,6 @@ 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(
|
|
|
- model: string,
|
|
|
- modelfile: string,
|
|
|
- baseDir: string = process.cwd(),
|
|
|
- ): Promise<CreateRequest> {
|
|
|
- const lines = modelfile.split('\n');
|
|
|
- const request: CreateRequest = {
|
|
|
- model,
|
|
|
- files: {},
|
|
|
- adapters: {},
|
|
|
- parameters: {},
|
|
|
- };
|
|
|
-
|
|
|
- let multilineBuffer = '';
|
|
|
- let currentCommand = '';
|
|
|
-
|
|
|
- for (const line of lines) {
|
|
|
- const [command, ...rest] = line.split(' ');
|
|
|
- let lineArgs = rest.join(' ').trim();
|
|
|
-
|
|
|
- // Handle multiline arguments
|
|
|
- if (lineArgs.startsWith('"""')) {
|
|
|
- if (lineArgs.endsWith('"""') && lineArgs.length > 6) {
|
|
|
- // Single-line block
|
|
|
- multilineBuffer = lineArgs.slice(3, -3);
|
|
|
- } else {
|
|
|
- // Start multiline block
|
|
|
- multilineBuffer = lineArgs.slice(3);
|
|
|
- currentCommand = command.toUpperCase();
|
|
|
- continue;
|
|
|
- }
|
|
|
- } else if (multilineBuffer) {
|
|
|
- // Accumulate multiline content
|
|
|
- if (lineArgs.endsWith('"""')) {
|
|
|
- multilineBuffer += '\n' + lineArgs.slice(0, -3);
|
|
|
- lineArgs = multilineBuffer;
|
|
|
- multilineBuffer = '';
|
|
|
- } else {
|
|
|
- multilineBuffer += '\n' + lineArgs;
|
|
|
- continue;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const args = multilineBuffer || lineArgs.replace(/^"(.*)"$/, '$1');
|
|
|
-
|
|
|
- // Handle commands
|
|
|
- switch ((currentCommand || command).toUpperCase()) {
|
|
|
- case 'FROM': {
|
|
|
- const path = this.resolvePath(args, baseDir);
|
|
|
- if (await this.fileExists(path)) {
|
|
|
- request.files = {
|
|
|
- ...request.files,
|
|
|
- [args]: await this.createBlob(path),
|
|
|
- };
|
|
|
- } else {
|
|
|
- request.from = args;
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'ADAPTER': {
|
|
|
- const path = this.resolvePath(args, baseDir);
|
|
|
- if (await this.fileExists(path)) {
|
|
|
- request.adapters = {
|
|
|
- ...request.adapters,
|
|
|
- [args]: await this.createBlob(path),
|
|
|
- };
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'TEMPLATE':
|
|
|
- request.template = args;
|
|
|
- break;
|
|
|
- case 'SYSTEM':
|
|
|
- request.system = args;
|
|
|
- break;
|
|
|
- case 'MESSAGE': {
|
|
|
- const [role, content] = args.split(': ', 2);
|
|
|
- request.messages = request.messages || [];
|
|
|
- request.messages.push({ role, content });
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'LICENSE':
|
|
|
- request.license = request.license || [];
|
|
|
- request.license.push(args);
|
|
|
- break;
|
|
|
- default: {
|
|
|
- if (!request.parameters) {
|
|
|
- request.parameters = {}
|
|
|
- }
|
|
|
- request.parameters[command.toLowerCase()] = args
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- currentCommand = '';
|
|
|
- multilineBuffer = '';
|
|
|
- }
|
|
|
-
|
|
|
- return request;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 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))
|
|
|
- }
|
|
|
- return resolve(mfDir, inputPath)
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* checks if a file exists
|
|
|
* @param path {string} - The path to the file
|
|
@@ -170,23 +51,19 @@ export class Ollama extends OllamaBrowser {
|
|
|
throw new Error('Streaming uploads are not supported in this environment.')
|
|
|
}
|
|
|
|
|
|
- // Create a stream for reading the file
|
|
|
- const fileStream = createReadStream(path)
|
|
|
|
|
|
- // Compute the SHA256 digest
|
|
|
- const sha256sum = await new Promise<string>((resolve, reject) => {
|
|
|
- const hash = createHash('sha256')
|
|
|
- fileStream.on('data', (data) => hash.update(data))
|
|
|
- fileStream.on('end', () => resolve(hash.digest('hex')))
|
|
|
- fileStream.on('error', reject)
|
|
|
- })
|
|
|
-
|
|
|
- const digest = `sha256:${sha256sum}`
|
|
|
+ const hash = createHash('sha256')
|
|
|
+ const stream = createReadStream(path)
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ hash.update(chunk)
|
|
|
+ }
|
|
|
+ const digest = `sha256:${hash.digest('hex')}`
|
|
|
|
|
|
try {
|
|
|
await utils.head(this.fetch, `${this.config.host}/api/blobs/${digest}`)
|
|
|
} catch (e) {
|
|
|
if (e instanceof Error && e.message.includes('404')) {
|
|
|
+ const fileStream = createReadStream(path)
|
|
|
// Create a new readable stream for the fetch request
|
|
|
const readableStream = new ReadableStream({
|
|
|
start(controller) {
|
|
@@ -216,31 +93,93 @@ export class Ollama extends OllamaBrowser {
|
|
|
|
|
|
return digest
|
|
|
}
|
|
|
+
|
|
|
+ async findModelFiles(path: string): Promise<string[]> {
|
|
|
+ const files: string[] = []
|
|
|
+ const modelPath = resolve(path)
|
|
|
+
|
|
|
+ // Check for various model file patterns
|
|
|
+ const patterns = [
|
|
|
+ 'model*.safetensors',
|
|
|
+ 'adapters.safetensors',
|
|
|
+ 'adapter_model.safetensors',
|
|
|
+ 'pytorch_model*.bin',
|
|
|
+ 'consolidated*.pth',
|
|
|
+ '*.gguf',
|
|
|
+ '*.bin'
|
|
|
+ ]
|
|
|
+
|
|
|
+ // Look for model files
|
|
|
+ for (const pattern of patterns) {
|
|
|
+ const matches = glob.sync(join(modelPath, pattern))
|
|
|
+ if (matches.length > 0) {
|
|
|
+ files.push(...matches)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (files.length === 0) {
|
|
|
+ throw new Error('No model files found')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add config and tokenizer files
|
|
|
+ try {
|
|
|
+ const configFiles = glob.sync(join(modelPath, '*.json'))
|
|
|
+ files.push(...configFiles)
|
|
|
+
|
|
|
+ const nestedConfigFiles = glob.sync(join(modelPath, '**/*.json'))
|
|
|
+ files.push(...nestedConfigFiles)
|
|
|
+
|
|
|
+ const tokenizerFiles = glob.sync(join(modelPath, 'tokenizer.model'))
|
|
|
+ if (tokenizerFiles.length > 0) {
|
|
|
+ files.push(...tokenizerFiles)
|
|
|
+ } else {
|
|
|
+ const nestedTokenizerFiles = glob.sync(join(modelPath, '**/tokenizer.model'))
|
|
|
+ files.push(...nestedTokenizerFiles)
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // Continue if config/tokenizer files not found
|
|
|
+ }
|
|
|
+
|
|
|
+ return files
|
|
|
+ }
|
|
|
+
|
|
|
+ async files(from: string): Promise<Record<string, string>> {
|
|
|
+ // Check if from is a local file/directory
|
|
|
+ const exists = await this.fileExists(from)
|
|
|
+ if (!exists) {
|
|
|
+ // If not a local path, assume it's a model name
|
|
|
+ return {}
|
|
|
+ }
|
|
|
+
|
|
|
+ const fileMap: Record<string, string> = {}
|
|
|
+ const stats = await promises.stat(from)
|
|
|
+ let files: string[]
|
|
|
+ if (stats.isDirectory()) {
|
|
|
+ files = await this.findModelFiles(from)
|
|
|
+ } else {
|
|
|
+ files = [from]
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const file of files) {
|
|
|
+ const digest = await this.createBlob(file)
|
|
|
+ fileMap[basename(file)] = digest
|
|
|
+ }
|
|
|
+
|
|
|
+ return fileMap
|
|
|
+ }
|
|
|
|
|
|
create(
|
|
|
request: CreateRequest & { stream: true },
|
|
|
): Promise<AbortableAsyncIterator<ProgressResponse>>
|
|
|
create(request: CreateRequest & { stream?: false }): Promise<ProgressResponse>
|
|
|
|
|
|
- async create(
|
|
|
- request: CreateRequest,
|
|
|
- ): Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>> {
|
|
|
- // let modelfileContent = ''
|
|
|
- // if (request.path) {
|
|
|
- // modelfileContent = await promises.readFile(request.path, { encoding: 'utf8' })
|
|
|
- // modelfileContent = await this.parseModelfile(
|
|
|
- // request.model,
|
|
|
- // modelfileContent,
|
|
|
- // dirname(request.path),
|
|
|
- // )
|
|
|
- // } else if (request.modelfile) {
|
|
|
- // modelfileContent = await this.parseModelfile(request.model, request.modelfile)
|
|
|
- // } else {
|
|
|
- // throw new Error('Must provide either path or modelfile to create a model')
|
|
|
- // }
|
|
|
- // request.modelfile = modelfileContent
|
|
|
-
|
|
|
- // check stream here so that typescript knows which overload to use
|
|
|
+ async create(request: CreateRequest): Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>> {
|
|
|
+ if (request.from && !request.files) {
|
|
|
+ request.files = await this.files(request.from)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle stream flag
|
|
|
if (request.stream) {
|
|
|
return super.create(request as CreateRequest & { stream: true })
|
|
|
} else {
|