Skip to content

Commit 8ac2d83

Browse files
committed
chore: refactoring
- created new constants file to gradually remove all dangling strings from the code for better readability - cleaned up the browser.ts file - cleaned up the index.ts file for better readability - created new utility functions for IO Operations
1 parent 7f06950 commit 8ac2d83

File tree

4 files changed

+188
-108
lines changed

4 files changed

+188
-108
lines changed

src/browser.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export class Ollama {
3838
}
3939

4040
this.fetch = fetch
41-
if (config?.fetch != null) {
41+
// NOTE: fetch could either be undefined or an instance of Fetch
42+
if (config?.fetch) {
4243
this.fetch = config.fetch
4344
}
4445

@@ -97,13 +98,12 @@ export class Ollama {
9798
}
9899
throw new Error('Did not receive done or success response in stream.')
99100
})()
100-
} else {
101-
const message = await itr.next()
102-
if (!message.value.done && (message.value as any).status !== MESSAGES.SUCCESS) {
103-
throw new Error('Expected a completed response.')
104-
}
105-
return message.value
106101
}
102+
const message = await itr.next()
103+
if (!message.value.done && (message.value as any).status !== MESSAGES.SUCCESS) {
104+
throw new Error('Expected a completed response.')
105+
}
106+
return message.value
107107
}
108108

