1
1
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'
3
4
import { dirname , join , resolve } from 'path'
4
5
import { createHash } from 'crypto'
5
6
import { homedir } from 'os'
6
7
import { Ollama as OllamaBrowser } from './browser.js'
7
8
8
9
import type { CreateRequest , ProgressResponse } from './interfaces.js'
9
10
import {
10
- EMPTY_STRING ,
11
+ CODE_404 ,
11
12
ENCODING ,
12
13
MESSAGES ,
13
14
MODEL_FILE_COMMANDS ,
14
15
SHA256 ,
16
+ STREAMING_EVENTS ,
15
17
} from './constants'
16
18
17
19
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
+ */
18
42
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 )
22
45
}
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 )
31
53
}
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 } `
34
67
}
35
68
36
69
/**
@@ -43,22 +76,11 @@ export class Ollama extends OllamaBrowser {
43
76
modelfile : string ,
44
77
mfDir : string = process . cwd ( ) ,
45
78
) : Promise < string > {
46
- const out : string [ ] = [ ]
47
79
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' )
62
84
}
63
85
64
86
/**
@@ -67,69 +89,59 @@ export class Ollama extends OllamaBrowser {
67
89
* @param mfDir {string} - The directory of the modelfile
68
90
* @private @internal
69
91
*/
70
- private resolvePath ( inputPath , mfDir ) {
92
+ private resolvePath ( inputPath : string , mfDir : string ) {
71
93
if ( inputPath . startsWith ( '~' ) ) {
72
94
return join ( homedir ( ) , inputPath . slice ( 1 ) )
73
95
}
74
96
return resolve ( mfDir , inputPath )
75
97
}
76
98
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
+ }
77
127
/**
78
- * checks if a file exists
128
+ * Create a blob from a file.
79
129
* @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
82
131
*/
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
-
92
132
private async createBlob ( path : string ) : Promise < string > {
93
133
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 )
97
135
}
98
136
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 )
110
138
const digest = `${ SHA256 } :${ sha256sum } `
111
139
112
140
try {
113
141
await utils . head ( this . fetch , `${ this . config . host } /api/blobs/${ digest } ` )
114
142
} 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 )
133
145
await utils . post (
134
146
this . fetch ,
135
147
`${ this . config . host } /api/blobs/${ digest } ` ,
@@ -148,31 +160,42 @@ export class Ollama extends OllamaBrowser {
148
160
) : Promise < AsyncGenerator < ProgressResponse > >
149
161
create ( request : CreateRequest & { stream ?: false } ) : Promise < ProgressResponse >
150
162
163
+ /**
164
+ * Create a model.
165
+ * @param request {CreateRequest} - The request object
166
+ * @returns {Promise<ProgressResponse | AsyncGenerator<ProgressResponse>> } - The progress response
167
+ */
151
168
async create (
152
169
request : CreateRequest ,
153
170
) : 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 )
169
172
170
- // check stream here so that typescript knows which overload to use
171
173
if ( request . stream ) {
172
174
return super . create ( request as CreateRequest & { stream : true } )
173
175
}
174
176
return super . create ( request as CreateRequest & { stream : false } )
175
177
}
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
+ }
176
199
}
177
200
178
201
export default new Ollama ( )
0 commit comments