langtrain 0.1.11 → 0.1.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langtrain",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Unified JavaScript SDK for Langtrain Ecosystem",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/cli.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { intro, outro, select, text, spinner, isCancel, cancel, password } from '@clack/prompts';
2
+ import {
3
+ intro, outro, text, select, confirm, spinner, isCancel, note, cancel, password
4
+ } from '@clack/prompts';
3
5
  import { bgCyan, black, red, green, yellow, gray } from 'kleur/colors';
4
6
  import { Command } from 'commander';
5
- import { Langvision, Langtune, AgentClient, Agent, AgentCreate } from './index';
7
+ import { AgentClient, AgentCreate, FileClient, TrainingClient, SubscriptionClient, Langvision, Langtune } from './index';
6
8
  import fs from 'fs';
7
9
  import path from 'path';
8
10
  import os from 'os';
@@ -64,6 +66,7 @@ async function main() {
64
66
  // Operation Handlers Map (O(1) lookup)
65
67
  const handlers: Record<string, (clients?: any) => Promise<void>> = {
66
68
  'login': handleLogin,
69
+ 'status': handleSubscriptionStatus,
67
70
  'tune-finetune': (c) => handleTuneFinetune(c.tune),
68
71
  'tune-generate': (c) => handleTuneGenerate(c.tune),
69
72
  'vision-finetune': (c) => handleVisionFinetune(c.vision),
@@ -144,12 +147,39 @@ async function handleLogin() {
144
147
  intro(green('API Key updated successfully!'));
145
148
  }
146
149
 
150
+ async function handleSubscriptionStatus() {
151
+ const config = getConfig();
152
+ if (!config.apiKey) {
153
+ intro(red('Not logged in. Run "login" first.'));
154
+ return;
155
+ }
156
+ const client = new SubscriptionClient({ apiKey: config.apiKey });
157
+ const s = spinner();
158
+ s.start('Fetching subscription status...');
159
+ try {
160
+ const info = await client.getStatus();
161
+ s.stop(green('Subscription Status:'));
162
+
163
+ console.log(gray('Plan: ') + (info.plan === 'pro' ? bgCyan(' PRO ') : info.plan.toUpperCase()));
164
+ console.log(gray('Active: ') + (info.is_active ? green('Yes') : red('No')));
165
+ if (info.expires_at) console.log(gray('Expires: ') + new Date(info.expires_at).toLocaleDateString());
166
+
167
+ console.log(gray('\nLimits:'));
168
+ console.log(` Models: ${info.limits.max_models === -1 ? 'Unlimited' : info.limits.max_models}`);
169
+ console.log(` Training Jobs: ${info.limits.max_training_jobs}`);
170
+
171
+ } catch (e: any) {
172
+ s.stop(red('Failed to fetch status.'));
173
+ console.error(e.message);
174
+ }
175
+ }
176
+
147
177
  async function handleAgentCreate(client: AgentClient) {
148
178
  const name = await text({
149
179
  message: 'Agent Name:',
150
180
  placeholder: 'e.g. Support Bot',
151
181
  validate(value) {
152
- if (value.length === 0) return 'Name is required!';
182
+ if (!value || value.length === 0) return 'API Key is required';
153
183
  },
154
184
  });
155
185
  if (isCancel(name)) return;
@@ -202,7 +232,7 @@ async function handleAgentCreate(client: AgentClient) {
202
232
  const wid = await text({
203
233
  message: 'Enter Workspace ID (UUID):',
204
234
  validate(value) {
205
- if (value.length === 0) return 'Required';
235
+ if (!value || value.length === 0) return 'Required';
206
236
  },
207
237
  });
208
238
  if (isCancel(wid)) return;
@@ -353,10 +383,60 @@ async function handleTuneFinetune(tune: Langtune) {
353
383
  });
354
384
  if (isCancel(epochs)) cancel('Operation cancelled.');
355
385
 
386
+ const track = await select({
387
+ message: 'Track this job on Langtrain Cloud?',
388
+ options: [
389
+ { value: 'yes', label: 'Yes', hint: 'Upload dataset and log job' },
390
+ { value: 'no', label: 'No', hint: 'Local only' }
391
+ ]
392
+ });
393
+ if (isCancel(track)) cancel('Operation cancelled.');
394
+
395
+ if (track === 'yes') {
396
+ const s = spinner();
397
+ s.start('Connecting to Cloud...');
398
+ try {
399
+ const config = getConfig();
400
+ if (!config.apiKey) throw new Error('API Key required. Run "login" first.');
401
+
402
+ // Check Subscription
403
+ const subClient = new SubscriptionClient({ apiKey: config.apiKey });
404
+ const sub = await subClient.getStatus();
405
+ if (!sub.features.includes('cloud_finetuning')) {
406
+ s.stop(red('Feature "cloud_finetuning" is not available on your plan.'));
407
+ const upgrade = await confirm({ message: 'Upgrade to Pro for cloud tracking?' });
408
+ if (upgrade && !isCancel(upgrade)) {
409
+ console.log(bgCyan(black(' Visit https://langtrain.ai/dashboard/billing to upgrade. ')));
410
+ }
411
+ return;
412
+ }
413
+
414
+ const fileClient = new FileClient({ apiKey: config.apiKey });
415
+ const trainingClient = new TrainingClient({ apiKey: config.apiKey });
416
+
417
+ s.message('Uploading dataset...');
418
+ const fileResp = await fileClient.upload(trainFile as string);
419
+
420
+ s.message('Creating Job...');
421
+ const job = await trainingClient.createJob({
422
+ name: `cli-sft-${Date.now()}`,
423
+ base_model: model as string,
424
+ dataset_id: fileResp.id,
425
+ task: 'text',
426
+ hyperparameters: {
427
+ n_epochs: parseInt(epochs as string)
428
+ }
429
+ });
430
+ s.stop(green(`Job tracked: ${job.id}`));
431
+ } catch (e: any) {
432
+ s.stop(red(`Tracking failed: ${e.message}`));
433
+ const cont = await confirm({ message: 'Continue with local training anyway?' });
434
+ if (!cont || isCancel(cont)) return;
435
+ }
436
+ }
437
+
356
438
  const s = spinner();
357
- s.start('Connecting to Langtrain Cloud...');
358
- await new Promise(r => setTimeout(r, 800)); // Simulatoin
359
- s.message('Starting fine-tuning job...');
439
+ s.start('Starting local fine-tuning...');
360
440
 
361
441
  try {
362
442
  // Check if FinetuneConfig types match what's needed.
@@ -430,6 +510,60 @@ async function handleVisionFinetune(vision: Langvision) {
430
510
  placeholder: '3',
431
511
  initialValue: '3'
432
512
  });
513
+ if (isCancel(epochs)) cancel('Operation cancelled');
514
+
515
+ const track = await select({
516
+ message: 'Track this job on Langtrain Cloud?',
517
+ options: [
518
+ { value: 'yes', label: 'Yes', hint: 'Upload dataset and log job' },
519
+ { value: 'no', label: 'No', hint: 'Local only' }
520
+ ]
521
+ });
522
+ if (isCancel(track)) cancel('Operation cancelled');
523
+
524
+ if (track === 'yes') {
525
+ const s = spinner();
526
+ s.start('Connecting to Cloud...');
527
+ try {
528
+ const config = getConfig();
529
+ if (!config.apiKey) throw new Error('API Key required. Run "login" first.');
530
+
531
+ // Check Subscription
532
+ const subClient = new SubscriptionClient({ apiKey: config.apiKey });
533
+ const sub = await subClient.getStatus();
534
+ if (!sub.features.includes('cloud_finetuning')) {
535
+ s.stop(red('Feature "cloud_finetuning" is not available on your plan.'));
536
+ const upgrade = await confirm({ message: 'Upgrade to Pro for cloud tracking?' });
537
+ if (upgrade && !isCancel(upgrade)) {
538
+ console.log(bgCyan(black(' Visit https://langtrain.ai/dashboard/billing to upgrade. ')));
539
+ }
540
+ return;
541
+ }
542
+
543
+ const fileClient = new FileClient({ apiKey: config.apiKey });
544
+ const trainingClient = new TrainingClient({ apiKey: config.apiKey });
545
+
546
+ s.message('Uploading dataset...');
547
+ const fileResp = await fileClient.upload(dataset as string, undefined, 'fine-tune-vision');
548
+
549
+ s.message('Creating Job...');
550
+ const job = await trainingClient.createJob({
551
+ name: `cli-vision-${Date.now()}`,
552
+ base_model: model as string,
553
+ dataset_id: fileResp.id,
554
+ task: 'vision',
555
+ training_method: 'lora',
556
+ hyperparameters: {
557
+ n_epochs: parseInt(epochs as string)
558
+ }
559
+ });
560
+ s.stop(green(`Job tracked: ${job.id}`));
561
+ } catch (e: any) {
562
+ s.stop(red(`Tracking failed: ${e.message}`));
563
+ const cont = await confirm({ message: 'Continue with local training anyway?' });
564
+ if (!cont || isCancel(cont)) return;
565
+ }
566
+ }
433
567
 
434
568
  const s = spinner();
435
569
  s.start('Analyzing dataset structure...');
package/src/files.ts ADDED
@@ -0,0 +1,53 @@
1
+ export class FileClient {
2
+ private client: any; // AxiosInstance
3
+
4
+ constructor(config: { apiKey: string, baseUrl?: string }) {
5
+ const axios = require('axios');
6
+ this.client = axios.create({
7
+ baseURL: config.baseUrl || 'https://api.langtrain.ai/api/v1',
8
+ headers: {
9
+ 'X-API-Key': config.apiKey,
10
+ }
11
+ });
12
+ }
13
+
14
+ async upload(file: any, workspaceId?: string, purpose: string = 'fine-tune'): Promise<FileResponse> {
15
+ const FormData = require('form-data');
16
+ const fs = require('fs');
17
+
18
+ const form = new FormData();
19
+ // Check if file is a path or buffer. Assuming path for CLI
20
+ if (typeof file === 'string') {
21
+ if (!fs.existsSync(file)) throw new Error(`File not found: ${file}`);
22
+ form.append('file', fs.createReadStream(file));
23
+ } else {
24
+ // Handle buffer or other types if needed, but for now strict to path
25
+ throw new Error('File path required');
26
+ }
27
+
28
+ if (workspaceId) form.append('workspace_id', workspaceId);
29
+ form.append('purpose', purpose);
30
+
31
+ const response = await this.client.post('/files', form, {
32
+ headers: {
33
+ ...form.getHeaders()
34
+ }
35
+ });
36
+ return response.data;
37
+ }
38
+
39
+ async list(workspaceId: string, purpose?: string): Promise<FileResponse[]> {
40
+ const params: any = { workspace_id: workspaceId };
41
+ if (purpose) params.purpose = purpose;
42
+ const response = await this.client.get('/files', { params });
43
+ return response.data.data;
44
+ }
45
+ }
46
+
47
+ export interface FileResponse {
48
+ id: string;
49
+ filename: string;
50
+ purpose: string;
51
+ bytes: number;
52
+ created_at: string;
53
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,9 @@ export { Langtune } from 'langtune';
5
5
 
6
6
  // Export Agent Client
7
7
  export { AgentClient, Agent, AgentRun, AgentCreate } from './agent';
8
+ export { FileClient, FileResponse } from './files';
9
+ export { TrainingClient, FineTuneJobCreate, FineTuneJobResponse } from './training';
10
+ export { SubscriptionClient, SubscriptionInfo, FeatureCheck } from './subscription';
8
11
 
9
12
  // Export Types with Namespaces to avoid collisions
10
13
  import * as Vision from 'langvision';
@@ -0,0 +1,45 @@
1
+ export class SubscriptionClient {
2
+ private client: any; // AxiosInstance
3
+
4
+ constructor(config: { apiKey: string, baseUrl?: string }) {
5
+ const axios = require('axios');
6
+ this.client = axios.create({
7
+ baseURL: config.baseUrl || 'https://api.langtrain.ai/api/v1',
8
+ headers: {
9
+ 'X-API-Key': config.apiKey,
10
+ 'Content-Type': 'application/json'
11
+ }
12
+ });
13
+ }
14
+
15
+ async getStatus(): Promise<SubscriptionInfo> {
16
+ const response = await this.client.get('/subscription/status');
17
+ return response.data;
18
+ }
19
+
20
+ async checkFeature(feature: string): Promise<FeatureCheck> {
21
+ const response = await this.client.get(`/subscription/check/${feature}`);
22
+ return response.data;
23
+ }
24
+
25
+ async getLimits(): Promise<any> {
26
+ const response = await this.client.get('/subscription/analytics');
27
+ return response.data;
28
+ }
29
+ }
30
+
31
+ export interface SubscriptionInfo {
32
+ is_active: boolean;
33
+ plan: string;
34
+ plan_name: string;
35
+ expires_at?: string;
36
+ features: string[];
37
+ limits: any;
38
+ }
39
+
40
+ export interface FeatureCheck {
41
+ feature: string;
42
+ allowed: boolean;
43
+ limit?: number;
44
+ used?: number;
45
+ }
@@ -0,0 +1,63 @@
1
+ export class TrainingClient {
2
+ private client: any; // AxiosInstance
3
+
4
+ constructor(config: { apiKey: string, baseUrl?: string }) {
5
+ const axios = require('axios');
6
+ this.client = axios.create({
7
+ baseURL: config.baseUrl || 'https://api.langtrain.ai/api/v1',
8
+ headers: {
9
+ 'X-API-Key': config.apiKey,
10
+ 'Content-Type': 'application/json'
11
+ }
12
+ });
13
+ }
14
+
15
+ async createJob(job: FineTuneJobCreate): Promise<FineTuneJobResponse> {
16
+ const response = await this.client.post('/finetune/jobs', job);
17
+ return response.data;
18
+ }
19
+
20
+ async listJobs(workspaceId: string, limit: number = 10): Promise<FineTuneJobList> {
21
+ const response = await this.client.get('/finetune/jobs', {
22
+ params: { workspace_id: workspaceId, limit }
23
+ });
24
+ return response.data;
25
+ }
26
+
27
+ async getJob(jobId: string): Promise<FineTuneJobResponse> {
28
+ const response = await this.client.get(`/finetune/jobs/${jobId}`);
29
+ return response.data;
30
+ }
31
+
32
+ async cancelJob(jobId: string): Promise<FineTuneJobResponse> {
33
+ const response = await this.client.post(`/finetune/jobs/${jobId}/cancel`);
34
+ return response.data;
35
+ }
36
+ }
37
+
38
+ export interface FineTuneJobCreate {
39
+ name?: string;
40
+ base_model: string;
41
+ model_id?: string;
42
+ dataset_id: string;
43
+ guardrail_id?: string;
44
+ task?: 'text' | 'vision';
45
+ training_method?: 'sft' | 'dpo' | 'rlhf' | 'lora' | 'qlora';
46
+ hyperparameters?: any;
47
+ [key: string]: any;
48
+ }
49
+
50
+ export interface FineTuneJobResponse {
51
+ id: string;
52
+ name: string;
53
+ status: string;
54
+ progress: number;
55
+ error_message?: string;
56
+ created_at: string;
57
+ [key: string]: any;
58
+ }
59
+
60
+ export interface FineTuneJobList {
61
+ data: FineTuneJobResponse[];
62
+ has_more: boolean;
63
+ }