@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/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
+ }