|
@@ -1,9 +1,5 @@
|
|
|
import * as utils from './utils.js'
|
|
|
import 'whatwg-fetch'
|
|
|
-import fs, { promises, createReadStream } from 'fs'
|
|
|
-import { join, resolve, dirname } from 'path'
|
|
|
-import { createHash } from 'crypto'
|
|
|
-import { homedir } from 'os'
|
|
|
|
|
|
import type {
|
|
|
Fetch,
|
|
@@ -104,111 +100,19 @@ export class Ollama {
|
|
|
const result = Buffer.from(image).toString('base64')
|
|
|
return result
|
|
|
}
|
|
|
- try {
|
|
|
- if (fs.existsSync(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')
|
|
|
+ if (utils.isNode()) {
|
|
|
+ const { readImage } = await import('../src/node.js');
|
|
|
+ try {
|
|
|
+ // if this succeeds the image exists locally at this filepath and has been read
|
|
|
+ return await readImage(image)
|
|
|
+ } catch {
|
|
|
+ // couldn't read an image at the filepath, continue
|
|
|
}
|
|
|
- } catch {
|
|
|
- // continue
|
|
|
}
|
|
|
- // the string may be base64 encoded
|
|
|
+ // the string should be base64 encoded already
|
|
|
return image
|
|
|
}
|
|
|
|
|
|
- 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')
|
|
|
- }
|
|
|
-
|
|
|
- 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.')
|
|
|
- }
|
|
|
-
|
|
|
- // 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
|
|
|
- }
|
|
|
-
|
|
|
generate(
|
|
|
request: GenerateRequest & { stream: true },
|
|
|
): Promise<AsyncGenerator<GenerateResponse>>
|
|
@@ -273,23 +177,22 @@ export class Ollama {
|
|
|
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')
|
|
|
+
|
|
|
+ if (utils.isNode()) {
|
|
|
+ const { readModelfile } = await import('../src/node.js');
|
|
|
+ let modelfileContent = await readModelfile(this, request);
|
|
|
+ request.modelfile = modelfileContent
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.modelfile == "") {
|
|
|
+ // request.path will resolve to a modelfile in node environments, otherwise is it required
|
|
|
+ throw new Error("modelfile is requrired")
|
|
|
}
|
|
|
|
|
|
return this.processStreamableRequest<ProgressResponse>('create', {
|
|
|
name: request.model,
|
|
|
stream: request.stream,
|
|
|
- modelfile: modelfileContent,
|
|
|
+ modelfile: request.modelfile,
|
|
|
})
|
|
|
}
|
|
|
|