@ciphyrshq/sdk 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/errors.js ADDED
@@ -0,0 +1,52 @@
1
+ export class CiphyrsError extends Error {
2
+ constructor(message, { status, code } = {}) {
3
+ super(message);
4
+ this.name = 'CiphyrsError';
5
+ this.status = status;
6
+ this.code = code;
7
+ }
8
+ }
9
+
10
+ export class CiphyrsAuthError extends CiphyrsError {
11
+ constructor(message = 'Unauthorized') {
12
+ super(message, { status: 401, code: 'AUTH_ERROR' });
13
+ this.name = 'CiphyrsAuthError';
14
+ }
15
+ }
16
+
17
+ export class CiphyrsPermissionError extends CiphyrsError {
18
+ constructor(message = 'Forbidden') {
19
+ super(message, { status: 403, code: 'PERMISSION_ERROR' });
20
+ this.name = 'CiphyrsPermissionError';
21
+ }
22
+ }
23
+
24
+ export class CiphyrsNotFoundError extends CiphyrsError {
25
+ constructor(message = 'Not found') {
26
+ super(message, { status: 404, code: 'NOT_FOUND' });
27
+ this.name = 'CiphyrsNotFoundError';
28
+ }
29
+ }
30
+
31
+ export class CiphyrsRateLimitError extends CiphyrsError {
32
+ constructor(message = 'Rate limit exceeded', { retryAfter } = {}) {
33
+ super(message, { status: 429, code: 'RATE_LIMIT' });
34
+ this.name = 'CiphyrsRateLimitError';
35
+ this.retryAfter = retryAfter ? parseInt(retryAfter) : null;
36
+ }
37
+ }
38
+
39
+ export class CiphyrsTimeoutError extends CiphyrsError {
40
+ constructor(message = 'Request timed out') {
41
+ super(message, { code: 'TIMEOUT' });
42
+ this.name = 'CiphyrsTimeoutError';
43
+ }
44
+ }
45
+
46
+ export class CiphyrsJobTimeoutError extends CiphyrsError {
47
+ constructor(jobId, timeoutMs) {
48
+ super(`Job ${jobId} did not complete within ${timeoutMs}ms`, { code: 'JOB_TIMEOUT' });
49
+ this.name = 'CiphyrsJobTimeoutError';
50
+ this.jobId = jobId;
51
+ }
52
+ }
@@ -0,0 +1,127 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // EvalRunner — run evaluation datasets against agent functions
3
+ // Port of the Python SDK's EvalRunner
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ export class EvalRunner {
7
+ #client;
8
+
9
+ /**
10
+ * @param {import('./client.js').CiphyrsClient} client
11
+ */
12
+ constructor(client) {
13
+ this.#client = client;
14
+ }
15
+
16
+ /**
17
+ * Load a dataset by ID from the Ciphyrs eval service.
18
+ * @param {string} datasetId
19
+ * @returns {Promise<object>}
20
+ */
21
+ async loadDataset(datasetId) {
22
+ return this.#client._request(
23
+ `${this.#client._baseUrl}/v1/evals/datasets/${encodeURIComponent(datasetId)}`,
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Run an agent function against every test case in a dataset.
29
+ *
30
+ * @param {(input: any) => Promise<any>} agentFn — The agent under test
31
+ * @param {string} datasetId — Dataset to evaluate against
32
+ * @param {object} [opts]
33
+ * @param {(input: any, expected: any, actual: any) => Promise<number>} [opts.judgeFn]
34
+ * Custom judge that returns a score 0-1. If omitted, strict equality is used.
35
+ * @param {string} [opts.agentName] — Name of the agent being evaluated
36
+ * @param {string} [opts.agentVersion] — Version string for the agent
37
+ * @returns {Promise<{run_id: string, score: number, passed: number, failed: number, total: number, duration_ms: number, results: Array}>}
38
+ */
39
+ async run(agentFn, datasetId, { judgeFn, agentName, agentVersion } = {}) {
40
+ const { dataset } = await this.loadDataset(datasetId);
41
+ const cases = dataset.test_cases || [];
42
+
43
+ // Create eval run on the backend
44
+ const { run: evalRun } = await this.#client._request(
45
+ `${this.#client._baseUrl}/v1/evals/runs`,
46
+ {
47
+ method: 'POST',
48
+ body: {
49
+ dataset_id: datasetId,
50
+ agent_name: agentName,
51
+ agent_version: agentVersion,
52
+ total_cases: cases.length,
53
+ },
54
+ },
55
+ );
56
+
57
+ const results = [];
58
+ let passed = 0;
59
+ let failed = 0;
60
+ let totalScore = 0;
61
+ const startTime = Date.now();
62
+
63
+ for (const testCase of cases) {
64
+ const caseStart = Date.now();
65
+ try {
66
+ const actual = await agentFn(testCase.input);
67
+ const score = judgeFn
68
+ ? await judgeFn(testCase.input, testCase.expected, actual)
69
+ : (actual === testCase.expected ? 1.0 : 0.0);
70
+ const pass = score >= (testCase.threshold || 0.5);
71
+
72
+ results.push({
73
+ input: testCase.input,
74
+ expected: testCase.expected,
75
+ actual,
76
+ score,
77
+ passed: pass,
78
+ latency_ms: Date.now() - caseStart,
79
+ });
80
+
81
+ if (pass) passed++;
82
+ else failed++;
83
+ totalScore += score;
84
+ } catch (err) {
85
+ results.push({
86
+ input: testCase.input,
87
+ expected: testCase.expected,
88
+ actual: null,
89
+ score: 0,
90
+ passed: false,
91
+ error: err.message,
92
+ latency_ms: Date.now() - caseStart,
93
+ });
94
+ failed++;
95
+ }
96
+ }
97
+
98
+ const avgScore = cases.length > 0 ? totalScore / cases.length : 0;
99
+
100
+ // Update the eval run with results
101
+ await this.#client._request(
102
+ `${this.#client._baseUrl}/v1/evals/runs/${encodeURIComponent(evalRun.id)}`,
103
+ {
104
+ method: 'PATCH',
105
+ body: {
106
+ status: 'completed',
107
+ passed,
108
+ failed,
109
+ avg_score: avgScore,
110
+ avg_latency_ms: cases.length > 0 ? Math.round((Date.now() - startTime) / cases.length) : 0,
111
+ results,
112
+ completed_at: new Date().toISOString(),
113
+ },
114
+ },
115
+ );
116
+
117
+ return {
118
+ run_id: evalRun.id,
119
+ score: avgScore,
120
+ passed,
121
+ failed,
122
+ total: cases.length,
123
+ duration_ms: Date.now() - startTime,
124
+ results,
125
+ };
126
+ }
127
+ }
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export { CiphyrsClient } from './client.js';
2
+ export {
3
+ CiphyrsError,
4
+ CiphyrsAuthError,
5
+ CiphyrsPermissionError,
6
+ CiphyrsNotFoundError,
7
+ CiphyrsRateLimitError,
8
+ CiphyrsTimeoutError,
9
+ CiphyrsJobTimeoutError,
10
+ } from './errors.js';
11
+ export { CiphyrsTracer, Trace, Span } from './tracer.js';
12
+ export { SecretDetector } from './secret-detector.js';
13
+ export { EvalRunner } from './eval-runner.js';
@@ -0,0 +1,49 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // SecretDetector — scan text for leaked credentials and API keys
3
+ // Port of the Python SDK's SecretDetector
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ const PATTERNS = [
7
+ { type: 'aws_access_key', pattern: /AKIA[0-9A-Z]{16}/g, confidence: 0.95 },
8
+ { type: 'aws_secret_key', pattern: /[A-Za-z0-9/+=]{40}(?=\s|$|")/g, confidence: 0.7 },
9
+ { type: 'github_token', pattern: /gh[psortu]_[A-Za-z0-9_]{36,}/g, confidence: 0.99 },
10
+ { type: 'gitlab_token', pattern: /glpat-[A-Za-z0-9\-_]{20,}/g, confidence: 0.95 },
11
+ { type: 'slack_token', pattern: /xox[bpras]-[A-Za-z0-9\-]+/g, confidence: 0.95 },
12
+ { type: 'stripe_key', pattern: /[sr]k_(test|live)_[A-Za-z0-9]{24,}/g, confidence: 0.99 },
13
+ { type: 'generic_api_key', pattern: /(?:api[_-]?key|apikey|token|secret)['":\s]*[=:]\s*['"]?([A-Za-z0-9_\-]{20,})['"]?/gi, confidence: 0.6 },
14
+ { type: 'jwt', pattern: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, confidence: 0.9 },
15
+ { type: 'private_key', pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g, confidence: 0.99 },
16
+ ];
17
+
18
+ export class SecretDetector {
19
+ /**
20
+ * Scan text for potential secrets and credentials.
21
+ * @param {string} text — The text to scan
22
+ * @returns {Array<{type: string, start: number, end: number, value_masked: string, confidence: number}>}
23
+ */
24
+ detect(text) {
25
+ if (!text || typeof text !== 'string') return [];
26
+
27
+ const findings = [];
28
+
29
+ for (const { type, pattern, confidence } of PATTERNS) {
30
+ // Clone the regex so lastIndex resets for each call
31
+ const re = new RegExp(pattern.source, pattern.flags);
32
+ let match;
33
+ while ((match = re.exec(text)) !== null) {
34
+ const value = match[0];
35
+ findings.push({
36
+ type,
37
+ start: match.index,
38
+ end: match.index + value.length,
39
+ value_masked: value.length > 8
40
+ ? value.slice(0, 4) + '***' + value.slice(-4)
41
+ : value.slice(0, 2) + '***',
42
+ confidence,
43
+ });
44
+ }
45
+ }
46
+
47
+ return findings;
48
+ }
49
+ }
package/src/tracer.js ADDED
@@ -0,0 +1,220 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // CiphyrsTracer — buffered span/metric collection with auto-flush
5
+ // Mirrors the Python SDK's tracer.py
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ export class CiphyrsTracer {
9
+ #client;
10
+ #projectName;
11
+ #config;
12
+ #spanBuffer = [];
13
+ #metricBuffer = [];
14
+ #flushInterval;
15
+
16
+ /**
17
+ * @param {import('./client.js').CiphyrsClient} client
18
+ * @param {object} opts
19
+ * @param {string} opts.projectName — Project name for grouping traces
20
+ * @param {number} [opts.flushIntervalMs=5000] — Auto-flush interval in ms
21
+ * @param {number} [opts.batchSize=50] — Flush when buffer reaches this size
22
+ */
23
+ constructor(client, { projectName, flushIntervalMs = 5000, batchSize = 50 } = {}) {
24
+ this.#client = client;
25
+ this.#projectName = projectName;
26
+ this.#config = { flushIntervalMs, batchSize };
27
+ this.#flushInterval = setInterval(() => this.flush().catch(() => {}), flushIntervalMs);
28
+ // Prevent the interval from keeping the process alive
29
+ if (this.#flushInterval.unref) this.#flushInterval.unref();
30
+ }
31
+
32
+ /**
33
+ * Start a new trace.
34
+ * @param {string} name — Human-readable trace name
35
+ * @param {object} [opts]
36
+ * @param {string} [opts.traceId] — Provide your own trace ID, or one is generated
37
+ * @returns {Trace}
38
+ */
39
+ trace(name, opts = {}) {
40
+ return new Trace(this, name, opts);
41
+ }
42
+
43
+ /** @internal — called by Span.end() to enqueue completed spans */
44
+ _enqueueSpan(span) {
45
+ this.#spanBuffer.push(span);
46
+ if (this.#spanBuffer.length >= this.#config.batchSize) {
47
+ this.flush().catch(() => {});
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Record a custom metric.
53
+ * @param {string} name — Metric name (e.g. "agent.latency")
54
+ * @param {number} value — Metric value
55
+ * @param {object} [opts]
56
+ * @param {string} [opts.unit] — Unit label (e.g. "ms", "usd")
57
+ * @param {Record<string,string>} [opts.tags] — Key-value tags
58
+ * @param {string} [opts.agentName] — Agent that emitted the metric
59
+ */
60
+ emitMetric(name, value, { unit, tags, agentName } = {}) {
61
+ this.#metricBuffer.push({ name, value, unit, tags, agent_name: agentName });
62
+ }
63
+
64
+ /**
65
+ * Flush buffered spans and metrics to the Ciphyrs backend.
66
+ * Called automatically on the flush interval, or when batchSize is reached.
67
+ */
68
+ async flush() {
69
+ if (!this.#spanBuffer.length && !this.#metricBuffer.length) return;
70
+
71
+ const spans = this.#spanBuffer.splice(0);
72
+ const metrics = this.#metricBuffer.splice(0);
73
+
74
+ const promises = [];
75
+
76
+ if (spans.length) {
77
+ // Group spans by trace_id so each ingest call contains one trace
78
+ const byTrace = {};
79
+ for (const s of spans) {
80
+ (byTrace[s.trace_id] ||= []).push(s);
81
+ }
82
+ for (const [traceId, traceSpans] of Object.entries(byTrace)) {
83
+ promises.push(
84
+ this.#client.trace.ingest(
85
+ { name: this.#projectName },
86
+ { trace_id: traceId, name: traceSpans[0]?.trace_name || traceId },
87
+ traceSpans,
88
+ ).catch(err => console.error('Ciphyrs flush error:', err.message)),
89
+ );
90
+ }
91
+ }
92
+
93
+ if (metrics.length) {
94
+ promises.push(
95
+ this.#client._request(`${this.#client._baseUrl}/v1/observe/metrics`, {
96
+ method: 'POST',
97
+ body: { metrics },
98
+ }).catch(err => console.error('Ciphyrs metrics flush error:', err.message)),
99
+ );
100
+ }
101
+
102
+ await Promise.allSettled(promises);
103
+ }
104
+
105
+ /**
106
+ * Stop the auto-flush timer and flush any remaining data.
107
+ * Always call this before process exit to avoid data loss.
108
+ */
109
+ async shutdown() {
110
+ clearInterval(this.#flushInterval);
111
+ await this.flush();
112
+ }
113
+ }
114
+
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+ // Trace — groups related spans under a single trace ID
117
+ // ─────────────────────────────────────────────────────────────────────────────
118
+
119
+ export class Trace {
120
+ #tracer;
121
+ #traceId;
122
+ #name;
123
+ #startTime;
124
+
125
+ constructor(tracer, name, opts = {}) {
126
+ this.#tracer = tracer;
127
+ this.#traceId = opts.traceId || randomUUID();
128
+ this.#name = name;
129
+ this.#startTime = Date.now();
130
+ }
131
+
132
+ get traceId() { return this.#traceId; }
133
+ get name() { return this.#name; }
134
+
135
+ /**
136
+ * Create a new span within this trace.
137
+ * @param {string} name — Operation name for the span
138
+ * @param {object} [opts]
139
+ * @param {string} [opts.parentSpanId] — Parent span ID for nested spans
140
+ * @param {string} [opts.agentName] — Agent performing this operation
141
+ * @param {string} [opts.kind] — Span kind: 'agent' | 'tool' | 'llm' | 'retriever'
142
+ * @returns {Span}
143
+ */
144
+ span(name, opts = {}) {
145
+ return new Span(this.#tracer, this.#traceId, this.#name, name, opts);
146
+ }
147
+ }
148
+
149
+ // ─────────────────────────────────────────────────────────────────────────────
150
+ // Span — a single timed operation within a trace
151
+ // ─────────────────────────────────────────────────────────────────────────────
152
+
153
+ export class Span {
154
+ #tracer;
155
+ #data;
156
+ #startTime;
157
+
158
+ constructor(tracer, traceId, traceName, name, opts = {}) {
159
+ this.#tracer = tracer;
160
+ this.#startTime = Date.now();
161
+ this.#data = {
162
+ span_id: randomUUID(),
163
+ trace_id: traceId,
164
+ trace_name: traceName,
165
+ parent_span_id: opts.parentSpanId || null,
166
+ agent_name: opts.agentName || null,
167
+ kind: opts.kind || 'agent',
168
+ operation_name: name,
169
+ input_text: null,
170
+ output_text: null,
171
+ status: 'ok',
172
+ cost_usd: null,
173
+ model_name: null,
174
+ prompt_tokens: null,
175
+ completion_tokens: null,
176
+ };
177
+ }
178
+
179
+ get spanId() { return this.#data.span_id; }
180
+
181
+ /** Set the input text for this span. */
182
+ setInput(text) { this.#data.input_text = text; return this; }
183
+
184
+ /** Set the output text for this span. */
185
+ setOutput(text) { this.#data.output_text = text; return this; }
186
+
187
+ /** Set span status ('ok' | 'error'). */
188
+ setStatus(s) { this.#data.status = s; return this; }
189
+
190
+ /** Mark span as errored with a message. */
191
+ setError(msg) { this.#data.status = 'error'; this.#data.error_message = msg; return this; }
192
+
193
+ /** Set the LLM cost in USD. */
194
+ setCost(usd) { this.#data.cost_usd = usd; return this; }
195
+
196
+ /** Set the model name (e.g. 'gpt-4', 'claude-3-opus'). */
197
+ setModel(name) { this.#data.model_name = name; return this; }
198
+
199
+ /** Set prompt and completion token counts. */
200
+ setTokens(prompt, completion) {
201
+ this.#data.prompt_tokens = prompt;
202
+ this.#data.completion_tokens = completion;
203
+ return this;
204
+ }
205
+
206
+ /** Attach arbitrary metadata / attributes. */
207
+ setMetadata(meta) { this.#data.attributes = meta; return this; }
208
+
209
+ /**
210
+ * End the span, recording timestamps and duration, then enqueue for flush.
211
+ * @returns {this}
212
+ */
213
+ end() {
214
+ this.#data.started_at = new Date(this.#startTime).toISOString();
215
+ this.#data.ended_at = new Date().toISOString();
216
+ this.#data.duration_ms = Date.now() - this.#startTime;
217
+ this.#tracer._enqueueSpan(this.#data);
218
+ return this;
219
+ }
220
+ }