|
@@ -1,10 +1,7 @@
|
|
|
-import * as utils from './utils.js'
|
|
|
import { AbortableAsyncIterator } from './utils.js'
|
|
|
|
|
|
-import fs, { createReadStream, promises } from 'fs'
|
|
|
-import { dirname, join, resolve } from 'path'
|
|
|
-import { createHash } from 'crypto'
|
|
|
-import { homedir } from 'os'
|
|
|
+import fs, { promises } from 'fs'
|
|
|
+import { resolve } from 'path'
|
|
|
import { Ollama as OllamaBrowser } from './browser.js'
|
|
|
|
|
|
import type { CreateRequest, ProgressResponse } from './interfaces.js'
|
|
@@ -28,47 +25,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(
|
|
|
- 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')
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 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
|
|
@@ -84,60 +40,6 @@ export class Ollama extends OllamaBrowser {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- 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.')
|
|
|
- }
|
|
|
-
|
|
|
- // 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}`
|
|
|
-
|
|
|
- try {
|
|
|
- await utils.head(this.fetch, `${this.config.host}/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.host}/api/blobs/${digest}`,
|
|
|
- readableStream,
|
|
|
- )
|
|
|
- } else {
|
|
|
- throw e
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return digest
|
|
|
- }
|
|
|
-
|
|
|
create(
|
|
|
request: CreateRequest & { stream: true },
|
|
|
): Promise<AbortableAsyncIterator<ProgressResponse>>
|
|
@@ -146,21 +48,12 @@ export class Ollama extends OllamaBrowser {
|
|
|
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(
|
|
|
- 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')
|
|
|
+ // fail if request.from is a local path
|
|
|
+ // TODO: https://github.com/ollama/ollama-js/issues/191
|
|
|
+ if (request.from && await this.fileExists(resolve(request.from))) {
|
|
|
+ throw Error('Creating with a local path is not currently supported from ollama-js')
|
|
|
}
|
|
|
- request.modelfile = modelfileContent
|
|
|
|
|
|
- // check stream here so that typescript knows which overload to use
|
|
|
if (request.stream) {
|
|
|
return super.create(request as CreateRequest & { stream: true })
|
|
|
} else {
|