@zhanla/sdk-ts 0.3.4 → 0.3.5

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/bin/cli.js CHANGED
@@ -30,7 +30,7 @@ if (process.env.BENCH_SDK_TS_LOADER_ACTIVE !== "1") {
30
30
  const [, , subcommand, ...args] = process.argv;
31
31
 
32
32
  if (!subcommand) {
33
- console.error("Usage: zhanla-sdk-ts <discover|run> <target>");
33
+ console.error("Usage: zhanla-sdk-ts <discover|run|eval-only> <target>");
34
34
  process.exit(1);
35
35
  }
36
36
  if (subcommand === "discover") {
@@ -61,7 +61,15 @@ if (subcommand === "discover") {
61
61
  }
62
62
  const { runComponent } = await import("./run.js");
63
63
  await runComponent(target, evalTarget);
64
+ } else if (subcommand === "eval-only") {
65
+ const [evalTarget] = args;
66
+ if (!evalTarget) {
67
+ console.error("Usage: zhanla-sdk-ts eval-only <file.ts:name>");
68
+ process.exit(1);
69
+ }
70
+ const { runEvalOnly } = await import("./run.js");
71
+ await runEvalOnly(evalTarget);
64
72
  } else {
65
- console.error(`Unknown subcommand: ${subcommand}. Use 'discover' or 'run'.`);
73
+ console.error(`Unknown subcommand: ${subcommand}. Use 'discover', 'run', or 'eval-only'.`);
66
74
  process.exit(1);
67
75
  }
package/bin/discover.js CHANGED
@@ -11,7 +11,17 @@
11
11
 
12
12
  import { pathToFileURL } from "url";
13
13
  import { resolve } from "path";
14
- import { loadDotenvLocal, splitFileSpec } from "./env.js";
14
+
15
+ export function splitFileSpec(fileSpec) {
16
+ const colonIndex = fileSpec.lastIndexOf(":");
17
+ if (colonIndex > 1) {
18
+ return {
19
+ filePath: fileSpec.slice(0, colonIndex),
20
+ symbolName: fileSpec.slice(colonIndex + 1) || null,
21
+ };
22
+ }
23
+ return { filePath: fileSpec, symbolName: null };
24
+ }
15
25
 
16
26
  function isComponentLike(value) {
17
27
  return (
@@ -216,7 +226,6 @@ export async function runDiscover(fileSpec) {
216
226
  const { filePath, symbolName } = splitFileSpec(fileSpec);
217
227
 
218
228
  const absolutePath = resolve(filePath);
219
- loadDotenvLocal(absolutePath);
220
229
  const fileUrl = pathToFileURL(absolutePath).href;
221
230
 
222
231
  let moduleExports;
package/bin/run.js CHANGED
@@ -9,9 +9,11 @@
9
9
  */
10
10
 
11
11
  import { pathToFileURL } from "url";
12
+ import { readFileSync } from "fs";
12
13
  import { resolve } from "path";
14
+ import vm from "vm";
13
15
  import readline from "readline";
14
- import { loadDotenvLocal } from "./env.js";
16
+ import ts from "typescript";
15
17
 
16
18
  function redirectConsoleToStderr() {
17
19
  const stringifyArg = (arg) => {
@@ -55,7 +57,6 @@ async function loadTargetComponent(target, collectExportedComponents) {
55
57
  const { filePath, symbolName } = parseTarget(target);
56
58
 
57
59
  const absolutePath = resolve(filePath);
58
- loadDotenvLocal(absolutePath);
59
60
  const fileUrl = pathToFileURL(absolutePath).href;
60
61
 
61
62
  let moduleExports;
@@ -78,6 +79,344 @@ async function loadTargetComponent(target, collectExportedComponents) {
78
79
  return component;
79
80
  }
80
81
 
82
+ function isExportedStatement(statement) {
83
+ return statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
84
+ }
85
+
86
+ function getStringLiteralValue(node) {
87
+ if (!node) return null;
88
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
89
+ return node.text;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function getManagedCodeEvalName(initializer) {
95
+ if (!initializer || !ts.isNewExpression(initializer)) return null;
96
+ if (!ts.isIdentifier(initializer.expression) || initializer.expression.text !== "CodeEval") {
97
+ return null;
98
+ }
99
+ const [config] = initializer.arguments ?? [];
100
+ if (!config || !ts.isObjectLiteralExpression(config)) return null;
101
+ for (const property of config.properties) {
102
+ if (
103
+ ts.isPropertyAssignment(property) &&
104
+ ts.isIdentifier(property.name) &&
105
+ property.name.text === "name"
106
+ ) {
107
+ return getStringLiteralValue(property.initializer);
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function buildManagedTsIsolationContext(code) {
114
+ const sourceFile = ts.createSourceFile(
115
+ "managed-eval.ts",
116
+ code,
117
+ ts.ScriptTarget.ES2020,
118
+ true,
119
+ ts.ScriptKind.TS,
120
+ );
121
+ const topLevelDeclarations = new Map();
122
+ const importedBindings = new Map();
123
+ const sdkImportStatements = [];
124
+
125
+ for (const statement of sourceFile.statements) {
126
+ if (ts.isImportDeclaration(statement)) {
127
+ const moduleSpecifier = getStringLiteralValue(statement.moduleSpecifier);
128
+ if (moduleSpecifier === "@zhanla/sdk-ts") {
129
+ sdkImportStatements.push(statement);
130
+ }
131
+ const importClause = statement.importClause;
132
+ if (moduleSpecifier && importClause) {
133
+ if (importClause.name) {
134
+ importedBindings.set(importClause.name.text, moduleSpecifier);
135
+ }
136
+ const namedBindings = importClause.namedBindings;
137
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
138
+ for (const element of namedBindings.elements) {
139
+ importedBindings.set(element.name.text, moduleSpecifier);
140
+ }
141
+ } else if (namedBindings && ts.isNamespaceImport(namedBindings)) {
142
+ importedBindings.set(namedBindings.name.text, moduleSpecifier);
143
+ }
144
+ }
145
+ continue;
146
+ }
147
+
148
+ if (
149
+ ts.isFunctionDeclaration(statement) ||
150
+ ts.isClassDeclaration(statement) ||
151
+ ts.isEnumDeclaration(statement)
152
+ ) {
153
+ if (statement.name) {
154
+ topLevelDeclarations.set(statement.name.text, statement);
155
+ }
156
+ continue;
157
+ }
158
+
159
+ if (ts.isVariableStatement(statement)) {
160
+ for (const declaration of statement.declarationList.declarations) {
161
+ if (ts.isIdentifier(declaration.name)) {
162
+ topLevelDeclarations.set(declaration.name.text, statement);
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ return { sourceFile, topLevelDeclarations, importedBindings, sdkImportStatements };
169
+ }
170
+
171
+ function scanManagedTsDependencies(statement, context) {
172
+ const referencedDeclarations = new Set();
173
+ const disallowedImports = new Map();
174
+
175
+ const visit = (node) => {
176
+ if (ts.isIdentifier(node)) {
177
+ if (
178
+ (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) ||
179
+ (ts.isPropertyAssignment(node.parent) && node.parent.name === node)
180
+ ) {
181
+ return;
182
+ }
183
+
184
+ const localStatement = context.topLevelDeclarations.get(node.text);
185
+ if (localStatement && localStatement !== statement) {
186
+ referencedDeclarations.add(localStatement);
187
+ }
188
+
189
+ const importedFrom = context.importedBindings.get(node.text);
190
+ if (importedFrom && importedFrom !== "@zhanla/sdk-ts") {
191
+ const bindings = disallowedImports.get(importedFrom) ?? new Set();
192
+ bindings.add(node.text);
193
+ disallowedImports.set(importedFrom, bindings);
194
+ }
195
+ }
196
+
197
+ ts.forEachChild(node, visit);
198
+ };
199
+
200
+ visit(statement);
201
+ return { referencedDeclarations, disallowedImports };
202
+ }
203
+
204
+ function isolateManagedTypeScriptEvalSource(code, evalName) {
205
+ const context = buildManagedTsIsolationContext(code);
206
+
207
+ let targetStatement = null;
208
+ for (const statement of context.sourceFile.statements) {
209
+ if (!ts.isVariableStatement(statement) || !isExportedStatement(statement)) {
210
+ continue;
211
+ }
212
+ for (const declaration of statement.declarationList.declarations) {
213
+ if (!ts.isIdentifier(declaration.name)) continue;
214
+ const declaredEvalName = getManagedCodeEvalName(declaration.initializer);
215
+ if (declaredEvalName === evalName || declaration.name.text === evalName) {
216
+ targetStatement = statement;
217
+ break;
218
+ }
219
+ }
220
+ if (targetStatement) break;
221
+ }
222
+
223
+ if (!targetStatement) {
224
+ throw new Error(
225
+ `Managed TypeScript eval '${evalName}' was not found in the synced source file.`,
226
+ );
227
+ }
228
+
229
+ const includedStatements = new Set([targetStatement]);
230
+ const pendingStatements = [targetStatement];
231
+ const disallowedImports = new Map();
232
+
233
+ while (pendingStatements.length > 0) {
234
+ const statement = pendingStatements.pop();
235
+ if (!statement) continue;
236
+
237
+ const scan = scanManagedTsDependencies(statement, context);
238
+ for (const dependency of scan.referencedDeclarations) {
239
+ if (!includedStatements.has(dependency)) {
240
+ includedStatements.add(dependency);
241
+ pendingStatements.push(dependency);
242
+ }
243
+ }
244
+ for (const [moduleSpecifier, bindings] of scan.disallowedImports) {
245
+ const existing = disallowedImports.get(moduleSpecifier) ?? new Set();
246
+ for (const binding of bindings) {
247
+ existing.add(binding);
248
+ }
249
+ disallowedImports.set(moduleSpecifier, existing);
250
+ }
251
+ }
252
+
253
+ if (disallowedImports.size > 0) {
254
+ const formattedImports = Array.from(disallowedImports.entries())
255
+ .map(
256
+ ([moduleSpecifier, bindings]) =>
257
+ `${moduleSpecifier} (${Array.from(bindings).sort().join(", ")})`,
258
+ )
259
+ .join("; ");
260
+ throw new Error(
261
+ `Managed TypeScript eval '${evalName}' uses unsupported imports: ${formattedImports}. ` +
262
+ "Keep managed evals self-contained and move helpers into the same file.",
263
+ );
264
+ }
265
+
266
+ return context.sourceFile.statements
267
+ .filter(
268
+ (statement) =>
269
+ context.sdkImportStatements.includes(statement) || includedStatements.has(statement),
270
+ )
271
+ .map((statement) => statement.getFullText(context.sourceFile))
272
+ .join("");
273
+ }
274
+
275
+ function buildEvalOnlySandbox(code) {
276
+ const transpiled = ts.transpileModule(code, {
277
+ compilerOptions: {
278
+ module: ts.ModuleKind.CommonJS,
279
+ target: ts.ScriptTarget.ES2020,
280
+ esModuleInterop: true,
281
+ },
282
+ }).outputText;
283
+
284
+ const moduleRecord = { exports: {} };
285
+ const sdkStub = class {
286
+ constructor(opts) {
287
+ Object.assign(this, opts);
288
+ }
289
+ };
290
+
291
+ const sandbox = vm.createContext({
292
+ JSON,
293
+ Math,
294
+ Array,
295
+ Object,
296
+ String,
297
+ Number,
298
+ Boolean,
299
+ Date,
300
+ RegExp,
301
+ Promise,
302
+ Error,
303
+ TypeError,
304
+ RangeError,
305
+ ReferenceError,
306
+ SyntaxError,
307
+ Map,
308
+ Set,
309
+ WeakMap,
310
+ WeakSet,
311
+ Symbol,
312
+ parseInt,
313
+ parseFloat,
314
+ isNaN,
315
+ isFinite,
316
+ encodeURIComponent,
317
+ decodeURIComponent,
318
+ console,
319
+ exports: moduleRecord.exports,
320
+ module: moduleRecord,
321
+ require: (specifier) => {
322
+ if (specifier === "@zhanla/sdk-ts") {
323
+ return {
324
+ CodeEval: sdkStub,
325
+ LLMEval: sdkStub,
326
+ Checklist: sdkStub,
327
+ EvalTree: sdkStub,
328
+ Branch: sdkStub,
329
+ Leaf: sdkStub,
330
+ Edge: sdkStub,
331
+ Tool: sdkStub,
332
+ Skill: sdkStub,
333
+ Agent: sdkStub,
334
+ Orchestration: sdkStub,
335
+ Step: sdkStub,
336
+ ComponentValue: null,
337
+ };
338
+ }
339
+ throw new Error(
340
+ `Cannot import '${specifier}' in the managed TypeScript eval sandbox. ` +
341
+ "Only @zhanla/sdk-ts imports and same-file helpers are supported.",
342
+ );
343
+ },
344
+ });
345
+ sandbox.globalThis = sandbox;
346
+ new vm.Script(transpiled).runInContext(sandbox, { timeout: 10000 });
347
+ return moduleRecord;
348
+ }
349
+
350
+ function findEvalComponent(moduleRecord, evalName) {
351
+ for (const value of Object.values(moduleRecord.exports)) {
352
+ if (value && typeof value === "object" && value.name === evalName && typeof value.fn === "function") {
353
+ return value;
354
+ }
355
+ }
356
+ return null;
357
+ }
358
+
359
+ function loadIsolatedEvalComponent(target) {
360
+ const { filePath, symbolName } = parseTarget(target);
361
+ const absolutePath = resolve(filePath);
362
+ const sourceText = readFileSync(absolutePath, "utf8");
363
+ const isolatedSource = isolateManagedTypeScriptEvalSource(sourceText, symbolName);
364
+ const moduleRecord = buildEvalOnlySandbox(isolatedSource);
365
+ const component = findEvalComponent(moduleRecord, symbolName);
366
+ if (!component) {
367
+ throw new Error(`Component '${symbolName}' not found in isolated eval source.`);
368
+ }
369
+ return component;
370
+ }
371
+
372
+ export async function runEvalOnly(evalTarget) {
373
+ redirectConsoleToStderr();
374
+ let evalComponent;
375
+ try {
376
+ evalComponent = loadIsolatedEvalComponent(evalTarget);
377
+ } catch (err) {
378
+ console.error(err instanceof Error ? err.message : String(err));
379
+ process.exit(1);
380
+ }
381
+
382
+ const rl = readline.createInterface({
383
+ input: process.stdin,
384
+ crlfDelay: Infinity,
385
+ });
386
+
387
+ for await (const line of rl) {
388
+ const trimmed = line.trim();
389
+ if (!trimmed) continue;
390
+
391
+ let context;
392
+ try {
393
+ context = JSON.parse(trimmed);
394
+ } catch (err) {
395
+ process.stdout.write(JSON.stringify({ status: "error", error: `Invalid JSON: ${err.message}` }) + "\n");
396
+ continue;
397
+ }
398
+
399
+ try {
400
+ const evalOutput = evalComponent.fn.length <= 1
401
+ ? await Promise.resolve(evalComponent.fn(context))
402
+ : await Promise.resolve(
403
+ evalComponent.fn(
404
+ context.model_response,
405
+ context.expected_output,
406
+ context.model_input,
407
+ ),
408
+ );
409
+ process.stdout.write(
410
+ JSON.stringify({ status: "ok", name: evalComponent.name, eval_output: evalOutput }) + "\n"
411
+ );
412
+ } catch (err) {
413
+ process.stdout.write(
414
+ JSON.stringify({ status: "error", name: evalComponent.name, error: err instanceof Error ? err.message : String(err) }) + "\n"
415
+ );
416
+ }
417
+ }
418
+ }
419
+
81
420
  export async function runComponent(target, evalTarget = null) {
82
421
  redirectConsoleToStderr();
83
422
  // Dynamically import from SDK
@@ -57,10 +57,8 @@ export interface ComponentManifest {
57
57
  output_schema?: JsonSchema;
58
58
  file_path?: string;
59
59
  symbol_name?: string;
60
- source_code?: string;
61
- source_language?: string;
62
- source_format?: string;
63
60
  model_response_format?: string;
61
+ questions?: string[];
64
62
  }
65
63
  export declare function toManifest(component: ZhanlaComponent, opts?: {
66
64
  filePath?: string;
package/dist/manifest.js CHANGED
@@ -2,7 +2,6 @@
2
2
  * Manifest emission — serialize SDK component instances into ComponentManifest objects.
3
3
  * The manifest shape mirrors the Python CLI's ComponentManifest dataclass.
4
4
  */
5
- import { readFileSync } from "node:fs";
6
5
  // ---------------------------------------------------------------------------
7
6
  // Tree serialization helpers
8
7
  // ---------------------------------------------------------------------------
@@ -54,87 +53,6 @@ function serializeBranch(branch) {
54
53
  if_fail: branch.ifFail.map(serializeEdge),
55
54
  };
56
55
  }
57
- function escapeRegExp(value) {
58
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59
- }
60
- function findMatchingParen(source, startIndex) {
61
- let depth = 0;
62
- let quote = null;
63
- let inLineComment = false;
64
- let inBlockComment = false;
65
- for (let i = startIndex; i < source.length; i += 1) {
66
- const char = source[i];
67
- const next = source[i + 1];
68
- const prev = source[i - 1];
69
- if (inLineComment) {
70
- if (char === "\n")
71
- inLineComment = false;
72
- continue;
73
- }
74
- if (inBlockComment) {
75
- if (prev === "*" && char === "/")
76
- inBlockComment = false;
77
- continue;
78
- }
79
- if (quote) {
80
- if (char === quote && prev !== "\\")
81
- quote = null;
82
- continue;
83
- }
84
- if (char === "/" && next === "/") {
85
- inLineComment = true;
86
- i += 1;
87
- continue;
88
- }
89
- if (char === "/" && next === "*") {
90
- inBlockComment = true;
91
- i += 1;
92
- continue;
93
- }
94
- if (char === "'" || char === '"' || char === "`") {
95
- quote = char;
96
- continue;
97
- }
98
- if (char === "(") {
99
- depth += 1;
100
- continue;
101
- }
102
- if (char === ")") {
103
- depth -= 1;
104
- if (depth === 0)
105
- return i;
106
- }
107
- }
108
- return -1;
109
- }
110
- function extractExportedComponentSource(filePath, symbolName) {
111
- if (!filePath || !symbolName)
112
- return undefined;
113
- let source = "";
114
- try {
115
- source = readFileSync(filePath, "utf8");
116
- }
117
- catch {
118
- return undefined;
119
- }
120
- const pattern = new RegExp(String.raw `(?:^|\n)\s*export\s+(?:const|let|var)\s+${escapeRegExp(symbolName)}\s*=\s*new\s+[A-Za-z_$][\w$.]*\s*\(`, "m");
121
- const match = pattern.exec(source);
122
- if (!match || match.index == null)
123
- return undefined;
124
- const start = source.lastIndexOf("export", match.index + match[0].length);
125
- const openParenIndex = source.indexOf("(", match.index + match[0].length - 1);
126
- if (start < 0 || openParenIndex < 0)
127
- return undefined;
128
- const closeParenIndex = findMatchingParen(source, openParenIndex);
129
- if (closeParenIndex < 0)
130
- return undefined;
131
- let end = closeParenIndex + 1;
132
- while (end < source.length && /\s/.test(source[end]))
133
- end += 1;
134
- if (source[end] === ";")
135
- end += 1;
136
- return source.slice(start, end).trim();
137
- }
138
56
  // ---------------------------------------------------------------------------
139
57
  // to_manifest
140
58
  // ---------------------------------------------------------------------------
@@ -150,7 +68,6 @@ const EXECUTION_MODES = {
150
68
  eval_tree: "composite",
151
69
  };
152
70
  export function toManifest(component, opts = {}) {
153
- const componentSource = extractExportedComponentSource(opts.filePath, opts.symbolName);
154
71
  const key = component.key;
155
72
  const keySource = "explicit";
156
73
  const executionMode = EXECUTION_MODES[component.componentType] ?? "unknown";
@@ -175,9 +92,6 @@ export function toManifest(component, opts = {}) {
175
92
  fn_present: true,
176
93
  is_async: tool.isAsync,
177
94
  output_schema: tool.outputSchema,
178
- source_code: componentSource || tool.fn.toString(),
179
- source_language: "typescript",
180
- source_format: componentSource ? "component" : "function",
181
95
  };
182
96
  }
183
97
  case "code_eval": {
@@ -186,9 +100,6 @@ export function toManifest(component, opts = {}) {
186
100
  ...base,
187
101
  fn_present: true,
188
102
  is_async: codeEval.isAsync,
189
- source_code: componentSource || codeEval.fn.toString(),
190
- source_language: "typescript",
191
- source_format: componentSource ? "component" : "function",
192
103
  model_response_format: codeEval.modelResponseFormat,
193
104
  };
194
105
  }
@@ -201,9 +112,6 @@ export function toManifest(component, opts = {}) {
201
112
  is_async: skill.isAsync,
202
113
  tool_refs: skill.tools.length > 0 ? skill.tools.map(resolveComponentRef) : undefined,
203
114
  output_schema: skill.outputSchema,
204
- source_code: componentSource || skill.fn?.toString(),
205
- source_language: "typescript",
206
- source_format: componentSource ? "component" : skill.fn ? "function" : undefined,
207
115
  };
208
116
  }
209
117
  case "agent": {
@@ -233,7 +141,8 @@ export function toManifest(component, opts = {}) {
233
141
  const llmEval = component;
234
142
  return {
235
143
  ...base,
236
- instructions: llmEval.instructions,
144
+ ...(llmEval.instructions !== undefined ? { instructions: llmEval.instructions } : {}),
145
+ ...(llmEval.questions !== undefined ? { questions: llmEval.questions } : {}),
237
146
  model: llmEval.model,
238
147
  temperature: llmEval.temperature,
239
148
  output_schema: llmEval.outputSchema,
@@ -12,7 +12,7 @@ export interface LLMCall {
12
12
  sequenceOrder: number;
13
13
  autoraterRunId: string | null;
14
14
  datasetItemId: string | null;
15
- provider: "anthropic" | "openai" | "gemini";
15
+ provider: "anthropic" | "openai" | "gemini" | "openrouter";
16
16
  model: string;
17
17
  inputMessages: unknown[];
18
18
  output: unknown | null;
package/dist/types.d.ts CHANGED
@@ -209,7 +209,8 @@ export declare class LLMProcessor extends ZhanlaComponent {
209
209
  export interface LLMEvalOptions {
210
210
  name: string;
211
211
  description: string;
212
- instructions: string;
212
+ instructions?: string;
213
+ questions?: string[];
213
214
  model: string;
214
215
  client?: unknown;
215
216
  runner?: Runner;
@@ -223,7 +224,8 @@ export declare class LLMEval extends ZhanlaComponent {
223
224
  readonly componentType: "llm_eval";
224
225
  readonly name: string;
225
226
  readonly description: string;
226
- readonly instructions: string;
227
+ readonly instructions?: string;
228
+ readonly questions?: string[];
227
229
  readonly model: string;
228
230
  readonly runner?: Runner;
229
231
  readonly outputSchema?: JsonSchema;
package/dist/types.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Mirrors the Python SDK's class-based component model.
4
4
  */
5
5
  import { parseJsonResponse } from "./json.js";
6
- import { isAnthropicClient, isGeminiClient, isOpenAIClient, wrap } from "./wrap.js";
6
+ import { isAnthropicClient, isGeminiClient, isOpenAIClient, isOpenRouterClient, wrap } from "./wrap.js";
7
7
  export const RUNNABLE_TYPES = new Set([
8
8
  "tool",
9
9
  "agent",
@@ -210,6 +210,8 @@ export class Runner {
210
210
  providerName() {
211
211
  if (isAnthropicClient(this.client))
212
212
  return "anthropic";
213
+ if (isOpenRouterClient(this.client))
214
+ return "openrouter";
213
215
  if (isOpenAIClient(this.client))
214
216
  return "openai";
215
217
  if (isGeminiClient(this.client))
@@ -583,6 +585,7 @@ export class LLMEval extends ZhanlaComponent {
583
585
  name;
584
586
  description;
585
587
  instructions;
588
+ questions;
586
589
  model;
587
590
  runner;
588
591
  outputSchema;
@@ -592,12 +595,21 @@ export class LLMEval extends ZhanlaComponent {
592
595
  key;
593
596
  constructor(opts) {
594
597
  super();
598
+ const hasInstructions = opts.instructions !== undefined && opts.instructions !== "";
599
+ const hasQuestions = opts.questions !== undefined && opts.questions.length > 0;
600
+ if (hasInstructions && hasQuestions) {
601
+ throw new Error(`LLMEval '${opts.name}': provide exactly one of 'instructions' or 'questions', not both.`);
602
+ }
603
+ if (!hasInstructions && !hasQuestions) {
604
+ throw new Error(`LLMEval '${opts.name}': must provide exactly one of 'instructions' or 'questions'.`);
605
+ }
595
606
  if (opts.client !== undefined && opts.runner !== undefined) {
596
607
  throw new Error("Specify client or runner, not both.");
597
608
  }
598
609
  this.name = opts.name;
599
610
  this.description = opts.description;
600
611
  this.instructions = opts.instructions;
612
+ this.questions = opts.questions;
601
613
  this.model = opts.model;
602
614
  this.runner = opts.client !== undefined ? new Runner({ client: opts.client }) : opts.runner;
603
615
  this.outputSchema = opts.outputSchema;
package/dist/wrap.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * Supported clients:
8
8
  * - Anthropic (from @anthropic-ai/sdk)
9
9
  * - OpenAI (from openai)
10
+ * - OpenRouter (OpenAI client with baseURL containing "openrouter")
10
11
  * - GoogleGenAI (from @google/genai)
11
12
  */
12
13
  export declare function isAnthropicClient(client: unknown): client is {
@@ -21,6 +22,14 @@ export declare function isOpenAIClient(client: unknown): client is {
21
22
  };
22
23
  };
23
24
  };
25
+ export declare function isOpenRouterClient(client: unknown): client is {
26
+ chat: {
27
+ completions: {
28
+ create: (...args: unknown[]) => unknown;
29
+ };
30
+ };
31
+ baseURL: string;
32
+ };
24
33
  export declare function isGeminiClient(client: unknown): client is {
25
34
  models: {
26
35
  generateContent: (...args: unknown[]) => unknown;
package/dist/wrap.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * Supported clients:
8
8
  * - Anthropic (from @anthropic-ai/sdk)
9
9
  * - OpenAI (from openai)
10
+ * - OpenRouter (OpenAI client with baseURL containing "openrouter")
10
11
  * - GoogleGenAI (from @google/genai)
11
12
  */
12
13
  import { traceStorage } from "./trace_store.js";
@@ -104,7 +105,7 @@ function wrapAnthropic(client) {
104
105
  // ---------------------------------------------------------------------------
105
106
  // OpenAI
106
107
  // ---------------------------------------------------------------------------
107
- function recordOpenAICall(ctx, kwargs, response, latencyMs) {
108
+ function recordOpenAICall(ctx, kwargs, response, latencyMs, provider = "openai") {
108
109
  if (!ctx)
109
110
  return;
110
111
  const choices = response["choices"] ?? [];
@@ -119,7 +120,7 @@ function recordOpenAICall(ctx, kwargs, response, latencyMs) {
119
120
  sequenceOrder: ctx.nextSequence(),
120
121
  autoraterRunId: ctx.autoraterRunId,
121
122
  datasetItemId: ctx.datasetItemId,
122
- provider: "openai",
123
+ provider,
123
124
  model: String(response["model"] ?? (kwargs["model"] ?? "")),
124
125
  inputMessages: serialize(kwargs["messages"] ?? []),
125
126
  output: serialize(message),
@@ -132,7 +133,7 @@ function recordOpenAICall(ctx, kwargs, response, latencyMs) {
132
133
  metadata: null,
133
134
  });
134
135
  }
135
- function wrapOpenAI(client) {
136
+ function wrapOpenAI(client, provider = "openai") {
136
137
  const originalCreate = client.chat.completions.create.bind(client.chat.completions);
137
138
  client.chat.completions.create = async function (...args) {
138
139
  const ctx = traceStorage.getStore();
@@ -140,7 +141,7 @@ function wrapOpenAI(client) {
140
141
  const start = performance.now();
141
142
  const response = await originalCreate(...args);
142
143
  const latencyMs = Math.round(performance.now() - start);
143
- recordOpenAICall(ctx, kwargs, response, latencyMs);
144
+ recordOpenAICall(ctx, kwargs, response, latencyMs, provider);
144
145
  return response;
145
146
  };
146
147
  return client;
@@ -208,6 +209,12 @@ export function isOpenAIClient(client) {
208
209
  client.chat !== null &&
209
210
  typeof client.chat.completions === "object");
210
211
  }
212
+ export function isOpenRouterClient(client) {
213
+ if (!isOpenAIClient(client))
214
+ return false;
215
+ const baseURL = client["baseURL"];
216
+ return typeof baseURL === "string" && baseURL.includes("openrouter");
217
+ }
211
218
  export function isGeminiClient(client) {
212
219
  return (client !== null &&
213
220
  typeof client === "object" &&
@@ -235,13 +242,15 @@ export function wrap(client) {
235
242
  let wrapped;
236
243
  if (isAnthropicClient(client))
237
244
  wrapped = wrapAnthropic(client);
245
+ else if (isOpenRouterClient(client))
246
+ wrapped = wrapOpenAI(client, "openrouter");
238
247
  else if (isOpenAIClient(client))
239
248
  wrapped = wrapOpenAI(client);
240
249
  else if (isGeminiClient(client))
241
250
  wrapped = wrapGemini(client);
242
251
  else {
243
252
  throw new TypeError(`bench.wrap() does not support ${Object.getPrototypeOf(client)?.constructor?.name ?? typeof client}. ` +
244
- "Supported clients: Anthropic, OpenAI, GoogleGenAI.");
253
+ "Supported clients: Anthropic, OpenAI, OpenRouter (OpenAI client with OpenRouter baseURL), GoogleGenAI.");
245
254
  }
246
255
  if (wrapped != null && typeof wrapped === "object") {
247
256
  Object.defineProperty(wrapped, WRAPPED_CLIENT, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhanla/sdk-ts",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "TypeScript SDK for the zhanla CLI — define and run AI components locally",
5
5
  "homepage": "https://benchmark-black.vercel.app/",
6
6
  "repository": {
@@ -20,7 +20,8 @@
20
20
  "postinstall": "node ./bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "tsx": "^4.7.0"
23
+ "tsx": "^4.7.0",
24
+ "typescript": "^5.4.0"
24
25
  },
25
26
  "files": [
26
27
  "dist",
package/bin/env.js DELETED
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, readFileSync, statSync } from "fs";
4
- import { dirname, join, resolve } from "path";
5
-
6
- function isDirectory(path) {
7
- try {
8
- return statSync(path).isDirectory();
9
- } catch {
10
- return false;
11
- }
12
- }
13
-
14
- export function splitFileSpec(fileSpec) {
15
- const colonIndex = fileSpec.lastIndexOf(":");
16
- // Preserve Windows drive letters (e.g. C:\...) by only splitting on later colons.
17
- if (colonIndex > 1) {
18
- return {
19
- filePath: fileSpec.slice(0, colonIndex),
20
- symbolName: fileSpec.slice(colonIndex + 1) || null,
21
- };
22
- }
23
- return { filePath: fileSpec, symbolName: null };
24
- }
25
-
26
- export function findDotenvLocal(startPath) {
27
- let current = resolve(startPath);
28
- if (!isDirectory(current)) {
29
- current = dirname(current);
30
- }
31
-
32
- while (true) {
33
- const candidate = join(current, ".env.local");
34
- if (existsSync(candidate)) {
35
- return candidate;
36
- }
37
- const parent = dirname(current);
38
- if (parent === current) {
39
- return null;
40
- }
41
- current = parent;
42
- }
43
- }
44
-
45
- export function loadDotenvLocal(startPath) {
46
- const envPath = findDotenvLocal(startPath);
47
- if (!envPath) {
48
- return null;
49
- }
50
-
51
- const source = readFileSync(envPath, "utf8");
52
- for (const line of source.split(/\r?\n/)) {
53
- const trimmed = line.trim();
54
- if (!trimmed || trimmed.startsWith("#")) continue;
55
- const equalsIndex = trimmed.indexOf("=");
56
- if (equalsIndex === -1) continue;
57
-
58
- const key = trimmed.slice(0, equalsIndex).trim();
59
- let value = trimmed.slice(equalsIndex + 1).trim();
60
- if (
61
- (value.startsWith('"') && value.endsWith('"'))
62
- || (value.startsWith("'") && value.endsWith("'"))
63
- ) {
64
- value = value.slice(1, -1);
65
- }
66
- if (key && process.env[key] === undefined) {
67
- process.env[key] = value;
68
- }
69
- }
70
-
71
- return envPath;
72
- }