0g-orbit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +23 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/commands/fine-tuning.d.ts +21 -0
- package/dist/cli/commands/fine-tuning.d.ts.map +1 -0
- package/dist/cli/commands/fine-tuning.js +142 -0
- package/dist/cli/commands/fine-tuning.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/fine-tuning.d.ts +50 -0
- package/dist/fine-tuning.d.ts.map +1 -0
- package/dist/fine-tuning.js +240 -0
- package/dist/fine-tuning.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/orbit.d.ts +11 -1
- package/dist/orbit.d.ts.map +1 -1
- package/dist/orbit.js +28 -0
- package/dist/orbit.js.map +1 -1
- package/dist/storage.js +4 -4
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -2
- package/src/cli/cli.ts +26 -0
- package/src/cli/commands/fine-tuning.ts +169 -0
- package/src/errors.ts +11 -0
- package/src/fine-tuning.test.ts +299 -0
- package/src/fine-tuning.ts +330 -0
- package/src/index.ts +8 -0
- package/src/orbit.ts +45 -0
- package/src/storage.ts +3 -3
- package/src/types.ts +72 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { NETWORKS } from './networks.js'
|
|
3
|
+
import { FineTuningError } from './errors.js'
|
|
4
|
+
|
|
5
|
+
// Hoist mocks so they can be referenced in vi.mock factories
|
|
6
|
+
const {
|
|
7
|
+
mockListModel,
|
|
8
|
+
mockListService,
|
|
9
|
+
mockListTask,
|
|
10
|
+
mockGetTask,
|
|
11
|
+
mockGetLog,
|
|
12
|
+
mockCreateTask,
|
|
13
|
+
mockAcknowledgeModel,
|
|
14
|
+
mockAcknowledgeProviderSigner,
|
|
15
|
+
mockStore,
|
|
16
|
+
} = vi.hoisted(() => ({
|
|
17
|
+
mockListModel: vi.fn(),
|
|
18
|
+
mockListService: vi.fn(),
|
|
19
|
+
mockListTask: vi.fn(),
|
|
20
|
+
mockGetTask: vi.fn(),
|
|
21
|
+
mockGetLog: vi.fn(),
|
|
22
|
+
mockCreateTask: vi.fn(),
|
|
23
|
+
mockAcknowledgeModel: vi.fn(),
|
|
24
|
+
mockAcknowledgeProviderSigner: vi.fn(),
|
|
25
|
+
mockStore: vi.fn(),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
vi.mock('@0glabs/0g-serving-broker', () => ({
|
|
29
|
+
createZGComputeNetworkBroker: vi.fn().mockResolvedValue({
|
|
30
|
+
fineTuning: {
|
|
31
|
+
listModel: mockListModel,
|
|
32
|
+
listService: mockListService,
|
|
33
|
+
listTask: mockListTask,
|
|
34
|
+
getTask: mockGetTask,
|
|
35
|
+
getLog: mockGetLog,
|
|
36
|
+
createTask: mockCreateTask,
|
|
37
|
+
acknowledgeModel: mockAcknowledgeModel,
|
|
38
|
+
acknowledgeProviderSigner: mockAcknowledgeProviderSigner,
|
|
39
|
+
},
|
|
40
|
+
ledger: {
|
|
41
|
+
getLedger: vi.fn(),
|
|
42
|
+
addLedger: vi.fn(),
|
|
43
|
+
},
|
|
44
|
+
inference: {},
|
|
45
|
+
}),
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
vi.mock('./storage.js', () => ({
|
|
49
|
+
StorageClient: vi.fn().mockImplementation(() => ({
|
|
50
|
+
store: mockStore,
|
|
51
|
+
})),
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
import { FineTuningClient } from './fine-tuning.js'
|
|
55
|
+
import { StorageClient } from './storage.js'
|
|
56
|
+
|
|
57
|
+
const mockWallet = {
|
|
58
|
+
address: '0x1234567890abcdef1234567890abcdef12345678',
|
|
59
|
+
privateKey: '0xdeadbeef',
|
|
60
|
+
} as any
|
|
61
|
+
const testNetwork = { ...NETWORKS.testnet }
|
|
62
|
+
const mockStorageClient = new StorageClient(testNetwork, mockWallet)
|
|
63
|
+
|
|
64
|
+
describe('FineTuningClient', () => {
|
|
65
|
+
let client: FineTuningClient
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
vi.clearAllMocks()
|
|
69
|
+
client = new FineTuningClient(testNetwork, mockWallet, mockStorageClient)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('lazy broker init', () => {
|
|
73
|
+
it('initializes broker on first call', async () => {
|
|
74
|
+
mockListModel.mockResolvedValueOnce([[], []])
|
|
75
|
+
|
|
76
|
+
await client.listModels()
|
|
77
|
+
const { createZGComputeNetworkBroker } = await import('@0glabs/0g-serving-broker')
|
|
78
|
+
expect(createZGComputeNetworkBroker).toHaveBeenCalledTimes(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('reuses broker on subsequent calls', async () => {
|
|
82
|
+
mockListModel.mockResolvedValue([[], []])
|
|
83
|
+
|
|
84
|
+
await client.listModels()
|
|
85
|
+
await client.listModels()
|
|
86
|
+
const { createZGComputeNetworkBroker } = await import('@0glabs/0g-serving-broker')
|
|
87
|
+
expect(createZGComputeNetworkBroker).toHaveBeenCalledTimes(1)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('throws FineTuningError on broker init failure', async () => {
|
|
91
|
+
const { createZGComputeNetworkBroker } = await import('@0glabs/0g-serving-broker')
|
|
92
|
+
;(createZGComputeNetworkBroker as any).mockRejectedValueOnce(
|
|
93
|
+
new Error('Network unreachable')
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const freshClient = new FineTuningClient(testNetwork, mockWallet, mockStorageClient)
|
|
97
|
+
try {
|
|
98
|
+
await freshClient.listModels()
|
|
99
|
+
expect.unreachable('Should have thrown')
|
|
100
|
+
} catch (err) {
|
|
101
|
+
expect(err).toBeInstanceOf(FineTuningError)
|
|
102
|
+
expect((err as FineTuningError).message).toContain('Failed to initialize')
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('uploadDataset', () => {
|
|
108
|
+
it('uploads a dataset and returns root + tx hash', async () => {
|
|
109
|
+
mockStore.mockResolvedValueOnce({
|
|
110
|
+
root: '0xdataset_root',
|
|
111
|
+
txHash: '0xtx_hash',
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const result = await client.uploadDataset('/tmp/dataset.jsonl')
|
|
115
|
+
expect(result.root).toBe('0xdataset_root')
|
|
116
|
+
expect(result.txHash).toBe('0xtx_hash')
|
|
117
|
+
expect(mockStore).toHaveBeenCalledWith('/tmp/dataset.jsonl')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('throws FineTuningError on file error', async () => {
|
|
121
|
+
mockStore.mockRejectedValueOnce(new Error('ENOENT: no such file'))
|
|
122
|
+
|
|
123
|
+
await expect(client.uploadDataset('/tmp/missing.jsonl')).rejects.toThrow(
|
|
124
|
+
FineTuningError
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('createTask', () => {
|
|
130
|
+
it('creates a fine-tuning task', async () => {
|
|
131
|
+
mockAcknowledgeProviderSigner.mockResolvedValueOnce(undefined)
|
|
132
|
+
mockCreateTask.mockResolvedValueOnce('task-001')
|
|
133
|
+
|
|
134
|
+
const task = await client.createTask({
|
|
135
|
+
model: 'Qwen2.5-0.5B-Instruct',
|
|
136
|
+
dataset: '0xdataset_root',
|
|
137
|
+
providerAddress: '0xprovider',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
expect(task.id).toBe('task-001')
|
|
141
|
+
expect(task.model).toBe('Qwen2.5-0.5B-Instruct')
|
|
142
|
+
expect(task.dataset).toBe('0xdataset_root')
|
|
143
|
+
expect(task.provider).toBe('0xprovider')
|
|
144
|
+
expect(task.status).toBe('init')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('throws FineTuningError on invalid model', async () => {
|
|
148
|
+
mockAcknowledgeProviderSigner.mockResolvedValueOnce(undefined)
|
|
149
|
+
mockCreateTask.mockRejectedValueOnce(new Error('Model not found'))
|
|
150
|
+
|
|
151
|
+
await expect(
|
|
152
|
+
client.createTask({
|
|
153
|
+
model: 'nonexistent-model',
|
|
154
|
+
dataset: '0xdataset_root',
|
|
155
|
+
providerAddress: '0xprovider',
|
|
156
|
+
})
|
|
157
|
+
).rejects.toThrow(FineTuningError)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('throws FineTuningError on missing provider', async () => {
|
|
161
|
+
mockAcknowledgeProviderSigner.mockRejectedValueOnce(
|
|
162
|
+
new Error('Provider not found')
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
await expect(
|
|
166
|
+
client.createTask({
|
|
167
|
+
model: 'Qwen2.5-0.5B-Instruct',
|
|
168
|
+
dataset: '0xdataset_root',
|
|
169
|
+
providerAddress: '0xbad_provider',
|
|
170
|
+
})
|
|
171
|
+
).rejects.toThrow(FineTuningError)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('getTask', () => {
|
|
176
|
+
it('returns a task by ID', async () => {
|
|
177
|
+
mockGetTask.mockResolvedValueOnce({
|
|
178
|
+
id: 'task-001',
|
|
179
|
+
preTrainedModelHash: '0xmodel',
|
|
180
|
+
datasetHash: '0xdata',
|
|
181
|
+
progress: 'Training',
|
|
182
|
+
createdAt: '2024-01-01',
|
|
183
|
+
updatedAt: '2024-01-02',
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const task = await client.getTask('0xprovider', 'task-001')
|
|
187
|
+
expect(task.id).toBe('task-001')
|
|
188
|
+
expect(task.status).toBe('training')
|
|
189
|
+
expect(task.provider).toBe('0xprovider')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('throws FineTuningError when task not found', async () => {
|
|
193
|
+
mockGetTask.mockRejectedValueOnce(new Error('No task found'))
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
client.getTask('0xprovider', 'nonexistent')
|
|
197
|
+
).rejects.toThrow(FineTuningError)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('getTaskLog', () => {
|
|
202
|
+
it('returns the training log', async () => {
|
|
203
|
+
mockGetLog.mockResolvedValueOnce('Epoch 1/3: loss=0.5\nEpoch 2/3: loss=0.3')
|
|
204
|
+
|
|
205
|
+
const log = await client.getTaskLog('0xprovider', 'task-001')
|
|
206
|
+
expect(log).toContain('Epoch 1/3')
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('listTasks', () => {
|
|
211
|
+
it('returns all tasks for a provider', async () => {
|
|
212
|
+
mockListTask.mockResolvedValueOnce([
|
|
213
|
+
{ id: 'task-001', preTrainedModelHash: '0xm1', datasetHash: '0xd1', progress: 'Finished' },
|
|
214
|
+
{ id: 'task-002', preTrainedModelHash: '0xm2', datasetHash: '0xd2', progress: 'Training' },
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
const tasks = await client.listTasks('0xprovider')
|
|
218
|
+
expect(tasks).toHaveLength(2)
|
|
219
|
+
expect(tasks[0].status).toBe('finished')
|
|
220
|
+
expect(tasks[1].status).toBe('training')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('downloadModel', () => {
|
|
225
|
+
it('downloads a model via acknowledgeModel', async () => {
|
|
226
|
+
mockAcknowledgeModel.mockResolvedValueOnce(undefined)
|
|
227
|
+
|
|
228
|
+
await expect(
|
|
229
|
+
client.downloadModel('0xprovider', 'task-001', '/tmp/model')
|
|
230
|
+
).resolves.toBeUndefined()
|
|
231
|
+
|
|
232
|
+
expect(mockAcknowledgeModel).toHaveBeenCalledWith(
|
|
233
|
+
'0xprovider',
|
|
234
|
+
'task-001',
|
|
235
|
+
'/tmp/model'
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('throws FineTuningError when task not ready', async () => {
|
|
240
|
+
mockAcknowledgeModel.mockRejectedValueOnce(
|
|
241
|
+
new Error('No deliverable found')
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
await expect(
|
|
245
|
+
client.downloadModel('0xprovider', 'task-001', '/tmp/model')
|
|
246
|
+
).rejects.toThrow(FineTuningError)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('listModels', () => {
|
|
251
|
+
it('returns combined standard and customized models', async () => {
|
|
252
|
+
mockListModel.mockResolvedValueOnce([
|
|
253
|
+
[['Qwen2.5-0.5B-Instruct', { turbo: '0xhash', description: 'Qwen model' }]],
|
|
254
|
+
[['my-custom', { description: 'Custom fine-tuned', provider: '0xp1' }]],
|
|
255
|
+
])
|
|
256
|
+
|
|
257
|
+
const models = await client.listModels()
|
|
258
|
+
expect(models).toHaveLength(2)
|
|
259
|
+
expect(models[0].name).toBe('Qwen2.5-0.5B-Instruct')
|
|
260
|
+
expect(models[1].name).toBe('my-custom')
|
|
261
|
+
expect(models[1].config.provider).toBe('0xp1')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('returns empty array when no models', async () => {
|
|
265
|
+
mockListModel.mockResolvedValueOnce([[], []])
|
|
266
|
+
|
|
267
|
+
const models = await client.listModels()
|
|
268
|
+
expect(models).toHaveLength(0)
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('listProviders', () => {
|
|
273
|
+
it('returns fine-tuning providers', async () => {
|
|
274
|
+
mockListService.mockResolvedValueOnce([
|
|
275
|
+
{ provider: '0xprovider1', url: 'https://p1.example.com', models: ['model1'] },
|
|
276
|
+
{ provider: '0xprovider2', url: 'https://p2.example.com', models: [] },
|
|
277
|
+
])
|
|
278
|
+
|
|
279
|
+
const providers = await client.listProviders()
|
|
280
|
+
expect(providers).toHaveLength(2)
|
|
281
|
+
expect(providers[0].address).toBe('0xprovider1')
|
|
282
|
+
expect(providers[0].url).toBe('https://p1.example.com')
|
|
283
|
+
expect(providers[0].models).toEqual(['model1'])
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('error suggestions', () => {
|
|
288
|
+
it('FineTuningError includes default suggestion', () => {
|
|
289
|
+
const err = new FineTuningError('Something failed')
|
|
290
|
+
expect(err.code).toBe('FINE_TUNING_ERROR')
|
|
291
|
+
expect(err.suggestion).toContain('listModels')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('FineTuningError allows custom suggestion', () => {
|
|
295
|
+
const err = new FineTuningError('Oops', 'Try this instead')
|
|
296
|
+
expect(err.suggestion).toBe('Try this instead')
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
})
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { Wallet } from 'ethers'
|
|
2
|
+
import {
|
|
3
|
+
createZGComputeNetworkBroker,
|
|
4
|
+
type ZGComputeNetworkBroker,
|
|
5
|
+
} from '@0glabs/0g-serving-broker'
|
|
6
|
+
import type { NetworkConfig } from './networks.js'
|
|
7
|
+
import type {
|
|
8
|
+
DatasetUploadResult,
|
|
9
|
+
CreateTaskOptions,
|
|
10
|
+
FineTuneTask,
|
|
11
|
+
FineTuneModel,
|
|
12
|
+
FineTuneProvider,
|
|
13
|
+
FineTuneStatus,
|
|
14
|
+
} from './types.js'
|
|
15
|
+
import { FineTuningError } from './errors.js'
|
|
16
|
+
import { withRetry } from './retry.js'
|
|
17
|
+
import { StorageClient } from './storage.js'
|
|
18
|
+
|
|
19
|
+
const DEFAULT_TRAINING_PARAMS = {
|
|
20
|
+
nEpochs: 3,
|
|
21
|
+
batchSize: 4,
|
|
22
|
+
learningRate: 5e-5,
|
|
23
|
+
loraRank: 8,
|
|
24
|
+
loraAlpha: 16,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class FineTuningClient {
|
|
28
|
+
private broker: ZGComputeNetworkBroker | null = null
|
|
29
|
+
private initialized = false
|
|
30
|
+
private wallet: Wallet
|
|
31
|
+
private network: NetworkConfig
|
|
32
|
+
private storageClient: StorageClient
|
|
33
|
+
|
|
34
|
+
constructor(network: NetworkConfig, wallet: Wallet, storageClient: StorageClient) {
|
|
35
|
+
this.network = network
|
|
36
|
+
this.wallet = wallet
|
|
37
|
+
this.storageClient = storageClient
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async ensureBroker(): Promise<ZGComputeNetworkBroker> {
|
|
41
|
+
if (this.broker && this.initialized) return this.broker
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
this.broker = await createZGComputeNetworkBroker(
|
|
45
|
+
this.wallet as any,
|
|
46
|
+
this.network.ledgerContractAddress,
|
|
47
|
+
this.network.inferenceContractAddress,
|
|
48
|
+
this.network.fineTuningContractAddress
|
|
49
|
+
)
|
|
50
|
+
this.initialized = true
|
|
51
|
+
return this.broker
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
54
|
+
throw new FineTuningError(
|
|
55
|
+
`Failed to initialize fine-tuning broker: ${msg}`,
|
|
56
|
+
'Check your network connection and wallet private key. The compute contracts may be temporarily unavailable.'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Dataset ---
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Upload a dataset to 0G Storage for fine-tuning.
|
|
65
|
+
* Uses the existing StorageClient for upload, returning the root hash
|
|
66
|
+
* needed to create a fine-tuning task.
|
|
67
|
+
*/
|
|
68
|
+
async uploadDataset(filePath: string): Promise<DatasetUploadResult> {
|
|
69
|
+
return withRetry(
|
|
70
|
+
async () => {
|
|
71
|
+
try {
|
|
72
|
+
const result = await this.storageClient.store(filePath)
|
|
73
|
+
return {
|
|
74
|
+
root: result.root,
|
|
75
|
+
txHash: result.txHash,
|
|
76
|
+
}
|
|
77
|
+
} catch (err: unknown) {
|
|
78
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
79
|
+
throw new FineTuningError(
|
|
80
|
+
`Failed to upload dataset: ${msg}`,
|
|
81
|
+
'Check that the dataset file exists, is valid JSONL, and you have sufficient OG balance.'
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{ maxAttempts: 3 }
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- Tasks ---
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a fine-tuning task. Requires a dataset already uploaded to 0G Storage.
|
|
93
|
+
*/
|
|
94
|
+
async createTask(options: CreateTaskOptions): Promise<FineTuneTask> {
|
|
95
|
+
const broker = await this.ensureBroker()
|
|
96
|
+
|
|
97
|
+
if (!broker.fineTuning) {
|
|
98
|
+
throw new FineTuningError(
|
|
99
|
+
'Fine-tuning broker not available.',
|
|
100
|
+
'Fine-tuning requires a Wallet signer (not JsonRpcSigner).'
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const params = { ...DEFAULT_TRAINING_PARAMS, ...options.trainingParams }
|
|
105
|
+
|
|
106
|
+
return withRetry(
|
|
107
|
+
async () => {
|
|
108
|
+
try {
|
|
109
|
+
// Acknowledge the provider signer (required before task creation)
|
|
110
|
+
await broker.fineTuning!.acknowledgeProviderSigner(
|
|
111
|
+
options.providerAddress
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Write training params as JSON string (broker expects a file path,
|
|
115
|
+
// but the underlying createTask reads the file content — we'll pass
|
|
116
|
+
// the JSON string directly via a temp file approach)
|
|
117
|
+
const { writeFileSync, unlinkSync } = await import('node:fs')
|
|
118
|
+
const { tmpdir } = await import('node:os')
|
|
119
|
+
const { join } = await import('node:path')
|
|
120
|
+
const { randomBytes } = await import('node:crypto')
|
|
121
|
+
|
|
122
|
+
const tempPath = join(tmpdir(), `orbit-params-${randomBytes(8).toString('hex')}.json`)
|
|
123
|
+
writeFileSync(tempPath, JSON.stringify(params))
|
|
124
|
+
|
|
125
|
+
let taskId: string
|
|
126
|
+
try {
|
|
127
|
+
taskId = await broker.fineTuning!.createTask(
|
|
128
|
+
options.providerAddress,
|
|
129
|
+
options.model,
|
|
130
|
+
options.dataset,
|
|
131
|
+
tempPath
|
|
132
|
+
)
|
|
133
|
+
} finally {
|
|
134
|
+
try { unlinkSync(tempPath) } catch { /* ignore cleanup errors */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
id: taskId,
|
|
139
|
+
model: options.model,
|
|
140
|
+
dataset: options.dataset,
|
|
141
|
+
provider: options.providerAddress,
|
|
142
|
+
status: 'init' as FineTuneStatus,
|
|
143
|
+
}
|
|
144
|
+
} catch (err: unknown) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
146
|
+
if (msg.includes('User opted not to continue')) {
|
|
147
|
+
throw new FineTuningError(msg, 'The provider has pending tasks in queue. Try again later or use a different provider.')
|
|
148
|
+
}
|
|
149
|
+
throw new FineTuningError(
|
|
150
|
+
`Failed to create task: ${msg}`,
|
|
151
|
+
'Verify the model name, dataset hash, and provider address. Run orbit.listModels() to see available options.'
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{ maxAttempts: 2 }
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the status of a fine-tuning task.
|
|
161
|
+
*/
|
|
162
|
+
async getTask(providerAddress: string, taskId: string): Promise<FineTuneTask> {
|
|
163
|
+
const broker = await this.ensureBroker()
|
|
164
|
+
|
|
165
|
+
if (!broker.fineTuning) {
|
|
166
|
+
throw new FineTuningError('Fine-tuning broker not available.')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const task = await broker.fineTuning.getTask(providerAddress, taskId)
|
|
171
|
+
return this.mapTask(task, providerAddress)
|
|
172
|
+
} catch (err: unknown) {
|
|
173
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
174
|
+
if (msg.includes('No task found') || msg.includes('not found')) {
|
|
175
|
+
throw new FineTuningError(
|
|
176
|
+
`Task "${taskId}" not found for provider ${providerAddress}.`,
|
|
177
|
+
'Check the task ID and provider address. Run orbit.listTasks() to see your tasks.'
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
throw new FineTuningError(`Failed to get task: ${msg}`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the training log for a fine-tuning task.
|
|
186
|
+
*/
|
|
187
|
+
async getTaskLog(providerAddress: string, taskId: string): Promise<string> {
|
|
188
|
+
const broker = await this.ensureBroker()
|
|
189
|
+
|
|
190
|
+
if (!broker.fineTuning) {
|
|
191
|
+
throw new FineTuningError('Fine-tuning broker not available.')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
return await broker.fineTuning.getLog(providerAddress, taskId)
|
|
196
|
+
} catch (err: unknown) {
|
|
197
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
198
|
+
throw new FineTuningError(`Failed to get task log: ${msg}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* List all fine-tuning tasks for a given provider.
|
|
204
|
+
*/
|
|
205
|
+
async listTasks(providerAddress: string): Promise<FineTuneTask[]> {
|
|
206
|
+
const broker = await this.ensureBroker()
|
|
207
|
+
|
|
208
|
+
if (!broker.fineTuning) {
|
|
209
|
+
throw new FineTuningError('Fine-tuning broker not available.')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const tasks = await broker.fineTuning.listTask(providerAddress)
|
|
214
|
+
return tasks.map((t) => this.mapTask(t, providerAddress))
|
|
215
|
+
} catch (err: unknown) {
|
|
216
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
217
|
+
throw new FineTuningError(`Failed to list tasks: ${msg}`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Models ---
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Download a fine-tuned model. Combines acknowledge + download.
|
|
225
|
+
* The task must be in 'delivered' status.
|
|
226
|
+
*/
|
|
227
|
+
async downloadModel(
|
|
228
|
+
providerAddress: string,
|
|
229
|
+
taskId: string,
|
|
230
|
+
outputPath: string
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
const broker = await this.ensureBroker()
|
|
233
|
+
|
|
234
|
+
if (!broker.fineTuning) {
|
|
235
|
+
throw new FineTuningError('Fine-tuning broker not available.')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return withRetry(
|
|
239
|
+
async () => {
|
|
240
|
+
try {
|
|
241
|
+
await broker.fineTuning!.acknowledgeModel(
|
|
242
|
+
providerAddress,
|
|
243
|
+
taskId,
|
|
244
|
+
outputPath
|
|
245
|
+
)
|
|
246
|
+
} catch (err: unknown) {
|
|
247
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
248
|
+
if (msg.includes('No deliverable found')) {
|
|
249
|
+
throw new FineTuningError(
|
|
250
|
+
`Model not ready for task "${taskId}". The task may still be training.`,
|
|
251
|
+
'Check task status with orbit.getFineTuneTask(). The task must be in "delivered" status.'
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
throw new FineTuningError(
|
|
255
|
+
`Failed to download model: ${msg}`,
|
|
256
|
+
'Ensure the task is in "delivered" status and the output path is writable.'
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{ maxAttempts: 2 }
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* List available base models for fine-tuning.
|
|
266
|
+
*/
|
|
267
|
+
async listModels(): Promise<FineTuneModel[]> {
|
|
268
|
+
const broker = await this.ensureBroker()
|
|
269
|
+
|
|
270
|
+
if (!broker.fineTuning) {
|
|
271
|
+
throw new FineTuningError('Fine-tuning broker not available.')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const [standardModels, customizedModels] = await broker.fineTuning.listModel()
|
|
276
|
+
|
|
277
|
+
const models: FineTuneModel[] = []
|
|
278
|
+
|
|
279
|
+
for (const [name, config] of standardModels) {
|
|
280
|
+
models.push({ name, config })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const [name, config] of customizedModels) {
|
|
284
|
+
models.push({ name, config })
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return models
|
|
288
|
+
} catch (err: unknown) {
|
|
289
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
290
|
+
throw new FineTuningError(`Failed to list models: ${msg}`)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* List fine-tuning service providers.
|
|
296
|
+
*/
|
|
297
|
+
async listProviders(): Promise<FineTuneProvider[]> {
|
|
298
|
+
const broker = await this.ensureBroker()
|
|
299
|
+
|
|
300
|
+
if (!broker.fineTuning) {
|
|
301
|
+
throw new FineTuningError('Fine-tuning broker not available.')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const services = await broker.fineTuning.listService()
|
|
306
|
+
return services.map((s: any) => ({
|
|
307
|
+
address: s.provider ?? '',
|
|
308
|
+
url: s.url ?? '',
|
|
309
|
+
models: s.models ?? [],
|
|
310
|
+
}))
|
|
311
|
+
} catch (err: unknown) {
|
|
312
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
313
|
+
throw new FineTuningError(`Failed to list providers: ${msg}`)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Helpers ---
|
|
318
|
+
|
|
319
|
+
private mapTask(task: any, providerAddress: string): FineTuneTask {
|
|
320
|
+
return {
|
|
321
|
+
id: task.id ?? '',
|
|
322
|
+
model: task.preTrainedModelHash ?? '',
|
|
323
|
+
dataset: task.datasetHash ?? '',
|
|
324
|
+
provider: providerAddress,
|
|
325
|
+
status: (task.progress?.toLowerCase() ?? 'init') as FineTuneStatus,
|
|
326
|
+
createdAt: task.createdAt,
|
|
327
|
+
updatedAt: task.updatedAt,
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { Orbit } from './orbit.js'
|
|
|
4
4
|
// Clients (for direct access)
|
|
5
5
|
export { StorageClient } from './storage.js'
|
|
6
6
|
export { InferenceClient } from './inference.js'
|
|
7
|
+
export { FineTuningClient } from './fine-tuning.js'
|
|
7
8
|
|
|
8
9
|
// Types
|
|
9
10
|
export type {
|
|
@@ -15,6 +16,12 @@ export type {
|
|
|
15
16
|
InferOptions,
|
|
16
17
|
ServiceInfo,
|
|
17
18
|
AccountStatus,
|
|
19
|
+
DatasetUploadResult,
|
|
20
|
+
CreateTaskOptions,
|
|
21
|
+
FineTuneTask,
|
|
22
|
+
FineTuneStatus,
|
|
23
|
+
FineTuneModel,
|
|
24
|
+
FineTuneProvider,
|
|
18
25
|
} from './types.js'
|
|
19
26
|
|
|
20
27
|
// Network config
|
|
@@ -34,4 +41,5 @@ export {
|
|
|
34
41
|
InsufficientBalanceError,
|
|
35
42
|
ProviderNotFoundError,
|
|
36
43
|
TimeoutError,
|
|
44
|
+
FineTuningError,
|
|
37
45
|
} from './errors.js'
|