@mlx-node/trl 0.0.13 → 0.0.15

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.
@@ -0,0 +1,562 @@
1
+ /**
2
+ * SFT Dataset handling for Supervised Fine-Tuning
3
+ *
4
+ * Supports two data formats (auto-detected):
5
+ * 1. Prompt-Completion: { prompt: ChatMessage[], completion: ChatMessage }
6
+ * 2. Full Conversation: { messages: ChatMessage[] }
7
+ *
8
+ * Both formats produce tokenized batches with labels masked appropriately.
9
+ */
10
+
11
+ import { readFileSync } from 'node:fs';
12
+ import { resolve as resolvePath } from 'node:path';
13
+
14
+ import type { Qwen3Tokenizer } from '@mlx-node/core';
15
+
16
+ import type { ChatMessage } from '../types.js';
17
+ import { validatePathContainment, getAllowedRoot, type PathValidationOptions } from '../utils/path-security.js';
18
+
19
+ // -100 is the standard ignore index for cross-entropy loss
20
+ const IGNORE_INDEX = -100;
21
+
22
+ /**
23
+ * Special token IDs for SFT label masking
24
+ *
25
+ * These are used to detect assistant message boundaries in tokenized conversations.
26
+ * The IDs can be derived from the tokenizer or provided explicitly.
27
+ */
28
+ export interface SpecialTokenIds {
29
+ /** Token ID for <|im_start|> */
30
+ imStart: number;
31
+ /** Token ID for <|im_end|> */
32
+ imEnd: number;
33
+ /** Token IDs that represent newlines (for detecting end of role header) */
34
+ newlineTokens: number[];
35
+ }
36
+
37
+ /**
38
+ * Get special token IDs from a tokenizer
39
+ *
40
+ * Queries the tokenizer to get the actual token IDs for special tokens.
41
+ * This ensures portability across different tokenizers/vocabularies.
42
+ *
43
+ * @param tokenizer - The tokenizer instance
44
+ * @returns Special token IDs derived from the tokenizer
45
+ * @throws Error if required special tokens are not found
46
+ */
47
+ function getSpecialTokenIds(tokenizer: Qwen3Tokenizer): SpecialTokenIds {
48
+ // Get im_start and im_end tokens using the tokenizer's special token getters
49
+ const imStartToken = tokenizer.getImStartToken(); // "<|im_start|>"
50
+ const imEndToken = tokenizer.getImEndToken(); // "<|im_end|>"
51
+
52
+ const imStart = tokenizer.tokenToId(imStartToken);
53
+ const imEnd = tokenizer.tokenToId(imEndToken);
54
+
55
+ // Validate that we got valid IDs (tokenToId returns null for unknown tokens)
56
+ if (imStart === null || imEnd === null) {
57
+ throw new Error(
58
+ `Tokenizer does not have required special tokens for ChatML format. ` +
59
+ `Got im_start=${imStart}, im_end=${imEnd}. ` +
60
+ `This tokenizer may not be compatible with ChatML format.`,
61
+ );
62
+ }
63
+
64
+ // Get newline token IDs - these vary by tokenizer
65
+ // Try common newline representations
66
+ const newlineTokens: number[] = [];
67
+ const potentialNewlines = ['\n', ' \n', '\r\n', '\n\n'];
68
+ for (const nl of potentialNewlines) {
69
+ const id = tokenizer.tokenToId(nl);
70
+ if (id !== null && !newlineTokens.includes(id)) {
71
+ newlineTokens.push(id);
72
+ }
73
+ }
74
+
75
+ // If no newline tokens found, we'll rely on the fallback in tokenizeConversation
76
+ return {
77
+ imStart,
78
+ imEnd,
79
+ newlineTokens,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Prompt-Completion format for tool-use training
85
+ */
86
+ export interface SFTPromptCompletionExample {
87
+ prompt: ChatMessage[];
88
+ completion: ChatMessage;
89
+ }
90
+
91
+ /**
92
+ * Full conversation format for multi-turn dialogue
93
+ */
94
+ export interface SFTConversationExample {
95
+ messages: ChatMessage[];
96
+ }
97
+
98
+ /**
99
+ * Union type for SFT examples
100
+ */
101
+ export type SFTExample = SFTPromptCompletionExample | SFTConversationExample;
102
+
103
+ /**
104
+ * A tokenized batch ready for SFT training
105
+ */
106
+ export interface SFTBatch {
107
+ inputIds: Int32Array;
108
+ labels: Int32Array;
109
+ shape: [number, number]; // [batch_size, seq_len]
110
+ }
111
+
112
+ /**
113
+ * Configuration for SFT dataset
114
+ */
115
+ export interface SFTDatasetConfig {
116
+ maxSeqLength?: number;
117
+ completionOnly?: boolean; // If true, only train on completion tokens (default: false for TRL parity)
118
+ enableThinking?: boolean; // Enable thinking mode for tokenizer
119
+ seed?: number; // Random seed for reproducible shuffling (default: 42)
120
+
121
+ /**
122
+ * Special token IDs for label masking.
123
+ *
124
+ * If not provided, these are automatically derived from the tokenizer.
125
+ * This option allows explicit overriding for custom tokenizers or
126
+ * non-standard vocabularies.
127
+ */
128
+ specialTokenIds?: Partial<SpecialTokenIds>;
129
+ }
130
+
131
+ /**
132
+ * Detect the format of an SFT example
133
+ */
134
+ function detectFormat(example: SFTExample): 'prompt-completion' | 'conversation' {
135
+ if ('prompt' in example && 'completion' in example) {
136
+ return 'prompt-completion';
137
+ }
138
+ if ('messages' in example) {
139
+ return 'conversation';
140
+ }
141
+ throw new Error('Invalid SFT example format. Expected either {prompt, completion} or {messages}');
142
+ }
143
+
144
+ /**
145
+ * SFT Dataset class for handling SFT training data
146
+ */
147
+ export class SFTDataset {
148
+ private examples: SFTExample[];
149
+ private tokenizer: Qwen3Tokenizer;
150
+ private config: Required<Omit<SFTDatasetConfig, 'seed' | 'specialTokenIds'>> & { seed: number };
151
+ private format: 'prompt-completion' | 'conversation';
152
+ private shuffledIndices: number[];
153
+ private rng: () => number;
154
+ /** Cached special token IDs for label masking */
155
+ private specialTokenIds: SpecialTokenIds;
156
+
157
+ constructor(examples: SFTExample[], tokenizer: Qwen3Tokenizer, config: SFTDatasetConfig = {}) {
158
+ if (examples.length === 0) {
159
+ throw new Error('SFT dataset must contain at least one example');
160
+ }
161
+
162
+ this.examples = examples;
163
+ this.tokenizer = tokenizer;
164
+ this.config = {
165
+ maxSeqLength: config.maxSeqLength ?? 2048,
166
+ completionOnly: config.completionOnly ?? false, // Changed to false for TRL parity
167
+ enableThinking: config.enableThinking ?? false,
168
+ seed: config.seed ?? 42,
169
+ };
170
+ this.rng = this.createSeededRandom(this.config.seed);
171
+
172
+ // Get special token IDs from tokenizer, with optional overrides
173
+ const derivedTokenIds = getSpecialTokenIds(tokenizer);
174
+ this.specialTokenIds = {
175
+ imStart: config.specialTokenIds?.imStart ?? derivedTokenIds.imStart,
176
+ imEnd: config.specialTokenIds?.imEnd ?? derivedTokenIds.imEnd,
177
+ newlineTokens: config.specialTokenIds?.newlineTokens ?? derivedTokenIds.newlineTokens,
178
+ };
179
+
180
+ // Detect format from first example
181
+ this.format = detectFormat(examples[0]);
182
+
183
+ // Validate all examples have the same format
184
+ for (let i = 1; i < examples.length; i++) {
185
+ const fmt = detectFormat(examples[i]);
186
+ if (fmt !== this.format) {
187
+ throw new Error(`Inconsistent SFT data format: example 0 is ${this.format}, example ${i} is ${fmt}`);
188
+ }
189
+ }
190
+
191
+ // Initialize indices
192
+ this.shuffledIndices = Array.from({ length: examples.length }, (_, i) => i);
193
+ }
194
+
195
+ /**
196
+ * Get the number of examples in the dataset
197
+ */
198
+ get length(): number {
199
+ return this.examples.length;
200
+ }
201
+
202
+ /**
203
+ * Shuffle dataset for a specific epoch using epoch-based seeding.
204
+ * This ensures reproducible shuffles across training resumes.
205
+ * Each epoch gets a deterministic shuffle based on (baseSeed + epoch).
206
+ *
207
+ * @param epoch - The epoch number (used as seed offset)
208
+ */
209
+ shuffleForEpoch(epoch: number): void {
210
+ // Reset RNG with epoch-specific seed for reproducibility
211
+ this.rng = this.createSeededRandom(this.config.seed + epoch);
212
+ // Reset indices to original order
213
+ this.shuffledIndices = Array.from({ length: this.examples.length }, (_, i) => i);
214
+ // Fisher-Yates shuffle
215
+ for (let i = this.shuffledIndices.length - 1; i > 0; i--) {
216
+ const j = Math.floor(this.rng() * (i + 1));
217
+ [this.shuffledIndices[i], this.shuffledIndices[j]] = [this.shuffledIndices[j], this.shuffledIndices[i]];
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Create a seeded pseudo-random number generator (Linear Congruential Generator)
223
+ */
224
+ private createSeededRandom(seed: number): () => number {
225
+ let s = seed;
226
+ return () => {
227
+ s = (s * 1103515245 + 12345) & 0x7fffffff;
228
+ return s / 0x7fffffff;
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Find length of common prefix between two token arrays
234
+ * Handles chat template quirks where prompt tokens may not be exact prefix of full tokens
235
+ */
236
+ private findCommonPrefixLength(prompt: number[], full: number[]): number {
237
+ let i = 0;
238
+ const maxLen = Math.min(prompt.length, full.length);
239
+ while (i < maxLen && prompt[i] === full[i]) {
240
+ i++;
241
+ }
242
+ return i;
243
+ }
244
+
245
+ /**
246
+ * Tokenize a prompt-completion example
247
+ */
248
+ private async tokenizePromptCompletion(
249
+ example: SFTPromptCompletionExample,
250
+ ): Promise<{ inputIds: number[]; labels: number[] }> {
251
+ // Tokenize prompt with generation prompt (so the model learns to continue)
252
+ const promptTokens = await this.tokenizer.applyChatTemplate(
253
+ example.prompt,
254
+ true, // add generation prompt
255
+ null,
256
+ this.config.enableThinking,
257
+ );
258
+
259
+ // Create full messages for tokenization
260
+ const fullMessages = [...example.prompt, example.completion];
261
+ const fullTokens = await this.tokenizer.applyChatTemplate(
262
+ fullMessages,
263
+ false, // no generation prompt at the end
264
+ null,
265
+ this.config.enableThinking,
266
+ );
267
+
268
+ // Convert to regular arrays for manipulation
269
+ const promptArr = Array.from(promptTokens, Number);
270
+ const inputIds = Array.from(fullTokens, Number);
271
+
272
+ // Use common prefix detection to handle chat template quirks
273
+ // (some templates may not produce prompt tokens as exact prefix of full tokens)
274
+ const promptLen = this.findCommonPrefixLength(promptArr, inputIds);
275
+
276
+ if (promptLen !== promptArr.length) {
277
+ console.warn(
278
+ `[SFT Dataset] Prompt tokens differ from prefix of full sequence ` +
279
+ `(${promptArr.length} vs ${promptLen}). Using common prefix for masking.`,
280
+ );
281
+ }
282
+
283
+ // Create labels: -100 for prompt tokens, actual tokens for completion
284
+ const labels = inputIds.map((id, i) => {
285
+ if (this.config.completionOnly && i < promptLen) {
286
+ return IGNORE_INDEX;
287
+ }
288
+ return id;
289
+ });
290
+
291
+ return { inputIds, labels };
292
+ }
293
+
294
+ /**
295
+ * Tokenize a conversation example
296
+ *
297
+ * For conversations, we train on all assistant turns.
298
+ * Non-assistant tokens (system, user) are masked with -100.
299
+ *
300
+ * Uses single-pass tokenization with token-based boundary detection.
301
+ * Token IDs are derived from the tokenizer for portability across models.
302
+ */
303
+ private async tokenizeConversation(
304
+ example: SFTConversationExample,
305
+ ): Promise<{ inputIds: number[]; labels: number[] }> {
306
+ const messages = example.messages;
307
+
308
+ // Single tokenization pass
309
+ const fullTokens = await this.tokenizer.applyChatTemplate(messages, false, null, this.config.enableThinking);
310
+
311
+ const inputIds = Array.from(fullTokens, Number);
312
+
313
+ // If not masking prompts, all tokens are trainable
314
+ if (!this.config.completionOnly) {
315
+ return { inputIds, labels: inputIds.slice() };
316
+ }
317
+
318
+ // Token-based boundary detection using special tokens (derived from tokenizer)
319
+ const { imStart, imEnd } = this.specialTokenIds;
320
+
321
+ // Get "assistant" token ID (it's a single token in Qwen3)
322
+ const assistantTokenId = this.tokenizer.tokenToId('assistant');
323
+
324
+ const labels = Array.from({ length: inputIds.length }, () => IGNORE_INDEX);
325
+ let inAssistant = false;
326
+
327
+ for (let i = 0; i < inputIds.length; i++) {
328
+ // Detect assistant region: <|im_start|> followed by "assistant" token
329
+ if (inputIds[i] === imStart && i + 1 < inputIds.length && inputIds[i + 1] === assistantTokenId) {
330
+ // Skip the <|im_start|>assistant\n header, start training from content
331
+ // Find the newline after "assistant"
332
+ let j = i + 2;
333
+ while (j < inputIds.length && inputIds[j] !== imEnd) {
334
+ // Look for newline token (dynamically derived from tokenizer)
335
+ if (this.specialTokenIds.newlineTokens.includes(inputIds[j])) {
336
+ inAssistant = true;
337
+ i = j; // Skip to after header
338
+ break;
339
+ }
340
+ j++;
341
+ }
342
+ if (!inAssistant) {
343
+ // Fallback: just start after assistant token
344
+ inAssistant = true;
345
+ i = i + 1;
346
+ }
347
+ continue;
348
+ }
349
+
350
+ if (inAssistant && inputIds[i] !== imEnd) {
351
+ labels[i] = inputIds[i];
352
+ }
353
+
354
+ if (inputIds[i] === imEnd) {
355
+ inAssistant = false;
356
+ }
357
+ }
358
+
359
+ return { inputIds, labels };
360
+ }
361
+
362
+ /**
363
+ * Tokenize a single example based on its format
364
+ */
365
+ private async tokenizeExample(example: SFTExample): Promise<{ inputIds: number[]; labels: number[] }> {
366
+ if (this.format === 'prompt-completion') {
367
+ return this.tokenizePromptCompletion(example as SFTPromptCompletionExample);
368
+ } else {
369
+ return this.tokenizeConversation(example as SFTConversationExample);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Collate multiple examples into a padded batch
375
+ */
376
+ async collateBatch(indices: number[]): Promise<SFTBatch> {
377
+ const examples = indices.map((i) => this.examples[this.shuffledIndices[i]]);
378
+
379
+ // Tokenize all examples
380
+ const tokenized: Array<{ inputIds: number[]; labels: number[] }> = [];
381
+ for (const example of examples) {
382
+ tokenized.push(await this.tokenizeExample(example));
383
+ }
384
+
385
+ // Find max length (capped at maxSeqLength)
386
+ const maxLen = Math.min(this.config.maxSeqLength, Math.max(...tokenized.map((t) => t.inputIds.length)));
387
+
388
+ // Pad and truncate
389
+ const batchSize = examples.length;
390
+ const paddedInputIds = new Int32Array(batchSize * maxLen);
391
+ const paddedLabels = new Int32Array(batchSize * maxLen);
392
+
393
+ const padTokenId = this.tokenizer.getPadTokenId();
394
+
395
+ for (let b = 0; b < batchSize; b++) {
396
+ const { inputIds, labels } = tokenized[b];
397
+ const seqLen = Math.min(inputIds.length, maxLen);
398
+
399
+ // Truncate from the left if necessary (keep the end of the sequence)
400
+ const startIdx = Math.max(0, inputIds.length - maxLen);
401
+
402
+ for (let s = 0; s < maxLen; s++) {
403
+ const offset = b * maxLen + s;
404
+ if (s < seqLen) {
405
+ paddedInputIds[offset] = inputIds[startIdx + s];
406
+ paddedLabels[offset] = labels[startIdx + s];
407
+ } else {
408
+ // Pad
409
+ paddedInputIds[offset] = padTokenId;
410
+ paddedLabels[offset] = IGNORE_INDEX;
411
+ }
412
+ }
413
+ }
414
+
415
+ return {
416
+ inputIds: paddedInputIds,
417
+ labels: paddedLabels,
418
+ shape: [batchSize, maxLen],
419
+ };
420
+ }
421
+
422
+ /**
423
+ * Generate batches for training
424
+ */
425
+ async *batches(batchSize: number): AsyncGenerator<SFTBatch> {
426
+ for (let i = 0; i < this.examples.length; i += batchSize) {
427
+ const end = Math.min(i + batchSize, this.examples.length);
428
+ const indices = Array.from({ length: end - i }, (_, j) => i + j);
429
+ yield await this.collateBatch(indices);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Get total number of batches for a given batch size
435
+ */
436
+ numBatches(batchSize: number): number {
437
+ return Math.ceil(this.examples.length / batchSize);
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Read JSONL file and parse into records
443
+ */
444
+ function readJsonl<T>(path: string, limit?: number): T[] {
445
+ let fileContents: string;
446
+ try {
447
+ fileContents = readFileSync(path, 'utf8');
448
+ } catch (error) {
449
+ const message = error instanceof Error ? error.message : String(error);
450
+ throw new Error(`Failed to read SFT dataset at ${path}: ${message}`);
451
+ }
452
+
453
+ const lines = fileContents.split(/\r?\n/).filter((line) => line.trim().length > 0);
454
+ const records: T[] = [];
455
+ const max = typeof limit === 'number' && limit > 0 ? limit : Number.POSITIVE_INFINITY;
456
+
457
+ for (let i = 0; i < lines.length && records.length < max; i++) {
458
+ const line = lines[i];
459
+ try {
460
+ const parsed = JSON.parse(line) as T;
461
+ records.push(parsed);
462
+ } catch (error) {
463
+ const message = error instanceof Error ? error.message : String(error);
464
+ throw new Error(`Failed to parse JSONL at ${path}:${i + 1} - ${message}`);
465
+ }
466
+ }
467
+
468
+ return records;
469
+ }
470
+
471
+ /**
472
+ * Validate an SFT example
473
+ */
474
+ function validateSFTExample(example: unknown, index: number): SFTExample {
475
+ if (typeof example !== 'object' || example === null) {
476
+ throw new Error(`SFT example ${index} must be an object`);
477
+ }
478
+
479
+ const obj = example as Record<string, unknown>;
480
+
481
+ // Check for prompt-completion format
482
+ if ('prompt' in obj && 'completion' in obj) {
483
+ if (!Array.isArray(obj.prompt)) {
484
+ throw new Error(`SFT example ${index}: prompt must be an array of messages`);
485
+ }
486
+ if (typeof obj.completion !== 'object' || obj.completion === null) {
487
+ throw new Error(`SFT example ${index}: completion must be a message object`);
488
+ }
489
+ const completion = obj.completion as Record<string, unknown>;
490
+ if (completion.role !== 'assistant') {
491
+ throw new Error(`SFT example ${index}: completion.role must be 'assistant'`);
492
+ }
493
+ if (typeof completion.content !== 'string') {
494
+ throw new Error(`SFT example ${index}: completion.content must be a string`);
495
+ }
496
+ return {
497
+ prompt: obj.prompt as ChatMessage[],
498
+ completion: obj.completion as ChatMessage,
499
+ };
500
+ }
501
+
502
+ // Check for conversation format
503
+ if ('messages' in obj) {
504
+ if (!Array.isArray(obj.messages)) {
505
+ throw new Error(`SFT example ${index}: messages must be an array`);
506
+ }
507
+ if (obj.messages.length === 0) {
508
+ throw new Error(`SFT example ${index}: messages cannot be empty`);
509
+ }
510
+ // Check that at least one message is from assistant
511
+ const hasAssistant = obj.messages.some(
512
+ (m: unknown) => typeof m === 'object' && m !== null && (m as Record<string, unknown>).role === 'assistant',
513
+ );
514
+ if (!hasAssistant) {
515
+ throw new Error(`SFT example ${index}: messages must contain at least one assistant message`);
516
+ }
517
+ return { messages: obj.messages as ChatMessage[] };
518
+ }
519
+
520
+ throw new Error(`SFT example ${index}: must have either {prompt, completion} or {messages}`);
521
+ }
522
+
523
+ /**
524
+ * Load SFT dataset from a JSONL file
525
+ *
526
+ * Supports two formats:
527
+ * 1. Prompt-Completion: {"prompt": [...], "completion": {...}}
528
+ * 2. Conversation: {"messages": [...]}
529
+ *
530
+ * @param path - Path to the JSONL file (relative to cwd or allowedRoot)
531
+ * @param tokenizer - Qwen3 tokenizer instance
532
+ * @param config - Optional configuration including path validation options
533
+ */
534
+ export async function loadSFTDataset(
535
+ path: string,
536
+ tokenizer: Qwen3Tokenizer,
537
+ config?: SFTDatasetConfig & { limit?: number } & PathValidationOptions,
538
+ ): Promise<SFTDataset> {
539
+ const allowedRoot = getAllowedRoot(config);
540
+ const absolutePath = resolvePath(allowedRoot, path);
541
+
542
+ // Validate the path stays within allowed root to prevent directory traversal
543
+ validatePathContainment(absolutePath, allowedRoot);
544
+
545
+ const rawRecords = readJsonl<unknown>(absolutePath, config?.limit);
546
+
547
+ // Validate and convert
548
+ const examples: SFTExample[] = rawRecords.map((record, i) => validateSFTExample(record, i));
549
+
550
+ return new SFTDataset(examples, tokenizer, config);
551
+ }
552
+
553
+ /**
554
+ * Create SFT dataset from examples directly
555
+ */
556
+ export function createSFTDataset(
557
+ examples: SFTExample[],
558
+ tokenizer: Qwen3Tokenizer,
559
+ config?: SFTDatasetConfig,
560
+ ): SFTDataset {
561
+ return new SFTDataset(examples, tokenizer, config);
562
+ }
package/src/index.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @mlx-node/trl - Training utilities for MLX models
3
+ *
4
+ * This package provides everything needed for training ML models,
5
+ * aligned with Python's TRL (Transformer Reinforcement Learning) library.
6
+ *
7
+ * For model loading and inference, import from @mlx-node/lm.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { GRPOTrainer, GRPOConfig, loadLocalGsm8kDataset } from '@mlx-node/trl';
12
+ * import { loadModel } from '@mlx-node/lm';
13
+ *
14
+ * const model = await loadModel('./models/qwen3-0.6b');
15
+ * const trainer = await GRPOTrainer.create({ modelPath: './models/qwen3-0.6b' });
16
+ * ```
17
+ */
18
+
19
+ // =============================================================================
20
+ // Re-exports from @mlx-node/core for training
21
+ // =============================================================================
22
+
23
+ // Tool calling types (for tool-use training)
24
+ export type { ToolDefinition, FunctionDefinition, FunctionParameters } from '@mlx-node/core';
25
+
26
+ // Note: MxArray, convertModel, convertParquetToJsonl are available from @mlx-node/core directly.
27
+ // They are not re-exported here to avoid multiple-package export confusion.
28
+
29
+ // =============================================================================
30
+ // TRL-specific exports
31
+ // =============================================================================
32
+
33
+ // Trainers
34
+ export {
35
+ GRPOTrainer,
36
+ type GRPOTrainerConfig,
37
+ DEFAULT_GRPO_CONFIG,
38
+ createRewardRegistry,
39
+ computeDatasetHash,
40
+ RewardTimeoutError,
41
+ type GenerateBatchResult,
42
+ type TrainStepMetrics,
43
+ type TrainingMetrics,
44
+ type TrainingState,
45
+ type DatasetMetadata,
46
+ // Re-export native types from trainer
47
+ GrpoTrainingEngine,
48
+ NativeRewardRegistry,
49
+ type GrpoEngineConfig,
50
+ type EngineStepMetrics,
51
+ type EngineEpochMetrics,
52
+ type BuiltinRewardConfig,
53
+ } from './trainers/grpo-trainer.js';
54
+
55
+ // Unified Training Logger (recommended)
56
+ export {
57
+ TrainingLogger,
58
+ createTrainingLogger,
59
+ type TrainingLoggerConfig,
60
+ type TrainingMetrics as TrainingLoggerMetrics,
61
+ type GenerationSample,
62
+ type TrainingConfigFields,
63
+ type TuiMessage,
64
+ type LogEvent,
65
+ type PromptChoice,
66
+ type PromptOptions,
67
+ } from './trainers/training-logger.js';
68
+
69
+ // SFT Trainer
70
+ export {
71
+ SFTTrainer,
72
+ SftTrainingEngine,
73
+ type SFTTrainStepResult,
74
+ type SFTTrainingState,
75
+ type SftEngineConfig,
76
+ type SftStepMetrics,
77
+ type SftEpochMetrics,
78
+ } from './trainers/sft-trainer.js';
79
+
80
+ export {
81
+ type SFTTrainerConfig,
82
+ SFTConfigError,
83
+ getDefaultSFTConfig,
84
+ mergeSFTConfig,
85
+ loadSFTTomlConfig,
86
+ applySFTOverrides,
87
+ DEFAULT_SFT_CONFIG,
88
+ } from './trainers/sft-config.js';
89
+
90
+ // Data
91
+ export {
92
+ loadLocalGsm8kDataset,
93
+ LocalGsm8kDatasetLoader,
94
+ createDatasetExample,
95
+ extractGsm8kAnswer,
96
+ validateDatasetExample,
97
+ type LocalDatasetOptions,
98
+ } from './data/dataset.js';
99
+ export {
100
+ SFTDataset,
101
+ loadSFTDataset,
102
+ createSFTDataset,
103
+ type SFTExample,
104
+ type SFTPromptCompletionExample,
105
+ type SFTConversationExample,
106
+ type SFTBatch,
107
+ type SFTDatasetConfig,
108
+ type SpecialTokenIds,
109
+ } from './data/sft-dataset.js';
110
+
111
+ // Utils
112
+ export { parseXmlCot, extractXmlAnswer, extractXmlReasoning, extractHashAnswer } from './utils/xml-parser.js';
113
+ export {
114
+ validatePathContainment,
115
+ resolveAndValidatePath,
116
+ getAllowedRoot,
117
+ PathTraversalError,
118
+ type PathValidationOptions,
119
+ } from './utils/path-security.js';
120
+
121
+ // Types
122
+ export type {
123
+ ChatRole,
124
+ ChatMessage,
125
+ CompletionMessage,
126
+ Completion,
127
+ DatasetSplit,
128
+ DatasetExample,
129
+ XmlParseResult,
130
+ RewardComputationInput,
131
+ PromptFormatterOptions,
132
+ PromptTemplate,
133
+ DatasetLoader,
134
+ RewardFunction,
135
+ PromptFormatter,
136
+ // Reward function types
137
+ CompletionInfo,
138
+ RewardOutput,
139
+ } from './types.js';