langtrain 0.2.0 → 0.2.2
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/chunk-A7PMJDMV.mjs +3 -0
- package/dist/chunk-A7PMJDMV.mjs.map +1 -0
- package/dist/chunk-K5LXUJ4G.js +3 -0
- package/dist/chunk-K5LXUJ4G.js.map +1 -0
- package/dist/cli.js +21 -14
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +21 -14
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/auth.ts +102 -14
- package/src/lib/base.ts +162 -12
- package/dist/chunk-36QS5AXY.mjs +0 -3
- package/dist/chunk-36QS5AXY.mjs.map +0 -1
- package/dist/chunk-ZN3AO753.js +0 -3
- package/dist/chunk-ZN3AO753.js.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -16,6 +16,23 @@ interface ClientConfig {
|
|
|
16
16
|
timeout?: number;
|
|
17
17
|
/** Maximum number of retries on transient errors (default: 2). */
|
|
18
18
|
maxRetries?: number;
|
|
19
|
+
/** Max requests per second (client-side rate limiting, default: 10). */
|
|
20
|
+
maxRequestsPerSecond?: number;
|
|
21
|
+
/** Enable debug logging to stderr (default: false). */
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
/** Optional callback invoked on every request for observability. */
|
|
24
|
+
onRequest?: (event: RequestEvent) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Emitted for every API request (success or failure). */
|
|
27
|
+
interface RequestEvent {
|
|
28
|
+
method: string;
|
|
29
|
+
path: string;
|
|
30
|
+
status?: number;
|
|
31
|
+
latencyMs: number;
|
|
32
|
+
attempt: number;
|
|
33
|
+
error?: LangtrainError;
|
|
34
|
+
rateLimitRemaining?: number;
|
|
35
|
+
rateLimitReset?: number;
|
|
19
36
|
}
|
|
20
37
|
declare class LangtrainError extends Error {
|
|
21
38
|
/** HTTP status code, if available. */
|
|
@@ -24,10 +41,13 @@ declare class LangtrainError extends Error {
|
|
|
24
41
|
readonly code?: string;
|
|
25
42
|
/** The original error, if any. */
|
|
26
43
|
readonly cause?: Error;
|
|
44
|
+
/** Seconds until rate limit resets (from Retry-After header). */
|
|
45
|
+
readonly retryAfter?: number;
|
|
27
46
|
constructor(message: string, options?: {
|
|
28
47
|
status?: number;
|
|
29
48
|
code?: string;
|
|
30
49
|
cause?: Error;
|
|
50
|
+
retryAfter?: number;
|
|
31
51
|
});
|
|
32
52
|
/** True if the error was a network/timeout issue (retryable). */
|
|
33
53
|
get isTransient(): boolean;
|
|
@@ -41,24 +61,32 @@ declare class LangtrainError extends Error {
|
|
|
41
61
|
/**
|
|
42
62
|
* BaseClient — abstract foundation for all Langtrain SDK clients.
|
|
43
63
|
*
|
|
44
|
-
*
|
|
64
|
+
* Features:
|
|
45
65
|
* - Shared axios instance with API key auth
|
|
46
66
|
* - Configurable timeouts
|
|
47
67
|
* - Automatic retry with exponential backoff on transient errors
|
|
68
|
+
* - Client-side token-bucket rate limiting
|
|
69
|
+
* - Retry-After header respect on 429s
|
|
48
70
|
* - Structured error wrapping (LangtrainError)
|
|
71
|
+
* - Debug logging and request event hooks
|
|
49
72
|
*/
|
|
50
73
|
declare abstract class BaseClient {
|
|
51
74
|
protected readonly http: AxiosInstance;
|
|
52
75
|
protected readonly maxRetries: number;
|
|
76
|
+
private readonly rateLimiter;
|
|
77
|
+
private readonly debug;
|
|
78
|
+
private readonly onRequest?;
|
|
53
79
|
constructor(config: ClientConfig);
|
|
54
80
|
/**
|
|
55
|
-
* Execute a request with automatic retry and error wrapping.
|
|
81
|
+
* Execute a request with rate limiting, automatic retry, and error wrapping.
|
|
56
82
|
*/
|
|
57
83
|
protected request<T>(fn: () => Promise<T>): Promise<T>;
|
|
58
84
|
/**
|
|
59
85
|
* Wrap any thrown error into a structured LangtrainError.
|
|
60
86
|
*/
|
|
61
87
|
private wrapError;
|
|
88
|
+
private log;
|
|
89
|
+
private emitEvent;
|
|
62
90
|
private sleep;
|
|
63
91
|
}
|
|
64
92
|
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,23 @@ interface ClientConfig {
|
|
|
16
16
|
timeout?: number;
|
|
17
17
|
/** Maximum number of retries on transient errors (default: 2). */
|
|
18
18
|
maxRetries?: number;
|
|
19
|
+
/** Max requests per second (client-side rate limiting, default: 10). */
|
|
20
|
+
maxRequestsPerSecond?: number;
|
|
21
|
+
/** Enable debug logging to stderr (default: false). */
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
/** Optional callback invoked on every request for observability. */
|
|
24
|
+
onRequest?: (event: RequestEvent) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Emitted for every API request (success or failure). */
|
|
27
|
+
interface RequestEvent {
|
|
28
|
+
method: string;
|
|
29
|
+
path: string;
|
|
30
|
+
status?: number;
|
|
31
|
+
latencyMs: number;
|
|
32
|
+
attempt: number;
|
|
33
|
+
error?: LangtrainError;
|
|
34
|
+
rateLimitRemaining?: number;
|
|
35
|
+
rateLimitReset?: number;
|
|
19
36
|
}
|
|
20
37
|
declare class LangtrainError extends Error {
|
|
21
38
|
/** HTTP status code, if available. */
|
|
@@ -24,10 +41,13 @@ declare class LangtrainError extends Error {
|
|
|
24
41
|
readonly code?: string;
|
|
25
42
|
/** The original error, if any. */
|
|
26
43
|
readonly cause?: Error;
|
|
44
|
+
/** Seconds until rate limit resets (from Retry-After header). */
|
|
45
|
+
readonly retryAfter?: number;
|
|
27
46
|
constructor(message: string, options?: {
|
|
28
47
|
status?: number;
|
|
29
48
|
code?: string;
|
|
30
49
|
cause?: Error;
|
|
50
|
+
retryAfter?: number;
|
|
31
51
|
});
|
|
32
52
|
/** True if the error was a network/timeout issue (retryable). */
|
|
33
53
|
get isTransient(): boolean;
|
|
@@ -41,24 +61,32 @@ declare class LangtrainError extends Error {
|
|
|
41
61
|
/**
|
|
42
62
|
* BaseClient — abstract foundation for all Langtrain SDK clients.
|
|
43
63
|
*
|
|
44
|
-
*
|
|
64
|
+
* Features:
|
|
45
65
|
* - Shared axios instance with API key auth
|
|
46
66
|
* - Configurable timeouts
|
|
47
67
|
* - Automatic retry with exponential backoff on transient errors
|
|
68
|
+
* - Client-side token-bucket rate limiting
|
|
69
|
+
* - Retry-After header respect on 429s
|
|
48
70
|
* - Structured error wrapping (LangtrainError)
|
|
71
|
+
* - Debug logging and request event hooks
|
|
49
72
|
*/
|
|
50
73
|
declare abstract class BaseClient {
|
|
51
74
|
protected readonly http: AxiosInstance;
|
|
52
75
|
protected readonly maxRetries: number;
|
|
76
|
+
private readonly rateLimiter;
|
|
77
|
+
private readonly debug;
|
|
78
|
+
private readonly onRequest?;
|
|
53
79
|
constructor(config: ClientConfig);
|
|
54
80
|
/**
|
|
55
|
-
* Execute a request with automatic retry and error wrapping.
|
|
81
|
+
* Execute a request with rate limiting, automatic retry, and error wrapping.
|
|
56
82
|
*/
|
|
57
83
|
protected request<T>(fn: () => Promise<T>): Promise<T>;
|
|
58
84
|
/**
|
|
59
85
|
* Wrap any thrown error into a structured LangtrainError.
|
|
60
86
|
*/
|
|
61
87
|
private wrapError;
|
|
88
|
+
private log;
|
|
89
|
+
private emitEvent;
|
|
62
90
|
private sleep;
|
|
63
91
|
}
|
|
64
92
|
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var chunkK5LXUJ4G_js=require('./chunk-K5LXUJ4G.js');Object.defineProperty(exports,"AgentClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.d}});Object.defineProperty(exports,"AgentTypes",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.e}});Object.defineProperty(exports,"BaseClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.c}});Object.defineProperty(exports,"FileClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.f}});Object.defineProperty(exports,"GuardrailClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.m}});Object.defineProperty(exports,"LangtrainError",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.b}});Object.defineProperty(exports,"Langtune",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.r}});Object.defineProperty(exports,"Langvision",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.q}});Object.defineProperty(exports,"ModelClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.i}});Object.defineProperty(exports,"ModelTypes",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.j}});Object.defineProperty(exports,"SecretClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.k}});Object.defineProperty(exports,"SecretTypes",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.l}});Object.defineProperty(exports,"SubscriptionClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.h}});Object.defineProperty(exports,"Text",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.p}});Object.defineProperty(exports,"TrainingClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.g}});Object.defineProperty(exports,"UsageClient",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.n}});Object.defineProperty(exports,"Vision",{enumerable:true,get:function(){return chunkK5LXUJ4G_js.o}});//# sourceMappingURL=index.js.map
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export{d as AgentClient,e as AgentTypes,c as BaseClient,f as FileClient,m as GuardrailClient,b as LangtrainError,r as Langtune,q as Langvision,i as ModelClient,j as ModelTypes,k as SecretClient,l as SecretTypes,h as SubscriptionClient,p as Text,g as TrainingClient,n as UsageClient,o as Vision}from'./chunk-
|
|
1
|
+
export{d as AgentClient,e as AgentTypes,c as BaseClient,f as FileClient,m as GuardrailClient,b as LangtrainError,r as Langtune,q as Langvision,i as ModelClient,j as ModelTypes,k as SecretClient,l as SecretTypes,h as SubscriptionClient,p as Text,g as TrainingClient,n as UsageClient,o as Vision}from'./chunk-A7PMJDMV.mjs';//# sourceMappingURL=index.mjs.map
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/package.json
CHANGED
package/src/cli/auth.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import { password, isCancel, cancel, intro, green, yellow, red, bgMagenta, black, spinner, gray, cyan, dim, bold } from './ui';
|
|
1
|
+
import { password, isCancel, cancel, intro, green, yellow, red, bgMagenta, black, spinner, gray, cyan, dim, bold, note } from './ui';
|
|
2
2
|
import { getConfig, saveConfig } from './config';
|
|
3
3
|
import { SubscriptionClient, SubscriptionInfo } from '../index';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const API_BASE = 'https://api.langtrain.ai/api/v1';
|
|
10
|
+
|
|
11
|
+
async function openBrowser(url: string) {
|
|
12
|
+
try {
|
|
13
|
+
const command = process.platform === 'win32'
|
|
14
|
+
? `start ${url}`
|
|
15
|
+
: process.platform === 'darwin'
|
|
16
|
+
? `open "${url}"`
|
|
17
|
+
: `xdg-open "${url}"`;
|
|
18
|
+
await execAsync(command);
|
|
19
|
+
} catch {
|
|
20
|
+
// Silently fail if browser can't open
|
|
21
|
+
}
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
/**
|
|
6
25
|
* Quick check if API key is stored (no network call).
|
|
@@ -28,9 +47,86 @@ export async function ensureAuth(): Promise<string> {
|
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
/**
|
|
31
|
-
* Interactive login —
|
|
50
|
+
* Interactive login — browser-based OAuth flow with API key fallback.
|
|
32
51
|
*/
|
|
33
52
|
export async function handleLogin() {
|
|
53
|
+
const s = spinner();
|
|
54
|
+
s.start('Connecting to Langtrain...');
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// 1. Request device code
|
|
58
|
+
const { data: codeData } = await axios.post(`${API_BASE}/auth/device/code`);
|
|
59
|
+
const { device_code, user_code, verification_url, expires_in, interval } = codeData;
|
|
60
|
+
|
|
61
|
+
s.stop(green('Connected.'));
|
|
62
|
+
|
|
63
|
+
console.log('\n ' + bgMagenta(black(' AUTHENTICATION ')) + '\n');
|
|
64
|
+
console.log(` To log in, please open your browser to:\n ${cyan(verification_url + '?code=' + user_code)}\n`);
|
|
65
|
+
|
|
66
|
+
note(
|
|
67
|
+
`Confirmation Code:\n${bold(user_code)}`,
|
|
68
|
+
'Verify in Browser'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
console.log(gray(' Opening browser automatically...'));
|
|
72
|
+
await openBrowser(`${verification_url}?code=${user_code}`);
|
|
73
|
+
|
|
74
|
+
const pollSpinner = spinner();
|
|
75
|
+
pollSpinner.start('Waiting for approval in browser...');
|
|
76
|
+
|
|
77
|
+
// 2. Poll for token
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
const timeout = expires_in * 1000;
|
|
80
|
+
|
|
81
|
+
while (Date.now() - startTime < timeout) {
|
|
82
|
+
try {
|
|
83
|
+
const { data: tokenData } = await axios.post(`${API_BASE}/auth/device/token?device_code=${device_code}`);
|
|
84
|
+
|
|
85
|
+
if (tokenData.status === 'approved') {
|
|
86
|
+
const apiKey = tokenData.api_key;
|
|
87
|
+
pollSpinner.stop(green('Approved!'));
|
|
88
|
+
|
|
89
|
+
const verifySpinner = spinner();
|
|
90
|
+
verifySpinner.start('Verifying credentials...');
|
|
91
|
+
|
|
92
|
+
const client = new SubscriptionClient({ apiKey });
|
|
93
|
+
const info = await client.getStatus();
|
|
94
|
+
|
|
95
|
+
const planBadge = info.plan === 'pro'
|
|
96
|
+
? bgMagenta(black(' PRO '))
|
|
97
|
+
: info.plan === 'enterprise'
|
|
98
|
+
? bgMagenta(black(' ENTERPRISE '))
|
|
99
|
+
: ' FREE ';
|
|
100
|
+
|
|
101
|
+
verifySpinner.stop(green(`Authenticated ${planBadge}`));
|
|
102
|
+
|
|
103
|
+
const config = getConfig();
|
|
104
|
+
saveConfig({ ...config, apiKey });
|
|
105
|
+
console.log(green(' ✔ Credentials saved to ~/.langtrain/config.json\n'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (tokenData.status === 'expired') {
|
|
110
|
+
pollSpinner.stop(red('Device code expired.'));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Wait for the requested interval
|
|
115
|
+
await new Promise(r => setTimeout(r, interval * 1000));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Ignore network errors during polling
|
|
118
|
+
await new Promise(r => setTimeout(r, interval * 1000));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pollSpinner.stop(yellow('Login timed out.'));
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
s.stop(red('Could not reach Langtrain server.'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Fallback to manual entry if browser flow fails
|
|
128
|
+
console.log(gray('\n Browser login failed or timed out. Falling back to manual entry.'));
|
|
129
|
+
|
|
34
130
|
while (true) {
|
|
35
131
|
console.log(dim(' ─────────────────────────────────────'));
|
|
36
132
|
console.log(gray(' Get your API Key at: ') + cyan('https://app.langtrain.xyz/home/api'));
|
|
@@ -49,8 +145,8 @@ export async function handleLogin() {
|
|
|
49
145
|
process.exit(0);
|
|
50
146
|
}
|
|
51
147
|
|
|
52
|
-
const
|
|
53
|
-
|
|
148
|
+
const verifySpinner = spinner();
|
|
149
|
+
verifySpinner.start('Verifying API Key...');
|
|
54
150
|
|
|
55
151
|
try {
|
|
56
152
|
const client = new SubscriptionClient({ apiKey: apiKey as string });
|
|
@@ -62,22 +158,14 @@ export async function handleLogin() {
|
|
|
62
158
|
? bgMagenta(black(' ENTERPRISE '))
|
|
63
159
|
: ' FREE ';
|
|
64
160
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Show initial token info if available
|
|
68
|
-
if (info.usage) {
|
|
69
|
-
const used = info.usage.tokensUsedThisMonth || 0;
|
|
70
|
-
const limit = info.usage.tokenLimit || 10000;
|
|
71
|
-
const pct = Math.round((used / limit) * 100);
|
|
72
|
-
console.log(dim(` Tokens: ${used.toLocaleString()} / ${limit.toLocaleString()} (${pct}% used)`));
|
|
73
|
-
}
|
|
161
|
+
verifySpinner.stop(green(`Authenticated ${planBadge}`));
|
|
74
162
|
|
|
75
163
|
const config = getConfig();
|
|
76
164
|
saveConfig({ ...config, apiKey: apiKey as string });
|
|
77
165
|
console.log(green(' ✔ Credentials saved to ~/.langtrain/config.json\n'));
|
|
78
166
|
return;
|
|
79
167
|
} catch (e: any) {
|
|
80
|
-
|
|
168
|
+
verifySpinner.stop(red('Invalid API Key. Please try again.'));
|
|
81
169
|
}
|
|
82
170
|
}
|
|
83
171
|
}
|
package/src/lib/base.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
|
|
1
|
+
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
2
|
|
|
3
3
|
// ── Shared Configuration ───────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -12,11 +12,30 @@ export interface ClientConfig {
|
|
|
12
12
|
timeout?: number;
|
|
13
13
|
/** Maximum number of retries on transient errors (default: 2). */
|
|
14
14
|
maxRetries?: number;
|
|
15
|
+
/** Max requests per second (client-side rate limiting, default: 10). */
|
|
16
|
+
maxRequestsPerSecond?: number;
|
|
17
|
+
/** Enable debug logging to stderr (default: false). */
|
|
18
|
+
debug?: boolean;
|
|
19
|
+
/** Optional callback invoked on every request for observability. */
|
|
20
|
+
onRequest?: (event: RequestEvent) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Emitted for every API request (success or failure). */
|
|
24
|
+
export interface RequestEvent {
|
|
25
|
+
method: string;
|
|
26
|
+
path: string;
|
|
27
|
+
status?: number;
|
|
28
|
+
latencyMs: number;
|
|
29
|
+
attempt: number;
|
|
30
|
+
error?: LangtrainError;
|
|
31
|
+
rateLimitRemaining?: number;
|
|
32
|
+
rateLimitReset?: number;
|
|
15
33
|
}
|
|
16
34
|
|
|
17
35
|
const DEFAULT_BASE_URL = 'https://api.langtrain.ai/api/v1';
|
|
18
36
|
const DEFAULT_TIMEOUT = 30_000;
|
|
19
37
|
const DEFAULT_MAX_RETRIES = 2;
|
|
38
|
+
const DEFAULT_MAX_RPS = 10;
|
|
20
39
|
const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
21
40
|
|
|
22
41
|
// ── Custom Error ───────────────────────────────────────────────────────────
|
|
@@ -28,13 +47,21 @@ export class LangtrainError extends Error {
|
|
|
28
47
|
readonly code?: string;
|
|
29
48
|
/** The original error, if any. */
|
|
30
49
|
readonly cause?: Error;
|
|
50
|
+
/** Seconds until rate limit resets (from Retry-After header). */
|
|
51
|
+
readonly retryAfter?: number;
|
|
31
52
|
|
|
32
|
-
constructor(message: string, options?: {
|
|
53
|
+
constructor(message: string, options?: {
|
|
54
|
+
status?: number;
|
|
55
|
+
code?: string;
|
|
56
|
+
cause?: Error;
|
|
57
|
+
retryAfter?: number;
|
|
58
|
+
}) {
|
|
33
59
|
super(message);
|
|
34
60
|
this.name = 'LangtrainError';
|
|
35
61
|
this.status = options?.status;
|
|
36
62
|
this.code = options?.code;
|
|
37
63
|
this.cause = options?.cause;
|
|
64
|
+
this.retryAfter = options?.retryAfter;
|
|
38
65
|
}
|
|
39
66
|
|
|
40
67
|
/** True if the error was a network/timeout issue (retryable). */
|
|
@@ -59,23 +86,85 @@ export class LangtrainError extends Error {
|
|
|
59
86
|
}
|
|
60
87
|
}
|
|
61
88
|
|
|
89
|
+
// ── Token Bucket Rate Limiter ──────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Simple token-bucket rate limiter.
|
|
93
|
+
* Allows bursting up to `capacity` requests, refills at `refillRate` tokens/sec.
|
|
94
|
+
*/
|
|
95
|
+
class RateLimiter {
|
|
96
|
+
private tokens: number;
|
|
97
|
+
private lastRefill: number;
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
private readonly capacity: number,
|
|
101
|
+
private readonly refillRate: number,
|
|
102
|
+
) {
|
|
103
|
+
this.tokens = capacity;
|
|
104
|
+
this.lastRefill = Date.now();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Wait until a token is available, then consume it. */
|
|
108
|
+
async acquire(): Promise<void> {
|
|
109
|
+
this.refill();
|
|
110
|
+
|
|
111
|
+
if (this.tokens >= 1) {
|
|
112
|
+
this.tokens -= 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Wait for next token
|
|
117
|
+
const waitMs = ((1 - this.tokens) / this.refillRate) * 1000;
|
|
118
|
+
await this.sleep(Math.ceil(waitMs));
|
|
119
|
+
this.refill();
|
|
120
|
+
this.tokens -= 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Pause for `seconds` (e.g. from Retry-After header). */
|
|
124
|
+
async waitFor(seconds: number): Promise<void> {
|
|
125
|
+
await this.sleep(seconds * 1000);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private refill(): void {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
131
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
|
|
132
|
+
this.lastRefill = now;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private sleep(ms: number): Promise<void> {
|
|
136
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
62
140
|
// ── Base Client ────────────────────────────────────────────────────────────
|
|
63
141
|
|
|
64
142
|
/**
|
|
65
143
|
* BaseClient — abstract foundation for all Langtrain SDK clients.
|
|
66
144
|
*
|
|
67
|
-
*
|
|
145
|
+
* Features:
|
|
68
146
|
* - Shared axios instance with API key auth
|
|
69
147
|
* - Configurable timeouts
|
|
70
148
|
* - Automatic retry with exponential backoff on transient errors
|
|
149
|
+
* - Client-side token-bucket rate limiting
|
|
150
|
+
* - Retry-After header respect on 429s
|
|
71
151
|
* - Structured error wrapping (LangtrainError)
|
|
152
|
+
* - Debug logging and request event hooks
|
|
72
153
|
*/
|
|
73
154
|
export abstract class BaseClient {
|
|
74
155
|
protected readonly http: AxiosInstance;
|
|
75
156
|
protected readonly maxRetries: number;
|
|
157
|
+
private readonly rateLimiter: RateLimiter;
|
|
158
|
+
private readonly debug: boolean;
|
|
159
|
+
private readonly onRequest?: (event: RequestEvent) => void;
|
|
76
160
|
|
|
77
161
|
constructor(config: ClientConfig) {
|
|
78
162
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
163
|
+
this.debug = config.debug ?? false;
|
|
164
|
+
this.onRequest = config.onRequest;
|
|
165
|
+
|
|
166
|
+
const maxRps = config.maxRequestsPerSecond ?? DEFAULT_MAX_RPS;
|
|
167
|
+
this.rateLimiter = new RateLimiter(maxRps, maxRps);
|
|
79
168
|
|
|
80
169
|
this.http = axios.create({
|
|
81
170
|
baseURL: config.baseUrl || DEFAULT_BASE_URL,
|
|
@@ -83,30 +172,61 @@ export abstract class BaseClient {
|
|
|
83
172
|
headers: {
|
|
84
173
|
'X-API-Key': config.apiKey,
|
|
85
174
|
'Content-Type': 'application/json',
|
|
175
|
+
'User-Agent': 'langtrain-sdk/0.2.x',
|
|
86
176
|
},
|
|
87
177
|
});
|
|
88
178
|
}
|
|
89
179
|
|
|
90
180
|
/**
|
|
91
|
-
* Execute a request with automatic retry and error wrapping.
|
|
181
|
+
* Execute a request with rate limiting, automatic retry, and error wrapping.
|
|
92
182
|
*/
|
|
93
183
|
protected async request<T>(fn: () => Promise<T>): Promise<T> {
|
|
94
184
|
let lastError: LangtrainError | undefined;
|
|
95
185
|
|
|
96
186
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
187
|
+
// Acquire a rate limiter token before each attempt
|
|
188
|
+
await this.rateLimiter.acquire();
|
|
189
|
+
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
|
|
97
192
|
try {
|
|
98
|
-
|
|
193
|
+
const result = await fn();
|
|
194
|
+
const latencyMs = Date.now() - start;
|
|
195
|
+
|
|
196
|
+
this.log(`✓ request succeeded (${latencyMs}ms, attempt ${attempt + 1})`);
|
|
197
|
+
this.emitEvent({ method: '', path: '', status: 200, latencyMs, attempt });
|
|
198
|
+
|
|
199
|
+
return result;
|
|
99
200
|
} catch (error) {
|
|
201
|
+
const latencyMs = Date.now() - start;
|
|
100
202
|
lastError = this.wrapError(error);
|
|
101
203
|
|
|
102
|
-
|
|
204
|
+
this.log(`✗ request failed: ${lastError.message} (${latencyMs}ms, attempt ${attempt + 1})`);
|
|
205
|
+
this.emitEvent({
|
|
206
|
+
method: '', path: '',
|
|
207
|
+
status: lastError.status,
|
|
208
|
+
latencyMs,
|
|
209
|
+
attempt,
|
|
210
|
+
error: lastError,
|
|
211
|
+
rateLimitRemaining: lastError.isRateLimited ? 0 : undefined,
|
|
212
|
+
rateLimitReset: lastError.retryAfter,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Don't retry non-transient errors or on last attempt
|
|
103
216
|
if (!lastError.isTransient || attempt === this.maxRetries) {
|
|
104
217
|
throw lastError;
|
|
105
218
|
}
|
|
106
219
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
220
|
+
// If rate-limited with Retry-After, respect it
|
|
221
|
+
if (lastError.isRateLimited && lastError.retryAfter) {
|
|
222
|
+
this.log(`⏳ rate limited, waiting ${lastError.retryAfter}s (Retry-After)`);
|
|
223
|
+
await this.rateLimiter.waitFor(lastError.retryAfter);
|
|
224
|
+
} else {
|
|
225
|
+
// Exponential backoff: 500ms, 1000ms, 2000ms...
|
|
226
|
+
const delay = Math.min(500 * Math.pow(2, attempt), 5000);
|
|
227
|
+
this.log(`↻ retrying in ${delay}ms...`);
|
|
228
|
+
await this.sleep(delay);
|
|
229
|
+
}
|
|
110
230
|
}
|
|
111
231
|
}
|
|
112
232
|
|
|
@@ -121,21 +241,35 @@ export abstract class BaseClient {
|
|
|
121
241
|
|
|
122
242
|
if (error instanceof AxiosError) {
|
|
123
243
|
const status = error.response?.status;
|
|
244
|
+
const headers = error.response?.headers;
|
|
124
245
|
const data = error.response?.data as Record<string, unknown> | undefined;
|
|
125
246
|
const serverMessage = data?.detail ?? data?.message ?? data?.error;
|
|
126
247
|
|
|
248
|
+
// Parse Retry-After header (seconds or HTTP date)
|
|
249
|
+
let retryAfter: number | undefined;
|
|
250
|
+
const retryHeader = headers?.['retry-after'];
|
|
251
|
+
if (retryHeader) {
|
|
252
|
+
const parsed = Number(retryHeader);
|
|
253
|
+
retryAfter = isNaN(parsed)
|
|
254
|
+
? Math.max(0, Math.ceil((new Date(retryHeader).getTime() - Date.now()) / 1000))
|
|
255
|
+
: parsed;
|
|
256
|
+
}
|
|
257
|
+
|
|
127
258
|
const message = serverMessage
|
|
128
259
|
? String(serverMessage)
|
|
129
260
|
: error.code === 'ECONNABORTED'
|
|
130
261
|
? `Request timed out`
|
|
131
|
-
: status
|
|
132
|
-
? `
|
|
133
|
-
:
|
|
262
|
+
: status === 429
|
|
263
|
+
? `Rate limited${retryAfter ? ` — retry in ${retryAfter}s` : ''}`
|
|
264
|
+
: status
|
|
265
|
+
? `API request failed with status ${status}`
|
|
266
|
+
: `Network error: ${error.message}`;
|
|
134
267
|
|
|
135
268
|
return new LangtrainError(message, {
|
|
136
269
|
status,
|
|
137
270
|
code: error.code,
|
|
138
271
|
cause: error,
|
|
272
|
+
retryAfter,
|
|
139
273
|
});
|
|
140
274
|
}
|
|
141
275
|
|
|
@@ -146,6 +280,22 @@ export abstract class BaseClient {
|
|
|
146
280
|
return new LangtrainError(String(error));
|
|
147
281
|
}
|
|
148
282
|
|
|
283
|
+
private log(msg: string): void {
|
|
284
|
+
if (this.debug) {
|
|
285
|
+
process.stderr.write(`[langtrain] ${msg}\n`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private emitEvent(event: RequestEvent): void {
|
|
290
|
+
if (this.onRequest) {
|
|
291
|
+
try {
|
|
292
|
+
this.onRequest(event);
|
|
293
|
+
} catch {
|
|
294
|
+
// Never let user callbacks crash the SDK
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
149
299
|
private sleep(ms: number): Promise<void> {
|
|
150
300
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
151
301
|
}
|
package/dist/chunk-36QS5AXY.mjs
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import w,{AxiosError}from'axios';import k from'form-data';import y from'fs';import*as langvision from'langvision';export{langvision as o };export{Langvision as q}from'langvision';import*as langtune from'langtune';export{langtune as p };export{Langtune as r}from'langtune';var C=Object.defineProperty;var q=(n=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(n,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):n)(function(n){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+n+'" is not supported')});var p=(n,e)=>{for(var t in e)C(n,t,{get:e[t],enumerable:true});};var T="https://api.langtrain.ai/api/v1",A=3e4,P=2,F=[408,429,500,502,503,504],a=class extends Error{constructor(e,t){super(e),this.name="LangtrainError",this.status=t?.status,this.code=t?.code,this.cause=t?.cause;}get isTransient(){return this.code==="ECONNABORTED"||this.code==="NETWORK_ERROR"||this.status!==void 0&&F.includes(this.status)}get isAuthError(){return this.status===401||this.status===403}get isNotFound(){return this.status===404}get isRateLimited(){return this.status===429}},s=class{constructor(e){this.maxRetries=e.maxRetries??P,this.http=w.create({baseURL:e.baseUrl||T,timeout:e.timeout??A,headers:{"X-API-Key":e.apiKey,"Content-Type":"application/json"}});}async request(e){let t;for(let r=0;r<=this.maxRetries;r++)try{return await e()}catch(i){if(t=this.wrapError(i),!t.isTransient||r===this.maxRetries)throw t;let o=Math.min(500*Math.pow(2,r),5e3);await this.sleep(o);}throw t}wrapError(e){if(e instanceof a)return e;if(e instanceof AxiosError){let t=e.response?.status,r=e.response?.data,i=r?.detail??r?.message??r?.error,o=i?String(i):e.code==="ECONNABORTED"?"Request timed out":t?`API request failed with status ${t}`:`Network error: ${e.message}`;return new a(o,{status:t,code:e.code,cause:e})}return e instanceof Error?new a(e.message,{cause:e}):new a(String(e))}sleep(e){return new Promise(t=>setTimeout(t,e))}};var b={};p(b,{AgentClient:()=>u});var u=class extends s{constructor(e){super(e);}async list(e){return this.request(async()=>{let t={};return e&&(t.workspace_id=e),(await this.http.get("/agents",{params:t})).data.agents})}async get(e){return this.request(async()=>(await this.http.get(`/agents/${e}`)).data)}async create(e){return this.request(async()=>(await this.http.post("/agents/",e)).data)}async delete(e){return this.request(async()=>{await this.http.delete(`/agents/${e}`);})}async execute(e,t,r=[],i){return this.request(async()=>(await this.http.post(`/agents/${e}/execute`,{input:t,messages:r,conversation_id:i})).data)}async logs(e,t=100){return this.request(async()=>(await this.http.get(`/agents/${e}/logs`,{params:{limit:t}})).data.logs)}};var d=class extends s{constructor(e){super(e);}async upload(e,t,r="fine-tune"){if(!y.existsSync(e))throw new Error(`File not found: ${e}`);return this.request(async()=>{let i=new k;return i.append("file",y.createReadStream(e)),t&&i.append("workspace_id",t),i.append("purpose",r),(await this.http.post("/files",i,{headers:i.getHeaders(),maxContentLength:1/0,maxBodyLength:1/0})).data})}async list(e,t){return this.request(async()=>{let r={workspace_id:e};return t&&(r.purpose=t),(await this.http.get("/files",{params:r})).data.data})}async delete(e){return this.request(async()=>{await this.http.delete(`/files/${e}`);})}};var l=class extends s{constructor(e){super(e);}async createJob(e){return this.request(async()=>(await this.http.post("/finetune/jobs",e)).data)}async listJobs(e,t=10){return this.request(async()=>(await this.http.get("/finetune/jobs",{params:{workspace_id:e,limit:t}})).data)}async getJob(e){return this.request(async()=>(await this.http.get(`/finetune/jobs/${e}`)).data)}async cancelJob(e){return this.request(async()=>(await this.http.post(`/finetune/jobs/${e}/cancel`)).data)}};var m=class extends s{constructor(e){super(e);}async getStatus(){return this.request(async()=>(await this.http.get("/subscription/status")).data)}async checkFeature(e){return this.request(async()=>(await this.http.get(`/subscription/check/${e}`)).data)}async getLimits(){return this.request(async()=>(await this.http.get("/subscription/analytics")).data)}};var _={};p(_,{ModelClient:()=>c});var c=class extends s{constructor(e){super(e);}async list(e){return this.request(async()=>{let t={};return e&&(t.task=e),(await this.http.get("/models",{params:t})).data.data})}async get(e){return this.request(async()=>(await this.http.get(`/models/${e}`)).data)}};var x={};p(x,{SecretClient:()=>g});var g=class extends s{constructor(e){super(e);}async list(e){return this.request(async()=>{let t={};return e&&(t.workspace_id=e),(await this.http.get("/secrets",{params:t})).data.secrets})}async set(e,t,r){return this.request(async()=>(await this.http.post("/secrets",{key:e,value:t,workspace_id:r})).data)}async delete(e,t){return this.request(async()=>{let r={};t&&(r.workspace_id=t),await this.http.delete(`/secrets/${e}`,{params:r});})}};var h=class extends s{constructor(e){super(e);}async list(e){return this.request(async()=>{let t={};return e&&(t.workspace_id=e),(await this.http.get("/guardrails/",{params:t})).data})}async get(e){return this.request(async()=>(await this.http.get(`/guardrails/${e}`)).data)}async create(e){return this.request(async()=>(await this.http.post("/guardrails/",e)).data)}async delete(e){return this.request(async()=>{await this.http.delete(`/guardrails/${e}`);})}async apply(e,t){return this.request(async()=>(await this.http.post("/guardrails/apply",{dataset_id:e,guardrail_id:t})).data)}};var f=class extends s{constructor(e){super(e);}async getSummary(e){return this.request(async()=>(await this.http.get("/usage",{params:{workspace_id:e}})).data)}async getHistory(e,t=30){return this.request(async()=>(await this.http.get("/usage/history",{params:{workspace_id:e,days:t}})).data.history)}};
|
|
2
|
-
export{q as a,a as b,s as c,u as d,b as e,d as f,l as g,m as h,c as i,_ as j,g as k,x as l,h as m,f as n};//# sourceMappingURL=chunk-36QS5AXY.mjs.map
|
|
3
|
-
//# sourceMappingURL=chunk-36QS5AXY.mjs.map
|