@zhanla/sdk-ts 0.1.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/README.md +347 -0
- package/bin/cli.js +62 -0
- package/bin/discover.js +70 -0
- package/bin/postinstall.js +37 -0
- package/bin/run.js +144 -0
- package/dist/executor.d.ts +13 -0
- package/dist/executor.js +564 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +10 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +43 -0
- package/dist/manifest.d.ts +60 -0
- package/dist/manifest.js +275 -0
- package/dist/trace_store.d.ts +38 -0
- package/dist/trace_store.js +30 -0
- package/dist/types.d.ts +283 -0
- package/dist/types.js +697 -0
- package/dist/wrap.d.ts +37 -0
- package/dist/wrap.js +255 -0
- package/package.json +33 -0
package/dist/wrap.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bench.wrap() — observe LLM clients without executing them.
|
|
3
|
+
*
|
|
4
|
+
* Wraps an existing LLM client so every call is recorded in the active
|
|
5
|
+
* TraceContext. The underlying client is unchanged; bench only observes.
|
|
6
|
+
*
|
|
7
|
+
* Supported clients:
|
|
8
|
+
* - Anthropic (from @anthropic-ai/sdk)
|
|
9
|
+
* - OpenAI (from openai)
|
|
10
|
+
* - GoogleGenAI (from @google/genai)
|
|
11
|
+
*/
|
|
12
|
+
export declare function isAnthropicClient(client: unknown): client is {
|
|
13
|
+
messages: {
|
|
14
|
+
create: (...args: unknown[]) => unknown;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare function isOpenAIClient(client: unknown): client is {
|
|
18
|
+
chat: {
|
|
19
|
+
completions: {
|
|
20
|
+
create: (...args: unknown[]) => unknown;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare function isGeminiClient(client: unknown): client is {
|
|
25
|
+
models: {
|
|
26
|
+
generateContent: (...args: unknown[]) => unknown;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Wrap an LLM client to record every call in the active TraceContext.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* import Anthropic from "@anthropic-ai/sdk";
|
|
34
|
+
* import bench from "@benchlabs/sdk";
|
|
35
|
+
* const client = bench.wrap(new Anthropic());
|
|
36
|
+
*/
|
|
37
|
+
export declare function wrap<T>(client: T): T;
|
package/dist/wrap.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bench.wrap() — observe LLM clients without executing them.
|
|
3
|
+
*
|
|
4
|
+
* Wraps an existing LLM client so every call is recorded in the active
|
|
5
|
+
* TraceContext. The underlying client is unchanged; bench only observes.
|
|
6
|
+
*
|
|
7
|
+
* Supported clients:
|
|
8
|
+
* - Anthropic (from @anthropic-ai/sdk)
|
|
9
|
+
* - OpenAI (from openai)
|
|
10
|
+
* - GoogleGenAI (from @google/genai)
|
|
11
|
+
*/
|
|
12
|
+
import { traceStorage } from "./trace_store.js";
|
|
13
|
+
const WRAPPED_CLIENT = Symbol.for("bench.sdk_ts.wrapped_client");
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Serialisation helper
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function serialize(value) {
|
|
18
|
+
if (value === null || value === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return value.map(serialize);
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "object") {
|
|
27
|
+
const obj = value;
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const key of Object.keys(obj)) {
|
|
30
|
+
if (!key.startsWith("_")) {
|
|
31
|
+
result[key] = serialize(obj[key]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function getGeminiFunctionCallPart(part) {
|
|
39
|
+
if (part == null || typeof part !== "object") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const record = part;
|
|
43
|
+
const functionCall = record["functionCall"] ?? record["function_call"];
|
|
44
|
+
return functionCall != null && typeof functionCall === "object"
|
|
45
|
+
? functionCall
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
function extractGeminiToolCallParts(response) {
|
|
49
|
+
const candidates = Array.isArray(response["candidates"]) ? response["candidates"] : [];
|
|
50
|
+
const first = candidates[0];
|
|
51
|
+
if (first == null || typeof first !== "object") {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const content = first.content;
|
|
55
|
+
if (content == null || typeof content !== "object") {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const parts = Array.isArray(content.parts)
|
|
59
|
+
? content.parts
|
|
60
|
+
: [];
|
|
61
|
+
return parts.filter((part) => getGeminiFunctionCallPart(part) != null);
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Anthropic
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
function recordAnthropicCall(ctx, kwargs, response, latencyMs) {
|
|
67
|
+
if (!ctx)
|
|
68
|
+
return;
|
|
69
|
+
const content = response["content"] ?? [];
|
|
70
|
+
const toolCalls = content.filter((b) => b !== null && typeof b === "object" && b["type"] === "tool_use");
|
|
71
|
+
const usage = response["usage"];
|
|
72
|
+
ctx.record({
|
|
73
|
+
traceId: ctx.traceId,
|
|
74
|
+
parentId: null,
|
|
75
|
+
sequenceOrder: ctx.nextSequence(),
|
|
76
|
+
autoraterRunId: ctx.autoraterRunId,
|
|
77
|
+
datasetItemId: ctx.datasetItemId,
|
|
78
|
+
provider: "anthropic",
|
|
79
|
+
model: String(response["model"] ?? (kwargs["model"] ?? "")),
|
|
80
|
+
inputMessages: serialize(kwargs["messages"] ?? []),
|
|
81
|
+
output: serialize(content),
|
|
82
|
+
toolCalls: toolCalls.length > 0 ? serialize(toolCalls) : null,
|
|
83
|
+
rawResponse: serialize(response),
|
|
84
|
+
inputTokens: usage ? usage["input_tokens"] : null,
|
|
85
|
+
outputTokens: usage ? usage["output_tokens"] : null,
|
|
86
|
+
latencyMs,
|
|
87
|
+
stopReason: response["stop_reason"] ?? null,
|
|
88
|
+
metadata: null,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function wrapAnthropic(client) {
|
|
92
|
+
const originalCreate = client.messages.create.bind(client.messages);
|
|
93
|
+
client.messages.create = async function (...args) {
|
|
94
|
+
const ctx = traceStorage.getStore();
|
|
95
|
+
const kwargs = (args[0] ?? {});
|
|
96
|
+
const start = performance.now();
|
|
97
|
+
const response = await originalCreate(...args);
|
|
98
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
99
|
+
recordAnthropicCall(ctx, kwargs, response, latencyMs);
|
|
100
|
+
return response;
|
|
101
|
+
};
|
|
102
|
+
return client;
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// OpenAI
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
function recordOpenAICall(ctx, kwargs, response, latencyMs) {
|
|
108
|
+
if (!ctx)
|
|
109
|
+
return;
|
|
110
|
+
const choices = response["choices"] ?? [];
|
|
111
|
+
const firstChoice = (choices[0] ?? null);
|
|
112
|
+
const message = firstChoice ? firstChoice["message"] : null;
|
|
113
|
+
const toolCalls = message ? message["tool_calls"] : null;
|
|
114
|
+
const usage = response["usage"];
|
|
115
|
+
const finishReason = firstChoice ? firstChoice["finish_reason"] : null;
|
|
116
|
+
ctx.record({
|
|
117
|
+
traceId: ctx.traceId,
|
|
118
|
+
parentId: null,
|
|
119
|
+
sequenceOrder: ctx.nextSequence(),
|
|
120
|
+
autoraterRunId: ctx.autoraterRunId,
|
|
121
|
+
datasetItemId: ctx.datasetItemId,
|
|
122
|
+
provider: "openai",
|
|
123
|
+
model: String(response["model"] ?? (kwargs["model"] ?? "")),
|
|
124
|
+
inputMessages: serialize(kwargs["messages"] ?? []),
|
|
125
|
+
output: serialize(message),
|
|
126
|
+
toolCalls: toolCalls && toolCalls.length > 0 ? serialize(toolCalls) : null,
|
|
127
|
+
rawResponse: serialize(response),
|
|
128
|
+
inputTokens: usage ? usage["prompt_tokens"] : null,
|
|
129
|
+
outputTokens: usage ? usage["completion_tokens"] : null,
|
|
130
|
+
latencyMs,
|
|
131
|
+
stopReason: finishReason,
|
|
132
|
+
metadata: null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function wrapOpenAI(client) {
|
|
136
|
+
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
137
|
+
client.chat.completions.create = async function (...args) {
|
|
138
|
+
const ctx = traceStorage.getStore();
|
|
139
|
+
const kwargs = (args[0] ?? {});
|
|
140
|
+
const start = performance.now();
|
|
141
|
+
const response = await originalCreate(...args);
|
|
142
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
143
|
+
recordOpenAICall(ctx, kwargs, response, latencyMs);
|
|
144
|
+
return response;
|
|
145
|
+
};
|
|
146
|
+
return client;
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Gemini (@google/genai)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
function recordGeminiCall(ctx, kwargs, response, latencyMs) {
|
|
152
|
+
if (!ctx)
|
|
153
|
+
return;
|
|
154
|
+
const candidates = response["candidates"] ?? [];
|
|
155
|
+
const first = (candidates[0] ?? null);
|
|
156
|
+
const content = first ? serialize(first["content"]) : null;
|
|
157
|
+
const toolCalls = extractGeminiToolCallParts(response);
|
|
158
|
+
const finishReason = first ? String(first["finish_reason"] ?? "") : null;
|
|
159
|
+
const usageMeta = response["usageMetadata"];
|
|
160
|
+
ctx.record({
|
|
161
|
+
traceId: ctx.traceId,
|
|
162
|
+
parentId: null,
|
|
163
|
+
sequenceOrder: ctx.nextSequence(),
|
|
164
|
+
autoraterRunId: ctx.autoraterRunId,
|
|
165
|
+
datasetItemId: ctx.datasetItemId,
|
|
166
|
+
provider: "gemini",
|
|
167
|
+
model: String(kwargs["model"] ?? ""),
|
|
168
|
+
inputMessages: serialize(kwargs["contents"] ?? []),
|
|
169
|
+
output: content,
|
|
170
|
+
toolCalls: toolCalls.length > 0 ? serialize(toolCalls) : null,
|
|
171
|
+
rawResponse: serialize(response),
|
|
172
|
+
inputTokens: usageMeta ? usageMeta["promptTokenCount"] : null,
|
|
173
|
+
outputTokens: usageMeta ? usageMeta["candidatesTokenCount"] : null,
|
|
174
|
+
latencyMs,
|
|
175
|
+
stopReason: finishReason,
|
|
176
|
+
metadata: null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function wrapGemini(client) {
|
|
180
|
+
const originalGenerate = client.models.generateContent.bind(client.models);
|
|
181
|
+
client.models.generateContent = async function (...args) {
|
|
182
|
+
const ctx = traceStorage.getStore();
|
|
183
|
+
const kwargs = (args[0] ?? {});
|
|
184
|
+
const start = performance.now();
|
|
185
|
+
const response = await originalGenerate(...args);
|
|
186
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
187
|
+
recordGeminiCall(ctx, kwargs, response, latencyMs);
|
|
188
|
+
return response;
|
|
189
|
+
};
|
|
190
|
+
return client;
|
|
191
|
+
}
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Type guards
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
export function isAnthropicClient(client) {
|
|
196
|
+
return (client !== null &&
|
|
197
|
+
typeof client === "object" &&
|
|
198
|
+
"messages" in client &&
|
|
199
|
+
typeof client.messages === "object" &&
|
|
200
|
+
client.messages !== null &&
|
|
201
|
+
typeof client.messages.create === "function");
|
|
202
|
+
}
|
|
203
|
+
export function isOpenAIClient(client) {
|
|
204
|
+
return (client !== null &&
|
|
205
|
+
typeof client === "object" &&
|
|
206
|
+
"chat" in client &&
|
|
207
|
+
typeof client.chat === "object" &&
|
|
208
|
+
client.chat !== null &&
|
|
209
|
+
typeof client.chat.completions === "object");
|
|
210
|
+
}
|
|
211
|
+
export function isGeminiClient(client) {
|
|
212
|
+
return (client !== null &&
|
|
213
|
+
typeof client === "object" &&
|
|
214
|
+
"models" in client &&
|
|
215
|
+
typeof client.models === "object" &&
|
|
216
|
+
client.models !== null &&
|
|
217
|
+
typeof client.models.generateContent ===
|
|
218
|
+
"function");
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Public API
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
/**
|
|
224
|
+
* Wrap an LLM client to record every call in the active TraceContext.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* import Anthropic from "@anthropic-ai/sdk";
|
|
228
|
+
* import bench from "@benchlabs/sdk";
|
|
229
|
+
* const client = bench.wrap(new Anthropic());
|
|
230
|
+
*/
|
|
231
|
+
export function wrap(client) {
|
|
232
|
+
if (client != null && typeof client === "object" && client[WRAPPED_CLIENT] === true) {
|
|
233
|
+
return client;
|
|
234
|
+
}
|
|
235
|
+
let wrapped;
|
|
236
|
+
if (isAnthropicClient(client))
|
|
237
|
+
wrapped = wrapAnthropic(client);
|
|
238
|
+
else if (isOpenAIClient(client))
|
|
239
|
+
wrapped = wrapOpenAI(client);
|
|
240
|
+
else if (isGeminiClient(client))
|
|
241
|
+
wrapped = wrapGemini(client);
|
|
242
|
+
else {
|
|
243
|
+
throw new TypeError(`bench.wrap() does not support ${Object.getPrototypeOf(client)?.constructor?.name ?? typeof client}. ` +
|
|
244
|
+
"Supported clients: Anthropic, OpenAI, GoogleGenAI.");
|
|
245
|
+
}
|
|
246
|
+
if (wrapped != null && typeof wrapped === "object") {
|
|
247
|
+
Object.defineProperty(wrapped, WRAPPED_CLIENT, {
|
|
248
|
+
value: true,
|
|
249
|
+
configurable: false,
|
|
250
|
+
enumerable: false,
|
|
251
|
+
writable: false,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return wrapped;
|
|
255
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhanla/sdk-ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for the zhanla CLI — define and run AI components locally",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"zhanla-sdk-ts": "./bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"postinstall": "node ./bin/postinstall.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"tsx": "^4.7.0"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"bin"
|
|
23
|
+
],
|
|
24
|
+
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.4.0",
|
|
27
|
+
"vitest": "^1.4.0",
|
|
28
|
+
"@types/node": "^20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|