@respan/cli 0.3.0 → 0.3.1
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/commands/auth/login.d.ts +1 -1
- package/dist/commands/auth/login.js +22 -13
- package/dist/commands/auth/status.js +1 -1
- package/dist/commands/logs/list.d.ts +3 -0
- package/dist/commands/logs/list.js +30 -22
- package/dist/commands/logs/summary.d.ts +3 -0
- package/dist/commands/logs/summary.js +24 -2
- package/dist/commands/traces/list.js +22 -12
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/auth.js +2 -1
- package/dist/lib/filters.d.ts +24 -0
- package/dist/lib/filters.js +100 -0
- package/oclif.manifest.json +234 -193
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ export default class AuthLogin extends BaseCommand {
|
|
|
4
4
|
static flags: {
|
|
5
5
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
6
|
'profile-name': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
enterprise: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -3,12 +3,14 @@ import { select, password } from '@inquirer/prompts';
|
|
|
3
3
|
import * as http from 'node:http';
|
|
4
4
|
import * as open from 'node:child_process';
|
|
5
5
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
6
|
-
import { setCredential } from '../../lib/config.js';
|
|
6
|
+
import { setCredential, setActiveProfile } from '../../lib/config.js';
|
|
7
7
|
import { printLoginSuccess } from '../../lib/banner.js';
|
|
8
|
+
import { DEFAULT_BASE_URL, ENTERPRISE_BASE_URL } from '../../lib/auth.js';
|
|
8
9
|
const CALLBACK_PORT = 18392;
|
|
9
10
|
const CALLBACK_PATH = '/callback';
|
|
10
11
|
const LOGIN_TIMEOUT_MS = 120_000;
|
|
11
12
|
const LOGIN_URL_BASE = 'https://platform.respan.ai/login';
|
|
13
|
+
const ENTERPRISE_LOGIN_URL_BASE = 'https://enterprise.respan.ai/login';
|
|
12
14
|
function openBrowser(url) {
|
|
13
15
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
14
16
|
open.exec(`${cmd} "${url}"`);
|
|
@@ -71,12 +73,10 @@ function waitForBrowserLogin(enterprise) {
|
|
|
71
73
|
});
|
|
72
74
|
server.listen(CALLBACK_PORT, '127.0.0.1', () => {
|
|
73
75
|
const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
74
|
-
const
|
|
76
|
+
const loginUrlBase = enterprise ? ENTERPRISE_LOGIN_URL_BASE : LOGIN_URL_BASE;
|
|
77
|
+
const loginUrl = new URL(loginUrlBase);
|
|
75
78
|
loginUrl.searchParams.set('mode', 'cli');
|
|
76
79
|
loginUrl.searchParams.set('redirect_uri', redirectUri);
|
|
77
|
-
if (enterprise) {
|
|
78
|
-
loginUrl.searchParams.set('enterprise', 'true');
|
|
79
|
-
}
|
|
80
80
|
console.log('');
|
|
81
81
|
console.log(' Opening browser to log in...');
|
|
82
82
|
console.log(` If the browser doesn't open, visit:`);
|
|
@@ -92,34 +92,43 @@ class AuthLogin extends BaseCommand {
|
|
|
92
92
|
const { flags } = await this.parse(AuthLogin);
|
|
93
93
|
this.globalFlags = flags;
|
|
94
94
|
const profile = flags['profile-name'] || 'default';
|
|
95
|
-
|
|
95
|
+
setActiveProfile(profile);
|
|
96
|
+
// Step 1: Determine environment (skip if --enterprise flag or --api-key with flag)
|
|
97
|
+
const enterprise = flags.enterprise || (!flags['api-key'] && await select({
|
|
98
|
+
message: 'Select your environment:',
|
|
99
|
+
choices: [
|
|
100
|
+
{ name: 'Respan Platform', value: false },
|
|
101
|
+
{ name: 'Enterprise', value: true },
|
|
102
|
+
],
|
|
103
|
+
}));
|
|
104
|
+
const baseUrl = flags['base-url'] || (enterprise ? ENTERPRISE_BASE_URL : DEFAULT_BASE_URL);
|
|
105
|
+
// If --api-key passed directly, skip auth method prompt
|
|
96
106
|
if (flags['api-key']) {
|
|
97
|
-
setCredential(profile, { type: 'api_key', apiKey: flags['api-key'], baseUrl
|
|
107
|
+
setCredential(profile, { type: 'api_key', apiKey: flags['api-key'], baseUrl });
|
|
98
108
|
await printLoginSuccess(undefined, profile);
|
|
99
109
|
return;
|
|
100
110
|
}
|
|
111
|
+
// Step 2: Choose auth method
|
|
101
112
|
const method = await select({
|
|
102
|
-
message: 'How would you like to
|
|
113
|
+
message: 'How would you like to authenticate?',
|
|
103
114
|
choices: [
|
|
104
115
|
{ name: 'Browser login (recommended)', value: 'browser' },
|
|
105
|
-
{ name: 'Enterprise SSO', value: 'enterprise' },
|
|
106
116
|
{ name: 'API key', value: 'api_key' },
|
|
107
117
|
],
|
|
108
118
|
});
|
|
109
119
|
if (method === 'api_key') {
|
|
110
120
|
const apiKey = await password({ message: 'Enter your Respan API key:' });
|
|
111
|
-
setCredential(profile, { type: 'api_key', apiKey, baseUrl
|
|
121
|
+
setCredential(profile, { type: 'api_key', apiKey, baseUrl });
|
|
112
122
|
await printLoginSuccess(undefined, profile);
|
|
113
123
|
return;
|
|
114
124
|
}
|
|
115
|
-
const enterprise = method === 'enterprise' || flags.enterprise;
|
|
116
125
|
const result = await waitForBrowserLogin(enterprise);
|
|
117
126
|
setCredential(profile, {
|
|
118
127
|
type: 'jwt',
|
|
119
128
|
accessToken: result.token,
|
|
120
129
|
refreshToken: result.refreshToken,
|
|
121
130
|
email: result.email || '',
|
|
122
|
-
baseUrl
|
|
131
|
+
baseUrl,
|
|
123
132
|
});
|
|
124
133
|
await printLoginSuccess(result.email, profile);
|
|
125
134
|
}
|
|
@@ -129,7 +138,7 @@ AuthLogin.flags = {
|
|
|
129
138
|
...BaseCommand.baseFlags,
|
|
130
139
|
'api-key': Flags.string({ description: 'API key to store (skips interactive prompt)' }),
|
|
131
140
|
'profile-name': Flags.string({ description: 'Profile name', default: 'default' }),
|
|
132
|
-
'base-url': Flags.string({ description: 'API base URL
|
|
141
|
+
'base-url': Flags.string({ description: 'API base URL (auto-detected from login method if not set)' }),
|
|
133
142
|
enterprise: Flags.boolean({ description: 'Use enterprise SSO login', default: false }),
|
|
134
143
|
};
|
|
135
144
|
export default AuthLogin;
|
|
@@ -4,7 +4,7 @@ class AuthStatus extends BaseCommand {
|
|
|
4
4
|
async run() {
|
|
5
5
|
const { flags } = await this.parse(AuthStatus);
|
|
6
6
|
this.globalFlags = flags;
|
|
7
|
-
const profile = getActiveProfile();
|
|
7
|
+
const profile = flags.profile || getActiveProfile();
|
|
8
8
|
const cred = getCredential(profile);
|
|
9
9
|
if (!cred) {
|
|
10
10
|
this.log('Not authenticated. Run `respan auth login`.');
|
|
@@ -8,6 +8,9 @@ export default class LogsList extends BaseCommand {
|
|
|
8
8
|
'start-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
'end-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'all-envs': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'is-test': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'include-fields': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
14
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
15
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
16
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
+
import { parseFilters, FILTER_SYNTAX_HELP, LOG_FIELDS_HELP, FILTER_EXAMPLES } from '../../lib/filters.js';
|
|
3
4
|
import { extractPagination, formatPaginationInfo } from '../../lib/pagination.js';
|
|
4
5
|
class LogsList extends BaseCommand {
|
|
5
6
|
async run() {
|
|
@@ -7,27 +8,23 @@ class LogsList extends BaseCommand {
|
|
|
7
8
|
this.globalFlags = flags;
|
|
8
9
|
try {
|
|
9
10
|
const client = this.getClient();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
if (flags['sort-by'])
|
|
15
|
-
params.sort_by = flags['sort-by'];
|
|
16
|
-
if (flags['start-time'])
|
|
17
|
-
params.start_time = flags['start-time'];
|
|
18
|
-
if (flags['end-time'])
|
|
19
|
-
params.end_time = flags['end-time'];
|
|
20
|
-
if (flags.filter && flags.filter.length > 0)
|
|
21
|
-
params.filters = flags.filter;
|
|
22
|
-
// listSpans requires start_time, end_time, sort_by, operator
|
|
11
|
+
let filters;
|
|
12
|
+
if (flags.filter && flags.filter.length > 0) {
|
|
13
|
+
filters = parseFilters(flags.filter);
|
|
14
|
+
}
|
|
23
15
|
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
|
24
16
|
const data = await this.spin('Fetching logs', () => client.logs.listSpans({
|
|
25
|
-
start_time:
|
|
26
|
-
end_time:
|
|
27
|
-
sort_by:
|
|
17
|
+
start_time: flags['start-time'] || oneHourAgo,
|
|
18
|
+
end_time: flags['end-time'] || new Date().toISOString(),
|
|
19
|
+
sort_by: flags['sort-by'] || '-id',
|
|
28
20
|
operator: '',
|
|
29
|
-
page_size:
|
|
30
|
-
page:
|
|
21
|
+
page_size: flags.limit,
|
|
22
|
+
page: flags.page,
|
|
23
|
+
is_test: flags['is-test'],
|
|
24
|
+
all_envs: flags['all-envs'],
|
|
25
|
+
fetch_filters: 'false',
|
|
26
|
+
include_fields: flags['include-fields'],
|
|
27
|
+
filters,
|
|
31
28
|
}));
|
|
32
29
|
this.outputResult(data, ['id', 'model', 'prompt_tokens', 'completion_tokens', 'cost', 'latency', 'timestamp']);
|
|
33
30
|
const pagination = extractPagination(data, flags.page);
|
|
@@ -38,14 +35,25 @@ class LogsList extends BaseCommand {
|
|
|
38
35
|
}
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
|
-
LogsList.description =
|
|
38
|
+
LogsList.description = `List and filter LLM request logs (spans).
|
|
39
|
+
|
|
40
|
+
Supports pagination, sorting, time range, and server-side filtering.
|
|
41
|
+
|
|
42
|
+
${FILTER_SYNTAX_HELP}
|
|
43
|
+
|
|
44
|
+
${LOG_FIELDS_HELP}
|
|
45
|
+
|
|
46
|
+
${FILTER_EXAMPLES}`;
|
|
42
47
|
LogsList.flags = {
|
|
43
48
|
...BaseCommand.baseFlags,
|
|
44
|
-
limit: Flags.integer({ description: 'Number of results per page', default: 50 }),
|
|
49
|
+
limit: Flags.integer({ description: 'Number of results per page (max 1000)', default: 50 }),
|
|
45
50
|
page: Flags.integer({ description: 'Page number', default: 1 }),
|
|
46
|
-
'sort-by': Flags.string({ description: 'Sort field' }),
|
|
51
|
+
'sort-by': Flags.string({ description: 'Sort field (prefix with - for descending, e.g. -cost, -latency)' }),
|
|
47
52
|
'start-time': Flags.string({ description: 'Start time filter (ISO 8601)' }),
|
|
48
53
|
'end-time': Flags.string({ description: 'End time filter (ISO 8601)' }),
|
|
49
|
-
filter: Flags.string({ description: 'Filter
|
|
54
|
+
filter: Flags.string({ description: 'Filter in field:operator:value format (repeatable)', multiple: true }),
|
|
55
|
+
'all-envs': Flags.string({ description: 'Include all environments (true/false)' }),
|
|
56
|
+
'is-test': Flags.string({ description: 'Filter by test (true) or production (false) environment' }),
|
|
57
|
+
'include-fields': Flags.string({ description: 'Comma-separated fields to include in response' }),
|
|
50
58
|
};
|
|
51
59
|
export default LogsList;
|
|
@@ -4,6 +4,9 @@ export default class LogsSummary extends BaseCommand {
|
|
|
4
4
|
static flags: {
|
|
5
5
|
'start-time': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
6
|
'end-time': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'all-envs': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'is-test': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
10
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
11
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
12
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
+
import { parseFilters, FILTER_SYNTAX_HELP, LOG_FIELDS_HELP, FILTER_EXAMPLES } from '../../lib/filters.js';
|
|
3
4
|
class LogsSummary extends BaseCommand {
|
|
4
5
|
async run() {
|
|
5
6
|
const { flags } = await this.parse(LogsSummary);
|
|
6
7
|
this.globalFlags = flags;
|
|
7
8
|
try {
|
|
8
9
|
const client = this.getClient();
|
|
9
|
-
|
|
10
|
+
let filters;
|
|
11
|
+
if (flags.filter && flags.filter.length > 0) {
|
|
12
|
+
filters = parseFilters(flags.filter);
|
|
13
|
+
}
|
|
14
|
+
const data = await this.spin('Fetching summary', () => client.logs.getSpansSummary({
|
|
15
|
+
start_time: flags['start-time'],
|
|
16
|
+
end_time: flags['end-time'],
|
|
17
|
+
is_test: flags['is-test'],
|
|
18
|
+
all_envs: flags['all-envs'],
|
|
19
|
+
filters,
|
|
20
|
+
}));
|
|
10
21
|
this.log(JSON.stringify(data, null, 2));
|
|
11
22
|
}
|
|
12
23
|
catch (error) {
|
|
@@ -14,10 +25,21 @@ class LogsSummary extends BaseCommand {
|
|
|
14
25
|
}
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
|
-
LogsSummary.description =
|
|
28
|
+
LogsSummary.description = `Get aggregated summary statistics for log spans in a time range.
|
|
29
|
+
|
|
30
|
+
Returns total cost, total tokens, request count, and score summaries.
|
|
31
|
+
|
|
32
|
+
${FILTER_SYNTAX_HELP}
|
|
33
|
+
|
|
34
|
+
${LOG_FIELDS_HELP}
|
|
35
|
+
|
|
36
|
+
${FILTER_EXAMPLES}`;
|
|
18
37
|
LogsSummary.flags = {
|
|
19
38
|
...BaseCommand.baseFlags,
|
|
20
39
|
'start-time': Flags.string({ description: 'Start time (ISO 8601)', required: true }),
|
|
21
40
|
'end-time': Flags.string({ description: 'End time (ISO 8601)', required: true }),
|
|
41
|
+
filter: Flags.string({ description: 'Filter in field:operator:value format (repeatable)', multiple: true }),
|
|
42
|
+
'all-envs': Flags.string({ description: 'Include all environments (true/false)' }),
|
|
43
|
+
'is-test': Flags.string({ description: 'Filter by test (true) or production (false) environment' }),
|
|
22
44
|
};
|
|
23
45
|
export default LogsSummary;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
+
import { parseFilters, FILTER_SYNTAX_HELP, TRACE_FIELDS_HELP, FILTER_EXAMPLES } from '../../lib/filters.js';
|
|
3
4
|
import { extractPagination, formatPaginationInfo } from '../../lib/pagination.js';
|
|
4
5
|
class TracesList extends BaseCommand {
|
|
5
6
|
async run() {
|
|
@@ -7,21 +8,22 @@ class TracesList extends BaseCommand {
|
|
|
7
8
|
this.globalFlags = flags;
|
|
8
9
|
try {
|
|
9
10
|
const client = this.getClient();
|
|
10
|
-
|
|
11
|
+
let bodyFilters = {};
|
|
12
|
+
if (flags.filter && flags.filter.length > 0) {
|
|
13
|
+
bodyFilters = parseFilters(flags.filter);
|
|
14
|
+
}
|
|
15
|
+
const queryParams = {
|
|
11
16
|
page_size: flags.limit,
|
|
12
17
|
page: flags.page,
|
|
18
|
+
sort_by: flags['sort-by'],
|
|
13
19
|
};
|
|
14
|
-
if (flags['sort-by'])
|
|
15
|
-
params.sort_by = flags['sort-by'];
|
|
16
20
|
if (flags['start-time'])
|
|
17
|
-
|
|
21
|
+
queryParams.start_time = flags['start-time'];
|
|
18
22
|
if (flags['end-time'])
|
|
19
|
-
|
|
23
|
+
queryParams.end_time = flags['end-time'];
|
|
20
24
|
if (flags.environment)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
params.filters = flags.filter;
|
|
24
|
-
const data = await this.spin('Fetching traces', () => client.traces.list(params));
|
|
25
|
+
queryParams.environment = flags.environment;
|
|
26
|
+
const data = await this.spin('Fetching traces', () => client.traces.list({ filters: bodyFilters }, { queryParams }));
|
|
25
27
|
this.outputResult(data, [
|
|
26
28
|
'trace_unique_id', 'name', 'duration', 'span_count', 'total_cost', 'error_count', 'start_time',
|
|
27
29
|
]);
|
|
@@ -33,15 +35,23 @@ class TracesList extends BaseCommand {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
|
-
TracesList.description =
|
|
38
|
+
TracesList.description = `List and filter traces.
|
|
39
|
+
|
|
40
|
+
A trace represents a complete workflow execution containing multiple spans.
|
|
41
|
+
|
|
42
|
+
${FILTER_SYNTAX_HELP}
|
|
43
|
+
|
|
44
|
+
${TRACE_FIELDS_HELP}
|
|
45
|
+
|
|
46
|
+
${FILTER_EXAMPLES}`;
|
|
37
47
|
TracesList.flags = {
|
|
38
48
|
...BaseCommand.baseFlags,
|
|
39
49
|
limit: Flags.integer({ description: 'Number of results per page', default: 10 }),
|
|
40
50
|
page: Flags.integer({ description: 'Page number', default: 1 }),
|
|
41
|
-
'sort-by': Flags.string({ description: 'Sort field', default: '-timestamp' }),
|
|
51
|
+
'sort-by': Flags.string({ description: 'Sort field (prefix with - for descending)', default: '-timestamp' }),
|
|
42
52
|
'start-time': Flags.string({ description: 'Start time filter (ISO 8601)' }),
|
|
43
53
|
'end-time': Flags.string({ description: 'End time filter (ISO 8601)' }),
|
|
44
54
|
environment: Flags.string({ description: 'Environment filter' }),
|
|
45
|
-
filter: Flags.string({ description: 'Filter
|
|
55
|
+
filter: Flags.string({ description: 'Filter in field:operator:value format (repeatable)', multiple: true }),
|
|
46
56
|
};
|
|
47
57
|
export default TracesList;
|
package/dist/lib/auth.d.ts
CHANGED
package/dist/lib/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCredential } from './config.js';
|
|
2
|
-
const DEFAULT_BASE_URL = 'https://api.respan.ai
|
|
2
|
+
export const DEFAULT_BASE_URL = 'https://api.respan.ai';
|
|
3
|
+
export const ENTERPRISE_BASE_URL = 'https://endpoint.respan.ai';
|
|
3
4
|
export function resolveAuth(flags) {
|
|
4
5
|
if (flags['api-key']) {
|
|
5
6
|
return { apiKey: flags['api-key'], baseUrl: DEFAULT_BASE_URL };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filter parser for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Parses --filter strings in `field:operator:value` format into the API's
|
|
5
|
+
* `{ field: { operator, value } }` body format.
|
|
6
|
+
*
|
|
7
|
+
* Syntax: field:operator:value
|
|
8
|
+
* - field::value → exact match (empty operator)
|
|
9
|
+
* - field:gt:value → greater than
|
|
10
|
+
* - field:in:a,b,c → value in list
|
|
11
|
+
* - field:isnull:true → null check
|
|
12
|
+
*
|
|
13
|
+
* Numeric values are auto-detected and converted.
|
|
14
|
+
* Multiple --filter flags are merged into a single filters object.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseFilters(filterStrings: string[]): Record<string, {
|
|
17
|
+
operator: string;
|
|
18
|
+
value: unknown[];
|
|
19
|
+
}>;
|
|
20
|
+
/** Filter syntax documentation for use in command descriptions */
|
|
21
|
+
export declare const FILTER_SYNTAX_HELP = "FILTER SYNTAX: field:operator:value\n\nOPERATORS:\n (empty) Exact match model::gpt-4\n not Not equal status_code:not:200\n gt Greater than cost:gt:0.01\n gte Greater than/equal latency:gte:1.0\n lt Less than cost:lt:0.5\n lte Less than/equal prompt_tokens:lte:100\n contains Contains substring error_message:contains:timeout\n icontains Case-insensitive model:icontains:gpt\n startswith Starts with model:startswith:gpt\n endswith Ends with model:endswith:mini\n in Value in list model:in:gpt-4,gpt-4o\n isnull Is null error_message:isnull:true\n iexact Case-insens. exact status:iexact:success";
|
|
22
|
+
export declare const LOG_FIELDS_HELP = "FILTERABLE FIELDS (logs):\n model, status_code, status, cost, latency, prompt_tokens,\n completion_tokens, customer_identifier, custom_identifier,\n thread_identifier, trace_unique_id, span_name, span_workflow_name,\n environment, log_type, error_message, failed, provider_id,\n deployment_name, prompt_name, prompt_id, unique_id, stream,\n temperature, max_tokens, tokens_per_second, time_to_first_token,\n total_request_tokens, metadata__<key>, scores__<evaluator_id>";
|
|
23
|
+
export declare const TRACE_FIELDS_HELP = "FILTERABLE FIELDS (traces):\n trace_unique_id, customer_identifier, environment, span_count,\n llm_call_count, error_count, total_cost, total_tokens,\n total_prompt_tokens, total_completion_tokens, duration,\n span_workflow_name, metadata__<key>";
|
|
24
|
+
export declare const FILTER_EXAMPLES = "EXAMPLES:\n --filter model::gpt-4o --filter cost:gt:0.01\n --filter status_code:not:200\n --filter metadata__env::production\n --filter model:in:gpt-4,gpt-4o";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filter parser for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Parses --filter strings in `field:operator:value` format into the API's
|
|
5
|
+
* `{ field: { operator, value } }` body format.
|
|
6
|
+
*
|
|
7
|
+
* Syntax: field:operator:value
|
|
8
|
+
* - field::value → exact match (empty operator)
|
|
9
|
+
* - field:gt:value → greater than
|
|
10
|
+
* - field:in:a,b,c → value in list
|
|
11
|
+
* - field:isnull:true → null check
|
|
12
|
+
*
|
|
13
|
+
* Numeric values are auto-detected and converted.
|
|
14
|
+
* Multiple --filter flags are merged into a single filters object.
|
|
15
|
+
*/
|
|
16
|
+
const VALID_OPERATORS = new Set([
|
|
17
|
+
'', 'not', 'lt', 'lte', 'gt', 'gte',
|
|
18
|
+
'contains', 'icontains', 'startswith', 'endswith',
|
|
19
|
+
'in', 'isnull', 'iexact',
|
|
20
|
+
]);
|
|
21
|
+
function coerceValue(v) {
|
|
22
|
+
if (v === 'true')
|
|
23
|
+
return true;
|
|
24
|
+
if (v === 'false')
|
|
25
|
+
return false;
|
|
26
|
+
const n = Number(v);
|
|
27
|
+
if (!Number.isNaN(n) && v.trim() !== '')
|
|
28
|
+
return n;
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
export function parseFilters(filterStrings) {
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const raw of filterStrings) {
|
|
34
|
+
// Split on first two colons: field:operator:value
|
|
35
|
+
// field::value means operator is empty string
|
|
36
|
+
const firstColon = raw.indexOf(':');
|
|
37
|
+
if (firstColon === -1) {
|
|
38
|
+
throw new Error(`Invalid filter format: "${raw}". Expected field:operator:value (e.g. model::gpt-4 or cost:gt:0.01)`);
|
|
39
|
+
}
|
|
40
|
+
const field = raw.slice(0, firstColon);
|
|
41
|
+
const rest = raw.slice(firstColon + 1);
|
|
42
|
+
const secondColon = rest.indexOf(':');
|
|
43
|
+
if (secondColon === -1) {
|
|
44
|
+
throw new Error(`Invalid filter format: "${raw}". Expected field:operator:value (e.g. model::gpt-4 or cost:gt:0.01)`);
|
|
45
|
+
}
|
|
46
|
+
const operator = rest.slice(0, secondColon);
|
|
47
|
+
const valueStr = rest.slice(secondColon + 1);
|
|
48
|
+
if (!VALID_OPERATORS.has(operator)) {
|
|
49
|
+
throw new Error(`Unknown filter operator: "${operator}". Valid operators: ${[...VALID_OPERATORS].filter(Boolean).join(', ')} (or empty for exact match)`);
|
|
50
|
+
}
|
|
51
|
+
if (!field) {
|
|
52
|
+
throw new Error(`Filter field cannot be empty: "${raw}"`);
|
|
53
|
+
}
|
|
54
|
+
// For "in" operator, split on comma to create array
|
|
55
|
+
let values;
|
|
56
|
+
if (operator === 'in') {
|
|
57
|
+
values = valueStr.split(',').map((v) => coerceValue(v.trim()));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
values = [coerceValue(valueStr)];
|
|
61
|
+
}
|
|
62
|
+
result[field] = { operator, value: values };
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** Filter syntax documentation for use in command descriptions */
|
|
67
|
+
export const FILTER_SYNTAX_HELP = `FILTER SYNTAX: field:operator:value
|
|
68
|
+
|
|
69
|
+
OPERATORS:
|
|
70
|
+
(empty) Exact match model::gpt-4
|
|
71
|
+
not Not equal status_code:not:200
|
|
72
|
+
gt Greater than cost:gt:0.01
|
|
73
|
+
gte Greater than/equal latency:gte:1.0
|
|
74
|
+
lt Less than cost:lt:0.5
|
|
75
|
+
lte Less than/equal prompt_tokens:lte:100
|
|
76
|
+
contains Contains substring error_message:contains:timeout
|
|
77
|
+
icontains Case-insensitive model:icontains:gpt
|
|
78
|
+
startswith Starts with model:startswith:gpt
|
|
79
|
+
endswith Ends with model:endswith:mini
|
|
80
|
+
in Value in list model:in:gpt-4,gpt-4o
|
|
81
|
+
isnull Is null error_message:isnull:true
|
|
82
|
+
iexact Case-insens. exact status:iexact:success`;
|
|
83
|
+
export const LOG_FIELDS_HELP = `FILTERABLE FIELDS (logs):
|
|
84
|
+
model, status_code, status, cost, latency, prompt_tokens,
|
|
85
|
+
completion_tokens, customer_identifier, custom_identifier,
|
|
86
|
+
thread_identifier, trace_unique_id, span_name, span_workflow_name,
|
|
87
|
+
environment, log_type, error_message, failed, provider_id,
|
|
88
|
+
deployment_name, prompt_name, prompt_id, unique_id, stream,
|
|
89
|
+
temperature, max_tokens, tokens_per_second, time_to_first_token,
|
|
90
|
+
total_request_tokens, metadata__<key>, scores__<evaluator_id>`;
|
|
91
|
+
export const TRACE_FIELDS_HELP = `FILTERABLE FIELDS (traces):
|
|
92
|
+
trace_unique_id, customer_identifier, environment, span_count,
|
|
93
|
+
llm_call_count, error_count, total_cost, total_tokens,
|
|
94
|
+
total_prompt_tokens, total_completion_tokens, duration,
|
|
95
|
+
span_workflow_name, metadata__<key>`;
|
|
96
|
+
export const FILTER_EXAMPLES = `EXAMPLES:
|
|
97
|
+
--filter model::gpt-4o --filter cost:gt:0.01
|
|
98
|
+
--filter status_code:not:200
|
|
99
|
+
--filter metadata__env::production
|
|
100
|
+
--filter model:in:gpt-4,gpt-4o`;
|