109109
/**
@@ -112,14 +112,14 @@ export class Ollama {
112112
* @returns {Promise<string>} - The base64 encoded image.
113113
*/
114114
async encodeImage(image: Uint8Array | string): Promise<string> {
115-
if (typeof image !== 'string') {
116-
// image is Uint8Array convert it to base64
117-
const uint8Array = new Uint8Array(image)
118-
const numberArray = Array.from(uint8Array)
119-
return btoa(String.fromCharCode.apply(null, numberArray))
115+
if (typeof image === 'string') {
116+
// image is already base64 encoded
117+
return image
120118
}
121-
// the string may be base64 encoded
122-
return image
119+
// image is Uint8Array convert it to base64
120+
const uint8Array = new Uint8Array(image)
121+
const numberArray = Array.from(uint8Array)
122+
return btoa(String.fromCharCode.apply(null, numberArray))
123123
}
124124

125125
generate(
@@ -198,7 +198,7 @@ export class Ollama {
198198
async pull(
199199
request: PullRequest,
200200
): Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> {
201-
return this.processStreamableRequest<ProgressResponse>('pull', {
201+
return this.processStreamableRequest<ProgressResponse>(REQUEST_CONSTANTS.PULL, {
202202
name: request.model,
203203
stream: request.stream,
204204
insecure: request.insecure,

src/constants/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
const EMPTY_STRING = ''
2+
const CODE_404 = '404'
3+
const PROTOCOLS = {
4+
HTTP: 'http',
5+
HTTPS: 'https',
6+
} as const
7+
const PORTS = {
8+
HTTP: '80',
9+
HTTPS: '443',
10+
} as const
211
const MESSAGES = {
312
MISSING_BODY: 'Missing body',
413
SUCCESS: 'Success',
514
FETCHING_TEXT: 'Getting text from response',
615
ERROR_FETCHING_TEXT: 'Failed to get text from error response',
716
ERROR_NO_MODEL_FILE: 'Must provide either path or modelfile to create a model',
817
ERROR_JSON_PARSE: 'Failed to parse error response as JSON',
18+
STREAMING_UPLOADS_NOT_SUPPORTED:
19+
'Streaming uploads are not supported in this environment.',
920
} as const
1021
const REQUEST_CONSTANTS = {
1122
GENERATE: 'generate',
1223
CREATE: 'create',
1324
PUSH: 'push',
25+
PULL: 'pull',
26+
} as const
27+
const STREAMING_EVENTS = {
28+
DATA: 'data',
29+
END: 'end',
30+
ERROR: 'error',
1431
} as const
1532
const MODEL_FILE_COMMANDS = ['FROM', 'ADAPTER']
1633
const OLLAMA_LOCAL_URL = 'http://127.0.0.1:11434'
@@ -22,8 +39,12 @@ const ENCODING = {
2239
} as const
2340
export {
2441
EMPTY_STRING,
42+
CODE_404,
43+
PROTOCOLS,
44+
PORTS,
2545
MESSAGES,
2646
REQUEST_CONSTANTS,
47+
STREAMING_EVENTS,
2748
MODEL_FILE_COMMANDS,
2849
OLLAMA_LOCAL_URL,
2950
SHA256,

src/index.ts

Lines changed: 114 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,69 @@
11
import * as utils from './utils.js'
2-
import fs, { createReadStream, promises } from 'fs'
2+
import { fileExists, isFilePath } from './utils.js'
3+
import { createReadStream, promises } from 'fs'
34
import { dirname, join, resolve } from 'path'
45
import { createHash } from 'crypto'
56
import { homedir } from 'os'
67
import { Ollama as OllamaBrowser } from './browser.js'
78

89
import type { CreateRequest, ProgressResponse } from './interfaces.js'
910
import {
10-
EMPTY_STRING,
11+
CODE_404,
1112
ENCODING,
1213
MESSAGES,
1314
MODEL_FILE_COMMANDS,
1415
SHA256,
16+
STREAMING_EVENTS,
1517
} from './constants'
1618

1719
export class Ollama extends OllamaBrowser {
20+
private async encodeImageFromString(image: string): Promise<string> {
21+
const isPath = await isFilePath(image)
22+
if (isPath) {
23+
return this.encodeImageFromFile(image)
24+
}
25+
return image
26+
}
27+
28+
private async encodeImageFromBuffer(image: Uint8Array | Buffer): Promise<string> {
29+
return Buffer.from(image).toString(ENCODING.BASE64)
30+
}
31+
32+
private async encodeImageFromFile(path: string): Promise<string> {
33+
const fileBuffer = await promises.readFile(resolve(path))
34+
return Buffer.from(fileBuffer).toString(ENCODING.BASE64)
35+
}
36+
37+
/**
38+
* Encode an image to base64.
39+
* @param image {Uint8Array | Buffer | string} - The image to encode
40+
* @returns {Promise<string>} - The base64 encoded image
41+
*/
1842
async encodeImage(image: Uint8Array | Buffer | string): Promise<string> {
19-
if (typeof image !== 'string') {
20-
// image is Uint8Array or Buffer, convert it to base64
21-
return Buffer.from(image).toString(ENCODING.BASE64)
43+
if (typeof image === 'string') {
44+
return this.encodeImageFromString(image)
2245
}
23-
try {
24-
if (fs.existsSync(image)) {
25-
// this is a filepath, read the file and convert it to base64
26-
const fileBuffer = await promises.readFile(resolve(image))
27-
return Buffer.from(fileBuffer).toString(ENCODING.BASE64)
28-
}
29-
} catch {
30-
// continue
46+
return this.encodeImageFromBuffer(image)
47+
}
48+
49+
private async parseLine(line: string, mfDir: string): Promise<string> {
50+
const [command, args] = line.split(' ', 2)
51+
if (MODEL_FILE_COMMANDS.includes(command.toUpperCase())) {
52+
return this.parseCommand(command, args.trim(), mfDir)
3153
}
32-
// the string may be base64 encoded
33-
return image
54+
return line
55+
}
56+
57+
private async parseCommand(
58+
command: string,
59+
args: string,
60+
mfDir: string,
61+
): Promise<string> {
62+
const path = this.resolvePath(args, mfDir)
63+
if (await fileExists(path)) {
64+
return `${command} @${await this.createBlob(path)}`
65+
}
66+
return `${command} ${args}`
3467
}
3568

3669
/**
@@ -43,22 +76,11 @@ export class Ollama extends OllamaBrowser {
4376
modelfile: string,
4477
mfDir: string = process.cwd(),
4578
): Promise<string> {
46-
const out: string[] = []
4779
const lines = modelfile.split('\n')
48-
for (const line of lines) {
49-
const [command, args] = line.split(' ', 2)
50-
if (MODEL_FILE_COMMANDS.includes(command.toUpperCase())) {
51-
const path = this.resolvePath(args.trim(), mfDir)
52-
if (await this.fileExists(path)) {
53-
out.push(`${command} @${await this.createBlob(path)}`)
54-
} else {
55-
out.push(`${command} ${args}`)
56-
}
57-
} else {
58-
out.push(line)
59-
}
60-
}
61-
return out.join('\n')
80+
const parsedLines = await Promise.all(
81+
lines.map((line) => this.parseLine(line, mfDir)),
82+
)
83+
return parsedLines.join('\n')
6284
}
6385

6486
/**
@@ -67,69 +89,59 @@ export class Ollama extends OllamaBrowser {
6789
* @param mfDir {string} - The directory of the modelfile
6890
* @private @internal
6991
*/
70-
private resolvePath(inputPath, mfDir) {
92+
private resolvePath(inputPath: string, mfDir: string) {
7193
if (inputPath.startsWith('~')) {
7294
return join(homedir(), inputPath.slice(1))
7395
}
7496
return resolve(mfDir, inputPath)
7597
}
7698

99+
private async computeSha256(path: string): Promise<string> {
100+
return new Promise<string>((resolve, reject) => {
101+
const fileStream = createReadStream(path)
102+
const hash = createHash(SHA256)
103+
fileStream.on('data', (data) => hash.update(data))
104+
fileStream.on('end', () => resolve(hash.digest(ENCODING.HEX)))
105+
fileStream.on('error', reject)
106+
})
107+
}
108+
109+
private createReadableStream(path: string): ReadableStream {
110+
const fileStream = createReadStream(path)
111+
return new ReadableStream({
112+
start(controller) {
113+
fileStream.on(STREAMING_EVENTS.DATA, (chunk) => {
114+
controller.enqueue(chunk)
115+
})
116+
117+
fileStream.on(STREAMING_EVENTS.END, () => {
118+
controller.close()
119+
})
120+
121+
fileStream.on(STREAMING_EVENTS.ERROR, (err) => {
122+
controller.error(err)
123+
})
124+
},
125+
})
126+
}
77127
/**
78-
* checks if a file exists
128+
* Create a blob from a file.
79129
* @param path {string} - The path to the file
80-
* @private @internal
81-
* @returns {Promise<boolean>} - Whether the file exists or not
130+
* @returns {Promise<string>} - The digest of the blob
82131
*/
83-
private async fileExists(path: string): Promise<boolean> {
84-
try {
85-
await promises.access(path)
86-
return true
87-
} catch {
88-
return false
89-
}
90-
}
91-
92132
private async createBlob(path: string): Promise<string> {
93133
if (typeof ReadableStream === 'undefined') {
94-
// Not all fetch implementations support streaming
95-
// TODO: support non-streaming uploads
96-
throw new Error('Streaming uploads are not supported in this environment.')
134+
throw new Error(MESSAGES.STREAMING_UPLOADS_NOT_SUPPORTED)
97135
}
98136

99-
// Create a stream for reading the file
100-
const fileStream = createReadStream(path)
101-
102-
// Compute the SHA256 digest
103-
const sha256sum = await new Promise<string>((resolve, reject) => {
104-
const hash = createHash(SHA256)
105-
fileStream.on('data', (data) => hash.update(data))
106-
fileStream.on('end', () => resolve(hash.digest(ENCODING.HEX)))
107-
fileStream.on('error', reject)
108-
})
109-
137+
const sha256sum = await this.computeSha256(path)
110138
const digest = `${SHA256}:${sha256sum}`
111139

112140
try {
113141
await utils.head(this.fetch, `${this.config.host}/api/blobs/${digest}`)
114142
} catch (e) {
115-
if (e instanceof Error && e.message.includes('404')) {
116-
// Create a new readable stream for the fetch request
117-
const readableStream = new ReadableStream({
118-
start(controller) {
119-
fileStream.on('data', (chunk) => {
120-
controller.enqueue(chunk) // Enqueue the chunk directly
121-
})
122-
123-
fileStream.on('end', () => {
124-
controller.close() // Close the stream when the file ends
125-
})
126-
127-
fileStream.on('error', (err) => {
128-
controller.error(err) // Propagate errors to the stream
129-
})
130-
},
131-
})
132-
143+
if (e instanceof Error && e.message.includes(CODE_404)) {
144+
const readableStream = this.createReadableStream(path)
133145
await utils.post(
134146
this.fetch,
135147
`${this.config.host}/api/blobs/${digest}`,
@@ -148,31 +160,42 @@ export class Ollama extends OllamaBrowser {
148160
): Promise<AsyncGenerator<ProgressResponse>>
149161
create(request: CreateRequest & { stream?: false }): Promise<ProgressResponse>
150162

163+
/**
164+
* Create a model.
165+
* @param request {CreateRequest} - The request object
166+
* @returns {Promise<ProgressResponse | AsyncGenerator<ProgressResponse>>} - The progress response
167+
*/
151168
async create(
152169
request: CreateRequest,
153170
): Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> {
154-
let modelfileContent = EMPTY_STRING
155-
if (request.path) {
156-
modelfileContent = await promises.readFile(request.path, {
157-
encoding: ENCODING.UTF8,
158-
})
159-
modelfileContent = await this.parseModelfile(
160-
modelfileContent,
161-
dirname(request.path),
162-
)
163-
} else if (request.modelfile) {
164-
modelfileContent = await this.parseModelfile(request.modelfile)
165-
} else {
166-
throw new Error(MESSAGES.ERROR_NO_MODEL_FILE)
167-
}
168-
request.modelfile = modelfileContent
171+
request.modelfile = await this.getModelfileContent(request)
169172

170-
// check stream here so that typescript knows which overload to use
171173
if (request.stream) {
172174
return super.create(request as CreateRequest & { stream: true })
173175
}
174176
return super.create(request as CreateRequest & { stream: false })
175177
}
178+
179+
private async getModelfileContentFromPath(path: string): Promise<string> {
180+
const modelfileContent = await promises.readFile(path, {
181+
encoding: ENCODING.UTF8,
182+
})
183+
return this.parseModelfile(modelfileContent, dirname(path))
184+
}
185+
/**
186+
* Get the content of the modelfile.
187+
* @param request {CreateRequest} - The request object
188+
* @returns {Promise<string>} - The content of the modelfile
189+
*/
190+
private async getModelfileContent(request: CreateRequest): Promise<string> {
191+
if (request.path) {
192+
return this.getModelfileContentFromPath(request.path)
193+
} else if (request.modelfile) {
194+
return this.parseModelfile(request.modelfile)
195+
} else {
196+
throw new Error(MESSAGES.ERROR_NO_MODEL_FILE)
197+
}
198+
}
176199
}
177200

178201
export default new Ollama()

0 commit comments

Comments
 (0)